Compare commits

...

12 Commits

Author SHA1 Message Date
GitHub Actions a643852387 chore(release): wxt v0.19.19
vhs / vhs (push) Waiting to run
2024-12-06 00:28:20 +00:00
Nishu e221252325 fix: Exclude skipped entrypoints from Firefox sources zip (#1238)
Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
2024-12-05 18:23:00 -06:00
Aaron 88f126e658 fix: Ensure content scripts are registered immediately in dev mode (#1253) 2024-12-05 18:16:38 -06:00
Aaron 17af05acc8 fix: Prevent changing dev server port when reloading config (#1241) 2024-12-05 11:48:49 -06:00
Jeremie Corpinot fd436a0d63 docs: Add "aesthetic Notion, styled" to homepage (#1247) 2024-12-04 16:00:23 -06:00
Nishu 0d796b63c2 feat: Keyboard shortcut to reopen the browser without restarting the dev command (#1211)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
Co-authored-by: Florian Metz <me@timeraa.dev>
2024-12-03 21:42:44 -06:00
GitHub Actions cb143d2bbc chore(release): wxt v0.19.18
vhs / vhs (push) Waiting to run
2024-12-03 23:00:59 +00:00
Aaron f3e7758521 types: Add overloads to ContentScriptContext#addEventListener for different targets (#1245) 2024-12-03 16:55:33 -06:00
Aaron 23ef101954 chore: Refactor findEntrypoints to return all entrypoints with skipped set properly (#1244) 2024-12-03 12:36:11 -06:00
spookyuser c2f5efbde4 docs: Update injectScript docs to match example from examples repo (#1240)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-12-03 12:23:33 -06:00
Takuya HARA 4ce178c592 docs: Changed "Chrome Extension" to "web extension" (#1229)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-12-03 11:07:46 -06:00
dependabot[bot] 7aa30afa7f chore(deps): bump codecov/codecov-action from 4 to 5 (#1237)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-01 16:13:47 -06:00
24 changed files with 438 additions and 116 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- uses: oven-sh/setup-bun@v2
- name: pnpm test:coverage
run: pnpm test:coverage -- --reporter=default --reporter=hanging-process
- uses: codecov/codecov-action@v4
- uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
windows-tests:
@@ -59,6 +59,7 @@ const chromeExtensionIds = [
'macmkmchfoclhpbncclinhjflmdkaoom', // Wandpen - Instantly improve your writing with AI
'lhmgechokhmdekdpgkkemoeecelcaonm', // YouTube Hider - Remove Comments By Keywords, Usernames & Tools
'imgheieooppmahcgniieddodaliodeeg', // QA Compass - Record standardized bug reports easily
'npgghjedpchajflknnbngajkjkdhncdo', // aesthetic Notion, styled
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
+1 -1
View File
@@ -18,7 +18,7 @@ import { version as storageVersion } from '../../packages/storage/package.json';
const title = 'Next-gen Web Extension Framework';
const titleSuffix = ' WXT';
const description =
"WXT provides the best developer experience, making it quick, easy, and fun to develop chrome extensions for all browsers. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started.";
"WXT provides the best developer experience, making it quick, easy, and fun to develop web extensions. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started.";
const ogTitle = `${title}${titleSuffix}`;
const ogUrl = 'https://wxt.dev';
const ogImage = 'https://wxt.dev/social-preview.png';
+10 -5
View File
@@ -548,16 +548,21 @@ To use `injectScript`, we need two entrypoints, one content script and one unlis
```ts
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
console.log('Hello from the main world!');
console.log('Hello from the main world');
});
```
```ts
// entrypoints/example.content.ts
export default defineContentScript(async () => {
await injectScript('/example-main-world.js', {
keepInDom: true,
});
export default defineContentScript({
matches: ['*://*/*'],
async main() {
console.log('Injecting script...');
await injectScript('/example-main-world.js', {
keepInDom: true,
});
console.log('Done!');
},
});
```
+1 -1
View File
@@ -129,7 +129,7 @@ wxt zip
Firefox requires you to upload a ZIP of your source code. This allows them to rebuild your extension and review the code in a readable way. More details can be found in [Firefox's docs](https://extensionworkshop.com/documentation/publish/source-code-submission/).
When running `wxt zip -b firefox`, WXT will zip both your extension and sources. Certain files (such as config files, hidden files, and tests) are automatically excluded from your sources. However, it's important to manually check the ZIP to ensure it only contains the files necessary to rebuild your extension.
When running `wxt zip -b firefox`, WXT will zip both your extension and sources. Certain files (such as config files, hidden files, tests, and excluded entrypoints) are automatically excluded from your sources. However, it's important to manually check the ZIP to ensure it only contains the files necessary to rebuild your extension.
To customize which files are zipped, add the `zip` option to your config file.
@@ -75,3 +75,5 @@ Here are some examples:
</body>
</html>
```
Alternatively, you can use the [`filterEntrypoints` config](/api/reference/wxt/interfaces/InlineConfig#filterentrypoints) to list all the entrypoints you want to build.
+1 -1
View File
@@ -9,7 +9,7 @@ Check out the [comparison](/guide/resources/compare) to see how WXT compares to
## Prerequisites
These docs assume you have a basic knowledge of how Chrome extensions are structured and how you access the extension APIs.
These docs assume you have a basic knowledge of how web extensions are structured and how you access the extension APIs.
:::warning New to extension development?
If you have never written an extension before, follow Chrome's [Hello World tutorial](https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world) to first **_create an extension without WXT_**, then come back here.
+3 -3
View File
@@ -6,7 +6,7 @@ title: Next-gen Web Extension Framework
hero:
name: WXT
text: Next-gen Web Extension Framework
tagline: An open source tool that makes Chrome Extension development faster than ever before.
tagline: An open source tool that makes web extension development faster than ever before.
image:
src: /hero-logo.svg
alt: WXT
@@ -75,7 +75,7 @@ features:
## Put <span style="color: var(--vp-c-brand-1)">Developer Experience</span> First
WXT simplifies the chrome extension development process by providing tools for zipping and publishing, the best-in-class dev mode, an opinionated project structure, and more. Iterate faster, develop features not build scripts, and use everything the JS ecosystem has to offer.
WXT simplifies the web extension development process by providing tools for zipping and publishing, the best-in-class dev mode, an opinionated project structure, and more. Iterate faster, develop features not build scripts, and use everything the JS ecosystem has to offer.
<div style="margin: auto; width: 100%; max-width: 900px; text-align: center">
<video src="https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94" controls></video>
@@ -87,7 +87,7 @@ WXT simplifies the chrome extension development process by providing tools for z
## Who's Using WXT?
Battle tested and ready for production. Explore chrome extensions made with WXT.
Battle tested and ready for production. Explore web extensions made with WXT.
<ClientOnly>
<UsingWxtSection />
+41 -1
View File
@@ -1,5 +1,45 @@
# Changelog
## v0.19.19
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.18...wxt-v0.19.19)
### 🚀 Enhancements
- Keyboard shortcut to reopen the browser without restarting the dev command ([#1211](https://github.com/wxt-dev/wxt/pull/1211))
### 🩹 Fixes
- Prevent changing dev server port when reloading config ([#1241](https://github.com/wxt-dev/wxt/pull/1241))
- Ensure content scripts are registered immediately in dev mode ([#1253](https://github.com/wxt-dev/wxt/pull/1253))
- Exclude skipped entrypoints from Firefox sources zip ([#1238](https://github.com/wxt-dev/wxt/pull/1238))
### ❤️ Contributors
- Nishu ([@nishu-murmu](http://github.com/nishu-murmu))
- Aaron ([@aklinker1](http://github.com/aklinker1))
## v0.19.18
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.17...wxt-v0.19.18)
### 🩹 Fixes
- Correct `dev.reloadCommand` restriction to consider only commands with suggested keys ([#1226](https://github.com/wxt-dev/wxt/pull/1226))
### 🌊 Types
- Add overloads to `ContentScriptContext#addEventListener` for different targets ([#1245](https://github.com/wxt-dev/wxt/pull/1245))
### 🏡 Chore
- Refactor `findEntrypoints` to return all entrypoints with `skipped` set properly ([#1244](https://github.com/wxt-dev/wxt/pull/1244))
### ❤️ Contributors
- Aaron ([@aklinker1](http://github.com/aklinker1))
- Jaguar Zhou ([@aiktb](http://github.com/aiktb))
## v0.19.17
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.16...wxt-v0.19.17)
@@ -2667,4 +2707,4 @@ Initial release of WXT. Full support for production builds and initial toolkit f
### 🤖 CI
- Create validation workflow ([#12](https://github.com/wxt-dev/wxt/pull/12))
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
+24
View File
@@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
describe('Dev Mode', () => {
it('should not change ports when restarting the server', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {})',
);
const server = await project.startServer({
runner: {
disabled: true,
},
});
const initialPort = server.port;
await server.restart();
const finalPort = server.port;
await server.stop();
expect(finalPort).toBe(initialPort);
});
});
+1 -1
View File
@@ -154,7 +154,7 @@ describe('Hooks', () => {
'build:publicAssets': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
'entrypoints:resolved': 2,
'vite:build:extendConfig': 1,
'vite:devServer:extendConfig': false,
'zip:start': true,
+36
View File
@@ -181,6 +181,42 @@ describe('Zipping', () => {
);
});
it('should exclude skipped entrypoints from respective browser sources zip', async () => {
const project = new TestProject({
name: 'test',
version: '1.0.0',
});
project.addFile(
'entrypoints/not-firefox.content.ts',
`export default defineContentScript({
matches: ['*://*/*'],
exclude: ['firefox'],
main() {},
});`,
);
project.addFile(
'entrypoints/all.content.ts',
`export default defineContentScript({
matches: ['*://*/*'],
main(ctx) {},
});
`,
);
const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
await project.zip({
browser: 'firefox',
});
await extract(sourcesZip, { dir: unzipDir });
expect(
await project.fileExists(unzipDir, 'entrypoints/not-firefox.content.ts'),
).toBe(false);
expect(
await project.fileExists(unzipDir, 'entrypoints/all.content.ts'),
).toBe(true);
});
it.each(['firefox', 'opera'])(
'should create sources zip for "%s" browser when sourcesZip is undefined',
async (browser) => {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.19.17",
"version": "0.19.19",
"description": "Next gen framework for developing web extensions",
"repository": {
"type": "git",
@@ -3,7 +3,15 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ContentScriptContext } from '..';
import { fakeBrowser } from '@webext-core/fake-browser';
import { sleep } from '../../../core/utils/time';
/**
* When dispatching events on document/window/etc, they are fired on the next
* tick of the event loop. So waiting a timeout of 0 will ensure they've been
* fired.
*/
function waitForEventsToFire() {
return new Promise((res) => setTimeout(res));
}
describe('Content Script Context', () => {
beforeEach(() => {
@@ -31,11 +39,11 @@ describe('Content Script Context', () => {
ctx.onInvalidated(onInvalidated);
// Wait for events to run before next tick next tick
await sleep(0);
await waitForEventsToFire();
// Create a new context after first is initialized, and wait for it to initialize
new ContentScriptContext(name);
await sleep(0);
await waitForEventsToFire();
expect(onInvalidated).toBeCalled();
expect(ctx.isValid).toBe(false);
@@ -47,13 +55,35 @@ describe('Content Script Context', () => {
ctx.onInvalidated(onInvalidated);
// Wait for events to run before next tick next tick
await sleep(0);
await waitForEventsToFire();
// Create a new context after first is initialized, and wait for it to initialize
new ContentScriptContext('test2');
await sleep(0);
await waitForEventsToFire();
expect(onInvalidated).not.toBeCalled();
expect(ctx.isValid).toBe(true);
});
describe('addEventListener', () => {
const context = new ContentScriptContext('test');
it('should infer types correctly for the window target', () => {
context.addEventListener(window, 'DOMContentLoaded', (_) => {});
context.addEventListener(window, 'orientationchange', (_) => {});
context.addEventListener(window, 'wxt:locationchange', (_) => {});
// @ts-expect-error
context.addEventListener(window, 'visibilitychange', (_) => {});
});
it('should infer types correctly for the document target', () => {
context.addEventListener(document, 'visibilitychange', (_) => {});
context.addEventListener(document, 'readystatechange', (_) => {});
});
it('should infer types correctly for HTML element targets', () => {
const button = document.createElement('button');
context.addEventListener(button, 'click', (_) => {});
context.addEventListener(button, 'mouseover', (_) => {});
});
});
});
@@ -175,19 +175,32 @@ export class ContentScriptContext implements AbortController {
* ctx.addEventListener(document, "visibilitychange", () => {
* // ...
* });
* ctx.addEventListener(document, "wxt:locationchange", () => {
* ctx.addEventListener(window, "wxt:locationchange", () => {
* // ...
* });
*/
addEventListener<
TTarget extends EventTarget,
TType extends keyof WxtContentScriptEventMap,
>(
target: TTarget,
addEventListener<TType extends keyof WxtWindowEventMap>(
target: Window,
type: TType,
handler: (event: WxtContentScriptEventMap[TType]) => void,
handler: (event: WxtWindowEventMap[TType]) => void,
options?: AddEventListenerOptions,
) {
): void;
addEventListener<TType extends keyof DocumentEventMap>(
target: Document,
type: keyof DocumentEventMap,
handler: (event: DocumentEventMap[TType]) => void,
options?: AddEventListenerOptions,
): void;
addEventListener<TTarget extends EventTarget>(
target: TTarget,
...params: Parameters<TTarget['addEventListener']>
): void;
addEventListener(
target: EventTarget,
type: string,
handler: (event: Event) => void,
options?: AddEventListenerOptions,
): void {
if (type === 'wxt:locationchange') {
// Start the location watcher when adding the event for the first time
if (this.isValid) this.locationWatcher.run();
@@ -195,7 +208,6 @@ export class ContentScriptContext implements AbortController {
target.addEventListener?.(
type.startsWith('wxt:') ? getUniqueEventName(type) : type,
// @ts-expect-error: Event don't match, but that's OK, EventTarget doesn't allow custom types in the callback
handler,
{
...options,
@@ -247,6 +259,6 @@ export class ContentScriptContext implements AbortController {
}
}
interface WxtContentScriptEventMap extends WindowEventMap {
export interface WxtWindowEventMap extends WindowEventMap {
'wxt:locationchange': WxtLocationChangeEvent;
}
+15 -4
View File
@@ -27,6 +27,7 @@ import {
getContentScriptJs,
mapWxtOptionsToRegisteredContentScript,
} from './utils/content-scripts';
import { createKeyboardShortcuts } from './keyboard-shortcuts';
/**
* Creates a dev server and pre-builds all the files that need to exist before loading the extension.
@@ -96,21 +97,27 @@ async function createServerInternal(): Promise<WxtDevServer> {
wxt.logger.success(`Started dev server @ ${server.origin}`);
await wxt.hooks.callHook('server:started', wxt, server);
await buildAndOpenBrowser();
// Register content scripts for the first time after the background starts up since they're not
// listed in the manifest
// Register content scripts for the first time after the background starts
// up since they're not listed in the manifest.
// Add listener before opening the browser to guarentee it is present when
// the extension sends back the initialization message.
server.ws.on('wxt:background-initialized', () => {
if (server.currentOutput == null) return;
reloadContentScripts(server.currentOutput.steps, server);
});
await buildAndOpenBrowser();
// Listen for file changes and reload different parts of the extension accordingly
const reloadOnChange = createFileReloader(server);
server.watcher.on('all', reloadOnChange);
keyboardShortcuts.start();
keyboardShortcuts.printHelp();
},
async stop() {
wasStopped = true;
keyboardShortcuts.stop();
await runner.closeBrowser();
await builderServer.close();
await wxt.hooks.callHook('server:closed', wxt, server);
@@ -136,11 +143,14 @@ async function createServerInternal(): Promise<WxtDevServer> {
},
async restartBrowser() {
await runner.closeBrowser();
keyboardShortcuts.stop();
await wxt.reloadConfig();
runner = await createExtensionRunner();
await runner.openBrowser();
keyboardShortcuts.start();
},
};
const keyboardShortcuts = createKeyboardShortcuts(server);
const buildAndOpenBrowser = async () => {
// Build after starting the dev server so it can be used to transform HTML files
@@ -230,6 +240,7 @@ function createFileReloader(server: WxtDevServer) {
break;
case 'content-script-reload':
reloadContentScripts(changes.changedSteps, server);
const rebuiltNames = changes.rebuildGroups
.flat()
.map((entry) => entry.name);
@@ -0,0 +1,60 @@
import readline from 'node:readline';
import { WxtDevServer } from '../types';
import { wxt } from './wxt';
import pc from 'picocolors';
export interface KeyboardShortcutWatcher {
start(): void;
stop(): void;
printHelp(): void;
}
/**
* Function that creates a keyboard shortcut handler for the extension.
*/
export function createKeyboardShortcuts(
server: WxtDevServer,
): KeyboardShortcutWatcher {
let isWatching = false;
let rl: readline.Interface | undefined;
const handleInput = (line: string) => {
// Only handle our specific command
if (line.trim() === 'o') {
server.restartBrowser();
}
};
return {
start() {
if (isWatching) return;
rl = readline.createInterface({
input: process.stdin,
terminal: false, // Don't intercept ctrl+C, ctrl+Z, etc
});
rl.on('line', handleInput);
isWatching = true;
},
stop() {
if (!isWatching) return;
if (rl) {
rl.close();
rl = undefined;
}
isWatching = false;
},
printHelp() {
if (!wxt.config.runnerConfig.config.disabled) {
wxt.logger.info(
`${pc.dim('Press')} ${pc.bold('o + enter')} ${pc.dim('to reopen the browser')}`,
);
}
},
};
}
@@ -701,7 +701,7 @@ describe('findEntrypoints', () => {
});
describe('include option', () => {
it("should filter out the background when include doesn't contain the target browser", async () => {
it("should mark the background as skipped when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointMock.mockResolvedValue({
include: ['not' + config.browser],
@@ -709,10 +709,15 @@ describe('findEntrypoints', () => {
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
expect(entrypoints).toEqual([
expect.objectContaining({
name: 'background',
skipped: true,
}),
]);
});
it("should filter out content scripts when include doesn't contain the target browser", async () => {
it("should mark content scripts as skipped when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['example.content.ts']);
importEntrypointMock.mockResolvedValue({
include: ['not' + config.browser],
@@ -720,10 +725,15 @@ describe('findEntrypoints', () => {
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
expect(entrypoints).toEqual([
expect.objectContaining({
name: 'example',
skipped: true,
}),
]);
});
it("should filter out the popup when include doesn't contain the target browser", async () => {
it("should mark the popup as skipped when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['popup.html']);
readFileMock.mockResolvedValueOnce(
`<html>
@@ -737,10 +747,15 @@ describe('findEntrypoints', () => {
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
expect(entrypoints).toEqual([
expect.objectContaining({
name: 'popup',
skipped: true,
}),
]);
});
it("should filter out the options page when include doesn't contain the target browser", async () => {
it("should mark the options page as skipped when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['options.html']);
readFileMock.mockResolvedValueOnce(
`<html>
@@ -754,10 +769,15 @@ describe('findEntrypoints', () => {
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
expect(entrypoints).toEqual([
expect.objectContaining({
name: 'options',
skipped: true,
}),
]);
});
it("should filter out an unlisted page when include doesn't contain the target browser", async () => {
it("should mark unlisted pages as skipped when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['unlisted.html']);
readFileMock.mockResolvedValueOnce(
`<html>
@@ -771,12 +791,17 @@ describe('findEntrypoints', () => {
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
expect(entrypoints).toEqual([
expect.objectContaining({
name: 'unlisted',
skipped: true,
}),
]);
});
});
describe('exclude option', () => {
it('should filter out the background when exclude contains the target browser', async () => {
it('should mark the background as skipped when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointMock.mockResolvedValue({
exclude: [config.browser],
@@ -784,10 +809,15 @@ describe('findEntrypoints', () => {
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
expect(entrypoints).toEqual([
expect.objectContaining({
name: 'background',
skipped: true,
}),
]);
});
it('should filter out content scripts when exclude contains the target browser', async () => {
it('should mark content scripts as skipped when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['example.content.ts']);
importEntrypointMock.mockResolvedValue({
exclude: [config.browser],
@@ -795,10 +825,15 @@ describe('findEntrypoints', () => {
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
expect(entrypoints).toEqual([
expect.objectContaining({
name: 'example',
skipped: true,
}),
]);
});
it('should filter out the popup when exclude contains the target browser', async () => {
it('should mark the popup as skipped when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['popup.html']);
readFileMock.mockResolvedValueOnce(
`<html>
@@ -810,10 +845,15 @@ describe('findEntrypoints', () => {
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
expect(entrypoints).toEqual([
expect.objectContaining({
name: 'popup',
skipped: true,
}),
]);
});
it('should filter out the options page when exclude contains the target browser', async () => {
it('should mark the options page as skipped when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['options.html']);
readFileMock.mockResolvedValueOnce(
`<html>
@@ -825,10 +865,15 @@ describe('findEntrypoints', () => {
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
expect(entrypoints).toEqual([
expect.objectContaining({
name: 'options',
skipped: true,
}),
]);
});
it('should filter out an unlisted page when exclude contains the target browser', async () => {
it('should mark unlisted pages as skipped when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['unlisted.html']);
readFileMock.mockResolvedValueOnce(
`<html>
@@ -840,12 +885,17 @@ describe('findEntrypoints', () => {
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
expect(entrypoints).toEqual([
expect.objectContaining({
name: 'unlisted',
skipped: true,
}),
]);
});
});
describe('filterEntrypoints option', () => {
it('should control entrypoints accessible', async () => {
it('should override include/exclude of individual entrypoint options', async () => {
globMock.mockResolvedValue([
'options/index.html',
'popup/index.html',
@@ -867,9 +917,25 @@ describe('findEntrypoints', () => {
importEntrypointMock.mockResolvedValue({});
const entrypoints = await findEntrypoints();
const names = entrypoints.map((item) => item.name);
expect(names).toHaveLength(2);
expect(names).toEqual(filterEntrypoints);
expect(entrypoints).toEqual([
expect.objectContaining({
name: 'injected',
skipped: true,
}),
expect.objectContaining({
name: 'options',
skipped: true,
}),
expect.objectContaining({
name: 'popup',
skipped: false,
}),
expect.objectContaining({
name: 'ui',
skipped: false,
}),
]);
});
});
});
@@ -152,12 +152,16 @@ describe('groupEntrypoints', () => {
options: {
type: 'module',
},
skipped: false,
});
const popup = fakePopupEntrypoint({
skipped: false,
});
const popup = fakePopupEntrypoint();
const sandbox = fakeGenericEntrypoint({
inputPath: '/entrypoints/sandbox.html',
name: 'sandbox',
type: 'sandbox',
skipped: false,
});
const actual = groupEntrypoints([background, popup, sandbox]);
@@ -165,6 +169,22 @@ describe('groupEntrypoints', () => {
expect(actual).toEqual([[background, popup], [sandbox]]);
});
it('should exclude skipped entrypoints from the groups to build', () => {
const background = fakeBackgroundEntrypoint({
options: {
type: 'module',
},
skipped: false,
});
const popup = fakePopupEntrypoint({
skipped: true,
});
const actual = groupEntrypoints([background, popup]);
expect(actual).toEqual([[background]]);
});
it.todo(
'should group ESM compatible sandbox scripts with sandbox pages',
() => {
@@ -65,14 +65,7 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
);
if (matchingGlob) {
const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
results.push({
name,
inputPath,
type,
skipped:
wxt.config.filterEntrypoints != null &&
!wxt.config.filterEntrypoints.has(name),
});
results.push({ name, inputPath, type });
}
return results;
}, []);
@@ -84,7 +77,7 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
// Import entrypoints to get their config
let hasBackground = false;
const env = createExtensionEnvironment();
const entrypoints: Entrypoint[] = await env.run(() =>
const entrypointsWithoutSkipped: Entrypoint[] = await env.run(() =>
Promise.all(
entrypointInfos.map(async (info): Promise<Entrypoint> => {
const { type } = info;
@@ -130,61 +123,44 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
);
if (wxt.config.command === 'serve' && !hasBackground) {
entrypoints.push(
entrypointsWithoutSkipped.push(
await getBackgroundEntrypoint({
inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
name: 'background',
type: 'background',
skipped: false,
}),
);
}
// Mark entrypoints as skipped or not
const entrypoints = entrypointsWithoutSkipped.map((entry) => ({
...entry,
skipped: isEntrypointSkipped(entry),
}));
wxt.logger.debug('All entrypoints:', entrypoints);
const skippedEntrypointNames = entrypointInfos
const skippedEntrypointNames = entrypoints
.filter((item) => item.skipped)
.map((item) => item.name);
if (skippedEntrypointNames.length) {
wxt.logger.warn(
`Filter excluded the following entrypoints:\n${skippedEntrypointNames
.map((item) => `${pc.dim('-')} ${pc.cyan(item)}`)
.join('\n')}`,
[
'The following entrypoints have been skipped:',
...skippedEntrypointNames.map(
(item) => `${pc.dim('-')} ${pc.cyan(item)}`,
),
].join('\n'),
);
}
const targetEntrypoints = entrypoints.filter((entry) => {
const { include, exclude } = entry.options;
if (include?.length && exclude?.length) {
wxt.logger.warn(
`The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint ignored.`,
);
return false;
}
if (exclude?.length && !include?.length) {
return !exclude.includes(wxt.config.browser);
}
if (include?.length && !exclude?.length) {
return include.includes(wxt.config.browser);
}
if (skippedEntrypointNames.includes(entry.name)) {
return false;
}
await wxt.hooks.callHook('entrypoints:resolved', wxt, entrypoints);
return true;
});
wxt.logger.debug(`${wxt.config.browser} entrypoints:`, targetEntrypoints);
await wxt.hooks.callHook('entrypoints:resolved', wxt, targetEntrypoints);
return targetEntrypoints;
return entrypoints;
}
interface EntrypointInfo {
name: string;
inputPath: string;
type: Entrypoint['type'];
/**
* @default false
*/
skipped: boolean;
}
function preventDuplicateEntrypointNames(files: EntrypointInfo[]) {
@@ -252,7 +228,6 @@ async function getPopupEntrypoint(
options: resolvePerBrowserOptions(options, wxt.config.browser),
inputPath: info.inputPath,
outputDir: wxt.config.outDir,
skipped: info.skipped,
};
}
@@ -275,7 +250,6 @@ async function getOptionsEntrypoint(
options: resolvePerBrowserOptions(options, wxt.config.browser),
inputPath: info.inputPath,
outputDir: wxt.config.outDir,
skipped: info.skipped,
};
}
@@ -293,14 +267,12 @@ async function getUnlistedPageEntrypoint(
inputPath: info.inputPath,
outputDir: wxt.config.outDir,
options,
skipped: info.skipped,
};
}
async function getUnlistedScriptEntrypoint({
inputPath,
name,
skipped,
}: EntrypointInfo): Promise<GenericEntrypoint> {
const defaultExport =
await wxt.builder.importEntrypoint<UnlistedScriptDefinition>(inputPath);
@@ -316,14 +288,12 @@ async function getUnlistedScriptEntrypoint({
inputPath,
outputDir: wxt.config.outDir,
options: resolvePerBrowserOptions(options, wxt.config.browser),
skipped,
};
}
async function getBackgroundEntrypoint({
inputPath,
name,
skipped,
}: EntrypointInfo): Promise<BackgroundEntrypoint> {
let options: Omit<BackgroundDefinition, 'main'> = {};
if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
@@ -348,14 +318,12 @@ async function getBackgroundEntrypoint({
inputPath,
outputDir: wxt.config.outDir,
options: resolvePerBrowserOptions(options, wxt.config.browser),
skipped,
};
}
async function getContentScriptEntrypoint({
inputPath,
name,
skipped,
}: EntrypointInfo): Promise<ContentScriptEntrypoint> {
const defaultExport =
await wxt.builder.importEntrypoint<ContentScriptDefinition>(inputPath);
@@ -377,7 +345,6 @@ async function getContentScriptEntrypoint({
inputPath,
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
options: resolvePerBrowserOptions(options, wxt.config.browser),
skipped,
};
}
@@ -409,7 +376,6 @@ async function getSidepanelEntrypoint(
options: resolvePerBrowserOptions(options, wxt.config.browser),
inputPath: info.inputPath,
outputDir: wxt.config.outDir,
skipped: info.skipped,
};
}
@@ -457,6 +423,29 @@ async function getHtmlEntrypointOptions<T extends BaseEntrypointOptions>(
return options;
}
function isEntrypointSkipped(entry: Omit<Entrypoint, 'skipped'>): boolean {
if (wxt.config.filterEntrypoints != null) {
return !wxt.config.filterEntrypoints.has(entry.name);
}
const { include, exclude } = entry.options;
if (include?.length && exclude?.length) {
wxt.logger.warn(
`The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint skipped.`,
);
return true;
}
if (exclude?.length && !include?.length) {
return exclude.includes(wxt.config.browser);
}
if (include?.length && !exclude?.length) {
return !include.includes(wxt.config.browser);
}
return false;
}
const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type']> = {
'sandbox.html': 'sandbox',
'sandbox/index.html': 'sandbox',
@@ -1,8 +1,8 @@
import { Entrypoint, EntrypointGroup } from '../../../types';
/**
* Entrypoints can be build in groups. HTML pages can all be built together in a single step, while
* content scripts must be build individually.
* Entrypoints are built in groups. HTML pages can all be built together in a single step,
* content scripts must be build individually, etc.
*
* This function returns the entrypoints put into these types of groups.
*/
@@ -11,6 +11,8 @@ export function groupEntrypoints(entrypoints: Entrypoint[]): EntrypointGroup[] {
const groups: EntrypointGroup[] = [];
for (const entry of entrypoints) {
if (entry.skipped) continue;
let group = ENTRY_TYPE_TO_GROUP_MAP[entry.type];
if (entry.type === 'background' && entry.options.type === 'module') {
group = 'esm';
+8
View File
@@ -36,6 +36,14 @@ export async function registerWxt(
return config.logger;
},
async reloadConfig() {
// Prevent changing the server port when resolving config multiple times
// get-port-please doesn't always return the same port if it was recently closed.
if (wxt.config.dev.server?.port) {
inlineConfig.dev ??= {};
inlineConfig.dev.server ??= {};
inlineConfig.dev.server.port = wxt.config.dev.server.port;
}
wxt.config = await resolveConfig(inlineConfig, command);
await wxt.hooks.callHook('config:resolved', wxt);
},
+10 -2
View File
@@ -6,7 +6,7 @@ import { getPackageJson } from './utils/package';
import { minimatch } from 'minimatch';
import { formatDuration } from './utils/time';
import { printFileList } from './utils/log/printFileList';
import { internalBuild } from './utils/building';
import { findEntrypoints, internalBuild } from './utils/building';
import { registerWxt, wxt } from './wxt';
import JSZip from 'jszip';
import glob from 'fast-glob';
@@ -55,6 +55,14 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
await wxt.hooks.callHook('zip:extension:done', wxt, outZipPath);
if (wxt.config.zip.zipSources) {
const entrypoints = await findEntrypoints();
const skippedEntrypoints = entrypoints.filter((entry) => entry.skipped);
const excludeSources = [
...wxt.config.zip.excludeSources,
...skippedEntrypoints.map((entry) =>
path.relative(wxt.config.zip.sourcesRoot, entry.inputPath),
),
].map((paths) => paths.replaceAll('\\', '/'));
await wxt.hooks.callHook('zip:sources:start', wxt);
const { overrides, files: downloadedPackages } =
await downloadPrivatePackages();
@@ -65,7 +73,7 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
);
await zipDir(wxt.config.zip.sourcesRoot, sourcesZipPath, {
include: wxt.config.zip.includeSources,
exclude: wxt.config.zip.excludeSources,
exclude: excludeSources,
transform(absolutePath, zipPath, content) {
if (zipPath.endsWith('package.json')) {
return addOverridesToPackageJson(absolutePath, content, overrides);
+9 -1
View File
@@ -45,6 +45,7 @@ export interface InlineConfig {
* A list of entrypoint names (`"popup"`, `"options"`, etc.) to build. Will speed up the build if
* your extension has lots of entrypoints, and you don't need to build all of them to develop a
* feature.
* If specified, this completely overrides the `include`/`exclude` option provided per-entrypoint.
*/
filterEntrypoints?: string[];
/**
@@ -728,7 +729,14 @@ export interface BaseEntrypoint {
* subdirectory of it.
*/
outputDir: string;
skipped: boolean;
/**
* When true, the entrypoint will not be built by WXT. Normally this is set
* based on the `filterEntrypoints` config or the entrypoint's
* `include`/`exclude` options defined inside the file.
*
* See https://wxt.dev/guide/essentials/target-different-browsers.html#filtering-entrypoints
*/
skipped?: boolean;
}
export interface GenericEntrypoint extends BaseEntrypoint {