Compare commits

..

6 Commits

Author SHA1 Message Date
GitHub Actions ab6a34afb6 chore(release): v0.13.3 2024-01-05 05:14:03 +00:00
Aaron b0eb7da9b4 chore: Refactor manifest generation E2E tests to unit tests (#323) 2024-01-02 17:05:23 -06:00
Aaron 4f649fc872 feat(DX): Add ctrl+E/cmd+E shortcut to reload extension during development (#322) 2024-01-02 12:06:22 -06:00
dependabot[bot] 1af64aa02f chore(deps-dev): bump vitepress from 1.0.0-rc.31 to 1.0.0-rc.34 (#316)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 14:33:18 -06:00
dependabot[bot] c35acff259 chore(deps-dev): bump prettier from 3.1.0 to 3.1.1 (#318)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 14:31:32 -06:00
dependabot[bot] 995d4f8080 chore(deps-dev): bump tsx from 4.6.2 to 4.7.0 (#320)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 14:31:16 -06:00
11 changed files with 1291 additions and 1064 deletions
+15
View File
@@ -1,5 +1,20 @@
# Changelog
## v0.13.3
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.2...v0.13.3)
### 🚀 Enhancements
- **DX:** Add `ctrl+E`/`cmd+E` shortcut to reload extension during development ([#322](https://github.com/wxt-dev/wxt/pull/322))
### 🏡 Chore
- **deps-dev:** Bump tsx from 4.6.2 to 4.7.0 ([#320](https://github.com/wxt-dev/wxt/pull/320))
- **deps-dev:** Bump prettier from 3.1.0 to 3.1.1 ([#318](https://github.com/wxt-dev/wxt/pull/318))
- **deps-dev:** Bump vitepress from 1.0.0-rc.31 to 1.0.0-rc.34 ([#316](https://github.com/wxt-dev/wxt/pull/316))
- Refactor manifest generation E2E tests to unit tests ([#323](https://github.com/wxt-dev/wxt/pull/323))
## v0.13.2
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.1...v0.13.2)
+10
View File
@@ -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`.
:::
+4 -2
View File
@@ -26,11 +26,13 @@ If not provided via the `manifest` config, the [manifest's `name`](https://devel
## `version` and `version_name`
The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) fields are based on your package.json's `version` property.
The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) properties are based on the `version` field listed in your `package.json` or `wxt.config.ts`.
- `version_name` is the exact string listed in your package.json
- `version_name` is the exact string listed in your `package.json` or `wxt.config.ts` file
- `version` is the string cleaned up, with any invalid suffixes removed
If a version is not found, a warning is logged and the version defaults to `"0.0.0"`.
### Example
```json
-640
View File
@@ -1,556 +1,7 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
// TODO: move to unit tests to speed this up - this doesn't have to be in E2E tests
describe('Manifest Content', () => {
describe('popup', () => {
const popupContent = (type?: 'browser_action' | 'page_action') => `
<html>
<head>
${type == null ? '' : `<meta name="manifest.type" content="${type}">`}
<meta name="manifest.default_icon" content="{ '16': '/icon/16.png' }">
<title>Default Title</title>
</head>
</html>
`;
it('should include an action for mv3', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', popupContent());
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.action).toEqual({
default_icon: { '16': '/icon/16.png' },
default_title: 'Default Title',
default_popup: 'popup.html',
});
});
it.each([
[undefined, 'browser_action'],
['browser_action', 'browser_action'],
['page_action', 'page_action'],
] as const)(
'should include a browser_action for mv2',
async (type, expectedType) => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', popupContent(type));
await project.build({ manifestVersion: 2 });
const manifest = await project.getOutputManifest(
'.output/chrome-mv2/manifest.json',
);
expect(manifest[expectedType]).toEqual({
default_icon: { '16': '/icon/16.png' },
default_title: 'Default Title',
default_popup: 'popup.html',
});
},
);
});
describe('options', () => {
const optionsContent = `
<html>
<head>
<meta name="manifest.open_in_tab" content="false">
<meta name="manifest.chrome_style" content="true">
<meta name="manifest.browser_style" content="true">
</head>
</html>
`;
it('should include a options_ui and chrome_style for chrome', async () => {
const project = new TestProject();
project.addFile('entrypoints/options.html', optionsContent);
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.options_ui).toEqual({
open_in_tab: false,
chrome_style: true,
page: 'options.html',
});
});
it('should include a options_ui and browser_style for firefox', async () => {
const project = new TestProject();
project.addFile('entrypoints/options.html', optionsContent);
await project.build({ browser: 'firefox' });
const manifest = await project.getOutputManifest(
'.output/firefox-mv2/manifest.json',
);
expect(manifest.options_ui).toEqual({
open_in_tab: false,
browser_style: true,
page: 'options.html',
});
});
});
describe('background', () => {
const backgroundContent = `
export default defineBackground({
persistent: true,
type: "module",
main: () => {},
})
`;
it.each(['chrome', 'safari'])(
'should include scripts and persistent for %s mv2',
async (browser) => {
const project = new TestProject();
project.addFile('entrypoints/background.ts', backgroundContent);
await project.build({ browser, manifestVersion: 2 });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv2/manifest.json`,
);
expect(manifest.background).toEqual({
persistent: true,
scripts: ['background.js'],
});
},
);
it.each(['chrome', 'safari'])(
'should include a service worker and type for %s mv3',
async (browser) => {
const project = new TestProject();
project.addFile('entrypoints/background.ts', backgroundContent);
await project.build({ browser, manifestVersion: 3 });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv3/manifest.json`,
);
expect(manifest.background).toEqual({
type: 'module',
service_worker: 'background.js',
});
},
);
it('should include a background script and type for firefox mv3', async () => {
const project = new TestProject();
project.addFile('entrypoints/background.ts', backgroundContent);
await project.build({ browser: 'firefox', manifestVersion: 3 });
const manifest = await project.getOutputManifest(
'.output/firefox-mv3/manifest.json',
);
expect(manifest.background).toEqual({
type: 'module',
scripts: ['background.js'],
});
});
it('should include a background script and persistent for firefox mv2', async () => {
const project = new TestProject();
project.addFile('entrypoints/background.ts', backgroundContent);
await project.build({ browser: 'firefox', manifestVersion: 2 });
const manifest = await project.getOutputManifest(
'.output/firefox-mv2/manifest.json',
);
expect(manifest.background).toEqual({
persistent: true,
scripts: ['background.js'],
});
});
});
describe('icons', () => {
it('should auto-discover icons with the correct name', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.addFile('public/icon-16.png');
project.addFile('public/icon/32.png');
project.addFile('public/icon@48w.png');
project.addFile('public/icon-64x64.png');
project.addFile('public/icon@96.png');
project.addFile('public/icons/128x128.png');
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.icons).toEqual({
'16': 'icon-16.png',
'32': 'icon/32.png',
'48': 'icon@48w.png',
'64': 'icon-64x64.png',
'96': 'icon@96.png',
'128': 'icons/128x128.png',
});
});
it('should return undefined when no icons are found', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.addFile('public/logo.png');
project.addFile('public/icon.jpeg');
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.icons).toBeUndefined();
});
it('should allow icons to be overwritten from the wxt.config.ts file', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.addFile('public/icon-16.png');
project.addFile('public/icon-32.png');
project.addFile('public/logo-16.png');
project.addFile('public/logo-32.png');
project.addFile('public/logo-48.png');
const icons = {
'16': 'logo-16.png',
'32': 'logo-32.png',
'48': 'logo-48.png',
};
project.setConfigFileConfig({
manifest: {
icons,
},
});
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.icons).toEqual(icons);
});
});
describe('content_scripts', () => {
it('should group content scripts and styles together based on their manifest properties', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/one.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://google.com/*"],
main: () => {},
})`,
);
project.addFile(
'entrypoints/one.content/style.css',
`body { color: red }`,
);
project.addFile(
'entrypoints/two.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://google.com/*"],
runAt: "document_end",
main: () => {},
})`,
);
project.addFile(
'entrypoints/two.content/style.css',
`body { color: green }`,
);
project.addFile(
'entrypoints/three.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://google.com/*"],
runAt: "document_end",
main: () => {},
})`,
);
project.addFile(
'entrypoints/three.content/style.css',
`body { color: blue }`,
);
project.addFile(
'entrypoints/four.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://duckduckgo.com/*"],
runAt: "document_end",
main: () => {},
})`,
);
project.addFile(
'entrypoints/four.content/style.css',
`body { color: yellow }`,
);
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.content_scripts).toContainEqual({
matches: ['*://duckduckgo.com/*'],
run_at: 'document_end',
css: ['content-scripts/four.css'],
js: ['content-scripts/four.js'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
run_at: 'document_end',
css: ['content-scripts/three.css', 'content-scripts/two.css'],
js: ['content-scripts/three.js', 'content-scripts/two.js'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
css: ['content-scripts/one.css'],
js: ['content-scripts/one.js'],
});
});
it('should add to any content scripts declared in wxt.config.ts', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/one.content/index.ts',
`export default defineContentScript({
matches: ["*://google.com/*"],
main: () => {},
})`,
);
project.addFile(
'entrypoints/two.content/style.css',
`body {
background-color: red;
}`,
);
project.setConfigFileConfig({
manifest: {
content_scripts: [
{
css: ['content-scripts/two.css'],
matches: ['*://*.google.com/*'],
},
],
},
});
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.content_scripts).toContainEqual({
css: ['content-scripts/two.css'],
matches: ['*://*.google.com/*'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
js: ['content-scripts/one.js'],
});
});
it('should add a CSS entry when cssInjectionMode is undefined', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
main() {},
});`,
);
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"css":["content-scripts/content.css"],"js":["content-scripts/content.js"]}]}"
`);
});
it('should add a CSS entry when cssInjectionMode is "manifest"', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "manifest",
main() {},
});`,
);
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"css":["content-scripts/content.css"],"js":["content-scripts/content.js"]}]}"
`);
});
it('should not add an entry for CSS when cssInjectionMode is "manual"', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "manual",
main() {},
});`,
);
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}]}"
`);
});
it('should not add a content script entry for CSS when cssInjectionMode is "ui", but add a web_accessible_resources entry for MV2', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "ui",
main() {},
});`,
);
await project.build({
manifestVersion: 2,
});
expect(await project.serializeFile('.output/chrome-mv2/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv2/manifest.json
----------------------------------------
{"manifest_version":2,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}],"web_accessible_resources":["content-scripts/content.css"]}"
`);
});
it('should not add a content script entry for CSS when cssInjectionMode is "ui", but add a web_accessible_resources entry for MV3', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "ui",
main() {},
});`,
);
await project.build({
manifestVersion: 3,
});
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}],"web_accessible_resources":[{"resources":["content-scripts/content.css"],"matches":["https://*.google.com/*"]}]}"
`);
});
});
it('should combine web accessible resources', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "ui",
main() {},
});`,
);
project.setConfigFileConfig({
manifest: {
web_accessible_resources: [
{ resources: ['one.png'], matches: ['https://one.com/*'] },
],
},
});
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","web_accessible_resources":[{"resources":["one.png"],"matches":["https://one.com/*"]},{"resources":["content-scripts/content.css"],"matches":["https://*.google.com/*"]}],"content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}]}"
`);
});
it('should respect the transformManifest option', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.addFile(
'wxt.config.ts',
`import { defineConfig } from 'wxt';
export default defineConfig({
transformManifest(manifest) {
manifest.author = "Custom Author"
}
})`,
);
await project.build();
const output = await project.serializeFile(
'.output/chrome-mv3/manifest.json',
);
expect(output).toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","author":"Custom Author"}"
`);
});
it.each([
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
@@ -579,95 +30,4 @@ describe('Manifest Content', () => {
expect(safariManifest.background.persistent).toBe(expected);
},
);
describe('versions', () => {
it.each([
['chrome', 3] as const,
['safari', 2] as const,
['edge', 3] as const,
])(
'should include version_name on %s when it needs simplified',
async (browser, manifestVersion) => {
const project = new TestProject({
version: '1.0.0-alpha1',
});
project.addFile('entrypoints/unlisted.html');
await project.build({ browser, manifestVersion });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv${manifestVersion}/manifest.json`,
);
expect(manifest.version).toBe('1.0.0');
expect(manifest.version_name).toBe('1.0.0-alpha1');
},
);
it.each([['firefox', 2] as const])(
"should not include a version_name on %s because the browser doesn't support it",
async (browser, manifestVersion) => {
const project = new TestProject({
version: '1.0.0-alpha1',
});
project.addFile('entrypoints/unlisted.html');
await project.build({ browser, manifestVersion });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv${manifestVersion}/manifest.json`,
);
expect(manifest.version).toBe('1.0.0');
expect(manifest.version_name).toBeUndefined();
},
);
it.each([
['chrome', 3] as const,
['firefox', 2] as const,
['safari', 3] as const,
['edge', 3] as const,
])(
'should not include the version_name if it is equal to version',
async (browser, manifestVersion) => {
const project = new TestProject({
version: '1.0.0.1',
});
project.addFile('entrypoints/unlisted.html');
await project.build({ browser, manifestVersion });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv${manifestVersion}/manifest.json`,
);
expect(manifest.version).toBe('1.0.0.1');
expect(manifest.version_name).toBeUndefined();
},
);
});
it('should respect the action field in the manifest without a popup', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.setConfigFileConfig({
manifest: {
action: {
default_title: 'Hello world',
},
},
});
await project.build();
expect(await project.getOutputManifest()).toMatchInlineSnapshot(`
{
"action": {
"default_title": "Hello world",
},
"description": "Example description",
"manifest_version": 3,
"name": "E2E Extension",
"version": "0.0.0",
}
`);
});
});
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.13.2",
"version": "0.13.3",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18",
@@ -160,7 +160,7 @@
"typedoc-plugin-markdown": "4.0.0-next.23",
"typedoc-vitepress-theme": "1.0.0-next.3",
"typescript": "^5.3.2",
"vitepress": "1.0.0-rc.31",
"vitepress": "1.0.0-rc.34",
"vitest": "^1.1.0",
"vitest-mock-extended": "^1.3.1",
"vue": "^3.3.10"
+276 -403
View File
File diff suppressed because it is too large Load Diff
+912
View File
@@ -0,0 +1,912 @@
import { describe, expect, it } from 'vitest';
import { generateManifest } from '../manifest';
import {
fakeArray,
fakeBackgroundEntrypoint,
fakeBuildOutput,
fakeEntrypoint,
fakeInternalConfig,
fakeOptionsEntrypoint,
fakePopupEntrypoint,
} from '../testing/fake-objects';
import { Manifest } from 'webextension-polyfill';
import {
BuildOutput,
ContentScriptEntrypoint,
Entrypoint,
OutputAsset,
} from '~/types';
const outDir = '/output';
const contentScriptOutDir = '/output/content-scripts';
describe('Manifest Utils', () => {
describe('generateManifest', () => {
describe('popup', () => {
type ActionType = 'browser_action' | 'page_action';
const popupEntrypoint = (type?: ActionType) =>
fakePopupEntrypoint({
options: {
// @ts-expect-error: Force this to be undefined instead of inheriting the random value
mv2Key: type ?? null,
defaultIcon: {
'16': '/icon/16.png',
},
defaultTitle: 'Default Iitle',
},
outputDir: outDir,
});
it('should include an action for mv3', async () => {
const popup = popupEntrypoint();
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
manifestVersion: 3,
outDir,
});
const expected: Partial<Manifest.WebExtensionManifest> = {
action: {
default_icon: popup.options.defaultIcon,
default_title: popup.options.defaultTitle,
default_popup: 'popup.html',
},
};
const actual = await generateManifest([popup], buildOutput, config);
expect(actual).toMatchObject(expected);
});
it.each<{
inputType: ActionType | undefined;
expectedType: ActionType;
}>([
{ inputType: undefined, expectedType: 'browser_action' },
{ inputType: 'browser_action', expectedType: 'browser_action' },
{ inputType: 'page_action', expectedType: 'page_action' },
])(
'should use the correct action for mv2: %j',
async ({ inputType, expectedType }) => {
const popup = popupEntrypoint(inputType);
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
manifestVersion: 2,
outDir,
});
const expected = {
default_icon: popup.options.defaultIcon,
default_title: popup.options.defaultTitle,
default_popup: 'popup.html',
};
const actual = await generateManifest([popup], buildOutput, config);
expect(actual[expectedType]).toEqual(expected);
},
);
});
describe('action without popup', () => {
it('should respect the action field in the manifest without a popup', async () => {
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
outDir,
manifest: {
action: {
default_icon: 'icon-16.png',
default_title: 'Example title',
},
},
});
const expected: Partial<Manifest.WebExtensionManifest> = {
action: config.manifest.action,
};
const actual = await generateManifest([], buildOutput, config);
expect(actual).toMatchObject(expected);
});
});
describe('options', () => {
const options = fakeOptionsEntrypoint({
outputDir: outDir,
options: {
openInTab: false,
chromeStyle: true,
browserStyle: true,
},
});
it('should include a options_ui and chrome_style for chrome', async () => {
const config = fakeInternalConfig({
manifestVersion: 3,
outDir,
browser: 'chrome',
});
const buildOutput = fakeBuildOutput();
const expected = {
open_in_tab: false,
chrome_style: true,
page: 'options.html',
};
const actual = await generateManifest([options], buildOutput, config);
expect(actual.options_ui).toEqual(expected);
});
it('should include a options_ui and browser_style for firefox', async () => {
const config = fakeInternalConfig({
manifestVersion: 3,
browser: 'firefox',
outDir,
});
const buildOutput = fakeBuildOutput();
const expected = {
open_in_tab: false,
browser_style: true,
page: 'options.html',
};
const actual = await generateManifest([options], buildOutput, config);
expect(actual.options_ui).toEqual(expected);
});
});
describe('background', () => {
const background = fakeBackgroundEntrypoint({
outputDir: outDir,
options: {
persistent: true,
type: 'module',
},
});
describe('MV3', () => {
it.each(['chrome', 'safari'])(
'should include a service worker and type for %s',
async (browser) => {
const config = fakeInternalConfig({
outDir,
manifestVersion: 3,
browser,
});
const buildOutput = fakeBuildOutput();
const expected = {
type: 'module',
service_worker: 'background.js',
};
const actual = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
},
);
it('should include a background script and type for firefox', async () => {
const config = fakeInternalConfig({
outDir,
manifestVersion: 3,
browser: 'firefox',
});
const buildOutput = fakeBuildOutput();
const expected = {
type: 'module',
scripts: ['background.js'],
};
const actual = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
});
});
describe('MV2', () => {
it.each(['chrome', 'safari'])(
'should include scripts and persistent for %s',
async (browser) => {
const config = fakeInternalConfig({
outDir,
manifestVersion: 2,
browser,
});
const buildOutput = fakeBuildOutput();
const expected = {
persistent: true,
scripts: ['background.js'],
};
const actual = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
},
);
it('should include a background script and persistent for firefox mv2', async () => {
const config = fakeInternalConfig({
outDir,
manifestVersion: 2,
browser: 'firefox',
});
const buildOutput = fakeBuildOutput();
const expected = {
persistent: true,
scripts: ['background.js'],
};
const actual = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
});
});
});
describe('icons', () => {
it('should auto-discover icons with the correct name', async () => {
const entrypoints = fakeArray(fakeEntrypoint);
const buildOutput = fakeBuildOutput({
publicAssets: [
{ type: 'asset', fileName: 'icon-16.png' },
{ type: 'asset', fileName: 'icon/32.png' },
{ type: 'asset', fileName: 'icon@48w.png' },
{ type: 'asset', fileName: 'icon-64x64.png' },
{ type: 'asset', fileName: 'icon@96.png' },
{ type: 'asset', fileName: 'icons/128x128.png' },
],
});
const config = fakeInternalConfig();
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.icons).toEqual({
16: 'icon-16.png',
32: 'icon/32.png',
48: 'icon@48w.png',
64: 'icon-64x64.png',
96: 'icon@96.png',
128: 'icons/128x128.png',
});
});
it('should return undefined when no icons are found', async () => {
const entrypoints = fakeArray(fakeEntrypoint);
const buildOutput = fakeBuildOutput({
publicAssets: [
{ type: 'asset', fileName: 'logo.png' },
{ type: 'asset', fileName: 'icon-16.jpeg' },
],
});
const config = fakeInternalConfig();
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.icons).toBeUndefined();
});
it('should allow icons to be overwritten from the wxt.config.ts file', async () => {
const entrypoints = fakeArray(fakeEntrypoint);
const buildOutput = fakeBuildOutput({
publicAssets: [
{ type: 'asset', fileName: 'icon-16.png' },
{ type: 'asset', fileName: 'icon-32.png' },
{ type: 'asset', fileName: 'logo-16.png' },
{ type: 'asset', fileName: 'logo-32.png' },
{ type: 'asset', fileName: 'logo-48.png' },
],
});
const expected = {
16: 'logo-16.png',
32: 'logo-32.png',
48: 'logo-48.png',
};
const config = fakeInternalConfig({
manifest: {
icons: expected,
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.icons).toEqual(expected);
});
});
describe('content_scripts', () => {
it('should group content scripts and styles together based on their manifest properties', async () => {
const cs1: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content/index.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
},
};
const cs1Styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const cs2: ContentScriptEntrypoint = {
type: 'content-script',
name: 'two',
inputPath: 'entrypoints/two.content/index.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
runAt: 'document_end',
},
};
const cs2Styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/two.css',
};
const cs3: ContentScriptEntrypoint = {
type: 'content-script',
name: 'three',
inputPath: 'entrypoints/three.content/index.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
runAt: 'document_end',
},
};
const cs3Styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/three.css',
};
const cs4: ContentScriptEntrypoint = {
type: 'content-script',
name: 'four',
inputPath: 'entrypoints/four.content/index.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://duckduckgo.com/*'],
runAt: 'document_end',
},
};
const cs4Styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/four.css',
};
const cs5: ContentScriptEntrypoint = {
type: 'content-script',
name: 'five',
inputPath: 'entrypoints/five.content/index.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
world: 'MAIN',
},
};
const cs5Styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/five.css',
};
const entrypoints = [cs1, cs2, cs3, cs4, cs5];
const config = fakeInternalConfig({
command: 'build',
outDir,
manifestVersion: 3,
});
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [
{ entrypoints: cs1, chunks: [cs1Styles] },
{ entrypoints: cs2, chunks: [cs2Styles] },
{ entrypoints: cs3, chunks: [cs3Styles] },
{ entrypoints: cs4, chunks: [cs4Styles] },
{ entrypoints: cs5, chunks: [cs5Styles] },
],
};
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
css: ['content-scripts/one.css'],
js: ['content-scripts/one.js'],
});
expect(actual.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
run_at: 'document_end',
css: ['content-scripts/two.css', 'content-scripts/three.css'],
js: ['content-scripts/two.js', 'content-scripts/three.js'],
});
expect(actual.content_scripts).toContainEqual({
matches: ['*://duckduckgo.com/*'],
run_at: 'document_end',
css: ['content-scripts/four.css'],
js: ['content-scripts/four.js'],
});
expect(actual.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
css: ['content-scripts/five.css'],
js: ['content-scripts/five.js'],
world: 'MAIN',
});
});
it('should merge any content scripts declared in wxt.config.ts', async () => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
},
};
const generatedContentScript = {
matches: ['*://google.com/*'],
js: ['content-scripts/one.js'],
};
const userContentScript = {
css: ['content-scripts/two.css'],
matches: ['*://*.google.com/*'],
};
const entrypoints = [cs];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
outDir,
command: 'build',
manifest: {
content_scripts: [userContentScript],
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.content_scripts).toContainEqual(userContentScript);
expect(actual.content_scripts).toContainEqual(generatedContentScript);
});
describe('cssInjectionMode', () => {
it.each([undefined, 'manifest'] as const)(
'should add a CSS entry when cssInjectionMode is %s',
async (cssInjectionMode) => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
cssInjectionMode,
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.content_scripts).toEqual([
{
js: ['content-scripts/one.js'],
css: ['content-scripts/one.css'],
matches: ['*://google.com/*'],
},
]);
},
);
it.each(['manual', 'ui'] as const)(
'should not add an entry for CSS when cssInjectionMode is %s',
async (cssInjectionMode) => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
cssInjectionMode,
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.content_scripts).toEqual([
{
js: ['content-scripts/one.js'],
matches: ['*://google.com/*'],
},
]);
},
);
it('should add CSS file to `web_accessible_resources` when cssInjectionMode is "ui" for MV3', async () => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 3,
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
{
matches: ['*://google.com/*'],
resources: ['content-scripts/one.css'],
},
]);
});
it('should add CSS file to `web_accessible_resources` when cssInjectionMode is "ui" for MV2', async () => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 2,
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
'content-scripts/one.css',
]);
});
});
});
describe('web_accessible_resources', () => {
it('should combine user defined resources and generated resources for MV3', async () => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 3,
manifest: {
web_accessible_resources: [
{ resources: ['one.png'], matches: ['*://one.com/*'] },
],
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.web_accessible_resources).toEqual([
{ resources: ['one.png'], matches: ['*://one.com/*'] },
{
resources: ['content-scripts/one.css'],
matches: ['*://google.com/*'],
},
]);
});
it('should combine user defined resources and generated resources for MV2', async () => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 2,
manifest: {
web_accessible_resources: ['one.png'],
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.web_accessible_resources).toEqual([
'one.png',
'content-scripts/one.css',
]);
});
});
describe('transformManifest option', () => {
it("should call the transformManifest option after the manifest is generated, but before it's returned", async () => {
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const newAuthor = 'Custom Author';
const config = fakeInternalConfig({
transformManifest(manifest: any) {
manifest.author = newAuthor;
},
});
const expected = {
author: newAuthor,
};
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual).toMatchObject(expected);
});
});
describe('version', () => {
it.each(['chrome', 'safari', 'edge'] as const)(
'should include version and version_name as is on %s',
async (browser) => {
const version = '1.0.0';
const versionName = '1.0.0-alpha1';
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
browser,
manifest: {
version,
version_name: versionName,
},
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe(version);
expect(actual.version_name).toBe(versionName);
},
);
it.each(['firefox'] as const)(
'should not include a version_name on %s because it is unsupported',
async (browser) => {
const version = '1.0.0';
const versionName = '1.0.0-alpha1';
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
browser,
manifest: {
version,
version_name: versionName,
},
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe(version);
expect(actual.version_name).toBeUndefined();
},
);
it.each(['chrome', 'firefox', 'safari', 'edge'])(
'should not include the version_name if it is equal to version',
async (browser) => {
const version = '1.0.0';
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
browser,
manifest: {
version,
version_name: version,
},
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe(version);
expect(actual.version_name).toBeUndefined();
},
);
it('should log a warning if the version could not be detected', async () => {
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
manifest: {
// @ts-ignore: Purposefully removing version from fake object
version: null,
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.version).toBe('0.0.0');
expect(actual.version_name).toBeUndefined();
expect(config.logger.warn).toBeCalledTimes(1);
expect(config.logger.warn).toBeCalledWith(
expect.stringContaining('Extension version not found'),
);
});
});
describe('commands', () => {
const reloadCommandName = 'wxt:reload-extension';
const reloadCommand = {
suggested_key: {
default: 'Ctrl+E',
},
};
it('should include a command for reloading the extension during development', async () => {
const config = fakeInternalConfig({ command: 'serve' });
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const actual = await generateManifest(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 generateManifest(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 generateManifest(entrypoints, output, config);
expect(actual.commands).toBeUndefined();
});
});
});
});
+2 -2
View File
@@ -3,7 +3,7 @@ import { BuildOutput, EntrypointGroup, InternalConfig } from '~/types';
import { findEntrypoints } from './find-entrypoints';
import { generateTypesDir } from './generate-wxt-dir';
import { buildEntrypoints } from './build-entrypoints';
import { generateMainfest, writeManifest } from '~/core/utils/manifest';
import { generateManifest, writeManifest } from '~/core/utils/manifest';
/**
* Given a configuration, list of entrypoints, and an existing, partial output, build the
@@ -41,7 +41,7 @@ export async function rebuild(
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
};
const newManifest = await generateMainfest(
const newManifest = await generateManifest(
allEntrypoints,
mergedOutput,
config,
+28 -8
View File
@@ -50,29 +50,41 @@ export async function writeManifest(
/**
* Generates the manifest based on the config and entrypoints.
*/
export async function generateMainfest(
export async function generateManifest(
entrypoints: Entrypoint[],
buildOutput: Omit<BuildOutput, 'manifest'>,
config: InternalConfig,
): Promise<Manifest.WebExtensionManifest> {
const pkg = await getPackageJson(config);
const versionName = config.manifest.version_name ?? pkg?.version;
const version = config.manifest.version ?? simplifyVersion(pkg?.version);
let versionName =
config.manifest.version_name ?? config.manifest.version ?? pkg?.version;
if (versionName == null) {
versionName = '0.0.0';
config.logger.warn(
'Extension version not found, defaulting to "0.0.0". Add a version to your `package.json` or `wxt.config.ts` file. For more details, see: https://wxt.dev/guide/manifest.html#version-and-version-name',
);
}
let version = config.manifest.version ?? simplifyVersion(versionName);
const baseManifest: Manifest.WebExtensionManifest = {
manifest_version: config.manifestVersion,
name: pkg?.name,
description: pkg?.description,
version,
version_name:
// Firefox doesn't support version_name
config.browser === 'firefox' || versionName === version
? undefined
: versionName,
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(
@@ -80,6 +92,14 @@ export async function generateMainfest(
baseManifest,
) as Manifest.WebExtensionManifest;
// Apply the final version fields after merging the user manifest
manifest.version = version;
manifest.version_name =
// Firefox doesn't support version_name
config.browser === 'firefox' || versionName === version
? undefined
: versionName;
addEntrypoints(manifest, entrypoints, buildOutput, config);
if (config.command === 'serve') addDevModeCsp(manifest, config);
+36 -7
View File
@@ -17,6 +17,9 @@ import {
OutputChunk,
OutputFile,
OutputAsset,
BuildOutput,
BuildStepOutput,
UserManifest,
} from '~/types';
import { mock } from 'vitest-mock-extended';
@@ -39,6 +42,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<ContentScriptEntrypoint>(() => ({
type: 'content-script',
@@ -65,8 +78,8 @@ export const fakeContentScriptEntrypoint =
export const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(
() => ({
type: 'background',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
inputPath: 'entrypoints/background.ts',
name: 'background',
options: {
persistent: faker.helpers.arrayElement([true, false, undefined]),
type: faker.helpers.maybe(() => 'module'),
@@ -87,8 +100,8 @@ export const fakeUnlistedScriptEntrypoint =
export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
() => ({
type: 'options',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
inputPath: 'entrypoints/options.html',
name: 'options',
outputDir: fakeDir('.output'),
options: {
browserStyle: faker.helpers.arrayElement([true, false, undefined]),
@@ -100,8 +113,8 @@ export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
export const fakePopupEntrypoint = fakeObjectCreator<PopupEntrypoint>(() => ({
type: 'popup',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
inputPath: 'entrypoints/popup.html',
name: 'popup',
outputDir: fakeDir('.output'),
options: {
defaultTitle: faker.helpers.arrayElement([
@@ -165,6 +178,11 @@ export const fakeManifest = fakeObjectCreator<Manifest.WebExtensionManifest>(
}),
);
export const fakeUserManifest = fakeObjectCreator<UserManifest>(() => ({
name: faker.string.alphanumeric(),
version: `${faker.number.int()}.${faker.number.int()}.${faker.number.int()}`,
}));
export function fakeArray<T>(createItem: () => T, count = 3): T[] {
const array: T[] = [];
for (let i = 0; i < count; i++) {
@@ -187,7 +205,7 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
fsCache: mock<FsCache>(),
imports: {},
logger: mock(),
manifest: fakeManifest(),
manifest: fakeUserManifest(),
manifestVersion,
mode,
outBaseDir: fakeDir(),
@@ -222,3 +240,14 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
builder: mock(),
};
});
export const fakeBuildOutput = fakeObjectCreator<BuildOutput>(() => ({
manifest: fakeManifest(),
publicAssets: fakeArray(fakeOutputAsset),
steps: fakeArray(fakeBuildStepOutput),
}));
export const fakeBuildStepOutput = fakeObjectCreator<BuildStepOutput>(() => ({
chunks: fakeArray(fakeOutputChunk),
entrypoints: fakeArray(fakeEntrypoint),
}));
+6
View File
@@ -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 {