Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d767f3f598 | |||
| 48967c7c5f | |||
| 6c4da7225d | |||
| e9025fb13e | |||
| 8db6b3187b | |||
| a19f9e12c4 | |||
| a82eb46511 | |||
| e50f2b047a | |||
| 1b41cd26ee | |||
| e32fbed1b7 | |||
| f8acdd6352 | |||
| 9ea89c6340 | |||
| a521a80d43 | |||
| 4c6a57dd33 | |||
| 3ce8c8b1f4 | |||
| 4ee8f7837e | |||
| 1e8e5488af | |||
| 5704765cc4 |
@@ -1,5 +1,5 @@
|
||||
<h1 align="center">
|
||||
<img style="vertical-align:middle" width="44" src="./docs/public/hero-logo.svg" alt="WXT Logo">
|
||||
<img style="vertical-align:middle" width="44" src="https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/public/hero-logo.svg" alt="WXT Logo">
|
||||
<span>WXT</span>
|
||||
</h1>
|
||||
|
||||
@@ -22,22 +22,22 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://wxt.dev" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Installation</a>
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
|
||||
•
|
||||
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
|
||||
•
|
||||
<a href="https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md" target="_blank">Changelog</a>
|
||||
•
|
||||
<a href="https://discord.gg/ZFsZqGery9" target="_blank">Discord</a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
## Demo
|
||||
|
||||
https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94
|
||||
<video src="https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94"></video>
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ const chromeExtensionIds = [
|
||||
'oadbjpccljkplmhnjekgjamejnbadlne', // demo.fun - Interactive product demos that convert
|
||||
'iopdafdcollfgaoffingmahpffckmjni', // SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈
|
||||
'khjdmjcmpolknpccmaaipmidphjokhdf', // WorkFlowy MultiFlow
|
||||
'fencadnndhdeggodopebjgdfdlhcimfk', // 香草布丁🌿🍮- https://github.com/Xdy1579883916/vanilla-pudding
|
||||
'bnacincmbaknlbegecpioobkfgejlojp', // MaxFocus: Link Preview
|
||||
'bcpgdpedphodjcjlminjbdeejccjbimp', // 汇率转换-中文版本
|
||||
'loeilaonggnalkaiiaepbegccilkmjjp', // Currency Converter Plus
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
@@ -121,6 +121,7 @@ export default defineConfig({
|
||||
|
||||
// Files
|
||||
menuItem('.env', 'env'),
|
||||
menuItem('app.config.ts', 'app-config'),
|
||||
menuItem('package.json', 'package'),
|
||||
menuItem('tsconfig.json', 'tsconfig'),
|
||||
menuItem('web-ext.config.ts', 'web-ext-config'),
|
||||
|
||||
@@ -17,7 +17,7 @@ In WXT, you create an entrypoint by adding a file to the `entrypoints/` director
|
||||
|
||||
Some entrypoint filesname patterns are reserved by WXT and effect how the manifest is generated.
|
||||
|
||||
- `popup` adds a `action` to the manifest
|
||||
- `popup` adds an `action` to the manifest
|
||||
- `background` adds a background script/service worker
|
||||
- `*.content.ts` adds a content script
|
||||
- ...
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# `<srcDir>/app.config.ts`
|
||||
|
||||
:::warning Nuxt Users
|
||||
If you're familiar with Nuxt, this file is meant to be a direct equivalent to Nuxt's `app.config.ts` file.
|
||||
|
||||
However, some of Nuxt's features, like overriding the app config based on a `.env` file or automatically generating the config's types, are not implemented. They are planned, just not implemented yet. Feel free to open a PR!
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
Define runtime configuration in a single place.
|
||||
|
||||
```ts
|
||||
// <srcDir>/app.config.ts
|
||||
import { defineAppConfig } from 'wxt/sandbox';
|
||||
|
||||
// Define types for your config
|
||||
declare module 'wxt/sandbox' {
|
||||
export interface WxtAppConfig {
|
||||
theme?: 'light' | 'dark';
|
||||
}
|
||||
}
|
||||
|
||||
export default defineAppConfig({
|
||||
theme: 'dark',
|
||||
});
|
||||
```
|
||||
|
||||
Then access the config in your extension by calling `useAppConfig`:
|
||||
|
||||
```ts
|
||||
console.log(useAppConfig()); // { theme: "dark" }
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
If you have a `.env` file, you can access any variables defined in it here. You can convert them to better types (like booleans), add types for them, or leave them as is.
|
||||
|
||||
```txt
|
||||
# .env
|
||||
VITE_BUG_REPORTING_DISABLED=true
|
||||
VITE_API_KEY=...
|
||||
```
|
||||
|
||||
```ts
|
||||
// <srcDir>/app.config.ts
|
||||
|
||||
declare module 'wxt/sandbox' {
|
||||
export interface WxtAppConfig {
|
||||
bugReportingDisabled: boolean;
|
||||
apiKey?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineAppConfig({
|
||||
bugReportingDisabled: process.env.VITE_BUG_REPORTING_DISABLED === 'true',
|
||||
apiKey: process.env.VITE_API_KEY,
|
||||
});
|
||||
```
|
||||
|
||||
> You don't have to do this, you can use `process.env.VITE_*` anywhere in your runtime code, but putting them here consolidates them to one place and defines what variables are expected.
|
||||
@@ -18,9 +18,7 @@ export default defineConfig({
|
||||
|
||||
## Using Plugins
|
||||
|
||||
Plugins can be passed into the `vite` configuration in you `wxt.config.ts` file, just like any other option.
|
||||
|
||||
All plugins should work in WXT, but it is worth pointing out that since WXT orchestrates multiple vite builds to bundle an extension, plugins will be executed multiple times if necessary.
|
||||
Plugins can be passed into the `vite` configuration in your `wxt.config.ts` file, just like any other option.
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
@@ -33,3 +31,9 @@ export default defineConfig({
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
:::warning UNEXPECTED BEHAVIOR
|
||||
Due to the way WXT orchestrates Vite builds, some plugins may not work as expected. Search [GitHub issues](https://github.com/wxt-dev/wxt/issues?q=is%3Aissue+label%3A%22vite+plugin%22) if you run into issues with a specific plugin.
|
||||
|
||||
If one doesn't exist, please open a [new issue](https://github.com/wxt-dev/wxt/issues/new/choose)!
|
||||
:::
|
||||
|
||||
@@ -79,9 +79,7 @@ export default defineConfig({
|
||||
|
||||
## ESLint
|
||||
|
||||
By default, ESLint thinks auto-imported variables are not defined. You have to add them to the `globals` config so it knows these variables exist at runtime.
|
||||
|
||||
WXT generates a config file containing the globals you can extend! By default, WXT will attempt to detect if ESLint is installed in your project and generate the config file if so. If the config file isn't being generated automatically, you can manually tell WXT to generate it:
|
||||
ESLint doesn't know about the auto-imported variables unless they are explicitly defined in the `globals` config. By default, WXT will generate the config if it detects ESLint is installed in your project. If the config isn't generated automatically, you can manually tell WXT to generate it.
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
|
||||
@@ -229,7 +229,7 @@ The `manifest` option can also be set equal to a function, letting you use logic
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
manifest: ({ manifestVersion, browser, mode, comamnd }) => {
|
||||
manifest: ({ manifestVersion, browser, mode, command }) => {
|
||||
return { ... }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineAppConfig } from 'wxt/sandbox';
|
||||
|
||||
declare module 'wxt/sandbox' {
|
||||
export interface WxtAppConfig {
|
||||
example: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineAppConfig({
|
||||
example: 'value',
|
||||
});
|
||||
@@ -15,6 +15,8 @@ export default defineBackground({
|
||||
messages,
|
||||
});
|
||||
|
||||
console.log(useAppConfig());
|
||||
|
||||
// @ts-expect-error: should only accept entrypoints or public assets
|
||||
browser.runtime.getURL('/');
|
||||
browser.runtime.getURL('/background.js');
|
||||
|
||||
@@ -1,5 +1,74 @@
|
||||
# Changelog
|
||||
|
||||
## v0.18.13
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.12...wxt-v0.18.13)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **config:** `dev.server.hostname` ([#807](https://github.com/wxt-dev/wxt/pull/807))
|
||||
- Add XPath support to getAnchor() ([#813](https://github.com/wxt-dev/wxt/pull/813))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add debug logs for vite builder ([#816](https://github.com/wxt-dev/wxt/pull/816))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Ayub Kokabi ([@sir-kokabi](http://github.com/sir-kokabi))
|
||||
|
||||
## v0.18.12
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.11...wxt-v0.18.12)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Support runtime config in `app.config.ts` ([#792](https://github.com/wxt-dev/wxt/pull/792))
|
||||
|
||||
### 🔥 Performance
|
||||
|
||||
- Create zip using streams ([#793](https://github.com/wxt-dev/wxt/pull/793))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add missing name to ESLint v9 autoImports config ([#801](https://github.com/wxt-dev/wxt/pull/801))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update README ([#802](https://github.com/wxt-dev/wxt/pull/802))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade `web-ext-run` (0.2.0 to 0.2.1) ([#804](https://github.com/wxt-dev/wxt/pull/804))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Ntnyq ([@ntnyq](http://github.com/ntnyq))
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
|
||||
## v0.18.11
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.10...wxt-v0.18.11)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add eslint 9 config support ([#762](https://github.com/wxt-dev/wxt/pull/762))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Respect custom `outDir` when cleaning and zipping ([#774](https://github.com/wxt-dev/wxt/pull/774))
|
||||
- **dev:** Catch error when attempting to reload a tab in a saved tab group ([#786](https://github.com/wxt-dev/wxt/pull/786))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Replace consola with wxt.logger ([#776](https://github.com/wxt-dev/wxt/pull/776))
|
||||
- **deps:** Upgrade non-major deps ([#778](https://github.com/wxt-dev/wxt/pull/778))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- KnightYoshi ([@KnightYoshi](http://github.com/KnightYoshi))
|
||||
- Asakura Mizu ([@AsakuraMizu](http://github.com/AsakuraMizu))
|
||||
|
||||
## v0.18.10
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.9...wxt-v0.18.10)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<h1 align="center">
|
||||
<img style="vertical-align:middle" width="44" src="./docs/public/hero-logo.svg" alt="WXT Logo">
|
||||
<img style="vertical-align:middle" width="44" src="https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/public/hero-logo.svg" alt="WXT Logo">
|
||||
<span>WXT</span>
|
||||
</h1>
|
||||
|
||||
@@ -22,22 +22,22 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://wxt.dev" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Installation</a>
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
|
||||
•
|
||||
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
|
||||
•
|
||||
<a href="https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md" target="_blank">Changelog</a>
|
||||
•
|
||||
<a href="https://discord.gg/ZFsZqGery9" target="_blank">Discord</a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
## Demo
|
||||
|
||||
https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94
|
||||
<video src="https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94"></video>
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
@@ -12,13 +12,15 @@ exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file
|
||||
"createIframeUi": true,
|
||||
"createIntegratedUi": true,
|
||||
"createShadowRootUi": true,
|
||||
"defineAppConfig": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"storage": true
|
||||
"storage": true,
|
||||
"useAppConfig": true
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -35,16 +37,19 @@ const globals = {
|
||||
"createIframeUi": true,
|
||||
"createIntegratedUi": true,
|
||||
"createShadowRootUi": true,
|
||||
"defineAppConfig": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"storage": true
|
||||
"storage": true,
|
||||
"useAppConfig": true
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "wxt/auto-imports",
|
||||
languageOptions: {
|
||||
globals,
|
||||
sourceType: "module",
|
||||
@@ -65,13 +70,15 @@ exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config f
|
||||
"createIframeUi": true,
|
||||
"createIntegratedUi": true,
|
||||
"createShadowRootUi": true,
|
||||
"defineAppConfig": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"storage": true
|
||||
"storage": true,
|
||||
"useAppConfig": true
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -89,13 +96,15 @@ exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = `
|
||||
"createIframeUi": "readonly",
|
||||
"createIntegratedUi": "readonly",
|
||||
"createShadowRootUi": "readonly",
|
||||
"defineAppConfig": "readonly",
|
||||
"defineBackground": "readonly",
|
||||
"defineConfig": "readonly",
|
||||
"defineContentScript": "readonly",
|
||||
"defineUnlistedScript": "readonly",
|
||||
"defineWxtPlugin": "readonly",
|
||||
"fakeBrowser": "readonly",
|
||||
"storage": "readonly"
|
||||
"storage": "readonly",
|
||||
"useAppConfig": "readonly"
|
||||
}
|
||||
}
|
||||
"
|
||||
|
||||
@@ -24,6 +24,7 @@ describe('Auto Imports', () => {
|
||||
const createIframeUi: typeof import('wxt/client')['createIframeUi']
|
||||
const createIntegratedUi: typeof import('wxt/client')['createIntegratedUi']
|
||||
const createShadowRootUi: typeof import('wxt/client')['createShadowRootUi']
|
||||
const defineAppConfig: typeof import('wxt/sandbox')['defineAppConfig']
|
||||
const defineBackground: typeof import('wxt/sandbox')['defineBackground']
|
||||
const defineConfig: typeof import('wxt')['defineConfig']
|
||||
const defineContentScript: typeof import('wxt/sandbox')['defineContentScript']
|
||||
@@ -31,6 +32,7 @@ describe('Auto Imports', () => {
|
||||
const defineWxtPlugin: typeof import('wxt/sandbox')['defineWxtPlugin']
|
||||
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
|
||||
const storage: typeof import('wxt/storage')['storage']
|
||||
const useAppConfig: typeof import('wxt/client')['useAppConfig']
|
||||
}
|
||||
"
|
||||
`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.18.10",
|
||||
"version": "0.18.13",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -139,7 +139,7 @@
|
||||
"unimport": "^3.7.2",
|
||||
"vite": "^5.3.2",
|
||||
"vite-node": "^1.6.0",
|
||||
"web-ext-run": "^0.2.0",
|
||||
"web-ext-run": "^0.2.1",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -27,6 +27,7 @@ const preset = {
|
||||
external: [
|
||||
...virtualEntrypointModuleNames.map((name) => `virtual:user-${name}`),
|
||||
'virtual:wxt-plugins',
|
||||
'virtual:app-config',
|
||||
],
|
||||
} satisfies tsup.Options;
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ export function getEslint9ConfigEntry(
|
||||
text: `const globals = ${JSON.stringify(globals, null, 2)}
|
||||
|
||||
export default {
|
||||
name: "wxt/auto-imports",
|
||||
languageOptions: {
|
||||
globals,
|
||||
sourceType: "module",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// @ts-expect-error: Untyped virtual module
|
||||
import appConfig from 'virtual:app-config';
|
||||
import type { WxtAppConfig } from '~/sandbox/define-app-config';
|
||||
|
||||
export function useAppConfig(): WxtAppConfig {
|
||||
return appConfig;
|
||||
}
|
||||
@@ -252,6 +252,24 @@ describe('Content Script UIs', () => {
|
||||
document.querySelector('#parent > div[data-wxt-integrated]'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should append the element using an XPath string', () => {
|
||||
vi.stubGlobal('XPathResult', { FIRST_ORDERED_NODE_TYPE: 9 });
|
||||
document.evaluate = vi.fn().mockReturnValue({
|
||||
singleNodeValue: document.querySelector('#three'),
|
||||
});
|
||||
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
onMount: appendTestApp,
|
||||
anchor: '//p[@id="three"]',
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#three > div[data-wxt-integrated]'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Element', () => {
|
||||
|
||||
@@ -199,8 +199,25 @@ function getAnchor(options: ContentScriptAnchoredOptions): Element | undefined {
|
||||
|
||||
let resolved =
|
||||
typeof options.anchor === 'function' ? options.anchor() : options.anchor;
|
||||
if (typeof resolved === 'string')
|
||||
return document.querySelector<Element>(resolved) ?? undefined;
|
||||
|
||||
if (typeof resolved === 'string') {
|
||||
// If the string is an XPath expression (starts with '//' or '/')
|
||||
if (resolved.startsWith('/')) {
|
||||
// Evaluate the XPath and return the first ordered node
|
||||
const result = document.evaluate(
|
||||
resolved,
|
||||
document,
|
||||
null,
|
||||
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||
null,
|
||||
);
|
||||
return (result.singleNodeValue as Element) ?? undefined;
|
||||
} else {
|
||||
// If the string is a CSS selector, query the document and return the element
|
||||
return document.querySelector<Element>(resolved) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return resolved ?? undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ export type ContentScriptPositioningOptions =
|
||||
|
||||
export interface ContentScriptAnchoredOptions {
|
||||
/**
|
||||
* A CSS selector, element, or function that returns one of the two. Along with `append`, the
|
||||
* A CSS selector, XPath expression, element, or function that returns one of the three. Along with `append`, the
|
||||
* `anchor` dictates where in the page the UI will be added.
|
||||
*/
|
||||
anchor?:
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
* @module wxt/client
|
||||
*/
|
||||
export * from './content-scripts';
|
||||
export * from './app-config';
|
||||
|
||||
@@ -69,6 +69,7 @@ export async function createViteBuilder(
|
||||
wxtPlugins.excludeBrowserPolyfill(wxtConfig),
|
||||
wxtPlugins.defineImportMeta(),
|
||||
wxtPlugins.wxtPluginLoader(wxtConfig),
|
||||
wxtPlugins.resolveAppConfig(wxtConfig),
|
||||
);
|
||||
if (wxtConfig.analysis.enabled) {
|
||||
config.plugins.push(wxtPlugins.bundleAnalysis(wxtConfig));
|
||||
|
||||
@@ -14,3 +14,4 @@ export * from './entrypointGroupGlobals';
|
||||
export * from './defineImportMeta';
|
||||
export * from './removeEntrypointMainFunction';
|
||||
export * from './wxtPluginLoader';
|
||||
export * from './resolveAppConfig';
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { exists } from 'fs-extra';
|
||||
import { resolve } from 'node:path';
|
||||
import type * as vite from 'vite';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
|
||||
/**
|
||||
* When importing `virtual:app-config`, resolve it to the `app.config.ts` file in the project.
|
||||
*/
|
||||
export function resolveAppConfig(config: ResolvedConfig): vite.Plugin {
|
||||
const virtualModuleId = 'virtual:app-config';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
const appConfigFile = resolve(config.srcDir, 'app.config.ts');
|
||||
|
||||
return {
|
||||
name: 'wxt:resolve-app-config',
|
||||
async resolveId(id) {
|
||||
if (id !== virtualModuleId) return;
|
||||
|
||||
return (await exists(appConfigFile))
|
||||
? appConfigFile
|
||||
: resolvedVirtualModuleId;
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualModuleId) return `export default {}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -59,6 +59,9 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
target:
|
||||
wxt.config.browser === 'firefox' ? 'firefox-desktop' : 'chromium',
|
||||
sourceDir: wxt.config.outDir,
|
||||
// Don't add a "Reload Manager" extension alongside dev extension, WXT
|
||||
// already handles reloads intenrally.
|
||||
noReloadManagerExtension: true,
|
||||
// WXT handles reloads, so disable auto-reload behaviors in web-ext
|
||||
noReload: true,
|
||||
noInput: true,
|
||||
|
||||
@@ -55,6 +55,13 @@ export async function importEntrypointFile<T>(path: string): Promise<T> {
|
||||
wxt.config.wxtModuleDir,
|
||||
'dist/virtual/mock-browser.js',
|
||||
),
|
||||
// TODO: Resolve this virtual module to some file with
|
||||
// `export default {}` instead of this hack of using another file with
|
||||
// a default export.
|
||||
'virtual:app-config': resolve(
|
||||
wxt.config.wxtModuleDir,
|
||||
'dist/virtual/mock-browser.js',
|
||||
),
|
||||
},
|
||||
// Continue using node to load TS files even if `bun run --bun` is detected. Jiti does not
|
||||
// respect the custom transform function when using it's native bun option.
|
||||
|
||||
@@ -126,7 +126,7 @@ export async function resolveConfig(
|
||||
}
|
||||
devServerConfig = {
|
||||
port,
|
||||
hostname: 'localhost',
|
||||
hostname: mergedConfig.dev?.server?.hostname ?? 'localhost',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -228,11 +228,17 @@ async function mergeInlineConfig(
|
||||
inlineConfig.transformManifest?.(manifest);
|
||||
};
|
||||
|
||||
const merged = defu(inlineConfig, userConfig);
|
||||
|
||||
// Builders
|
||||
const builderConfig = await mergeBuilderConfig(inlineConfig, userConfig);
|
||||
const builderConfig = await mergeBuilderConfig(
|
||||
merged.logger ?? consola,
|
||||
inlineConfig,
|
||||
userConfig,
|
||||
);
|
||||
|
||||
return {
|
||||
...defu(inlineConfig, userConfig),
|
||||
...merged,
|
||||
// Custom merge values
|
||||
transformManifest,
|
||||
imports,
|
||||
@@ -397,10 +403,13 @@ const COMMAND_MODES: Record<WxtCommand, string> = {
|
||||
};
|
||||
|
||||
export async function mergeBuilderConfig(
|
||||
logger: Logger,
|
||||
inlineConfig: InlineConfig,
|
||||
userConfig: UserConfig,
|
||||
): Promise<Pick<InlineConfig, 'vite'>> {
|
||||
const vite = await import('vite').catch(() => void 0);
|
||||
const vite = await import('vite').catch((err) => {
|
||||
logger.debug('Failed to import vite:', err);
|
||||
});
|
||||
if (vite) {
|
||||
return {
|
||||
vite: async (env) => {
|
||||
|
||||
@@ -133,16 +133,22 @@ async function zipDir(
|
||||
}
|
||||
}
|
||||
await options?.additionalWork?.(archive);
|
||||
const buffer = await archive.generateAsync({
|
||||
type: 'base64',
|
||||
...(wxt.config.zip.compressionLevel === 0
|
||||
? { compression: 'STORE' }
|
||||
: {
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: { level: wxt.config.zip.compressionLevel },
|
||||
}),
|
||||
});
|
||||
await fs.writeFile(outputPath, buffer, 'base64');
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
archive
|
||||
.generateNodeStream({
|
||||
type: 'nodebuffer',
|
||||
...(wxt.config.zip.compressionLevel === 0
|
||||
? { compression: 'STORE' }
|
||||
: {
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: { level: wxt.config.zip.compressionLevel },
|
||||
}),
|
||||
})
|
||||
.pipe(fs.createWriteStream(outputPath))
|
||||
.on('error', reject)
|
||||
.on('close', resolve),
|
||||
);
|
||||
}
|
||||
|
||||
async function downloadPrivatePackages() {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface WxtAppConfig {}
|
||||
|
||||
/**
|
||||
* Runtime app config defined in `<srcDir>/app.config.ts`.
|
||||
*
|
||||
* You can add fields to this interface via ["Module Augmentation"](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation):
|
||||
*
|
||||
* ```ts
|
||||
* // app.config.ts
|
||||
* import 'wxt/sandbox';
|
||||
*
|
||||
* declare module "wxt/sandbox" {
|
||||
* export interface WxtAppConfig {
|
||||
* analytics: AnalyticsConfig
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function defineAppConfig(config: WxtAppConfig): WxtAppConfig {
|
||||
return config;
|
||||
}
|
||||
@@ -7,4 +7,5 @@ export * from './define-unlisted-script';
|
||||
export * from './define-background';
|
||||
export * from './define-content-script';
|
||||
export * from './define-wxt-plugin';
|
||||
export * from './define-app-config';
|
||||
export * from '@webext-core/match-patterns';
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
tsconfigPaths,
|
||||
globals,
|
||||
webextensionPolyfillMock,
|
||||
resolveAppConfig,
|
||||
} from '../core/builders/vite/plugins';
|
||||
import { resolveConfig } from '~/core/utils/building';
|
||||
import { InlineConfig } from '../types';
|
||||
@@ -32,6 +33,7 @@ export function WxtVitest(inlineConfig?: InlineConfig): vite.PluginOption {
|
||||
globals(config),
|
||||
download(config),
|
||||
tsconfigPaths(config),
|
||||
resolveAppConfig(config),
|
||||
];
|
||||
if (config.imports !== false) {
|
||||
const unimport = createUnimport(config.imports);
|
||||
|
||||
@@ -338,6 +338,12 @@ export interface InlineConfig {
|
||||
* Port to run the dev server on. Defaults to the first open port from 3000 to 3010.
|
||||
*/
|
||||
port?: number;
|
||||
/**
|
||||
* Hostname to run the dev server on.
|
||||
*
|
||||
* @default "localhost"
|
||||
*/
|
||||
hostname?: string;
|
||||
};
|
||||
/**
|
||||
* Controls whether a custom keyboard shortcut command, `Alt+R`, is added during dev mode to
|
||||
@@ -924,6 +930,10 @@ export interface ExtensionRunnerConfig {
|
||||
* }
|
||||
*/
|
||||
chromiumPref?: string;
|
||||
/**
|
||||
* By default, chrome opens a random port for debugging. Set this value to use a specific port.
|
||||
*/
|
||||
chromiumPort?: number;
|
||||
/**
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#pref
|
||||
*/
|
||||
|
||||
Generated
+36
-50
@@ -310,8 +310,8 @@ importers:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0(@types/node@20.14.9)
|
||||
web-ext-run:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1
|
||||
webextension-polyfill:
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0
|
||||
@@ -592,13 +592,6 @@ packages:
|
||||
resolution: {integrity: sha512-gFPqTG7otEJ8uP6wrhDv6mqwGWYZKNvAcCq6u9hOj0c+IKCEsY4L1oC9trPq2SaWIzAfHvqfBDxF591JkMf+kg==}
|
||||
dev: true
|
||||
|
||||
/@babel/code-frame@7.22.5:
|
||||
resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/highlight': 7.22.5
|
||||
dev: false
|
||||
|
||||
/@babel/code-frame@7.24.7:
|
||||
resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -748,15 +741,6 @@ packages:
|
||||
'@babel/template': 7.24.7
|
||||
'@babel/types': 7.24.7
|
||||
|
||||
/@babel/highlight@7.22.5:
|
||||
resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.24.5
|
||||
chalk: 2.4.2
|
||||
js-tokens: 4.0.0
|
||||
dev: false
|
||||
|
||||
/@babel/highlight@7.24.7:
|
||||
resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -811,8 +795,8 @@ packages:
|
||||
'@babel/helper-plugin-utils': 7.24.7
|
||||
dev: false
|
||||
|
||||
/@babel/runtime@7.23.9:
|
||||
resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==}
|
||||
/@babel/runtime@7.24.7:
|
||||
resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.0
|
||||
@@ -3627,8 +3611,8 @@ packages:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
/firefox-profile@4.5.0:
|
||||
resolution: {integrity: sha512-goE2XxbmYVSafvCjcy64/AK3xOr14HCUCD4+TpYWEIMy4nrJfNAacLGzwqKwZhCW0hHI2TYMGH+G/YBvOE8L6g==}
|
||||
/firefox-profile@4.6.0:
|
||||
resolution: {integrity: sha512-I9rAm1w8U3CdhgO4EzTJsCvgcbvynZn9lOySkZf78wUdUIQH2w9QOKf3pAX+THt2XMSSR3kJSuM8P7bYux9j8g==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
adm-zip: 0.5.10
|
||||
@@ -3695,6 +3679,7 @@ packages:
|
||||
|
||||
/fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
dev: true
|
||||
|
||||
/fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
@@ -3845,6 +3830,7 @@ packages:
|
||||
minimatch: 3.1.2
|
||||
once: 1.4.0
|
||||
path-is-absolute: 1.0.1
|
||||
dev: true
|
||||
|
||||
/glob@8.1.0:
|
||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||
@@ -4743,6 +4729,7 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
dev: true
|
||||
|
||||
/lunr@2.3.9:
|
||||
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
|
||||
@@ -4913,6 +4900,12 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
/mkdirp@3.0.1:
|
||||
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/mkdist@1.5.1(typescript@5.5.2):
|
||||
resolution: {integrity: sha512-lCu1spNiA52o7IaKgZnOjg28nNHwYqUDjBfXePXyUtzD7Xhe6rRTkGTalQ/ALfrZC/SrPw2+A/0qkeJ+fPDZtQ==}
|
||||
hasBin: true
|
||||
@@ -5048,7 +5041,7 @@ packages:
|
||||
dependencies:
|
||||
growly: 1.3.0
|
||||
is-wsl: 2.2.0
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
shellwords: 0.1.1
|
||||
uuid: 8.3.2
|
||||
which: 2.0.2
|
||||
@@ -5304,7 +5297,7 @@ packages:
|
||||
resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==}
|
||||
engines: {node: '>=16'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.22.5
|
||||
'@babel/code-frame': 7.24.7
|
||||
error-ex: 1.3.2
|
||||
json-parse-even-better-errors: 3.0.0
|
||||
lines-and-columns: 2.0.3
|
||||
@@ -6050,6 +6043,7 @@ packages:
|
||||
|
||||
/rimraf@2.4.5:
|
||||
resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==}
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
@@ -6057,13 +6051,6 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/rimraf@3.0.2:
|
||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
glob: 7.1.6
|
||||
dev: false
|
||||
|
||||
/rollup-plugin-dts@6.1.1(rollup@3.29.4)(typescript@5.5.2):
|
||||
resolution: {integrity: sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -6176,7 +6163,7 @@ packages:
|
||||
resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
dev: false
|
||||
|
||||
/semver@5.7.1:
|
||||
@@ -6194,6 +6181,7 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
dev: true
|
||||
|
||||
/semver@7.6.2:
|
||||
resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==}
|
||||
@@ -6708,11 +6696,9 @@ packages:
|
||||
resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
/tmp@0.2.1:
|
||||
resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==}
|
||||
engines: {node: '>=8.17.0'}
|
||||
dependencies:
|
||||
rimraf: 3.0.2
|
||||
/tmp@0.2.3:
|
||||
resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==}
|
||||
engines: {node: '>=14.14'}
|
||||
dev: false
|
||||
|
||||
/to-fast-properties@2.0.0:
|
||||
@@ -7036,7 +7022,7 @@ packages:
|
||||
is-yarn-global: 0.4.1
|
||||
latest-version: 7.0.0
|
||||
pupa: 3.1.0
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
semver-diff: 4.0.0
|
||||
xdg-basedir: 5.1.0
|
||||
dev: false
|
||||
@@ -7316,8 +7302,8 @@ packages:
|
||||
'@vue/shared': 3.4.31
|
||||
typescript: 5.5.2
|
||||
|
||||
/watchpack@2.4.0:
|
||||
resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
|
||||
/watchpack@2.4.1:
|
||||
resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
dependencies:
|
||||
glob-to-regexp: 0.4.1
|
||||
@@ -7330,20 +7316,20 @@ packages:
|
||||
defaults: 1.0.4
|
||||
dev: false
|
||||
|
||||
/web-ext-run@0.2.0:
|
||||
resolution: {integrity: sha512-lAm05ELMr2WDPniyaHmyuPK0rb9tsftC8f/Ui5AQvlU6F3LqoBDfyzOaaUVQrLxtm4F5oax8AHPWswf/XjZzAg==}
|
||||
/web-ext-run@0.2.1:
|
||||
resolution: {integrity: sha512-5D11VcjdGkA1/xax5UWL0YeAbDySKHzWFe6EpsoPNUMw5Uk9tKk9p6GUOfcaI5N7sINKfBMZYNsTBiu5dzJB9A==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.9
|
||||
'@babel/runtime': 7.24.7
|
||||
'@devicefarmer/adbkit': 3.2.6
|
||||
bunyan: 1.8.15
|
||||
chrome-launcher: 1.1.0
|
||||
debounce: 1.2.1
|
||||
es6-error: 4.1.1
|
||||
firefox-profile: 4.5.0
|
||||
firefox-profile: 4.6.0
|
||||
fs-extra: 11.2.0
|
||||
fx-runner: 1.4.0
|
||||
mkdirp: 1.0.4
|
||||
mkdirp: 3.0.1
|
||||
multimatch: 6.0.0
|
||||
mz: 2.7.0
|
||||
node-notifier: 10.0.1
|
||||
@@ -7353,10 +7339,10 @@ packages:
|
||||
source-map-support: 0.5.21
|
||||
strip-bom: 5.0.0
|
||||
strip-json-comments: 5.0.1
|
||||
tmp: 0.2.1
|
||||
tmp: 0.2.3
|
||||
update-notifier: 6.0.2
|
||||
watchpack: 2.4.0
|
||||
ws: 8.16.0
|
||||
watchpack: 2.4.1
|
||||
ws: 8.18.0
|
||||
zip-dir: 2.0.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@@ -7505,8 +7491,8 @@ packages:
|
||||
typedarray-to-buffer: 3.1.5
|
||||
dev: false
|
||||
|
||||
/ws@8.16.0:
|
||||
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
|
||||
/ws@8.18.0:
|
||||
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
|
||||
Reference in New Issue
Block a user