Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af9e842fe0 | |||
| 230962686f | |||
| 413cd9354f | |||
| 64a42de31f | |||
| f7f03e2669 | |||
| 1dc7c34bfd | |||
| 0fd5c3846c | |||
| e42e92f9df | |||
| e45c91f941 | |||
| 8f71d80f94 | |||
| 78e896e5fb |
@@ -4,11 +4,8 @@ import useListExtensionDetails, {
|
||||
ChromeExtension,
|
||||
} from '../composables/useListExtensionDetails';
|
||||
|
||||
// Add extension IDs here. Order doesn't matter, will be sorted by weekly active users
|
||||
// During the transition from chrome.google.com/webstore to
|
||||
// chromewebstore.google.com, queue.wxt.dev might return null for your
|
||||
// extension. If it does, use "<slug>/<id>" instead of just the ID. The slug
|
||||
// can be retrieved from the URL of the item on chromewebstore.google.com
|
||||
// Add extension IDs here. Order doesn't matter, will be sorted by a combination of weekly active users and rating.
|
||||
// Change the commit message or PR title to: "docs: Added "[extension name]" to the homepage"
|
||||
const chromeExtensionIds = [
|
||||
'ocfdgncpifmegplaglcnglhioflaimkd', // GitHub: Better Line Counts
|
||||
'mgmdkjcljneegjfajchedjpdhbadklcf', // Anime Skip Player
|
||||
@@ -16,7 +13,7 @@ const chromeExtensionIds = [
|
||||
'elfaihghhjjoknimpccccmkioofjjfkf', // StayFree - Website Blocker & Web Analytics
|
||||
'okifoaikfmpfcamplcfjkpdnhfodpkil', // Doozy: Ai Made Easy
|
||||
'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet
|
||||
'youtube中文配音/oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
|
||||
'oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
|
||||
'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD
|
||||
'aiakblgmlabokilgljkglggnpflljdgp', // Markdown Sticky Notes
|
||||
'nomnkbngkijpffepcgbbofhcnafpkiep', // DocVersionRedirector
|
||||
@@ -60,6 +57,7 @@ const chromeExtensionIds = [
|
||||
'lhmgechokhmdekdpgkkemoeecelcaonm', // YouTube Hider - Remove Comments By Keywords, Usernames & Tools
|
||||
'imgheieooppmahcgniieddodaliodeeg', // QA Compass - Record standardized bug reports easily
|
||||
'npgghjedpchajflknnbngajkjkdhncdo', // aesthetic Notion, styled
|
||||
'hmdcmlfkchdmnmnmheododdhjedfccka', // Eye Dropper
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
@@ -576,6 +576,39 @@ For MV3, `injectScript` is synchronous and the injected script will be evaluated
|
||||
However for MV2, `injectScript` has to `fetch` the script's text content and create an inline `<script>` block. This means for MV2, your script is injected asynchronously and it will not be evaluated at the same time as your content script's `run_at`.
|
||||
:::
|
||||
|
||||
## Mounting UI to dynamic element
|
||||
|
||||
In many cases, you may need to mount a UI to a DOM element that does not exist at the time the web page is initially loaded. To handle this, use the `autoMount` API to automatically mount the UI when the target element appears dynamically and unmount it when the element disappears. In WXT, the `anchor` option is used to target the element, enabling automatic mounting and unmounting based on its appearance and removal.
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
// It observes the anchor
|
||||
anchor: '#your-target-dynamic-element',
|
||||
onMount: (container) => {
|
||||
// Append children to the container
|
||||
const app = document.createElement('p');
|
||||
app.textContent = '...';
|
||||
container.append(app);
|
||||
},
|
||||
});
|
||||
|
||||
// Call autoMount to observe anchor element for add/remove.
|
||||
ui.autoMount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::tip
|
||||
When the `ui.remove` is called, `autoMount` also stops.
|
||||
:::
|
||||
|
||||
See the [API Reference](/api/reference/wxt/client/interfaces/ContentScriptUi.html#automount) for the complete list of options.
|
||||
|
||||
## Dealing with SPAs
|
||||
|
||||
It is difficult to write content scripts for SPAs (single page applications) and websites using HTML5 history mode for navigation because content scripts are only ran on full page reloads. SPAs and websites that take advantage of HTML5 history mode **_do not perform a full reload when changing paths_**, and thus your content script isn't going to be ran when you expect it to be.
|
||||
|
||||
@@ -20,7 +20,7 @@ In your background script, set `type: "module"`:
|
||||
|
||||
```ts
|
||||
export default defineBackground({
|
||||
type: 'module', // !code ++
|
||||
type: 'module', // [!code ++]
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
|
||||
@@ -109,27 +109,6 @@ You have two options:
|
||||
```
|
||||
2. Rename your `modules` directory to something else.
|
||||
|
||||
## v0.18.0 → v0.18.5
|
||||
|
||||
> When this version was released, it was not considered a breaking change... but it should have been.
|
||||
|
||||
### New `modules/` Directory
|
||||
|
||||
WXT now recognizes the `modules/` directory as a folder containing [WXT modules](/guide/essentials/wxt-modules).
|
||||
|
||||
If you already have `<srcDir>/modules` or `<srcDir>/Modules` directory, `wxt prepare` and other commands will fail.
|
||||
|
||||
You have two options:
|
||||
|
||||
1. [Recommended] Keep your files where they are and tell WXT to look in a different folder:
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
modulesDir: 'wxt-modules', // defaults to "modules"
|
||||
});
|
||||
```
|
||||
2. Rename your `modules` directory to something else.
|
||||
|
||||
## v0.17.0 → v0.18.0
|
||||
|
||||
### Automatic MV3 `host_permissions` to MV2 `permissions`
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'uno.css';
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['https://*.duckduckgo.com/*'],
|
||||
cssInjectionMode: 'manifest',
|
||||
|
||||
async main(ctx) {
|
||||
const dynamicUI = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
append: 'after',
|
||||
anchor: 'form[role=search]',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
container.id = 'automount-anchor';
|
||||
app.classList.add('m-4', 'text-center', 'text-red-500');
|
||||
app.textContent = i18n.t('prompt_for_name');
|
||||
container.append(app);
|
||||
return { container, app };
|
||||
},
|
||||
onRemove() {
|
||||
console.log('dynamicUI removed');
|
||||
},
|
||||
});
|
||||
|
||||
const autoMountUi = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
append: 'after',
|
||||
anchor: '#automount-anchor',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
app.id = 'automount-ui';
|
||||
app.classList.add('m-0', 'text-center', 'text-blue-500');
|
||||
app.textContent = `Hello, I'm automount UI.`;
|
||||
container.append(app);
|
||||
},
|
||||
onRemove() {
|
||||
console.log('autoMountUi removed');
|
||||
},
|
||||
});
|
||||
|
||||
autoMountUi.autoMount({
|
||||
onStop: () => {
|
||||
console.log('Auto mount stopped.');
|
||||
},
|
||||
});
|
||||
|
||||
const stopAutoMountButton = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
append: 'last',
|
||||
anchor: 'form[role=search]',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('button');
|
||||
container.classList.add('flex', 'flex-justify-center');
|
||||
app.classList.add('mt-4', 'p-2');
|
||||
app.textContent = 'Stop auto-mount';
|
||||
app.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
autoMountUi.remove();
|
||||
};
|
||||
container.append(app);
|
||||
return { container, app };
|
||||
},
|
||||
});
|
||||
|
||||
stopAutoMountButton.mount();
|
||||
|
||||
setInterval(() => {
|
||||
if (dynamicUI.mounted) {
|
||||
dynamicUI.remove();
|
||||
} else {
|
||||
dynamicUI.mount();
|
||||
}
|
||||
}, 2000);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
@@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## v0.19.21
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.20...wxt-v0.19.21)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Exclude skipped entrypoints from manifest ([#1265](https://github.com/wxt-dev/wxt/pull/1265))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v0.19.20
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.19...wxt-v0.19.20)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- `autoMount` content script UIs ([#1210](https://github.com/wxt-dev/wxt/pull/1210))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Only show reopen browser shortcut when it can be done ([#1263](https://github.com/wxt-dev/wxt/pull/1263))
|
||||
- Import entrypoint improvements ([#1207](https://github.com/wxt-dev/wxt/pull/1207))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
- 1natsu ([@1natsu172](http://github.com/1natsu172))
|
||||
|
||||
## v0.19.19
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.18...wxt-v0.19.19)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.19.19",
|
||||
"version": "0.19.21",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -81,6 +81,7 @@
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@1natsu/wait-element": "^4.1.2",
|
||||
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/webextension-polyfill": "^0.12.1",
|
||||
|
||||
@@ -1,12 +1,43 @@
|
||||
/** @vitest-environment happy-dom */
|
||||
import { describe, it, beforeEach, vi, expect } from 'vitest';
|
||||
import { createIntegratedUi, createIframeUi, createShadowRootUi } from '..';
|
||||
import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest';
|
||||
import {
|
||||
createIntegratedUi,
|
||||
createIframeUi,
|
||||
createShadowRootUi,
|
||||
ContentScriptUi,
|
||||
} from '..';
|
||||
import { ContentScriptContext } from '../../content-script-context';
|
||||
|
||||
/**
|
||||
* Util for floating promise.
|
||||
*/
|
||||
async function runMicrotasks() {
|
||||
return await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
function appendTestApp(container: HTMLElement) {
|
||||
container.innerHTML = '<app>Hello world</app>';
|
||||
}
|
||||
|
||||
function appendTestElement({
|
||||
tagName = 'div',
|
||||
id,
|
||||
}: {
|
||||
tagName?: string;
|
||||
id: string;
|
||||
}) {
|
||||
const parent = document.querySelector('#parent');
|
||||
if (!parent) {
|
||||
throw Error(
|
||||
'Parent element not found. Please check the testing environment DOM',
|
||||
);
|
||||
}
|
||||
const element = document.createElement(tagName);
|
||||
element.id = id;
|
||||
parent.append(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
const fetch = vi.fn();
|
||||
|
||||
describe('Content Script UIs', () => {
|
||||
@@ -480,4 +511,223 @@ describe('Content Script UIs', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Need call runMicrotasks after floating-promise and append/remove dom
|
||||
*/
|
||||
describe('auto mount', () => {
|
||||
const DYNAMIC_CHILD_ID = 'dynamic-child';
|
||||
let ui: ContentScriptUi<any>;
|
||||
beforeEach(async () => {
|
||||
ui?.remove();
|
||||
await runMicrotasks();
|
||||
});
|
||||
afterEach(async () => {
|
||||
ui?.remove();
|
||||
await runMicrotasks();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{
|
||||
name: 'integrated',
|
||||
createUiFunction: createIntegratedUi,
|
||||
uiSelector: 'div[data-wxt-integrated]',
|
||||
},
|
||||
{
|
||||
name: 'iframe',
|
||||
createUiFunction: createIframeUi,
|
||||
uiSelector: 'div[data-wxt-iframe]',
|
||||
},
|
||||
{
|
||||
name: 'shadow-root',
|
||||
createUiFunction: createShadowRootUi,
|
||||
uiSelector: 'test-component[data-wxt-shadow-root]',
|
||||
},
|
||||
] as const)(
|
||||
'built-in UI type: $name',
|
||||
({ name, createUiFunction, uiSelector }) => {
|
||||
it('should mount when an anchor is dynamically added and unmount when an anchor is removed', async () => {
|
||||
const onMount = vi.fn(appendTestApp);
|
||||
const onRemove = vi.fn();
|
||||
ui = await createUiFunction(ctx, {
|
||||
position: 'inline',
|
||||
onMount,
|
||||
onRemove,
|
||||
anchor: `#parent > #${DYNAMIC_CHILD_ID}`,
|
||||
page: name === 'iframe' ? '/page.html' : undefined,
|
||||
name: 'test-component',
|
||||
});
|
||||
let dynamicEl;
|
||||
ui.autoMount();
|
||||
await runMicrotasks();
|
||||
|
||||
for (let index = 0; index < 3; index++) {
|
||||
await expect
|
||||
.poll(() => document.querySelector(uiSelector))
|
||||
.toBeNull();
|
||||
|
||||
dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID });
|
||||
await runMicrotasks();
|
||||
await expect
|
||||
.poll(() => document.querySelector(uiSelector))
|
||||
.not.toBeNull();
|
||||
|
||||
dynamicEl.remove();
|
||||
await runMicrotasks();
|
||||
}
|
||||
|
||||
expect(onMount).toHaveBeenCalledTimes(3);
|
||||
expect(onRemove).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
describe('options', () => {
|
||||
it('should auto-mount only once mount and remove when the `once` option is true', async () => {
|
||||
const onMount = vi.fn(appendTestApp);
|
||||
const onRemove = vi.fn();
|
||||
ui = await createUiFunction(ctx, {
|
||||
position: 'inline',
|
||||
onMount,
|
||||
onRemove,
|
||||
anchor: `#parent > #${DYNAMIC_CHILD_ID}`,
|
||||
page: name === 'iframe' ? '/page.html' : undefined,
|
||||
name: 'test-component',
|
||||
});
|
||||
let dynamicEl;
|
||||
ui.autoMount({ once: true });
|
||||
await runMicrotasks();
|
||||
await expect
|
||||
.poll(() => document.querySelector(uiSelector))
|
||||
.toBeNull();
|
||||
|
||||
dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID });
|
||||
await runMicrotasks();
|
||||
await expect
|
||||
.poll(() => document.querySelector(uiSelector))
|
||||
.not.toBeNull();
|
||||
|
||||
dynamicEl.remove();
|
||||
await runMicrotasks();
|
||||
expect(onMount).toHaveBeenCalledTimes(1);
|
||||
expect(onRemove).toHaveBeenCalledTimes(1);
|
||||
|
||||
// re-append after once cycle
|
||||
dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID });
|
||||
await runMicrotasks();
|
||||
|
||||
// expect stop automount
|
||||
await expect
|
||||
.poll(() => document.querySelector(uiSelector))
|
||||
.toBeNull();
|
||||
expect(onMount).toHaveBeenCalledTimes(1);
|
||||
expect(onRemove).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid anchors', () => {
|
||||
it('should throw when anchor is set as type Element', async () => {
|
||||
ui = await createUiFunction(ctx, {
|
||||
position: 'inline',
|
||||
onMount: appendTestApp,
|
||||
anchor: document.documentElement,
|
||||
page: name === 'iframe' ? '/page.html' : undefined,
|
||||
name: 'test-component',
|
||||
});
|
||||
expect(() => ui.autoMount()).toThrowError(
|
||||
'autoMount and Element anchor option cannot be combined. Avoid passing `Element` directly or `() => Element` to the anchor.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when anchor is set as type `() => Element`', async () => {
|
||||
ui = await createUiFunction(ctx, {
|
||||
position: 'inline',
|
||||
onMount: appendTestApp,
|
||||
anchor: () => document.documentElement,
|
||||
page: name === 'iframe' ? '/page.html' : undefined,
|
||||
name: 'test-component',
|
||||
});
|
||||
expect(() => ui.autoMount()).toThrowError(
|
||||
'autoMount and Element anchor option cannot be combined. Avoid passing `Element` directly or `() => Element` to the anchor.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('StopAutoMount', () => {
|
||||
it('should stop auto-mounting and remove ui when `ui.remove` is called', async () => {
|
||||
const onMount = vi.fn(appendTestApp);
|
||||
const onRemove = vi.fn();
|
||||
ui = await createUiFunction(ctx, {
|
||||
position: 'inline',
|
||||
onMount,
|
||||
onRemove,
|
||||
anchor: `#parent > #${DYNAMIC_CHILD_ID}`,
|
||||
page: name === 'iframe' ? '/page.html' : undefined,
|
||||
name: 'test-component',
|
||||
});
|
||||
let dynamicEl;
|
||||
ui.autoMount();
|
||||
await runMicrotasks();
|
||||
|
||||
dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID });
|
||||
await runMicrotasks();
|
||||
await expect
|
||||
.poll(() => document.querySelector(uiSelector))
|
||||
.not.toBeNull();
|
||||
|
||||
dynamicEl.remove();
|
||||
await runMicrotasks();
|
||||
expect(onMount).toHaveBeenCalledTimes(1);
|
||||
expect(onRemove).toHaveBeenCalledTimes(1);
|
||||
|
||||
ui.remove();
|
||||
|
||||
dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID });
|
||||
dynamicEl.remove();
|
||||
await runMicrotasks();
|
||||
expect(onMount).toHaveBeenCalledTimes(1);
|
||||
expect(onRemove).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should call internal StopAutoMount when `ui.remove` is called', async () => {
|
||||
const onMount = vi.fn(appendTestApp);
|
||||
const onRemove = vi.fn();
|
||||
const onStop = vi.fn();
|
||||
ui = await createUiFunction(ctx, {
|
||||
position: 'inline',
|
||||
onMount,
|
||||
onRemove,
|
||||
anchor: `#parent > #${DYNAMIC_CHILD_ID}`,
|
||||
page: name === 'iframe' ? '/page.html' : undefined,
|
||||
name: 'test-component',
|
||||
});
|
||||
ui.autoMount({ onStop });
|
||||
ui.remove();
|
||||
expect(onStop).toHaveBeenCalledTimes(1);
|
||||
expect(onRemove).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should allow calling automount again after internal StopAutoMount is called', async () => {
|
||||
const onMount = vi.fn(appendTestApp);
|
||||
ui = await createUiFunction(ctx, {
|
||||
position: 'inline',
|
||||
onMount,
|
||||
anchor: `#parent > #${DYNAMIC_CHILD_ID}`,
|
||||
page: name === 'iframe' ? '/page.html' : undefined,
|
||||
name: 'test-component',
|
||||
});
|
||||
const onStop = vi.fn();
|
||||
ui.autoMount({ onStop });
|
||||
ui.autoMount({ onStop });
|
||||
|
||||
ui.remove();
|
||||
expect(onStop).toBeCalledTimes(1);
|
||||
|
||||
ui.autoMount({ onStop });
|
||||
|
||||
ui.remove();
|
||||
expect(onStop).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { browser } from 'wxt/browser';
|
||||
import { waitElement } from '@1natsu/wait-element';
|
||||
import {
|
||||
isExist as mountDetector,
|
||||
isNotExist as removeDetector,
|
||||
} from '@1natsu/wait-element/detectors';
|
||||
import { ContentScriptContext } from '..';
|
||||
import {
|
||||
AutoMount,
|
||||
AutoMountOptions,
|
||||
BaseMountFunctions,
|
||||
ContentScriptAnchoredOptions,
|
||||
ContentScriptPositioningOptions,
|
||||
ContentScriptUiOptions,
|
||||
IframeContentScriptUi,
|
||||
IframeContentScriptUiOptions,
|
||||
IntegratedContentScriptUi,
|
||||
IntegratedContentScriptUiOptions,
|
||||
MountFunctions,
|
||||
ShadowRootContentScriptUi,
|
||||
ShadowRootContentScriptUiOptions,
|
||||
} from './types';
|
||||
@@ -39,6 +49,14 @@ export function createIntegratedUi<TMounted>(
|
||||
mounted = undefined;
|
||||
};
|
||||
|
||||
const mountFunctions = createMountFunctions(
|
||||
{
|
||||
mount,
|
||||
remove,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
ctx.onInvalidated(remove);
|
||||
|
||||
return {
|
||||
@@ -46,8 +64,7 @@ export function createIntegratedUi<TMounted>(
|
||||
return mounted;
|
||||
},
|
||||
wrapper,
|
||||
mount,
|
||||
remove,
|
||||
...mountFunctions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,6 +96,14 @@ export function createIframeUi<TMounted>(
|
||||
mounted = undefined;
|
||||
};
|
||||
|
||||
const mountFunctions = createMountFunctions(
|
||||
{
|
||||
mount,
|
||||
remove,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
ctx.onInvalidated(remove);
|
||||
|
||||
return {
|
||||
@@ -87,8 +112,7 @@ export function createIframeUi<TMounted>(
|
||||
},
|
||||
iframe,
|
||||
wrapper,
|
||||
mount,
|
||||
remove,
|
||||
...mountFunctions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -137,7 +161,7 @@ export async function createShadowRootUi<TMounted>(
|
||||
const remove = () => {
|
||||
// Cleanup mounted state
|
||||
options.onRemove?.(mounted);
|
||||
// Detatch shadow root from DOM
|
||||
// Detach shadow root from DOM
|
||||
shadowHost.remove();
|
||||
// Remove children from uiContainer
|
||||
while (uiContainer.lastChild)
|
||||
@@ -146,14 +170,21 @@ export async function createShadowRootUi<TMounted>(
|
||||
mounted = undefined;
|
||||
};
|
||||
|
||||
const mountFunctions = createMountFunctions(
|
||||
{
|
||||
mount,
|
||||
remove,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
ctx.onInvalidated(remove);
|
||||
|
||||
return {
|
||||
shadow,
|
||||
shadowHost,
|
||||
uiContainer,
|
||||
mount,
|
||||
remove,
|
||||
...mountFunctions,
|
||||
get mounted() {
|
||||
return mounted;
|
||||
},
|
||||
@@ -256,6 +287,107 @@ function mountUi(
|
||||
}
|
||||
}
|
||||
|
||||
function createMountFunctions<TMounted>(
|
||||
baseFunctions: BaseMountFunctions,
|
||||
options: ContentScriptUiOptions<TMounted>,
|
||||
): MountFunctions {
|
||||
let autoMountInstance: AutoMount | undefined = undefined;
|
||||
|
||||
const stopAutoMount = () => {
|
||||
autoMountInstance?.stopAutoMount();
|
||||
autoMountInstance = undefined;
|
||||
};
|
||||
|
||||
const mount = () => {
|
||||
baseFunctions.mount();
|
||||
};
|
||||
|
||||
const unmount = baseFunctions.remove;
|
||||
|
||||
const remove = () => {
|
||||
stopAutoMount();
|
||||
baseFunctions.remove();
|
||||
};
|
||||
|
||||
const autoMount = (autoMountOptions?: AutoMountOptions) => {
|
||||
if (autoMountInstance) {
|
||||
logger.warn('autoMount is already set.');
|
||||
}
|
||||
autoMountInstance = autoMountUi(
|
||||
{ mount, unmount, stopAutoMount },
|
||||
{
|
||||
...options,
|
||||
...autoMountOptions,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
mount,
|
||||
remove,
|
||||
autoMount,
|
||||
};
|
||||
}
|
||||
|
||||
function autoMountUi(
|
||||
uiCallbacks: {
|
||||
mount: () => void;
|
||||
unmount: () => void;
|
||||
stopAutoMount: () => void;
|
||||
},
|
||||
options: ContentScriptAnchoredOptions & AutoMountOptions,
|
||||
): AutoMount {
|
||||
const abortController = new AbortController();
|
||||
const EXPLICIT_STOP_REASON = 'explicit_stop_auto_mount';
|
||||
const _stopAutoMount = () => {
|
||||
abortController.abort(EXPLICIT_STOP_REASON);
|
||||
options.onStop?.();
|
||||
};
|
||||
|
||||
let resolvedAnchor =
|
||||
typeof options.anchor === 'function' ? options.anchor() : options.anchor;
|
||||
if (resolvedAnchor instanceof Element) {
|
||||
throw Error(
|
||||
'autoMount and Element anchor option cannot be combined. Avoid passing `Element` directly or `() => Element` to the anchor.',
|
||||
);
|
||||
}
|
||||
|
||||
async function observeElement(selector: string | null | undefined) {
|
||||
let isAnchorExist = !!getAnchor(options);
|
||||
|
||||
while (!abortController.signal.aborted) {
|
||||
try {
|
||||
const changedAnchor = await waitElement(selector ?? 'body', {
|
||||
customMatcher: () => getAnchor(options) ?? null,
|
||||
detector: isAnchorExist ? removeDetector : mountDetector,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
isAnchorExist = !!changedAnchor;
|
||||
if (isAnchorExist) {
|
||||
uiCallbacks.mount();
|
||||
} else {
|
||||
uiCallbacks.unmount();
|
||||
if (options.once) {
|
||||
uiCallbacks.stopAutoMount();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
abortController.signal.aborted &&
|
||||
abortController.signal.reason === EXPLICIT_STOP_REASON
|
||||
) {
|
||||
break;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
observeElement(resolvedAnchor);
|
||||
|
||||
return { stopAutoMount: _stopAutoMount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the CSS for the current entrypoint.
|
||||
*/
|
||||
|
||||
@@ -36,18 +36,7 @@ export interface ShadowRootContentScriptUi<TMounted>
|
||||
shadow: ShadowRoot;
|
||||
}
|
||||
|
||||
export interface ContentScriptUi<TMounted> {
|
||||
/**
|
||||
* Function that mounts or remounts the UI on the page.
|
||||
*/
|
||||
mount: () => void;
|
||||
/**
|
||||
* Function that removes the UI from the webpage.
|
||||
*/
|
||||
remove: () => void;
|
||||
/**>
|
||||
* Custom data returned from the `options.mount` function.
|
||||
*/
|
||||
export interface ContentScriptUi<TMounted> extends MountFunctions {
|
||||
mounted: TMounted | undefined;
|
||||
}
|
||||
|
||||
@@ -216,3 +205,40 @@ export interface ContentScriptAnchoredOptions {
|
||||
*/
|
||||
append?: ContentScriptAppendMode | ((anchor: Element, ui: Element) => void);
|
||||
}
|
||||
|
||||
export interface BaseMountFunctions {
|
||||
/**
|
||||
* Function that mounts or remounts the UI on the page.
|
||||
*/
|
||||
mount: () => void;
|
||||
|
||||
/**
|
||||
* Function that removes the UI from the webpage.
|
||||
*/
|
||||
remove: () => void;
|
||||
}
|
||||
|
||||
export interface MountFunctions extends BaseMountFunctions {
|
||||
/**
|
||||
* Call `ui.autoMount()` to automatically mount and remove the UI as the anchor is dynamically added/removed by the webpage.
|
||||
*/
|
||||
autoMount: (options?: AutoMountOptions) => void;
|
||||
}
|
||||
|
||||
export type AutoMountOptions = {
|
||||
/**
|
||||
* When true, only mount and unmount a UI once.
|
||||
*/
|
||||
once?: boolean;
|
||||
/**
|
||||
* The callback triggered when `StopAutoMount` is called.
|
||||
*/
|
||||
onStop?: () => void;
|
||||
};
|
||||
export type StopAutoMount = () => void;
|
||||
export interface AutoMount {
|
||||
/**
|
||||
* Stop watching the anchor element for changes, but keep the UI mounted.
|
||||
*/
|
||||
stopAutoMount: StopAutoMount;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import { importEntrypointFile } from '../../utils/building';
|
||||
import { ViteNodeServer } from 'vite-node/server';
|
||||
import { ViteNodeRunner } from 'vite-node/client';
|
||||
import { installSourcemapsSupport } from 'vite-node/source-map';
|
||||
import { createExtensionEnvironment } from '../../utils/environments';
|
||||
import { relative } from 'node:path';
|
||||
|
||||
export async function createViteBuilder(
|
||||
wxtConfig: ResolvedConfig,
|
||||
@@ -220,55 +222,104 @@ export async function createViteBuilder(
|
||||
};
|
||||
};
|
||||
|
||||
const createViteNodeImporter = async (paths: string[]) => {
|
||||
const baseConfig = await getBaseConfig({
|
||||
excludeAnalysisPlugin: true,
|
||||
});
|
||||
// Disable dep optimization, as recommended by vite-node's README
|
||||
baseConfig.optimizeDeps ??= {};
|
||||
baseConfig.optimizeDeps.noDiscovery = true;
|
||||
baseConfig.optimizeDeps.include = [];
|
||||
const envConfig: vite.InlineConfig = {
|
||||
plugins: paths.map((path) =>
|
||||
wxtPlugins.removeEntrypointMainFunction(wxtConfig, path),
|
||||
),
|
||||
};
|
||||
const config = vite.mergeConfig(baseConfig, envConfig);
|
||||
const server = await vite.createServer(config);
|
||||
await server.pluginContainer.buildStart({});
|
||||
const node = new ViteNodeServer(
|
||||
// @ts-ignore: Some weird type error...
|
||||
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);
|
||||
},
|
||||
});
|
||||
return { runner, server };
|
||||
};
|
||||
|
||||
const requireDefaultExport = (path: string, mod: any) => {
|
||||
const relativePath = relative(wxtConfig.root, path);
|
||||
if (mod?.default == null) {
|
||||
const defineFn = relativePath.includes('.content')
|
||||
? 'defineContentScript'
|
||||
: relativePath.includes('background')
|
||||
? 'defineBackground'
|
||||
: 'defineUnlistedScript';
|
||||
|
||||
throw Error(
|
||||
`${relativePath}: Default export not found, did you forget to call "export default ${defineFn}(...)"?`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'Vite',
|
||||
version: vite.version,
|
||||
async importEntrypoint(path) {
|
||||
const env = createExtensionEnvironment();
|
||||
switch (wxtConfig.entrypointLoader) {
|
||||
default:
|
||||
case 'jiti': {
|
||||
return await importEntrypointFile(path);
|
||||
return await env.run(() => importEntrypointFile(path));
|
||||
}
|
||||
case 'vite-node': {
|
||||
const baseConfig = await getBaseConfig({
|
||||
excludeAnalysisPlugin: true,
|
||||
});
|
||||
// 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.removeEntrypointMainFunction(wxtConfig, path)],
|
||||
};
|
||||
const config = vite.mergeConfig(baseConfig, envConfig);
|
||||
const server = await vite.createServer(config);
|
||||
await server.pluginContainer.buildStart({});
|
||||
const node = new ViteNodeServer(
|
||||
// @ts-ignore: Some weird type error...
|
||||
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);
|
||||
const { runner, server } = await createViteNodeImporter([path]);
|
||||
const res = await env.run(() => runner.executeFile(path));
|
||||
await server.close();
|
||||
requireDefaultExport(path, res);
|
||||
return res.default;
|
||||
}
|
||||
}
|
||||
},
|
||||
async importEntrypoints(paths) {
|
||||
const env = createExtensionEnvironment();
|
||||
switch (wxtConfig.entrypointLoader) {
|
||||
default:
|
||||
case 'jiti': {
|
||||
return await env.run(() =>
|
||||
Promise.all(paths.map(importEntrypointFile)),
|
||||
);
|
||||
}
|
||||
case 'vite-node': {
|
||||
const { runner, server } = await createViteNodeImporter(paths);
|
||||
const res = await env.run(() =>
|
||||
Promise.all(
|
||||
paths.map(async (path) => {
|
||||
const mod = await runner.executeFile(path);
|
||||
requireDefaultExport(path, mod);
|
||||
return mod.default;
|
||||
}),
|
||||
),
|
||||
);
|
||||
await server.close();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
},
|
||||
async build(group) {
|
||||
let entryConfig;
|
||||
if (Array.isArray(group)) entryConfig = getMultiPageConfig(group);
|
||||
|
||||
@@ -99,7 +99,7 @@ async function createServerInternal(): Promise<WxtDevServer> {
|
||||
|
||||
// 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
|
||||
// Add listener before opening the browser to guarantee it is present when
|
||||
// the extension sends back the initialization message.
|
||||
server.ws.on('wxt:background-initialized', () => {
|
||||
if (server.currentOutput == null) return;
|
||||
@@ -112,7 +112,10 @@ async function createServerInternal(): Promise<WxtDevServer> {
|
||||
const reloadOnChange = createFileReloader(server);
|
||||
server.watcher.on('all', reloadOnChange);
|
||||
keyboardShortcuts.start();
|
||||
keyboardShortcuts.printHelp();
|
||||
keyboardShortcuts.printHelp({
|
||||
canReopenBrowser:
|
||||
!wxt.config.runnerConfig.config.disabled && !!runner.canOpen?.(),
|
||||
});
|
||||
},
|
||||
|
||||
async stop() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import pc from 'picocolors';
|
||||
export interface KeyboardShortcutWatcher {
|
||||
start(): void;
|
||||
stop(): void;
|
||||
printHelp(): void;
|
||||
printHelp(flags: { canReopenBrowser: boolean }): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,8 +49,8 @@ export function createKeyboardShortcuts(
|
||||
isWatching = false;
|
||||
},
|
||||
|
||||
printHelp() {
|
||||
if (!wxt.config.runnerConfig.config.disabled) {
|
||||
printHelp(flags) {
|
||||
if (flags.canReopenBrowser) {
|
||||
wxt.logger.info(
|
||||
`${pc.dim('Press')} ${pc.bold('o + enter')} ${pc.dim('to reopen the browser')}`,
|
||||
);
|
||||
|
||||
@@ -11,6 +11,9 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
let runner: WebExtRunInstance | undefined;
|
||||
|
||||
return {
|
||||
canOpen() {
|
||||
return true;
|
||||
},
|
||||
async openBrowser() {
|
||||
const startTime = Date.now();
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ describe('Manifest Utils', () => {
|
||||
defaultTitle: 'Default Iitle',
|
||||
},
|
||||
outputDir: outDir,
|
||||
skipped: false,
|
||||
});
|
||||
|
||||
it('should include an action for mv3', async () => {
|
||||
@@ -209,6 +210,7 @@ describe('Manifest Utils', () => {
|
||||
chromeStyle: true,
|
||||
browserStyle: true,
|
||||
},
|
||||
skipped: false,
|
||||
});
|
||||
|
||||
it('should include a options_ui and chrome_style for chrome', async () => {
|
||||
@@ -265,6 +267,7 @@ describe('Manifest Utils', () => {
|
||||
persistent: true,
|
||||
type: 'module',
|
||||
},
|
||||
skipped: false,
|
||||
});
|
||||
|
||||
describe('MV3', () => {
|
||||
@@ -368,7 +371,7 @@ describe('Manifest Utils', () => {
|
||||
|
||||
describe('icons', () => {
|
||||
it('should auto-discover icons with the correct name', async () => {
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
const entrypoints = fakeArray(() => fakeEntrypoint({ skipped: false }));
|
||||
const buildOutput = fakeBuildOutput({
|
||||
publicAssets: [
|
||||
{ type: 'asset', fileName: 'icon-16.png' },
|
||||
@@ -396,7 +399,7 @@ describe('Manifest Utils', () => {
|
||||
});
|
||||
|
||||
it('should return undefined when no icons are found', async () => {
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
const entrypoints = fakeArray(() => fakeEntrypoint({ skipped: false }));
|
||||
const buildOutput = fakeBuildOutput({
|
||||
publicAssets: [
|
||||
{ type: 'asset', fileName: 'logo.png' },
|
||||
@@ -413,7 +416,7 @@ describe('Manifest Utils', () => {
|
||||
});
|
||||
|
||||
it('should allow icons to be overwritten from the wxt.config.ts file', async () => {
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
const entrypoints = fakeArray(() => fakeEntrypoint({ skipped: false }));
|
||||
const buildOutput = fakeBuildOutput({
|
||||
publicAssets: [
|
||||
{ type: 'asset', fileName: 'icon-16.png' },
|
||||
@@ -837,6 +840,7 @@ describe('Manifest Utils', () => {
|
||||
options: {
|
||||
registration: 'runtime',
|
||||
},
|
||||
skipped: false,
|
||||
});
|
||||
|
||||
const entrypoints = [cs];
|
||||
@@ -902,6 +906,7 @@ describe('Manifest Utils', () => {
|
||||
async (browser) => {
|
||||
const sidepanel = fakeSidepanelEntrypoint({
|
||||
outputDir: outDir,
|
||||
skipped: false,
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
|
||||
@@ -934,6 +939,7 @@ describe('Manifest Utils', () => {
|
||||
async (browser) => {
|
||||
const sidepanel = fakeSidepanelEntrypoint({
|
||||
outputDir: outDir,
|
||||
skipped: false,
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
|
||||
@@ -1617,6 +1623,29 @@ describe('Manifest Utils', () => {
|
||||
expect(actual.content_security_policy).toEqual(expectedCsp);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not add skipped entrypoints to manifest', async () => {
|
||||
const popup = fakePopupEntrypoint({ skipped: true });
|
||||
const content = fakeContentScriptEntrypoint({ skipped: true });
|
||||
const sidePanel = fakeSidepanelEntrypoint({ skipped: true });
|
||||
const buildOutput = fakeBuildOutput();
|
||||
|
||||
setFakeWxt({
|
||||
config: {
|
||||
command: 'build',
|
||||
manifestVersion: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest } = await generateManifest(
|
||||
[popup, content, sidePanel],
|
||||
buildOutput,
|
||||
);
|
||||
|
||||
expect(manifest.action).toBeUndefined();
|
||||
expect(manifest.sidebar_action).toBeUndefined();
|
||||
expect(manifest.content_scripts).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stripPathFromMatchPattern', () => {
|
||||
|
||||
@@ -36,11 +36,12 @@ describe('findEntrypoints', () => {
|
||||
outDir: resolve('.output'),
|
||||
command: 'build',
|
||||
});
|
||||
let importEntrypointMock: Mock;
|
||||
let importEntrypointsMock: Mock<typeof wxt.builder.importEntrypoints>;
|
||||
|
||||
beforeEach(() => {
|
||||
setFakeWxt({ config });
|
||||
importEntrypointMock = vi.mocked(wxt.builder.importEntrypoint);
|
||||
importEntrypointsMock = vi.mocked(wxt.builder.importEntrypoints);
|
||||
importEntrypointsMock.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it.each<[string, string, PopupEntrypoint]>([
|
||||
@@ -210,13 +211,13 @@ describe('findEntrypoints', () => {
|
||||
matches: ['<all_urls>'],
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
importEntrypointsMock.mockResolvedValue([options]);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
|
||||
expect(importEntrypointsMock).toBeCalledWith([expected.inputPath]);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -244,17 +245,17 @@ describe('findEntrypoints', () => {
|
||||
])(
|
||||
'should find and load background entrypoint config from %s',
|
||||
async (path, expected) => {
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
const options = {
|
||||
type: 'module',
|
||||
};
|
||||
} satisfies BackgroundEntrypointOptions;
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
importEntrypointsMock.mockResolvedValue([options]);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
|
||||
expect(importEntrypointsMock).toBeCalledWith([expected.inputPath]);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -339,11 +340,11 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
builder: wxt.builder,
|
||||
});
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
const options = {
|
||||
type: 'module',
|
||||
};
|
||||
} satisfies BackgroundEntrypointOptions;
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
importEntrypointsMock.mockResolvedValue([options]);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
@@ -357,11 +358,11 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
builder: wxt.builder,
|
||||
});
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
const options = {
|
||||
type: 'module',
|
||||
};
|
||||
} satisfies BackgroundEntrypointOptions;
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
importEntrypointsMock.mockResolvedValue([options]);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
@@ -410,15 +411,15 @@ describe('findEntrypoints', () => {
|
||||
outputDir: config.outDir,
|
||||
skipped: false,
|
||||
};
|
||||
const options: BaseEntrypointOptions = {};
|
||||
const options = {} satisfies BaseEntrypointOptions;
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
importEntrypointsMock.mockResolvedValue([options]);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
|
||||
expect(importEntrypointsMock).toBeCalledWith([expected.inputPath]);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -703,9 +704,9 @@ describe('findEntrypoints', () => {
|
||||
describe('include option', () => {
|
||||
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],
|
||||
});
|
||||
importEntrypointsMock.mockResolvedValue([
|
||||
{ include: ['not' + config.browser] },
|
||||
]);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
@@ -719,9 +720,9 @@ describe('findEntrypoints', () => {
|
||||
|
||||
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],
|
||||
});
|
||||
importEntrypointsMock.mockResolvedValue([
|
||||
{ include: ['not' + config.browser] },
|
||||
]);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
@@ -803,9 +804,7 @@ describe('findEntrypoints', () => {
|
||||
describe('exclude option', () => {
|
||||
it('should mark the background as skipped when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointMock.mockResolvedValue({
|
||||
exclude: [config.browser],
|
||||
});
|
||||
importEntrypointsMock.mockResolvedValue([{ exclude: [config.browser] }]);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
@@ -819,9 +818,7 @@ describe('findEntrypoints', () => {
|
||||
|
||||
it('should mark content scripts as skipped when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['example.content.ts']);
|
||||
importEntrypointMock.mockResolvedValue({
|
||||
exclude: [config.browser],
|
||||
});
|
||||
importEntrypointsMock.mockResolvedValue([{ exclude: [config.browser] }]);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
@@ -914,7 +911,7 @@ describe('findEntrypoints', () => {
|
||||
builder: wxt.builder,
|
||||
});
|
||||
|
||||
importEntrypointMock.mockResolvedValue({});
|
||||
importEntrypointsMock.mockResolvedValue([{}]);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { relative, resolve } from 'path';
|
||||
import {
|
||||
BackgroundEntrypoint,
|
||||
BackgroundDefinition,
|
||||
BaseEntrypointOptions,
|
||||
ContentScriptDefinition,
|
||||
ContentScriptEntrypoint,
|
||||
Entrypoint,
|
||||
GenericEntrypoint,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
UnlistedScriptDefinition,
|
||||
PopupEntrypointOptions,
|
||||
OptionsEntrypointOptions,
|
||||
SidepanelEntrypoint,
|
||||
SidepanelEntrypointOptions,
|
||||
MainWorldContentScriptEntrypointOptions,
|
||||
IsolatedWorldContentScriptEntrypointOptions,
|
||||
} from '../../../types';
|
||||
import fs from 'fs-extra';
|
||||
import { minimatch } from 'minimatch';
|
||||
@@ -22,13 +17,15 @@ import JSON5 from 'json5';
|
||||
import glob from 'fast-glob';
|
||||
import {
|
||||
getEntrypointName,
|
||||
isHtmlEntrypoint,
|
||||
isJsEntrypoint,
|
||||
resolvePerBrowserOptions,
|
||||
} from '../../utils/entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../../utils/constants';
|
||||
import { CSS_EXTENSIONS_PATTERN } from '../../utils/paths';
|
||||
import pc from 'picocolors';
|
||||
import { wxt } from '../../wxt';
|
||||
import { createExtensionEnvironment } from '../environments';
|
||||
import { camelCase } from 'scule';
|
||||
|
||||
/**
|
||||
* Return entrypoints and their configuration by looking through the project's files.
|
||||
@@ -76,59 +73,61 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
|
||||
|
||||
// Import entrypoints to get their config
|
||||
let hasBackground = false;
|
||||
const env = createExtensionEnvironment();
|
||||
const entrypointsWithoutSkipped: Entrypoint[] = await env.run(() =>
|
||||
Promise.all(
|
||||
entrypointInfos.map(async (info): Promise<Entrypoint> => {
|
||||
const { type } = info;
|
||||
switch (type) {
|
||||
case 'popup':
|
||||
return await getPopupEntrypoint(info);
|
||||
case 'sidepanel':
|
||||
return await getSidepanelEntrypoint(info);
|
||||
case 'options':
|
||||
return await getOptionsEntrypoint(info);
|
||||
case 'background':
|
||||
hasBackground = true;
|
||||
return await getBackgroundEntrypoint(info);
|
||||
case 'content-script':
|
||||
return await getContentScriptEntrypoint(info);
|
||||
case 'unlisted-page':
|
||||
return await getUnlistedPageEntrypoint(info);
|
||||
case 'unlisted-script':
|
||||
return await getUnlistedScriptEntrypoint(info);
|
||||
case 'content-script-style':
|
||||
return {
|
||||
...info,
|
||||
type,
|
||||
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options: {
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...info,
|
||||
type,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: {
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
),
|
||||
const entrypointOptions = await importEntrypoints(entrypointInfos);
|
||||
const entrypointsWithoutSkipped: Entrypoint[] = await Promise.all(
|
||||
entrypointInfos.map(async (info): Promise<Entrypoint> => {
|
||||
const { type } = info;
|
||||
const options = entrypointOptions[info.inputPath] ?? {};
|
||||
switch (type) {
|
||||
case 'popup':
|
||||
return await getPopupEntrypoint(info, options);
|
||||
case 'sidepanel':
|
||||
return await getSidepanelEntrypoint(info, options);
|
||||
case 'options':
|
||||
return await getOptionsEntrypoint(info, options);
|
||||
case 'background':
|
||||
hasBackground = true;
|
||||
return await getBackgroundEntrypoint(info, options);
|
||||
case 'content-script':
|
||||
return await getContentScriptEntrypoint(info, options);
|
||||
case 'unlisted-page':
|
||||
return await getUnlistedPageEntrypoint(info, options);
|
||||
case 'unlisted-script':
|
||||
return await getUnlistedScriptEntrypoint(info, options);
|
||||
case 'content-script-style':
|
||||
return {
|
||||
...info,
|
||||
type,
|
||||
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options: {
|
||||
include: options.include,
|
||||
exclude: options.exclude,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...info,
|
||||
type,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: {
|
||||
include: options.include,
|
||||
exclude: options.exclude,
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (wxt.config.command === 'serve' && !hasBackground) {
|
||||
entrypointsWithoutSkipped.push(
|
||||
await getBackgroundEntrypoint({
|
||||
inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
|
||||
name: 'background',
|
||||
type: 'background',
|
||||
}),
|
||||
await getBackgroundEntrypoint(
|
||||
{
|
||||
inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
|
||||
name: 'background',
|
||||
type: 'background',
|
||||
},
|
||||
{},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,6 +162,61 @@ interface EntrypointInfo {
|
||||
type: Entrypoint['type'];
|
||||
}
|
||||
|
||||
/** Returns a map of input paths to the file's options. */
|
||||
async function importEntrypoints(infos: EntrypointInfo[]) {
|
||||
const resMap: Record<string, Record<string, any> | undefined> = {};
|
||||
|
||||
const htmlInfos = infos.filter((info) => isHtmlEntrypoint(info));
|
||||
const jsInfos = infos.filter((info) => isJsEntrypoint(info));
|
||||
|
||||
await Promise.all([
|
||||
// HTML
|
||||
...htmlInfos.map(async (info) => {
|
||||
const res = await importHtmlEntrypoint(info);
|
||||
resMap[info.inputPath] = res;
|
||||
}),
|
||||
// JS
|
||||
(async () => {
|
||||
const res = await wxt.builder.importEntrypoints(
|
||||
jsInfos.map((info) => info.inputPath),
|
||||
);
|
||||
res.forEach((res, i) => {
|
||||
resMap[jsInfos[i].inputPath] = res;
|
||||
});
|
||||
})(),
|
||||
// CSS - never has options
|
||||
]);
|
||||
|
||||
return resMap;
|
||||
}
|
||||
|
||||
/** Extract `manifest.` options from meta tags, converting snake_case keys to camelCase */
|
||||
async function importHtmlEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
): Promise<Record<string, any>> {
|
||||
const content = await fs.readFile(info.inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const metaTags = document.querySelectorAll('meta');
|
||||
const res: Record<string, any> = {
|
||||
title: document.querySelector('title')?.textContent || undefined,
|
||||
};
|
||||
|
||||
metaTags.forEach((tag) => {
|
||||
const name = tag.name;
|
||||
if (!name.startsWith('manifest.')) return;
|
||||
|
||||
const key = camelCase(name.slice(9));
|
||||
try {
|
||||
res[key] = JSON5.parse(tag.content);
|
||||
} catch {
|
||||
res[key] = tag.content;
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function preventDuplicateEntrypointNames(files: EntrypointInfo[]) {
|
||||
const namesToPaths = files.reduce<Record<string, string[]>>(
|
||||
(map, { name, inputPath }) => {
|
||||
@@ -200,32 +254,26 @@ function preventNoEntrypoints(files: EntrypointInfo[]) {
|
||||
|
||||
async function getPopupEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
options: Record<string, any>,
|
||||
): Promise<PopupEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<PopupEntrypointOptions>(
|
||||
info,
|
||||
const stictOptions: PopupEntrypoint['options'] = resolvePerBrowserOptions(
|
||||
{
|
||||
browserStyle: 'browser_style',
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
defaultIcon: 'default_icon',
|
||||
defaultTitle: 'default_title',
|
||||
mv2Key: 'type',
|
||||
},
|
||||
{
|
||||
defaultTitle: (document) =>
|
||||
document.querySelector('title')?.textContent || undefined,
|
||||
},
|
||||
{
|
||||
defaultTitle: (content) => content,
|
||||
mv2Key: (content) =>
|
||||
content === 'page_action' ? 'page_action' : 'browser_action',
|
||||
browserStyle: options.browserStyle,
|
||||
exclude: options.exclude,
|
||||
include: options.include,
|
||||
defaultIcon: options.defaultIcon,
|
||||
defaultTitle: options.title,
|
||||
mv2Key: options.type,
|
||||
},
|
||||
wxt.config.browser,
|
||||
);
|
||||
if (stictOptions.mv2Key && stictOptions.mv2Key !== 'page_action')
|
||||
stictOptions.mv2Key = 'browser_action';
|
||||
|
||||
return {
|
||||
type: 'popup',
|
||||
name: 'popup',
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
options: stictOptions,
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
};
|
||||
@@ -233,21 +281,21 @@ async function getPopupEntrypoint(
|
||||
|
||||
async function getOptionsEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
options: Record<string, any>,
|
||||
): Promise<OptionsEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<OptionsEntrypointOptions>(
|
||||
info,
|
||||
{
|
||||
browserStyle: 'browser_style',
|
||||
chromeStyle: 'chrome_style',
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
openInTab: 'open_in_tab',
|
||||
},
|
||||
);
|
||||
return {
|
||||
type: 'options',
|
||||
name: 'options',
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
options: resolvePerBrowserOptions(
|
||||
{
|
||||
browserStyle: options.browserStyle,
|
||||
chromeStyle: options.chromeStyle,
|
||||
exclude: options.exclude,
|
||||
include: options.include,
|
||||
openInTab: options.openInTab,
|
||||
},
|
||||
wxt.config.browser,
|
||||
),
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
};
|
||||
@@ -255,61 +303,56 @@ async function getOptionsEntrypoint(
|
||||
|
||||
async function getUnlistedPageEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
options: Record<string, any>,
|
||||
): Promise<GenericEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<BaseEntrypointOptions>(info, {
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'unlisted-page',
|
||||
name: info.name,
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options,
|
||||
options: {
|
||||
include: options.include,
|
||||
exclude: options.exclude,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getUnlistedScriptEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
}: EntrypointInfo): Promise<GenericEntrypoint> {
|
||||
const defaultExport =
|
||||
await wxt.builder.importEntrypoint<UnlistedScriptDefinition>(inputPath);
|
||||
if (defaultExport == null) {
|
||||
throw Error(
|
||||
`${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`,
|
||||
);
|
||||
}
|
||||
const { main: _, ...options } = defaultExport;
|
||||
async function getUnlistedScriptEntrypoint(
|
||||
{ inputPath, name }: EntrypointInfo,
|
||||
options: Record<string, any>,
|
||||
): Promise<GenericEntrypoint> {
|
||||
return {
|
||||
type: 'unlisted-script',
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
options: resolvePerBrowserOptions(
|
||||
{
|
||||
include: options.include,
|
||||
exclude: options.exclude,
|
||||
},
|
||||
wxt.config.browser,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function getBackgroundEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
}: EntrypointInfo): Promise<BackgroundEntrypoint> {
|
||||
let options: Omit<BackgroundDefinition, 'main'> = {};
|
||||
if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
|
||||
const defaultExport =
|
||||
await wxt.builder.importEntrypoint<BackgroundDefinition>(inputPath);
|
||||
if (defaultExport == null) {
|
||||
throw Error(
|
||||
`${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`,
|
||||
);
|
||||
}
|
||||
const { main: _, ...moduleOptions } = defaultExport;
|
||||
options = moduleOptions;
|
||||
}
|
||||
async function getBackgroundEntrypoint(
|
||||
{ inputPath, name }: EntrypointInfo,
|
||||
options: Record<string, any>,
|
||||
): Promise<BackgroundEntrypoint> {
|
||||
const strictOptions: BackgroundEntrypoint['options'] =
|
||||
resolvePerBrowserOptions(
|
||||
{
|
||||
include: options.include,
|
||||
exclude: options.exclude,
|
||||
persistent: options.persistent,
|
||||
type: options.type,
|
||||
},
|
||||
wxt.config.browser,
|
||||
);
|
||||
|
||||
if (wxt.config.manifestVersion !== 3) {
|
||||
delete options.type;
|
||||
delete strictOptions.type;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -317,112 +360,51 @@ async function getBackgroundEntrypoint({
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
options: strictOptions,
|
||||
};
|
||||
}
|
||||
|
||||
async function getContentScriptEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
}: EntrypointInfo): Promise<ContentScriptEntrypoint> {
|
||||
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(...)"?`,
|
||||
);
|
||||
}
|
||||
async function getContentScriptEntrypoint(
|
||||
{ inputPath, name }: EntrypointInfo,
|
||||
options: Record<string, any>,
|
||||
): Promise<ContentScriptEntrypoint> {
|
||||
return {
|
||||
type: 'content-script',
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
options: resolvePerBrowserOptions(
|
||||
options as
|
||||
| MainWorldContentScriptEntrypointOptions
|
||||
| IsolatedWorldContentScriptEntrypointOptions,
|
||||
wxt.config.browser,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function getSidepanelEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
options: Record<string, any>,
|
||||
): Promise<SidepanelEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<SidepanelEntrypointOptions>(
|
||||
info,
|
||||
{
|
||||
browserStyle: 'browser_style',
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
defaultIcon: 'default_icon',
|
||||
defaultTitle: 'default_title',
|
||||
openAtInstall: 'open_at_install',
|
||||
},
|
||||
{
|
||||
defaultTitle: (document) =>
|
||||
document.querySelector('title')?.textContent || undefined,
|
||||
},
|
||||
{
|
||||
defaultTitle: (content) => content,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'sidepanel',
|
||||
name: info.name,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
options: resolvePerBrowserOptions(
|
||||
{
|
||||
browserStyle: options.browserStyle,
|
||||
exclude: options.exclude,
|
||||
include: options.include,
|
||||
defaultIcon: options.defaultIcon,
|
||||
defaultTitle: options.title,
|
||||
openAtInstall: options.openAtInstall,
|
||||
},
|
||||
wxt.config.browser,
|
||||
),
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the HTML tags to extract options from them.
|
||||
*/
|
||||
async function getHtmlEntrypointOptions<T extends BaseEntrypointOptions>(
|
||||
info: EntrypointInfo,
|
||||
keyMap: Record<keyof T, string>,
|
||||
queries?: Partial<{
|
||||
[key in keyof T]: (
|
||||
document: Document,
|
||||
manifestKey: string,
|
||||
) => string | undefined;
|
||||
}>,
|
||||
parsers?: Partial<{ [key in keyof T]: (content: string) => T[key] }>,
|
||||
): Promise<T> {
|
||||
const content = await fs.readFile(info.inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const options = {} as T;
|
||||
|
||||
const defaultQuery = (manifestKey: string) =>
|
||||
document
|
||||
.querySelector(`meta[name='manifest.${manifestKey}']`)
|
||||
?.getAttribute('content');
|
||||
|
||||
Object.entries(keyMap).forEach(([_key, manifestKey]) => {
|
||||
const key = _key as keyof T;
|
||||
const content = queries?.[key]
|
||||
? queries[key]!(document, manifestKey)
|
||||
: defaultQuery(manifestKey);
|
||||
if (content) {
|
||||
try {
|
||||
options[key] = (parsers?.[key] ?? JSON5.parse)(content);
|
||||
} catch (err) {
|
||||
wxt.logger.fatal(
|
||||
`Failed to parse meta tag content. Usually this means you have invalid JSON5 content (content=${content})`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function isEntrypointSkipped(entry: Omit<Entrypoint, 'skipped'>): boolean {
|
||||
if (wxt.config.filterEntrypoints != null) {
|
||||
return !wxt.config.filterEntrypoints.has(entry.name);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ResolvedPerBrowserOptions,
|
||||
TargetBrowser,
|
||||
} from '../../types';
|
||||
import path, { relative, resolve } from 'node:path';
|
||||
import path, { relative, resolve, extname } from 'node:path';
|
||||
import { normalizePath } from './paths';
|
||||
|
||||
export function getEntrypointName(
|
||||
@@ -76,6 +76,21 @@ export function resolvePerBrowserOptions<
|
||||
*
|
||||
* Naively just checking the file extension of the input path.
|
||||
*/
|
||||
export function isHtmlEntrypoint(entrypoint: Entrypoint): boolean {
|
||||
return entrypoint.inputPath.endsWith('.html');
|
||||
export function isHtmlEntrypoint(
|
||||
entrypoint: Pick<Entrypoint, 'inputPath'>,
|
||||
): boolean {
|
||||
const ext = extname(entrypoint.inputPath);
|
||||
return ['.html'].includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the entrypoint is a JS entrypoint.
|
||||
*
|
||||
* Naively just checking the file extension of the input path.
|
||||
*/
|
||||
export function isJsEntrypoint(
|
||||
entrypoint: Pick<Entrypoint, 'inputPath'>,
|
||||
): boolean {
|
||||
const ext = extname(entrypoint.inputPath);
|
||||
return ['.js', '.jsx', '.ts', '.tsx'].includes(ext);
|
||||
}
|
||||
|
||||
@@ -47,9 +47,11 @@ export async function writeManifest(
|
||||
* Generates the manifest based on the config and entrypoints.
|
||||
*/
|
||||
export async function generateManifest(
|
||||
entrypoints: Entrypoint[],
|
||||
allEntrypoints: Entrypoint[],
|
||||
buildOutput: Omit<BuildOutput, 'manifest'>,
|
||||
): Promise<{ manifest: Manifest.WebExtensionManifest; warnings: any[][] }> {
|
||||
const entrypoints = allEntrypoints.filter((entry) => !entry.skipped);
|
||||
|
||||
const warnings: any[][] = [];
|
||||
const pkg = await getPackageJson();
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
UserManifest,
|
||||
Wxt,
|
||||
SidepanelEntrypoint,
|
||||
BaseEntrypoint,
|
||||
} from '../../../types';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { vi } from 'vitest';
|
||||
@@ -50,7 +51,7 @@ export function fakeDir(root = process.cwd()): string {
|
||||
return resolve(root, faker.string.alphanumeric());
|
||||
}
|
||||
|
||||
export const fakeEntrypoint = () =>
|
||||
export const fakeEntrypoint = (options?: DeepPartial<BaseEntrypoint>) =>
|
||||
faker.helpers.arrayElement([
|
||||
fakePopupEntrypoint,
|
||||
fakeGenericEntrypoint,
|
||||
@@ -58,7 +59,7 @@ export const fakeEntrypoint = () =>
|
||||
fakeBackgroundEntrypoint,
|
||||
fakeContentScriptEntrypoint,
|
||||
fakeUnlistedScriptEntrypoint,
|
||||
])();
|
||||
])(options);
|
||||
|
||||
export const fakeContentScriptEntrypoint =
|
||||
fakeObjectCreator<ContentScriptEntrypoint>(() => ({
|
||||
|
||||
@@ -1035,9 +1035,13 @@ export interface WxtBuilder {
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Import the entrypoint file, returning the default export containing the options.
|
||||
* Import a JS entrypoint file, returning the default export containing the options.
|
||||
*/
|
||||
importEntrypoint<T>(path: string): Promise<T>;
|
||||
/**
|
||||
* Import a list of JS entrypoint files, returning their options.
|
||||
*/
|
||||
importEntrypoints(paths: string[]): Promise<Record<string, unknown>[]>;
|
||||
/**
|
||||
* Build a single entrypoint group. This is effectively one of the multiple "steps" during the
|
||||
* build process.
|
||||
@@ -1411,6 +1415,8 @@ export interface FsCache {
|
||||
export interface ExtensionRunner {
|
||||
openBrowser(): Promise<void>;
|
||||
closeBrowser(): Promise<void>;
|
||||
/** Whether or not this runner actually opens the browser. */
|
||||
canOpen?(): boolean;
|
||||
}
|
||||
|
||||
export type EslintGlobalsPropValue =
|
||||
|
||||
Generated
+16
@@ -330,6 +330,9 @@ importers:
|
||||
|
||||
packages/wxt:
|
||||
dependencies:
|
||||
'@1natsu/wait-element':
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.2
|
||||
'@aklinker1/rollup-plugin-visualizer':
|
||||
specifier: 5.12.0
|
||||
version: 5.12.0(rollup@4.24.0)
|
||||
@@ -566,6 +569,9 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@1natsu/wait-element@4.1.2':
|
||||
resolution: {integrity: sha512-qWxSJD+Q5b8bKOvESFifvfZ92DuMsY+03SBNjTO34ipJLP6mZ9yK4bQz/vlh48aEQXoJfaZBqUwKL5BdI5iiWw==}
|
||||
|
||||
'@aklinker1/buildc@1.1.4':
|
||||
resolution: {integrity: sha512-3SQ2zVEsX+UWg/mHfp9TqU9OPI8abRGTOadChQB0Aqb4dtIy311pBddndVPuuD4uQfIhKbvPxkzZ0rwvlw52Rw==}
|
||||
hasBin: true
|
||||
@@ -3546,6 +3552,9 @@ packages:
|
||||
make-error@1.3.6:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
|
||||
many-keys-map@2.0.1:
|
||||
resolution: {integrity: sha512-DHnZAD4phTbZ+qnJdjoNEVU1NecYoSdbOOoVmTDH46AuxDkEVh3MxTVpXq10GtcTC6mndN9dkv1rNfpjRcLnOw==}
|
||||
|
||||
mark.js@8.11.1:
|
||||
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
|
||||
|
||||
@@ -5113,6 +5122,11 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@1natsu/wait-element@4.1.2':
|
||||
dependencies:
|
||||
defu: 6.1.4
|
||||
many-keys-map: 2.0.1
|
||||
|
||||
'@aklinker1/buildc@1.1.4':
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
@@ -8127,6 +8141,8 @@ snapshots:
|
||||
|
||||
make-error@1.3.6: {}
|
||||
|
||||
many-keys-map@2.0.1: {}
|
||||
|
||||
mark.js@8.11.1: {}
|
||||
|
||||
markdown-it-footnote@4.0.0(patch_hash=ue34jdgdx43siqdj557feoepzq): {}
|
||||
|
||||
Reference in New Issue
Block a user