diff --git a/.changeset/README.md b/.changeset/README.md
new file mode 100644
index 0000000..e5b6d8d
--- /dev/null
+++ b/.changeset/README.md
@@ -0,0 +1,8 @@
+# Changesets
+
+Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
+with multi-package repos, or single-package repos to help you version and publish your code. You can
+find the full documentation for it [in our repository](https://github.com/changesets/changesets)
+
+We have a quick list of common questions to get you started engaging with this project in
+[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
diff --git a/.changeset/config.json b/.changeset/config.json
new file mode 100644
index 0000000..edef2eb
--- /dev/null
+++ b/.changeset/config.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json",
+ "changelog": "@changesets/cli/changelog",
+ "commit": false,
+ "fixed": [],
+ "linked": [],
+ "access": "restricted",
+ "baseBranch": "main",
+ "updateInternalDependencies": "patch",
+ "ignore": []
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..ec134a9
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,2 @@
+indent_size = 2
+indent_style = space
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..12a500b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,26 @@
+.yarn
+.env*
+*.env
+**/node_modules/**
+.DS_Store
+*.tsbuildinfo
+
+dist
+.turbo
+
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Rush temporary files
+common/deploy/
+common/temp/
+common/autoinstallers/*/.npmrc
+**/.rush/temp/
+*.lock
+*.log
+*.chunks.jsonl
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..77d7134
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# refly-lib
+
+Supporting libraries for Refly.
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..d5cc61e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "refly-lib",
+ "private": true,
+ "engines": {
+ "pnpm": ">=8"
+ },
+ "scripts": {
+ "commit": "cz",
+ "dev": "turbo run dev",
+ "build": "turbo run build",
+ "clean": "turbo run clean",
+ "lint:ci": "turbo run lint:ci",
+ "lint": "turbo run lint",
+ "test": "pnpm -r --if-present test",
+ "test:unit": "pnpm -r --if-present test:unit",
+ "test:integration": "pnpm -r --if-present test:integration",
+ "prepare": "husky"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^1.9.0",
+ "@changesets/cli": "^2.27.8",
+ "@types/node": "20.14.8",
+ "commitizen": "^4.2.4",
+ "cz-conventional-changelog": "^3.3.0",
+ "husky": "^9.1.6",
+ "lint-staged": "^15.2.10",
+ "rimraf": "^5.0.10",
+ "turbo": "^2.1.2",
+ "typescript": "5.3.3",
+ "vitest": "^2.1.0"
+ },
+ "packageManager": "pnpm@8.15.8",
+ "config": {
+ "commitizen": {
+ "path": "cz-conventional-changelog"
+ }
+ }
+}
diff --git a/packages/plugin-vite-encoding/.gitignore b/packages/plugin-vite-encoding/.gitignore
new file mode 100644
index 0000000..3058771
--- /dev/null
+++ b/packages/plugin-vite-encoding/.gitignore
@@ -0,0 +1,2 @@
+/lib
+/types
\ No newline at end of file
diff --git a/packages/plugin-vite-encoding/CHANGELOG.md b/packages/plugin-vite-encoding/CHANGELOG.md
new file mode 100644
index 0000000..2a553d4
--- /dev/null
+++ b/packages/plugin-vite-encoding/CHANGELOG.md
@@ -0,0 +1,7 @@
+## 1.0.5
+
+`2022-01-21`
+
+### 💎 Optimization
+
+- Improve icons' loading speed
diff --git a/packages/plugin-vite-encoding/CHANGLOG.zh-CN.md b/packages/plugin-vite-encoding/CHANGLOG.zh-CN.md
new file mode 100644
index 0000000..ba91d30
--- /dev/null
+++ b/packages/plugin-vite-encoding/CHANGLOG.zh-CN.md
@@ -0,0 +1,7 @@
+## 1.0.5
+
+`2022-01-21`
+
+### 💎 功能优化
+
+- 提升图标的加载速度
diff --git a/packages/plugin-vite-encoding/README.md b/packages/plugin-vite-encoding/README.md
new file mode 100644
index 0000000..717f385
--- /dev/null
+++ b/packages/plugin-vite-encoding/README.md
@@ -0,0 +1,70 @@
+# @arco-plugins/vite-react
+
+## Feature
+
+1. `Style lazy load`
+2. `Theme import`
+3. `Icon replacement`
+
+> `Style lazy load` doesn't work during development for better experience.
+
+## Install
+
+```bash
+npm i @arco-plugins/vite-react -D
+```
+
+## Usage
+
+```js
+// vite.config.js
+
+import { vitePluginForArco } from '@arco-plugins/vite-react'
+
+export default {
+ ...
+ plugins: [
+ vitePluginForArco(options),
+ ],
+}
+```
+
+```tsx
+// react
+import { Button } from '@arco-design/web-react';
+
+export default () => (
+
+
+
+
+);
+```
+
+## Options
+
+The plugin supports the following parameters:
+
+| Params | Type | Default Value | Description |
+| :--------------: | :----------------: | :-----------: | :------------------------ |
+| **`theme`** | `{String}` | `''` | Theme package name |
+| **`iconBox`** | `{String}` | `''` | Icon library package name |
+| **`modifyVars`** | `{Object}` | `{}` | Less variables |
+| **`style`** | `{'css'\|Boolean}` | `true` | Style import method |
+|**`varsInjectScope`**|`{(string\|RegExp)[]}`|`[]`| Scope of injection of less variables (modifyVars and the theme package's variables) |
+
+**Style import methods **
+
+`style: true` will import less file
+
+```js
+import '@arco-design/web-react/Affix/style';
+```
+
+`style: 'css'` will import css file
+
+```js
+import '@arco-design/web-react/Affix/style/css';
+```
+
+`style: false` will not import any style file
diff --git a/packages/plugin-vite-encoding/README.zh-CN.md b/packages/plugin-vite-encoding/README.zh-CN.md
new file mode 100644
index 0000000..0e858c9
--- /dev/null
+++ b/packages/plugin-vite-encoding/README.zh-CN.md
@@ -0,0 +1,70 @@
+# @arco-plugins/vite-react
+
+## 特性
+
+1. `样式按需加载`
+2. `主题引入`
+3. `图标替换`
+
+> 为了开发体验,开发环境下样式为全量引入
+
+## 安装
+
+```bash
+npm i @arco-plugins/vite-react -D
+```
+
+## 用法
+
+```js
+// vite.config.js
+
+import { vitePluginForArco } from '@arco-plugins/vite-react'
+
+export default {
+ ...
+ plugins: [
+ vitePluginForArco(options),
+ ],
+}
+```
+
+```tsx
+// react
+import { Button } from '@arco-design/web-react';
+
+export default () => (
+
+
+
+
+);
+```
+
+## 参数
+
+插件支持以下参数:
+
+| 参数名 | 类型 | 默认值 | 描述 |
+| :--------------: | :----------------: | :----: | :----------- |
+| **`theme`** | `{String}` | `''` | 主题包名 |
+| **`iconBox`** | `{String}` | `''` | 图标库包名 |
+| **`modifyVars`** | `{Object}` | `{}` | Less 变量 |
+| **`style`** | `{'css'\|Boolean}` | `true` | 样式引入方式 |
+|**`varsInjectScope`**|`string[]`|`[]`| less 变量(modifyVars 和主题包的变量)注入的范围 |
+
+**样式引入方式 **
+
+`style: true` 将引入 less 文件
+
+```js
+import '@arco-design/web-react/Affix/style';
+```
+
+`style: 'css'` 将引入 css 文件
+
+```js
+import '@arco-design/web-react/Affix/style/css';
+```
+
+`style: false` 不引入样式
diff --git a/packages/plugin-vite-encoding/package.json b/packages/plugin-vite-encoding/package.json
new file mode 100644
index 0000000..99fcdc2
--- /dev/null
+++ b/packages/plugin-vite-encoding/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@refly/plugin-vite-encoding",
+ "version": "1.3.3",
+ "description": "For Vite build, load Arco Design styles on demand",
+ "main": "lib/index.js",
+ "types": "types/index.d.ts",
+ "files": [
+ "lib",
+ "types"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git@github.com:arco-design/arco-plugins.git",
+ "directory": "packages/plugin-vite-react"
+ },
+ "keywords": [
+ "arco",
+ "arco-design",
+ "arco-plugin",
+ "plugin",
+ "vite"
+ ],
+ "scripts": {
+ "prebuild": "rm -fr lib types",
+ "build": "npx tsc"
+ },
+ "author": "arco-design",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/generator": "^7.12.11",
+ "@babel/helper-module-imports": "^7.12.5",
+ "@babel/parser": "^7.12.11",
+ "@babel/traverse": "^7.12.12",
+ "@babel/types": "^7.12.12",
+ "rollup": "~4.18.0"
+ },
+ "devDependencies": {
+ "vite": "^2.6.14",
+ "esbuild": "^0.13.2"
+ }
+}
diff --git a/packages/plugin-vite-encoding/src/index.ts b/packages/plugin-vite-encoding/src/index.ts
new file mode 100644
index 0000000..a33a374
--- /dev/null
+++ b/packages/plugin-vite-encoding/src/index.ts
@@ -0,0 +1,5 @@
+import pluginViteEncoding from './plugin-vite-encoding';
+
+export default pluginViteEncoding;
+
+export { pluginViteEncoding };
diff --git a/packages/plugin-vite-encoding/src/plugin-vite-encoding/index.ts b/packages/plugin-vite-encoding/src/plugin-vite-encoding/index.ts
new file mode 100644
index 0000000..2682840
--- /dev/null
+++ b/packages/plugin-vite-encoding/src/plugin-vite-encoding/index.ts
@@ -0,0 +1,46 @@
+import type { Plugin } from 'vite';
+import type { OutputChunk, OutputAsset } from 'rollup';
+
+const pkg = require('../../package.json');
+
+export function toUtf8(str: string) {
+ return str
+ .split('')
+ .map((ch) => (ch.charCodeAt(0) <= 0x7f ? ch : '\\u' + ('0000' + ch.charCodeAt(0).toString(16)).slice(-4)))
+ .join('');
+}
+
+interface PluginOption {
+ filePatterns?: (string | RegExp)[]; // File to transform
+}
+
+export default function vitePluginEncoding(options: PluginOption): Plugin {
+ return {
+ name: pkg.name,
+
+ generateBundle(_, bundle) {
+ for (const file in bundle) {
+ const chunk = bundle[file] as OutputAsset | OutputChunk;
+
+ let shouldTransform = false;
+
+ for (const pattern of options.filePatterns) {
+ if (file.match(new RegExp(pattern))) {
+ shouldTransform = true;
+ }
+ }
+
+ if (!shouldTransform) {
+ continue;
+ }
+
+ // 如果是 JavaScript 文件
+ if (chunk.type === 'chunk' && chunk?.code && /\.js$/.test(file)) {
+ // 将内容转换为 UTF-8 编码
+ const utf8Content = toUtf8(chunk.code as string);
+ chunk.code = utf8Content;
+ }
+ }
+ },
+ };
+}
diff --git a/packages/plugin-vite-encoding/src/plugin-vite-encoding/typings.d.ts b/packages/plugin-vite-encoding/src/plugin-vite-encoding/typings.d.ts
new file mode 100644
index 0000000..910ab18
--- /dev/null
+++ b/packages/plugin-vite-encoding/src/plugin-vite-encoding/typings.d.ts
@@ -0,0 +1,2 @@
+declare module '*.json';
+declare module '@babel/helper-module-imports';
diff --git a/packages/plugin-vite-encoding/src/plugin-vite-encoding/utils.ts b/packages/plugin-vite-encoding/src/plugin-vite-encoding/utils.ts
new file mode 100644
index 0000000..ebc93e7
--- /dev/null
+++ b/packages/plugin-vite-encoding/src/plugin-vite-encoding/utils.ts
@@ -0,0 +1,89 @@
+import { readFileSync, readdirSync } from 'fs';
+import { dirname, extname, resolve, sep, win32, posix } from 'path';
+
+// read file content
+export function readFileStrSync(path: string): false | string {
+ try {
+ const resolvedPath = require.resolve(path);
+ return readFileSync(resolvedPath).toString();
+ } catch (error) {
+ return false;
+ }
+}
+
+// check if a module existed
+const modExistObj: Record = {};
+export function isModExist(path: string) {
+ if (modExistObj[path] === undefined) {
+ try {
+ require.resolve(path);
+ modExistObj[path] = true;
+ } catch (error) {
+ modExistObj[path] = false;
+ }
+ }
+ return modExistObj[path];
+}
+
+// the theme package's component list
+const componentsListObj: Record = {};
+export function getThemeComponentList(theme: string) {
+ if (!theme) return [];
+ if (!componentsListObj[theme]) {
+ try {
+ const packageRootDir = dirname(require.resolve(`${theme}/package.json`));
+ const dirPath = `${packageRootDir}/components`;
+ componentsListObj[theme] = readdirSync(dirPath) || [];
+ } catch (error) {
+ componentsListObj[theme] = [];
+ }
+ }
+ return componentsListObj[theme];
+}
+
+export const parse2PosixPath = (path: string) =>
+ sep === win32.sep ? path.replaceAll(win32.sep, posix.sep) : path;
+
+// filePath match
+export function pathMatch(path: string, conf: [string | RegExp, number?]): false | string {
+ const [regStr, order = 0] = conf;
+ const reg = new RegExp(regStr);
+ const posixPath = parse2PosixPath(path);
+ const matches = posixPath.match(reg);
+ if (!matches) return false;
+ return matches[order];
+}
+
+export function parseInclude2RegExp(include: (string | RegExp)[] = [], context?: string) {
+ if (include.length === 0) return false;
+ context = context || process.cwd();
+ const regStrList = [];
+ const folders = include
+ .map((el) => {
+ if (el instanceof RegExp) {
+ const regStr = el.toString();
+ if (regStr.slice(-1) === '/') {
+ regStrList.push(`(${regStr.slice(1, -1)})`);
+ }
+ return false;
+ }
+ const absolutePath = parse2PosixPath(resolve(context, el));
+ const idx = absolutePath.indexOf('/node_modules/');
+ const len = '/node_modules/'.length;
+ const isFolder = extname(absolutePath) === '';
+ if (idx > -1) {
+ const prexPath = absolutePath.slice(0, idx + len);
+ const packagePath = absolutePath.slice(idx + len);
+ return `(${prexPath}(\\.pnpm/.+/)?${packagePath}${isFolder ? '/' : ''})`;
+ }
+ return `(${absolutePath}${isFolder ? '/' : ''})`;
+ })
+ .filter((el) => el !== false);
+ if (folders.length) {
+ regStrList.push(`(^${folders.join('|')})`);
+ }
+ if (regStrList.length > 0) {
+ return new RegExp(regStrList.join('|'));
+ }
+ return false;
+}
diff --git a/packages/plugin-vite-encoding/tsconfig.json b/packages/plugin-vite-encoding/tsconfig.json
new file mode 100644
index 0000000..4bf38d3
--- /dev/null
+++ b/packages/plugin-vite-encoding/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./lib",
+ "declarationDir": "./types",
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "allowSyntheticDefaultImports": true,
+ "noUnusedParameters": true,
+ "noUnusedLocals": true,
+ "skipLibCheck": true,
+ "module": "commonjs",
+ "target": "esnext",
+ "declaration": true,
+ "lib": ["esnext", "dom"],
+ "types": ["node"],
+ "resolveJsonModule": true,
+ "sourceMap": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/plugin-vite-react/.gitignore b/packages/plugin-vite-react/.gitignore
new file mode 100644
index 0000000..3058771
--- /dev/null
+++ b/packages/plugin-vite-react/.gitignore
@@ -0,0 +1,2 @@
+/lib
+/types
\ No newline at end of file
diff --git a/packages/plugin-vite-react/.tokens.less b/packages/plugin-vite-react/.tokens.less
new file mode 100644
index 0000000..a2bdf0c
--- /dev/null
+++ b/packages/plugin-vite-react/.tokens.less
@@ -0,0 +1,3979 @@
+
+
+/*********** font ***********/
+
+@font-family: Inter, -apple-system, BlinkMacSystemFont, PingFang SC, Hiragino Sans GB, noto sans, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
+@font-size-display-3: 56px;
+@font-size-display-2: 48px;
+@font-size-display-1: 36px;
+@font-size-title-3: 24px;
+@font-size-title-2: 20px;
+@font-size-title-1: 16px;
+@font-size-body-3: 14px;
+@font-size-body-2: 13px;
+@font-size-body-1: 12px;
+@font-size-caption: 12px;
+@font-weight-100: 100;
+@font-weight-200: 200;
+@font-weight-300: 300;
+@font-weight-400: 400;
+@font-weight-500: 500;
+@font-weight-600: 600;
+@font-weight-700: 700;
+@font-weight-800: 800;
+@font-weight-900: 900;
+@font-size-body: 14px;
+
+
+/*********** red ***********/
+
+@red-1: #FFECE8;
+@red-2: #FDCDC5;
+@red-3: #FBACA3;
+@red-4: #F98981;
+@red-5: #F76560;
+@red-6: #f53f3f;
+@red-7: #CB272D;
+@red-8: #A1151E;
+@red-9: #770813;
+@red-10: #4D000A;
+
+
+/*********** orangered ***********/
+
+@orangered-1: #FFF3E8;
+@orangered-2: #FDDDC3;
+@orangered-3: #FCC59F;
+@orangered-4: #FAAC7B;
+@orangered-5: #F99057;
+@orangered-6: #f77234;
+@orangered-7: #CC5120;
+@orangered-8: #A23511;
+@orangered-9: #771F06;
+@orangered-10: #4D0E00;
+
+
+/*********** orange ***********/
+
+@orange-1: #FFF7E8;
+@orange-2: #FFE4BA;
+@orange-3: #FFCF8B;
+@orange-4: #FFB65D;
+@orange-5: #FF9A2E;
+@orange-6: #ff7d00;
+@orange-7: #D25F00;
+@orange-8: #A64500;
+@orange-9: #792E00;
+@orange-10: #4D1B00;
+
+
+/*********** gold ***********/
+
+@gold-1: #FFFCE8;
+@gold-2: #FDF4BF;
+@gold-3: #FCE996;
+@gold-4: #FADC6D;
+@gold-5: #F9CC45;
+@gold-6: #f7ba1e;
+@gold-7: #CC9213;
+@gold-8: #A26D0A;
+@gold-9: #774B04;
+@gold-10: #4D2D00;
+
+
+/*********** yellow ***********/
+
+@yellow-1: #FEFFE8;
+@yellow-2: #FEFEBE;
+@yellow-3: #FDFA94;
+@yellow-4: #FCF26B;
+@yellow-5: #FBE842;
+@yellow-6: #fadc19;
+@yellow-7: #CFAF0F;
+@yellow-8: #A38408;
+@yellow-9: #785D03;
+@yellow-10: #4D3800;
+
+
+/*********** lime ***********/
+
+@lime-1: #FCFFE8;
+@lime-2: #EDF8BB;
+@lime-3: #DCF190;
+@lime-4: #C9E968;
+@lime-5: #B5E241;
+@lime-6: #9fdb1d;
+@lime-7: #7EB712;
+@lime-8: #5F940A;
+@lime-9: #437004;
+@lime-10: #2A4D00;
+
+
+/*********** green ***********/
+
+@green-1: #E8FFEA;
+@green-2: #AFF0B5;
+@green-3: #7BE188;
+@green-4: #4CD263;
+@green-5: #23C343;
+@green-6: #00b42a;
+@green-7: #009A29;
+@green-8: #008026;
+@green-9: #006622;
+@green-10: #004D1C;
+
+
+/*********** cyan ***********/
+
+@cyan-1: #E8FFFB;
+@cyan-2: #B7F4EC;
+@cyan-3: #89E9E0;
+@cyan-4: #5EDFD6;
+@cyan-5: #37D4CF;
+@cyan-6: #14c9c9;
+@cyan-7: #0DA5AA;
+@cyan-8: #07828B;
+@cyan-9: #03616C;
+@cyan-10: #00424D;
+
+
+/*********** blue ***********/
+
+@blue-1: #E8F7FF;
+@blue-2: #C3E7FE;
+@blue-3: #9FD4FD;
+@blue-4: #7BC0FC;
+@blue-5: #57A9FB;
+@blue-6: #3491fa;
+@blue-7: #206CCF;
+@blue-8: #114BA3;
+@blue-9: #063078;
+@blue-10: #001A4D;
+
+
+/*********** arcoblue ***********/
+
+@arcoblue-1: #E8F3FF;
+@arcoblue-2: #BEDAFF;
+@arcoblue-3: #94BFFF;
+@arcoblue-4: #6AA1FF;
+@arcoblue-5: #4080FF;
+@arcoblue-6: #165dff;
+@arcoblue-7: #0E42D2;
+@arcoblue-8: #072CA6;
+@arcoblue-9: #031A79;
+@arcoblue-10: #000D4D;
+
+
+/*********** purple ***********/
+
+@purple-1: #F5E8FF;
+@purple-2: #DDBEF6;
+@purple-3: #C396ED;
+@purple-4: #A871E3;
+@purple-5: #8D4EDA;
+@purple-6: #722ed1;
+@purple-7: #551DB0;
+@purple-8: #3C108F;
+@purple-9: #27066E;
+@purple-10: #16004D;
+
+
+/*********** pinkpurple ***********/
+
+@pinkpurple-1: #FFE8FB;
+@pinkpurple-2: #F7BAEF;
+@pinkpurple-3: #F08EE6;
+@pinkpurple-4: #E865DF;
+@pinkpurple-5: #E13EDB;
+@pinkpurple-6: #d91ad9;
+@pinkpurple-7: #B010B6;
+@pinkpurple-8: #8A0993;
+@pinkpurple-9: #650370;
+@pinkpurple-10: #42004D;
+
+
+/*********** magenta ***********/
+
+@magenta-1: #FFE8F1;
+@magenta-2: #FDC2DB;
+@magenta-3: #FB9DC7;
+@magenta-4: #F979B7;
+@magenta-5: #F754A8;
+@magenta-6: #f5319d;
+@magenta-7: #CB1E83;
+@magenta-8: #A11069;
+@magenta-9: #77064F;
+@magenta-10: #4D0034;
+
+
+/*********** gray ***********/
+
+@gray-1: #f7f8fa;
+@gray-2: #f2f3f5;
+@gray-3: #e5e6eb;
+@gray-4: #c9cdd4;
+@gray-5: #a9aeb8;
+@gray-6: #86909c;
+@gray-7: #6b7785;
+@gray-8: #4e5969;
+@gray-9: #272e3b;
+@gray-10: #1d2129;
+
+
+/*********** dark ***********/
+
+@dark-red-1: #4D000A;
+@dark-red-2: #770611;
+@dark-red-3: #A1161F;
+@dark-red-4: #CB2E34;
+@dark-red-5: #F54E4E;
+@dark-red-6: #F76965;
+@dark-red-7: #F98D86;
+@dark-red-8: #FBB0A7;
+@dark-red-9: #FDD1CA;
+@dark-red-10: #FFF0EC;
+@dark-orangered-1: #4D0E00;
+@dark-orangered-2: #771E05;
+@dark-orangered-3: #A23714;
+@dark-orangered-4: #CC5729;
+@dark-orangered-5: #F77E45;
+@dark-orangered-6: #F9925A;
+@dark-orangered-7: #FAAD7D;
+@dark-orangered-8: #FCC6A1;
+@dark-orangered-9: #FDDEC5;
+@dark-orangered-10: #FFF4EB;
+@dark-orange-1: #4D1B00;
+@dark-orange-2: #793004;
+@dark-orange-3: #A64B0A;
+@dark-orange-4: #D26913;
+@dark-orange-5: #FF8D1F;
+@dark-orange-6: #FF9626;
+@dark-orange-7: #FFB357;
+@dark-orange-8: #FFCD87;
+@dark-orange-9: #FFE3B8;
+@dark-orange-10: #FFF7E8;
+@dark-gold-1: #4D2D00;
+@dark-gold-2: #774B04;
+@dark-gold-3: #A26F0F;
+@dark-gold-4: #CC961F;
+@dark-gold-5: #F7C034;
+@dark-gold-6: #F9CC44;
+@dark-gold-7: #FADC6C;
+@dark-gold-8: #FCE995;
+@dark-gold-9: #FDF4BE;
+@dark-gold-10: #FFFCE8;
+@dark-yellow-1: #4D3800;
+@dark-yellow-2: #785E07;
+@dark-yellow-3: #A38614;
+@dark-yellow-4: #CFB325;
+@dark-yellow-5: #FAE13C;
+@dark-yellow-6: #FBE94B;
+@dark-yellow-7: #FCF374;
+@dark-yellow-8: #FDFA9D;
+@dark-yellow-9: #FEFEC6;
+@dark-yellow-10: #FEFFF0;
+@dark-lime-1: #2A4D00;
+@dark-lime-2: #447006;
+@dark-lime-3: #629412;
+@dark-lime-4: #84B723;
+@dark-lime-5: #A8DB39;
+@dark-lime-6: #B8E24B;
+@dark-lime-7: #CBE970;
+@dark-lime-8: #DEF198;
+@dark-lime-9: #EEF8C2;
+@dark-lime-10: #FDFFEE;
+@dark-green-1: #004D1C;
+@dark-green-2: #046625;
+@dark-green-3: #0A802D;
+@dark-green-4: #129A37;
+@dark-green-5: #1DB440;
+@dark-green-6: #27C346;
+@dark-green-7: #50D266;
+@dark-green-8: #7EE18B;
+@dark-green-9: #B2F0B7;
+@dark-green-10: #EBFFEC;
+@dark-cyan-1: #00424D;
+@dark-cyan-2: #06616C;
+@dark-cyan-3: #11838B;
+@dark-cyan-4: #1FA6AA;
+@dark-cyan-5: #30C9C9;
+@dark-cyan-6: #3FD4CF;
+@dark-cyan-7: #66DFD7;
+@dark-cyan-8: #90E9E1;
+@dark-cyan-9: #BEF4ED;
+@dark-cyan-10: #F0FFFC;
+@dark-blue-1: #001A4D;
+@dark-blue-2: #052F78;
+@dark-blue-3: #134CA3;
+@dark-blue-4: #2971CF;
+@dark-blue-5: #469AFA;
+@dark-blue-6: #5AAAFB;
+@dark-blue-7: #7DC1FC;
+@dark-blue-8: #A1D5FD;
+@dark-blue-9: #C6E8FE;
+@dark-blue-10: #EAF8FF;
+@dark-arcoblue-1: #000D4D;
+@dark-arcoblue-2: #041B79;
+@dark-arcoblue-3: #0E32A6;
+@dark-arcoblue-4: #1D4DD2;
+@dark-arcoblue-5: #306FFF;
+@dark-arcoblue-6: #3C7EFF;
+@dark-arcoblue-7: #689FFF;
+@dark-arcoblue-8: #93BEFF;
+@dark-arcoblue-9: #BEDAFF;
+@dark-arcoblue-10: #EAF4FF;
+@dark-purple-1: #16004D;
+@dark-purple-2: #27066E;
+@dark-purple-3: #3E138F;
+@dark-purple-4: #5A25B0;
+@dark-purple-5: #7B3DD1;
+@dark-purple-6: #8E51DA;
+@dark-purple-7: #A974E3;
+@dark-purple-8: #C59AED;
+@dark-purple-9: #DFC2F6;
+@dark-purple-10: #F7EDFF;
+@dark-pinkpurple-1: #42004D;
+@dark-pinkpurple-2: #650370;
+@dark-pinkpurple-3: #8A0D93;
+@dark-pinkpurple-4: #B01BB6;
+@dark-pinkpurple-5: #D92ED9;
+@dark-pinkpurple-6: #E13DDB;
+@dark-pinkpurple-7: #E866DF;
+@dark-pinkpurple-8: #F092E6;
+@dark-pinkpurple-9: #F7C1F0;
+@dark-pinkpurple-10: #FFF2FD;
+@dark-magenta-1: #4D0034;
+@dark-magenta-2: #770850;
+@dark-magenta-3: #A1176C;
+@dark-magenta-4: #CB2B88;
+@dark-magenta-5: #F545A6;
+@dark-magenta-6: #F756A9;
+@dark-magenta-7: #F97AB8;
+@dark-magenta-8: #FB9EC8;
+@dark-magenta-9: #FDC3DB;
+@dark-magenta-10: #FFE8F1;
+@dark-gray-1: #17171a;
+@dark-gray-2: #2e2e30;
+@dark-gray-3: #484849;
+@dark-gray-4: #5f5f60;
+@dark-gray-5: #78787a;
+@dark-gray-6: #929293;
+@dark-gray-7: #ababac;
+@dark-gray-8: #c5c5c5;
+@dark-gray-9: #dfdfdf;
+@dark-gray-10: #f6f6f6;
+@dark-primary-1: #00464d;
+@dark-primary-2: #045a5f;
+@dark-primary-3: #096f71;
+@dark-primary-4: #108481;
+@dark-primary-5: #189690;
+@dark-primary-6: #22ab9f;
+@dark-primary-7: #49c0b2;
+@dark-primary-8: #77d5c7;
+@dark-primary-9: #adeadf;
+@dark-primary-10: #ebfffb;
+@dark-success-1: rgb(var(--green-1));
+@dark-success-2: rgb(var(--green-2));
+@dark-success-3: rgb(var(--green-3));
+@dark-success-4: rgb(var(--green-4));
+@dark-success-5: rgb(var(--green-5));
+@dark-success-6: rgb(var(--green-6));
+@dark-success-7: rgb(var(--green-7));
+@dark-success-8: rgb(var(--green-8));
+@dark-success-9: rgb(var(--green-9));
+@dark-success-10: rgb(var(--green-10));
+@dark-danger-1: rgb(var(--red-1));
+@dark-danger-2: rgb(var(--red-2));
+@dark-danger-3: rgb(var(--red-3));
+@dark-danger-4: rgb(var(--red-4));
+@dark-danger-5: rgb(var(--red-5));
+@dark-danger-6: rgb(var(--red-6));
+@dark-danger-7: rgb(var(--red-7));
+@dark-danger-8: rgb(var(--red-8));
+@dark-danger-9: rgb(var(--red-9));
+@dark-danger-10: rgb(var(--red-10));
+@dark-warning-1: rgb(var(--orange-1));
+@dark-warning-2: rgb(var(--orange-2));
+@dark-warning-3: rgb(var(--orange-3));
+@dark-warning-4: rgb(var(--orange-4));
+@dark-warning-5: rgb(var(--orange-5));
+@dark-warning-6: rgb(var(--orange-6));
+@dark-warning-7: rgb(var(--orange-7));
+@dark-warning-8: rgb(var(--orange-8));
+@dark-warning-9: rgb(var(--orange-9));
+@dark-warning-10: rgb(var(--orange-10));
+@dark-link-1: rgb(var(--arcoblue-1));
+@dark-link-2: rgb(var(--arcoblue-2));
+@dark-link-3: rgb(var(--arcoblue-3));
+@dark-link-4: rgb(var(--arcoblue-4));
+@dark-link-5: rgb(var(--arcoblue-5));
+@dark-link-6: rgb(var(--arcoblue-6));
+@dark-link-7: rgb(var(--arcoblue-7));
+@dark-link-8: rgb(var(--arcoblue-8));
+@dark-link-9: rgb(var(--arcoblue-9));
+@dark-link-10: rgb(var(--arcoblue-10));
+@dark-color-white: rgba(255, 255, 255, 0.9);
+@dark-color-black: #000000;
+@dark-mask-color-bg: rgba(23, 23, 26, 0.6);
+@dark-color-tooltip-bg: #373739;
+@dark-color-spin-layer-bg: rgba(51, 51, 51, 0.6);
+@dark-color-menu-dark-hover: var(--color-fill-2);
+@dark-color-border: #333335;
+@dark-color-bg-1: #17171A;
+@dark-color-bg-2: #232324;
+@dark-color-bg-3: #2a2a2b;
+@dark-color-bg-4: #313132;
+@dark-color-bg-5: #373739;
+@dark-color-bg-white: #f6f6f6;
+@dark-color-text-1: rgba(255, 255, 255, 0.9);
+@dark-color-text-2: rgba(255, 255, 255, 0.7);
+@dark-color-text-3: rgba(255, 255, 255, 0.5);
+@dark-color-text-4: rgba(255, 255, 255, 0.3);
+@dark-color-fill-1: rgba(255, 255, 255, 0.04);
+@dark-color-fill-2: rgba(255, 255, 255, 0.08);
+@dark-color-fill-3: rgba(255, 255, 255, 0.12);
+@dark-color-fill-4: rgba(255, 255, 255, 0.16);
+@dark-color-primary-light-1: rgba(var(--primary-6), 0.2);
+@dark-color-primary-light-2: rgba(var(--primary-6), 0.35);
+@dark-color-primary-light-3: rgba(var(--primary-6), 0.5);
+@dark-color-primary-light-4: rgba(var(--primary-6), 0.65);
+@dark-color-secondary: rgba(var(--gray-9), 0.08);
+@dark-color-secondary-hover: rgba(var(--gray-8), 0.16);
+@dark-color-secondary-active: rgba(var(--gray-7), 0.24);
+@dark-color-secondary-disabled: rgba(var(--gray-9), 0.08);
+@dark-color-danger-light-1: rgba(var(--danger-6), 0.2);
+@dark-color-danger-light-2: rgba(var(--danger-6), 0.35);
+@dark-color-danger-light-3: rgba(var(--danger-6), 0.5);
+@dark-color-danger-light-4: rgba(var(--danger-6), 0.65);
+@dark-color-success-light-1: rgb(var(--success-6), 0.2);
+@dark-color-success-light-2: rgb(var(--success-6), 0.35);
+@dark-color-success-light-3: rgb(var(--success-6), 0.5);
+@dark-color-success-light-4: rgb(var(--success-6), 0.65);
+@dark-color-warning-light-1: rgb(var(--warning-6), 0.2);
+@dark-color-warning-light-2: rgb(var(--warning-6), 0.35);
+@dark-color-warning-light-3: rgb(var(--warning-6), 0.5);
+@dark-color-warning-light-4: rgb(var(--warning-6), 0.65);
+@dark-color-link-light-1: rgba(var(--link-6), 0.2);
+@dark-color-link-light-2: rgba(var(--link-6), 0.35);
+@dark-color-link-light-3: rgba(var(--link-6), 0.5);
+@dark-color-link-light-4: rgba(var(--link-6), 0.65);
+@dark-color-border-1: #2e2e30;
+@dark-color-border-2: #484849;
+@dark-color-border-4: #929293;
+@dark-color-border-3: #5f5f60;
+
+
+/*********** primary ***********/
+
+@primary-1: #e8fffa;
+@primary-2: #aaeade;
+@primary-3: #74d5c6;
+@primary-4: #46c0b2;
+@primary-5: #1fab9f;
+@primary-6: #00968f;
+@primary-7: #008481;
+@primary-8: #006f71;
+@primary-9: #005a5f;
+@primary-10: #00464d;
+
+
+/*********** success ***********/
+
+@success-1: rgb(var(--green-1));
+@success-2: rgb(var(--green-2));
+@success-3: rgb(var(--green-3));
+@success-4: rgb(var(--green-4));
+@success-5: rgb(var(--green-5));
+@success-6: rgb(var(--green-6));
+@success-7: rgb(var(--green-7));
+@success-8: rgb(var(--green-8));
+@success-9: rgb(var(--green-9));
+@success-10: rgb(var(--green-10));
+
+
+/*********** danger ***********/
+
+@danger-1: rgb(var(--red-1));
+@danger-2: rgb(var(--red-2));
+@danger-3: rgb(var(--red-3));
+@danger-4: rgb(var(--red-4));
+@danger-5: rgb(var(--red-5));
+@danger-6: rgb(var(--red-6));
+@danger-7: rgb(var(--red-7));
+@danger-8: rgb(var(--red-8));
+@danger-9: rgb(var(--red-9));
+@danger-10: rgb(var(--red-10));
+
+
+/*********** warning ***********/
+
+@warning-1: rgb(var(--orange-1));
+@warning-2: rgb(var(--orange-2));
+@warning-3: rgb(var(--orange-3));
+@warning-4: rgb(var(--orange-4));
+@warning-5: rgb(var(--orange-5));
+@warning-6: rgb(var(--orange-6));
+@warning-7: rgb(var(--orange-7));
+@warning-8: rgb(var(--orange-8));
+@warning-9: rgb(var(--orange-9));
+@warning-10: rgb(var(--orange-10));
+
+
+/*********** link ***********/
+
+@link-1: rgb(var(--arcoblue-1));
+@link-2: rgb(var(--arcoblue-2));
+@link-3: rgb(var(--arcoblue-3));
+@link-4: rgb(var(--arcoblue-4));
+@link-5: rgb(var(--arcoblue-5));
+@link-6: rgb(var(--arcoblue-6));
+@link-7: rgb(var(--arcoblue-7));
+@link-8: rgb(var(--arcoblue-8));
+@link-9: rgb(var(--arcoblue-9));
+@link-10: rgb(var(--arcoblue-10));
+@link-font-size: 14px;
+@link-line-height: 1.5715;
+@link-color-bg_hover: var(--color-fill-2);
+@link-color-bg_active: var(--color-fill-3);
+@link-padding-horizontal: 4px;
+@link-color-text: rgb(var(--primary-7));
+@link-color-text_hover: rgb(var(--primary-7));
+@link-color-text_active: rgb(var(--primary-7));
+@link-color-text_disabled: rgb(var(--primary-3));
+@link-color-text_success: rgb(var(--success-6));
+@link-color-text_success_hover: rgb(var(--success-6));
+@link-color-text_success_active: rgb(var(--success-6));
+@link-color-text_success_disabled: var(--color-success-light-3);
+@link-color-text_error: rgb(var(--danger-6));
+@link-color-text_error_active: rgb(var(--danger-6));
+@link-color-text_error_hover: rgb(var(--danger-6));
+@link-color-text_error_disabled: var(--color-danger-light-3);
+@link-color-text_warning: rgb(var(--warning-6));
+@link-color-text_warning_hover: rgb(var(--warning-6));
+@link-color-text_warning_active: rgb(var(--warning-6));
+@link-color-text_warning_disabled: var(--color-warning-light-2);
+@link-margin-icon-right: 6px;
+@link-padding-vertical: 1px;
+@link-size-icon: 12px;
+@link-border-radius: var(--border-radius-small);
+@link-color-box-shadow: var(--color-link-light-3);
+@link-prefix-cls: ~'@{prefix}-link';
+
+
+/*********** global ***********/
+
+@data-1: rgb(var(--arcoblue-5));
+@data-2: rgb(var(--arcoblue-2));
+@data-3: #55c5fd;
+@data-4: #9cdcfc;
+@data-5: rgb(var(--orange-6));
+@data-6: rgb(var(--orange-3));
+@data-7: rgb(var(--green-4));
+@data-8: rgb(var(--green-2));
+@data-9: rgb(var(--purple-4));
+@data-10: rgb(var(--purple-2));
+@data-11: rgb(var(--gold-6));
+@data-12: rgb(var(--gold-4));
+@data-13: rgb(var(--lime-6));
+@data-14: rgb(var(--lime-4));
+@data-15: rgb(var(--magenta-4));
+@data-16: rgb(var(--magenta-3));
+@data-17: rgb(var(--cyan-6));
+@data-18: rgb(var(--cyan-3));
+@data-19: rgb(var(--pinkpurple-4));
+@data-20: rgb(var(--pinkpurple-2));
+@dark-data-1: rgb(var(--arcoblue-5));
+@dark-data-2: rgb(var(--arcoblue-3));
+@dark-data-3: rgb(var(--blue-5));
+@dark-data-4: rgb(var(--blue-3));
+@dark-data-5: rgb(var(--orange-6));
+@dark-data-6: rgb(var(--orange-3));
+@dark-data-7: rgb(var(--green-4));
+@dark-data-8: rgb(var(--green-3));
+@dark-data-9: rgb(var(--purple-4));
+@dark-data-10: rgb(var(--purple-3));
+@dark-data-11: rgb(var(--gold-6));
+@dark-data-12: rgb(var(--gold-4));
+@dark-data-13: rgb(var(--lime-6));
+@dark-data-14: rgb(var(--lime-4));
+@dark-data-15: rgb(var(--magenta-4));
+@dark-data-16: rgb(var(--magenta-3));
+@dark-data-17: rgb(var(--cyan-6));
+@dark-data-18: rgb(var(--cyan-3));
+@dark-data-19: rgb(var(--pinkpurple-4));
+@dark-data-20: rgb(var(--pinkpurple-2));
+@color-data-1: rgb(var(--arcoblue-5));
+@color-data-2: rgb(var(--arcoblue-3));
+@color-data-3: rgb(var(--blue-5));
+@color-data-4: rgb(var(--blue-3));
+@color-data-5: rgb(var(--orange-6));
+@color-data-6: rgb(var(--orange-3));
+@color-data-7: rgb(var(--green-4));
+@color-data-8: rgb(var(--green-3));
+@color-data-9: rgb(var(--purple-4));
+@color-data-10: rgb(var(--purple-3));
+@color-data-11: rgb(var(--gold-6));
+@color-data-12: rgb(var(--gold-4));
+@color-data-13: rgb(var(--lime-6));
+@color-data-14: rgb(var(--lime-4));
+@color-data-15: rgb(var(--magenta-4));
+@color-data-16: rgb(var(--magenta-3));
+@color-data-17: rgb(var(--cyan-6));
+@color-data-18: rgb(var(--cyan-3));
+@color-data-19: rgb(var(--pinkpurple-4));
+@color-data-20: rgb(var(--pinkpurple-2));
+
+
+/*********** border ***********/
+
+@border-none: 0;
+@border-1: 1px;
+@border-2: 2px;
+@border-3: 3px;
+@border-4: 4px;
+@border-5: 5px;
+@border-solid: solid;
+@border-dashed: dashed;
+@border-dotted: dotted;
+@border-radius-none: 0;
+@border-radius-small: 8px;
+@border-radius-medium: 4px;
+@border-radius-large: 8px;
+@border-radius-circle: 50%;
+
+
+/*********** shadow ***********/
+
+@shadow-distance-none: 0;
+@shadow-distance-1: 1px;
+@shadow-distance-2: 2px;
+@shadow-distance-3: 3px;
+@shadow-distance-4: 4px;
+@shadow-none: none;
+@shadow-special: 0 0 1px rgba(0, 0, 0, 0.3);
+
+
+/*********** size ***********/
+
+@size-none: 0;
+@size-1: 4px;
+@size-2: 8px;
+@size-3: 12px;
+@size-4: 16px;
+@size-5: 20px;
+@size-6: 24px;
+@size-7: 28px;
+@size-8: 32px;
+@size-9: 36px;
+@size-10: 40px;
+@size-11: 44px;
+@size-12: 48px;
+@size-13: 52px;
+@size-14: 56px;
+@size-15: 60px;
+@size-16: 64px;
+@size-17: 68px;
+@size-18: 72px;
+@size-19: 76px;
+@size-20: 80px;
+@size-21: 84px;
+@size-22: 88px;
+@size-23: 92px;
+@size-24: 96px;
+@size-25: 100px;
+@size-26: 104px;
+@size-27: 108px;
+@size-28: 112px;
+@size-29: 116px;
+@size-30: 120px;
+@size-31: 124px;
+@size-32: 128px;
+@size-33: 132px;
+@size-34: 136px;
+@size-35: 140px;
+@size-36: 144px;
+@size-37: 148px;
+@size-38: 152px;
+@size-39: 156px;
+@size-40: 160px;
+@size-41: 164px;
+@size-42: 168px;
+@size-43: 172px;
+@size-44: 176px;
+@size-45: 180px;
+@size-46: 184px;
+@size-47: 188px;
+@size-48: 192px;
+@size-49: 196px;
+@size-50: 200px;
+@size-mini: 24px;
+@size-small: 28px;
+@size-default: 32px;
+@size-large: 36px;
+
+
+/*********** spacing ***********/
+
+@spacing-none: 0;
+@spacing-1: 2px;
+@spacing-2: 4px;
+@spacing-3: 6px;
+@spacing-4: 8px;
+@spacing-5: 10px;
+@spacing-6: 12px;
+@spacing-7: 16px;
+@spacing-8: 20px;
+@spacing-9: 24px;
+@spacing-10: 32px;
+@spacing-11: 36px;
+@spacing-12: 40px;
+@spacing-13: 48px;
+@spacing-14: 56px;
+@spacing-15: 60px;
+@spacing-16: 64px;
+@spacing-17: 72px;
+@spacing-18: 80px;
+@spacing-19: 84px;
+@spacing-20: 96px;
+@spacing-21: 100px;
+@spacing-22: 120px;
+
+
+/*********** color ***********/
+
+@color-transparent: transparent;
+@color-primary-1: rgb(var(--primary-1));
+@color-primary-2: rgb(var(--primary-2));
+@color-primary-3: rgb(var(--primary-3));
+@color-primary-4: rgb(var(--primary-4));
+@color-primary-5: rgb(var(--primary-5));
+@color-primary-6: rgb(var(--primary-6));
+@color-primary-7: rgb(var(--primary-7));
+@color-primary-8: rgb(var(--primary-8));
+@color-primary-9: rgb(var(--primary-9));
+@color-primary-10: rgb(var(--primary-10));
+@color-success-1: rgb(var(--success-1));
+@color-success-2: rgb(var(--success-2));
+@color-success-3: rgb(var(--success-3));
+@color-success-4: rgb(var(--success-4));
+@color-success-5: rgb(var(--success-5));
+@color-success-6: rgb(var(--success-6));
+@color-success-7: rgb(var(--success-7));
+@color-success-8: rgb(var(--success-8));
+@color-success-9: rgb(var(--success-9));
+@color-success-10: rgb(var(--success-10));
+@color-warning-1: rgb(var(--warning-1));
+@color-warning-2: rgb(var(--warning-2));
+@color-warning-3: rgb(var(--warning-3));
+@color-warning-4: rgb(var(--warning-4));
+@color-warning-5: rgb(var(--warning-5));
+@color-warning-6: rgb(var(--warning-6));
+@color-warning-7: rgb(var(--warning-7));
+@color-warning-8: rgb(var(--warning-8));
+@color-warning-9: rgb(var(--warning-9));
+@color-warning-10: rgb(var(--warning-10));
+@color-danger-1: rgb(var(--danger-1));
+@color-danger-2: rgb(var(--danger-2));
+@color-danger-3: rgb(var(--danger-3));
+@color-danger-4: rgb(var(--danger-4));
+@color-danger-5: rgb(var(--danger-5));
+@color-danger-6: rgb(var(--danger-6));
+@color-danger-7: rgb(var(--danger-7));
+@color-danger-8: rgb(var(--danger-8));
+@color-danger-9: rgb(var(--danger-9));
+@color-danger-10: rgb(var(--danger-10));
+@color-link-1: rgb(var(--link-1));
+@color-link-2: rgb(var(--link-2));
+@color-link-3: rgb(var(--link-3));
+@color-link-4: rgb(var(--link-4));
+@color-link-5: rgb(var(--link-5));
+@color-link-6: rgb(var(--link-6));
+@color-link-7: rgb(var(--link-7));
+@color-link-8: rgb(var(--link-8));
+@color-link-9: rgb(var(--link-9));
+@color-link-10: rgb(var(--link-10));
+@color-white: #ffffff;
+@color-black: #000000;
+@color-menu-dark-bg: #232324;
+@color-menu-light-bg: #ffffff;
+@color-spin-layer-bg: rgba(255, 255, 255, 0.6);
+@color-menu-dark-hover: rgba(255, 255, 255, 0.04);
+@color-tooltip-bg: rgb(var(--gray-10));
+@color-border: rgb(var(--gray-3));
+@color-bg-popup: var(--color-bg-5);
+@color-bg-1: #ffffff;
+@color-bg-2: #ffffff;
+@color-bg-3: #ffffff;
+@color-bg-4: #ffffff;
+@color-bg-5: #ffffff;
+@color-bg-white: #ffffff;
+@color-text-1: var(--color-neutral-10);
+@color-text-2: var(--color-neutral-8);
+@color-text-3: var(--color-neutral-6);
+@color-text-4: var(--color-neutral-4);
+@color-fill-1: var(--color-neutral-1);
+@color-fill-2: var(--color-neutral-2);
+@color-fill-3: var(--color-neutral-3);
+@color-fill-4: var(--color-neutral-4);
+@color-border-1: var(--color-neutral-2);
+@color-border-2: var(--color-neutral-3);
+@color-border-3: var(--color-neutral-4);
+@color-border-4: var(--color-neutral-6);
+@color-primary-light-1: rgb(var(--primary-1));
+@color-primary-light-2: rgb(var(--primary-2));
+@color-primary-light-3: rgb(var(--primary-3));
+@color-primary-light-4: rgb(var(--primary-4));
+@color-secondary: var(--color-neutral-2);
+@color-secondary-hover: var(--color-neutral-3);
+@color-secondary-active: var(--color-neutral-4);
+@color-secondary-disabled: var(--color-neutral-1);
+@color-danger-light-1: rgb(var(--danger-1));
+@color-danger-light-2: rgb(var(--danger-2));
+@color-danger-light-3: rgb(var(--danger-3));
+@color-danger-light-4: rgb(var(--danger-4));
+@color-success-light-1: rgb(var(--success-1));
+@color-success-light-2: rgb(var(--success-2));
+@color-success-light-3: rgb(var(--success-3));
+@color-success-light-4: rgb(var(--success-4));
+@color-warning-light-1: rgb(var(--warning-1));
+@color-warning-light-2: rgb(var(--warning-2));
+@color-warning-light-3: rgb(var(--warning-3));
+@color-warning-light-4: rgb(var(--warning-4));
+@color-link-light-1: rgb(var(--link-1));
+@color-link-light-2: rgb(var(--link-2));
+@color-link-light-3: rgb(var(--link-3));
+@color-link-light-4: rgb(var(--link-4));
+@color-input-size-mini-padding-horizontal: 4px;
+@color-input-size-small-padding-horizontal: 4px;
+@color-input-size-default-padding-horizontal: 4px;
+@color-input-size-large-padding-horizontal: 5px;
+@color-preview-size-mini: 16px;
+@color-preview-size-small: 22px;
+@color-preview-size-default: 24px;
+@color-preview-size-large: 26px;
+@color-input-bg-color: var(--color-fill-2);
+@color-value-margin-left: 4px;
+@color-value-font-color: var(--color-text-1);
+@color-value-font-color_disabled: var(--color-text-4);
+@color-value-size-mini-font-size: 12px;
+@color-value-size-small-font-size: 14px;
+@color-value-size-default-font-size: 14px;
+@color-value-size-large-font-size: 14px;
+@color-input-border-radius: 8px;
+@color-preview-border-size: 1px;
+@color-preview-border-color: var(--color-border-2);
+@color-value-font-size: 400;
+@color-panel-width: 260px;
+@color-panel-padding: 12px;
+@color-panel-border-radius: 8px;
+@color-panel-bg-color: var(--color-bg-1);
+@color-panel-box-shadow: 0 8px 20px 0 rgba(0, 0, 0, 0.1);
+@color-palette-height: 178px;
+@color-palette-handle-size: 16px;
+@color-palette-handle-border-size: 2px;
+@color-control-bar-width: 182px;
+@color-control-bar-height: 14px;
+@color-control-bar-handle-size: 16px;
+@color-control-bar-alpha-margin-top: 12px;
+@color-panel-input-margin-top: 12px;
+@color-panel-input-group-margin-left: 12px;
+@color-panel-preview-size: 40px;
+@color-panel-format-select-width: 58px;
+@color-panel-alpha-input-width: 52px;
+@color-panel-section-title-font-size: 12px;
+@color-panel-empty-font-size: 12px;
+@color-panel-block-size: 16px;
+@color-panel-block-margin: 6px;
+@color-panel-block-border-radius: 2px;
+@color-panel-border-color: var(--color-border-2);
+@color-picker-prefix-cls: ~'@{prefix}-color-picker';
+@color-prefix-cls: ~'@{prefix}-color';
+
+
+/*********** shadow1 ***********/
+
+@shadow1-center: 0 0 5px rgba(0, 0, 0, 0.1);
+@shadow1-up: 0 -2px 5px rgba(0, 0, 0, 0.1);
+@shadow1-down: 0 2px 5px rgba(0, 0, 0, 0.1);
+@shadow1-left: -2px 0 5px rgba(0, 0, 0, 0.1);
+@shadow1-right: 2px 0 5px rgba(0, 0, 0, 0.1);
+@shadow1-left-up: -2px -2px 5px rgba(0, 0, 0, 0.1);
+@shadow1-left-down: -2px 2px 5px rgba(0, 0, 0, 0.1);
+@shadow1-right-up: 2px -2px 5px rgba(0, 0, 0, 0.1);
+@shadow1-right-down: 2px 2px 5px rgba(0, 0, 0, 0.1);
+
+
+/*********** shadow2 ***********/
+
+@shadow2-center: 0 0 10px rgba(0, 0, 0, 0.1);
+@shadow2-up: 0 -4px 10px rgba(0, 0, 0, 0.1);
+@shadow2-down: 0 4px 10px rgba(0, 0, 0, 0.1);
+@shadow2-left: -4px 0 10px rgba(0, 0, 0, 0.1);
+@shadow2-right: 4px 0 10px rgba(0, 0, 0, 0.1);
+@shadow2-left-up: -4px -4px 10px rgba(0, 0, 0, 0.1);
+@shadow2-left-down: -4px 4px 10px rgba(0, 0, 0, 0.1);
+@shadow2-right-up: 4px -4px 10px rgba(0, 0, 0, 0.1);
+@shadow2-right-down: 4px 4px 10px rgba(0, 0, 0, 0.1);
+
+
+/*********** shadow3 ***********/
+
+@shadow3-center: 0 0 20px rgba(0, 0, 0, 0.1);
+@shadow3-up: 0 -8px 20px rgba(0, 0, 0, 0.1);
+@shadow3-down: 0 8px 20px rgba(0, 0, 0, 0.1);
+@shadow3-left: -8px 0 20px rgba(0, 0, 0, 0.1);
+@shadow3-right: 8px 0 20px rgba(0, 0, 0, 0.1);
+@shadow3-left-up: -8px -8px 20px rgba(0, 0, 0, 0.1);
+@shadow3-left-down: -8px 8px 20px rgba(0, 0, 0, 0.1);
+@shadow3-right-up: 8px -8px 20px rgba(0, 0, 0, 0.1);
+@shadow3-right-down: 8px 8px 20px rgba(0, 0, 0, 0.1);
+
+
+/*********** opacity ***********/
+
+@opacity-none: 0;
+@opacity-1: 10%;
+@opacity-2: 20%;
+@opacity-3: 30%;
+@opacity-4: 40%;
+@opacity-5: 50%;
+@opacity-6: 60%;
+@opacity-7: 70%;
+@opacity-8: 80%;
+@opacity-9: 90%;
+@opacity-10: 100%;
+
+
+/*********** radius ***********/
+
+@radius-none: var(--border-radius-none);
+@radius-small: var(--border-radius-small);
+@radius-medium: var(--border-radius-medium);
+@radius-large: var(--border-radius-large);
+@radius-circle: var(--border-radius-circle);
+
+
+/*********** mask ***********/
+
+@mask-bg-opacity: 60%;
+@mask-color-bg: rgba(29, 33, 41, 0.6);
+
+
+/*********** icon ***********/
+
+@icon-hover-border-radius: var(--border-radius-circle);
+@icon-hover-color-bg: var(--color-fill-2);
+@icon-hover-size-default-height: 20px;
+@icon-hover-size-small-height: 20px;
+@icon-hover-size-mini-height: 20px;
+@icon-hover-size-large-height: 24px;
+@icon-hover-size-huge-height: 24px;
+@icon-hover-size-small-icon: 12px;
+@icon-hover-size-mini-icon: 12px;
+@icon-hover-size-default-icon: 12px;
+@icon-hover-size-large-icon: 12px;
+@icon-hover-size-huge-icon: 12px;
+
+
+/*********** prefix ***********/
+
+@prefix: arco;
+
+
+/*********** arco ***********/
+
+@arco-theme-tag: body;
+@arco-cssvars-prefix: -;
+@arco-draggable-prefix-cls: ~'@{prefix}-draggable';
+@arco-overflow-prefix-cls: ~'@{prefix}-overflow';
+@arco-vars-prefix: ~'';
+
+
+/*********** code ***********/
+
+@code-family: Consolas, Menlo;
+
+
+/*********** transition ***********/
+
+@transition-duration-1: 0.1s;
+@transition-duration-2: 0.2s;
+@transition-duration-3: 0.3s;
+@transition-duration-4: 0.4s;
+@transition-duration-5: 0.5s;
+@transition-duration-loading: 1s;
+@transition-timing-function-linear: cubic-bezier(0, 0, 1, 1);
+@transition-timing-function-standard: cubic-bezier(0.34, 0.69, 0.1, 1);
+@transition-timing-function-overshoot: cubic-bezier(0.3, 1.3, 0.3, 1);
+@transition-timing-function-decelerate: cubic-bezier(0.4, 0.8, 0.74, 1);
+@transition-timing-function-accelerate: cubic-bezier(0.26, 0, 0.6, 0.2);
+
+
+/*********** z ***********/
+
+@z-index-popup-base: 1000;
+@z-index-affix: 999;
+@z-index-popup: 1000;
+@z-index-drawer: 1001;
+@z-index-modal: 1001;
+@z-index-message: 1003;
+@z-index-notification: 1003;
+@z-index-image-preview: 1001;
+
+
+/*********** line ***********/
+
+@line-height-base: 1.5715;
+
+
+/*********** popup ***********/
+
+@popup-box-shadow-base: 0 2px 5px rgba(0, 0, 0, 0.1);
+@popup-color-content-text: var(--color-text-2);
+@popup-color-content-bg: var(--color-bg-popup);
+@popup-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+@popup-padding-horizontal: 16px;
+@popup-padding-vertical: 12px;
+@popup-color-title-text: var(--color-text-1);
+@popup-font-title-size: 16px;
+@popup-margin-content-top: 4px;
+@popup-color-border: var(--color-neutral-3);
+@popup-font-size: 14px;
+@popup-border-radius: var(--border-radius-medium);
+
+
+/*********** input ***********/
+
+@input-color-bg: var(--color-fill-2);
+@input-color-bg_hover: var(--color-fill-3);
+@input-color-bg_focus: var(--color-bg-2);
+@input-color-bg_disabled: var(--color-fill-2);
+@input-color-addon-bg: var(--color-fill-2);
+@input-color-addon-border: var(--color-neutral-3);
+@input-border-addon-separator-width: 1px;
+@input-color-border_focus: rgb(var(--primary-6));
+@input-color-shadow_focus: rgb(var(--primary-2));
+@input-size-shadow_focus: 0;
+@input-color-addon-border_default: transparent;
+@input-color-text: var(--color-text-1);
+@input-color-placeholder-text: var(--color-text-3);
+@input-color-text_disabled: var(--color-text-4);
+@input-color-addon-text: var(--color-text-1);
+@input-color-bg_error: var(--color-danger-light-1);
+@input-color-bg_error_hover: var(--color-danger-light-2);
+@input-color-bg_error_focus: var(--color-bg-2);
+@input-color-border_error_focus: rgb(var(--danger-6));
+@input-color-shadow_error_focus: var(--color-danger-light-2);
+@input-size-shadow_error_focus: 0;
+@input-color-bg_warning: var(--color-warning-light-1);
+@input-color-bg_warning_hover: var(--color-warning-light-2);
+@input-color-bg_warning_focus: var(--color-bg-2);
+@input-color-border_warning_focus: rgb(var(--warning-6));
+@input-color-shadow_warning_focus: var(--color-warning-light-2);
+@input-size-shadow_warning_focus: 0;
+@input-border-radius: var(--border-radius-small);
+@input-size-default-height: 32px;
+@input-size-mini-height: 24px;
+@input-size-small-height: 28px;
+@input-size-large-height: 36px;
+@input-border-width: 1px;
+@input-color-border: transparent;
+@input-color-border_disabled: transparent;
+@input-color-border_hover: transparent;
+@input-color-border_error: transparent;
+@input-color-border_error_hover: transparent;
+@input-color-border_warning: transparent;
+@input-color-border_warning_hover: transparent;
+@input-size-default-font-size: 14px;
+@input-size-small-font-size: 14px;
+@input-size-large-font-size: 14px;
+@input-size-mini-font-size: 12px;
+@input-font-tip-size: 12px;
+@input-size-mini-icon-suffix-size: 12px;
+@input-size-small-icon-suffix-size: 14px;
+@input-size-default-icon-suffix-size: 14px;
+@input-size-large-icon-suffix-size: 14px;
+@input-size-mini-icon-addon-size: 12px;
+@input-size-small-icon-addon-size: 14px;
+@input-size-default-icon-addon-size: 14px;
+@input-size-large-icon-addon-size: 14px;
+@input-size-icon-clear: 12px;
+@input-color-prefix-text: var(--color-text-2);
+@input-color-suffix-text: var(--color-text-2);
+@input-color-tip-text: var(--color-text-3);
+@input-color-icon-clear: var(--color-text-2);
+@input-color-icon-clear-bg_hover: var(--color-fill-4);
+@input-padding-horizontal: 12px;
+@input-size-mini-padding-horizontal: 8px;
+@input-size-small-padding-horizontal: 12px;
+@input-size-large-padding-horizontal: 16px;
+@input-spacing-clear-icon-right: 8px;
+@input-padding-word-limit-left: 8px;
+@input-group-border-radius_compact: var(--border-radius-small);
+@input-group-border-separator-width: 1px;
+@input-group-color-separator-border: var(--color-neutral-3);
+@input-tag-size-mini-height: 24px;
+@input-tag-size-small-height: 28px;
+@input-tag-size-default-height: 32px;
+@input-tag-size-large-height: 36px;
+@input-tag-size-mini-tag-height: 20px;
+@input-tag-size-small-tag-height: 20px;
+@input-tag-size-default-tag-height: 24px;
+@input-tag-size-large-tag-height: 28px;
+@input-tag-size-mini-font-size: 12px;
+@input-tag-size-small-font-size: 14px;
+@input-tag-size-default-font-size: 14px;
+@input-tag-size-large-font-size: 16px;
+@input-tag-size-mini-padding_no_tag: 8px;
+@input-tag-size-small-padding_no_tag: 12px;
+@input-tag-size-default-padding_no_tag: 12px;
+@input-tag-size-large-padding_no_tag: 16px;
+@input-tag-color-text_default: var(--color-text-1);
+@input-tag-color-text_error: var(--color-text-1);
+@input-tag-color-text_disabled: var(--color-text-4);
+@input-tag-color-placeholder: var(--color-text-3);
+@input-tag-color-icon-clear: var(--color-text-2);
+@input-tag-color-icon-clear-bg_hover: var(--color-fill-4);
+@input-tag-color-border_default: transparent;
+@input-tag-color-border_default_hover: transparent;
+@input-tag-color-border_default_focus: rgb(var(--primary-6));
+@input-tag-color-border_error: transparent;
+@input-tag-color-border_error_hover: transparent;
+@input-tag-color-border_error_focus: rgb(var(--danger-6));
+@input-tag-color-border_disabled: transparent;
+@input-tag-color-border_disabled_hover: transparent;
+@input-tag-color-border_disabled_focus: transparent;
+@input-tag-color-bg_default: var(--color-fill-2);
+@input-tag-color-bg_default_hover: var(--color-fill-3);
+@input-tag-color-bg_default_focus: var(--color-bg-2);
+@input-tag-color-bg_error: rgb(var(--danger-1));
+@input-tag-color-bg_error_hover: rgb(var(--danger-2));
+@input-tag-color-bg_error_focus: var(--color-bg-2);
+@input-tag-color-bg_disabled: var(--color-fill-2);
+@input-tag-color-bg_disabled_hover: var(--color-fill-2);
+@input-tag-color-shadow_default_focus: rgb(var(--primary-2));
+@input-tag-color-shadow_error_focus: rgb(var(--danger-2));
+@input-tag-size-shadow_error_focus: 0;
+@input-tag-size-shadow_default_focus: 0;
+@input-tag-tag-margin-right: 4px;
+@input-tag-tag-margin-vertical: 2px;
+@input-tag-padding-horizontal: 4px;
+@input-tag-border-radius: var(--border-radius-small);
+@input-tag-border-width: 1px;
+@input-tag-size-icon-clear: 12px;
+@input-tag-size-icon-clear_hover: 20px;
+@input-tag-tag-font-size: 12px;
+@input-tag-tag-color-bg: var(--color-bg-2);
+@input-tag-tag-color-bg_focus: var(--color-fill-2);
+@input-tag-tag-color-bg_disabled: var(--color-fill-2);
+@input-tag-tag-color-border: var(--color-fill-3);
+@input-tag-tag-color-border_disabled: var(--color-fill-3);
+@input-tag-tag-color-border_focus: var(--color-fill-2);
+@input-tag-tag-remove-icon-color-bg: var(--color-fill-2);
+@input-tag-tag-remove-icon-color-bg_focus: var(--color-fill-3);
+@input-tag-color-text_warning: var(--color-text-1);
+@input-tag-color-border_warning: transparent;
+@input-tag-color-border_warning_hover: transparent;
+@input-tag-color-border_warning_focus: rgb(var(--warning-6));
+@input-tag-color-bg_warning: var(--color-warning-light-1);
+@input-tag-color-bg_warning_hover: var(--color-warning-light-2);
+@input-tag-color-bg_warning_focus: var(--color-bg-2);
+@input-tag-color-shadow_warning_focus: var(--color-warning-light-2);
+@input-tag-size-shadow_warning_focus: 0;
+@input-tag-addon-padding-horizontal: 12px;
+@input-tag-color-addon-bg: var(--color-fill-2);
+@input-tag-color-addon-border: var(--color-border-2);
+@input-tag-color-addon-border_default: transparent;
+@input-tag-border-addon-separator-width: 1px;
+@input-tag-color-addon-text: var(--color-text-1);
+@input-tag-prefix-cls: ~'@{prefix}-input-tag';
+@input-prefix-cls: ~'@{prefix}-input';
+@input-number-border-radius: var(--border-radius-small);
+@input-number-step-layer-border-radius: 1px;
+@input-number-size-mini-step-button-width: 24px;
+@input-number-size-small-step-button-width: 28px;
+@input-number-size-default-step-button-width: 32px;
+@input-number-size-large-step-button-width: 36px;
+@input-number-step-button-color: var(--color-text-2);
+@input-number-step-button-color_disabled: var(--color-text-4);
+@input-number-step-button-color-border: var(--color-neutral-3);
+@input-number-step-button-color-bg_default: var(--color-fill-2);
+@input-number-step-button-color-bg_default_hover: var(--color-fill-3);
+@input-number-step-button-color-bg_default_active: var(--color-fill-4);
+@input-number-step-button-color-bg_disabled: var(--color-fill-2);
+@input-number-step-button-color-bg_disabled_hover: var(--color-fill-2);
+@input-number-step-button-color-bg_disabled_active: var(--color-fill-2);
+@input-number-color-illegal_value: rgb(var(--danger-6));
+@input-number-prefix-cls: ~'@{prefix}-input-number';
+
+
+/*********** textarea ***********/
+
+@textarea-color-tip-text: var(--color-text-3);
+@textarea-padding-horizontal: 12px;
+@textarea-padding-vertical: 4px;
+@textarea-font-size: 14px;
+@textarea-font-tip-size: 12px;
+@textarea-layout-tip-right: 10px;
+@textarea-layout-tip-bottom: 6px;
+@textarea-size-min-height: 32px;
+@textarea-size-icon-clear: 12px;
+@textarea-layout-top-icon-clear: 10px;
+@textarea-prefix-cls: ~'@{prefix}-textarea';
+
+
+/*********** search ***********/
+
+@search-color-icon: var(--color-text-2);
+@search-button-color-text: var(--color-white);
+@search-size-icon: 14px;
+@search-button-padding-horizontal: 8px;
+
+
+/*********** password ***********/
+
+@password-color-eye-icon: var(--color-text-2);
+@password-size-eye-icon: 12px;
+
+
+/*********** picker ***********/
+
+@picker-size-mini: 24px;
+@picker-size-small: 28px;
+@picker-size-default: 32px;
+@picker-size-large: 36px;
+@picker-size-mini-font-size-text: 12px;
+@picker-size-small-font-size-text: 14px;
+@picker-size-default-font-size-text: 14px;
+@picker-size-large-font-size-text: 14px;
+@picker-input-border-radius: var(--border-radius-small);
+@picker-color-shadow_focus: var(--color-primary-light-2);
+@picker-size-shadow_focus: 0;
+@picker-color-shadow_error_focus: var(--color-danger-light-2);
+@picker-size-shadow_error_focus: 0;
+@picker-color-bg: var(--color-fill-2);
+@picker-color-bg_hover: var(--color-fill-3);
+@picker-color-bg_focus: var(--color-bg-2);
+@picker-color-bg_disabled: var(--color-fill-2);
+@picker-color-bg_error: var(--color-danger-light-1);
+@picker-color-bg_error_hover: var(--color-danger-light-2);
+@picker-color-bg_error_focus: var(--color-bg-2);
+@picker-color-border: transparent;
+@picker-color-border_hover: transparent;
+@picker-color-border_focus: rgb(var(--primary-6));
+@picker-color-border_disabled: transparent;
+@picker-color-border_error: transparent;
+@picker-color-border_error_hover: transparent;
+@picker-color-border_error_focus: rgb(var(--danger-6));
+@picker-color-placeholder: var(--color-text-3);
+@picker-color-placeholder_disabled: var(--color-text-4);
+@picker-color-text: var(--color-text-1);
+@picker-color-text_disabled: var(--color-text-4);
+@picker-color-icon: var(--color-text-2);
+@picker-color-icon_disabled: var(--color-text-4);
+@picker-color-separator: var(--color-text-3);
+@picker-color-separator_disabled: var(--color-text-4);
+@picker-range-color-bg-input_focus: var(--color-primary-light-1);
+@picker-padding-horizontal: 4px;
+@picker-input-padding-horizontal: 8px;
+@picker-color-shadow_warning_focus: var(--color-warning-light-2);
+@picker-size-shadow_warning_focus: 0;
+@picker-color-bg_warning: var(--color-warning-light-1);
+@picker-color-bg_warning_hover: var(--color-warning-light-2);
+@picker-color-bg_warning_focus: var(--color-bg-2);
+@picker-color-border_warning: transparent;
+@picker-color-border_warning_hover: transparent;
+@picker-color-border_warning_focus: rgb(var(--warning-6));
+@picker-container-border-radius: var(--border-radius-medium);
+@picker-header-color-text: var(--color-text-1);
+@picker-header-font-weight-text: 500;
+@picker-header-font-size: 14px;
+@picker-header-padding-horizontal: 24px;
+@picker-header-padding-vertical: 24px;
+@picker-panel-border-width: 1px;
+@picker-panel-date-width: 265px;
+@picker-panel-month-width: 265px;
+@picker-panel-year-width: 265px;
+@picker-panel-week-width: 298px;
+@picker-panel-quarter-width: 265px;
+@picker-panel-time-cell-width: 36px;
+@picker-panel-time-cell-spacing-horizontal: 4px;
+@picker-panel-time-padding-horizontal: 10px;
+@picker-panel-cell-padding-vertical: 4px;
+@picker-panel-cell-circle-height: 24px;
+@picker-panel-row-padding-vertical: 2px;
+@picker-color-switch-icon: var(--color-text-2);
+@picker-color-bg-switch-icon: var(--color-bg-popup);
+@picker-color-bg-switch-icon_hover: var(--color-fill-3);
+@picker-cell-font-weight-in-view: 500;
+@picker-color-cell-text-in-view: var(--color-text-1);
+@picker-cell-font-weight-not-in-view: 500;
+@picker-color-cell-text-not-in-view: var(--color-text-4);
+@picker-color-bg-circle_selected: rgb(var(--primary-6));
+@picker-color-bg-cell-in-range: var(--color-primary-light-1);
+@picker-color-bg-cell-disabled: var(--color-fill-1);
+@picker-color-text-cell-range-boundary: var(--color-white);
+@picker-color-bg-cell-range-boundary: rgb(var(--primary-6));
+@picker-color-bg-cell-hover-in-range: var(--color-primary-light-2);
+@picker-color-text-cell-hover-range-boundary: var(--color-text-1);
+@picker-color-bg-cell-hover-range-boundary: var(--color-primary-light-2);
+@picker-color-text-week-list-item: var(--color-text-2);
+@picker-font-weight-week-list-item: 500;
+@picker-panel-color-border: var(--color-neutral-3);
+@picker-panel-color-text-cell_hover: var(--color-text-1);
+@picker-panel-color-bg-cell_hover: var(--color-fill-3);
+@picker-panel-color-text-cell_selected: var(--color-white);
+@picker-panel-color-bg-cell_selected: rgb(var(--primary-6));
+@picker-panel-color-current-time-dot: rgb(var(--primary-6));
+@picker-panel-color-text-holder: var(--color-text-3);
+@picker-panel-color-text-holder_active: var(--color-text-1);
+@picker-panel-color-bg-label_hover: var(--color-fill-3);
+@picker-panel-border-radius-cell_selected: 24px;
+@picker-panel-cell-boundary-border-radius: 24px;
+@picker-prefix-cls: ~'@{prefix}-picker';
+
+
+/*********** affix ***********/
+
+@affix-prefix-cls: ~'@{prefix}-affix';
+
+
+/*********** alert ***********/
+
+@alert-border-width: 1px;
+@alert-margin-close-icon-left: 8px;
+@alert-margin-icon-right: 8px;
+@alert-margin-action-right: 8px;
+@alert-margin-action-left: 8px;
+@alert-border-radius: var(--border-radius-small);
+@alert-line-height: 1.5715;
+@alert-title-line-height: 1.5;
+@alert-title-margin-bottom: 4px;
+@alert-padding-horizontal: 16px;
+@alert-padding-vertical: 9px;
+@alert-padding-horizontal_with_title: 16px;
+@alert-padding-vertical_with_title: 16px;
+@alert-font-weight-title: 500;
+@alert-font-size-text-title: 16px;
+@alert-font-size-text-content: 14px;
+@alert-font-size-icon: 16px;
+@alert-font-size-icon_with_title: 18px;
+@alert-font-size-close-icon: 12px;
+@alert-color-close-icon: var(--color-text-2);
+@alert-color-close-icon_hover: var(--color-text-1);
+@alert-info-color-bg: var(--color-primary-light-1);
+@alert-info-color-border: transparent;
+@alert-info-color-icon: rgb(var(--primary-6));
+@alert-info-color-text-title: var(--color-text-1);
+@alert-info-color-text-content: var(--color-text-1);
+@alert-info-color-text-content_title: var(--color-text-2);
+@alert-warning-color-bg: var(--color-warning-light-1);
+@alert-warning-color-border: transparent;
+@alert-warning-color-icon: rgb(var(--warning-6));
+@alert-warning-color-text-title: var(--color-text-1);
+@alert-warning-color-text-content: var(--color-text-1);
+@alert-warning-color-text-content_title: var(--color-text-2);
+@alert-error-color-bg: var(--color-danger-light-1);
+@alert-error-color-border: transparent;
+@alert-error-color-icon: rgb(var(--danger-6));
+@alert-error-color-text-title: var(--color-text-1);
+@alert-error-color-text-content: var(--color-text-1);
+@alert-error-color-text-content_title: var(--color-text-2);
+@alert-success-color-bg: var(--color-success-light-1);
+@alert-success-color-border: transparent;
+@alert-success-color-icon: rgb(var(--success-6));
+@alert-success-color-text-title: var(--color-text-1);
+@alert-success-color-text-content: var(--color-text-1);
+@alert-success-color-text-content_title: var(--color-text-2);
+@alert-prefix-cls: ~'@{prefix}-alert';
+
+
+/*********** anchor ***********/
+
+@anchor-width: 150px;
+@anchor-line-width: 2px;
+@anchor-line-slider-height: 12px;
+@anchor-line-margin-right: 12px;
+@anchor-color-bg-line: var(--color-fill-3);
+@anchor-color-bg-line_active: rgb(var(--primary-6));
+@anchor-border-radius-title-hover: var(--border-radius-small);
+@anchor-item-inner-margin-left: 16px;
+@anchor-title-padding-horizontal: 8px;
+@anchor-title-padding-vertical: 4px;
+@anchor-title-margin-bottom: 2px;
+@anchor-title-margin-left_horizontal: 16px;
+@anchor-color-title: var(--color-text-2);
+@anchor-color-title_hover: var(--color-text-1);
+@anchor-color-title_active: var(--color-text-1);
+@anchor-font-weight-title_hover: 500;
+@anchor-font-weight-title_horizontal_hover: 400;
+@anchor-font-weight-title_active: 500;
+@anchor-color-bg-title_hover: var(--color-fill-2);
+@anchor-font-size-title: 14px;
+@anchor-lineless-color-title_active: rgb(var(--primary-6));
+@anchor-lineless-bg-title_active: var(--color-fill-2);
+@anchor-lineless-font-weight-title_active: 500;
+@anchor-color-box-shadow: rgb(var(--primary-6));
+@anchor-prefix-cls: ~'@{prefix}-anchor';
+
+
+/*********** auto ***********/
+
+@auto-complete-popup-max-height: 200px;
+@auto-complete-popup-border-radius: var(--border-radius-medium);
+@auto-complete-popup-padding-vertical: 4px;
+@auto-complete-popup-font-size: 14px;
+@auto-complete-popup-color-border: var(--color-fill-3);
+@auto-complete-popup-box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+@auto-complete-option-height: 36px;
+@auto-complete-option-font-weight_selected: 500;
+@auto-complete-option-padding-horizontal: 12px;
+@auto-complete-option-color-bg_default: var(--color-bg-popup);
+@auto-complete-option-color-bg_hover: var(--color-fill-2);
+@auto-complete-option-color-bg_selected: var(--color-bg-popup);
+@auto-complete-option-color-bg_disabled: var(--color-bg-popup);
+@auto-complete-option-color-text_default: var(--color-text-1);
+@auto-complete-option-color-text_hover: var(--color-text-1);
+@auto-complete-option-color-text_selected: var(--color-text-1);
+@auto-complete-option-color-text_disabled: var(--color-text-4);
+@auto-complete-prefix-cls: ~'@{prefix}-autocomplete';
+
+
+/*********** select ***********/
+
+@select-prefix-cls: ~'@{prefix}-select';
+@select-size-mini-height: 24px;
+@select-size-small-height: 28px;
+@select-size-default-height: 32px;
+@select-size-large-height: 36px;
+@select-size-mini-font-size: 12px;
+@select-size-small-font-size: 14px;
+@select-size-default-font-size: 14px;
+@select-size-large-font-size: 14px;
+@select-signal-size-mini-padding: 8px;
+@select-signal-size-small-padding: 12px;
+@select-signal-size-default-padding: 12px;
+@select-signal-size-large-padding: 16px;
+@select-multi-padding: 4px;
+@select-size-icon: 12px;
+@select-size-icon-bg: 16px;
+@select-border-width: 1px;
+@select-border-radius: var(--border-radius-small);
+@select-color-text_default: var(--color-text-1);
+@select-color-text_disabled: var(--color-text-4);
+@select-color-text_focused: var(--color-text-1);
+@select-color-placeholder_default: var(--color-text-3);
+@select-color-placeholder_disabled: var(--color-text-4);
+@select-color-placeholder_focused: var(--color-text-3);
+@select-color-icon_default: var(--color-text-2);
+@select-color-icon_disabled: var(--color-text-4);
+@select-color-icon_focused: var(--color-text-2);
+@select-color-icon-bg_hover: var(--color-fill-4);
+@select-color-bg_default: var(--color-fill-2);
+@select-color-bg_default_hover: var(--color-fill-3);
+@select-color-bg_default_focus: var(--color-bg-2);
+@select-color-bg_error_focus: var(--color-bg-2);
+@select-color-bg_error: var(--color-danger-light-1);
+@select-color-bg_error_hover: var(--color-danger-light-2);
+@select-color-bg_disabled: var(--color-fill-2);
+@select-color-bg_disabled_hover: var(--color-fill-2);
+@select-color-border_default: transparent;
+@select-color-border_default_hover: transparent;
+@select-color-border_default_focus: rgb(var(--primary-6));
+@select-color-border_error: transparent;
+@select-color-border_error_hover: transparent;
+@select-color-border_error_focus: rgb(var(--danger-6));
+@select-color-border_disabled: transparent;
+@select-color-border_disabled_hover: transparent;
+@select-shadow-distance_default_focus: 0;
+@select-shadow-distance_error_focus: 0;
+@select-color-shadow_default_focus: var(--color-primary-light-2);
+@select-color-shadow_error_focus: var(--color-danger-light-2);
+@select-popup-max-height: 200px;
+@select-popup-border-radius: var(--border-radius-medium);
+@select-popup-padding-vertical: 4px;
+@select-popup-padding-horizontal: 0;
+@select-popup-font-size: 14px;
+@select-popup-color-bg: var(--color-bg-popup);
+@select-popup-color-border: var(--color-fill-3);
+@select-popup-box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+@select-popup-option-height: 36px;
+@select-popup-option-font-weight_selected: 500;
+@select-signal-popup-option-padding-horizontal: 12px;
+@select-multi-popup-option-padding-horizontal: 4px;
+@select-popup-option-border-radius: 0;
+@select-popup-option-color-bg_default: var(--color-bg-popup);
+@select-popup-option-color-bg_hover: var(--color-fill-2);
+@select-popup-option-color-bg_selected: var(--color-bg-popup);
+@select-popup-option-color-bg_disabled: var(--color-bg-popup);
+@select-popup-option-color-text_default: var(--color-text-1);
+@select-popup-option-color-text_hover: var(--color-text-1);
+@select-popup-option-color-text_selected: var(--color-text-1);
+@select-popup-option-color-text_disabled: var(--color-text-4);
+@select-popup-option-color-hightlight-text: var(--color-text-1);
+@select-popup-option-font-hightlight-weight: 500;
+@select-popup-group-title-height: 20px;
+@select-popup-group-title-padding-horizontal: 12px;
+@select-popup-group-title-padding-top: 8px;
+@select-popup-group-title-font-size: 12px;
+@select-popup-group-title-color-text: var(--color-text-3);
+@select-addon-padding-horizontal: 12px;
+@select-color-addon-bg: var(--color-fill-2);
+@select-color-addon-border: var(--color-border-2);
+@select-color-addon-border_default: transparent;
+@select-border-addon-separator-width: 1px;
+@select-color-addon-text: var(--color-text-1);
+@select-color-bg_warning_focus: var(--color-bg-2);
+@select-color-bg_warning: var(--color-warning-light-1);
+@select-color-bg_warning_hover: var(--color-warning-light-2);
+@select-color-border_warning: transparent;
+@select-color-border_warning_hover: transparent;
+@select-color-border_warning_focus: rgb(var(--warning-6));
+@select-color-shadow_warning_focus: var(--color-warning-light-2);
+@select-shadow-distance_warning_focus: 0;
+@select-prefix-cls-rtl: ~'@{prefix}-select-rtl';
+
+
+/*********** avatar ***********/
+
+@avatar-size-default: 40px;
+@avatar-color-text: var(--color-white);
+@avatar-color-bg: var(--color-fill-4);
+@avatar-color-group-item-border: var(--color-bg-2);
+@avatar-group-item-border-width: 2px;
+@avatar-group-item-margin-left: -10px;
+@avatar-group-popover-item-spacing: 4px;
+@avatar-font-weight-text: 500;
+@avatar-font-size-text: 20px;
+@avatar-circle-border-radius: var(--border-radius-circle);
+@avatar-square-border-radius: var(--border-radius-medium);
+@avatar-font-size-max-count: 20px;
+@avatar-color-max-count-text: var(--color-white);
+@avatar-size-trigger-button: 20px;
+@avatar-spacing-trigger-button-right: 4px;
+@avatar-spacing-trigger-button-bottom: 4px;
+@avatar-color-trigger-button-bg: var(--color-neutral-2);
+@avatar-color-trigger-button-bg_hover: var(--color-neutral-3);
+@avatar-color-trigger-mask-icon: var(--color-white);
+@avatar-opacity-trigger-mask-bg: 60%;
+@avatar-color-trigger-icon-button: var(--color-fill-4);
+@avatar-size-trigger-icon: 12px;
+@avatar-border-trigger-button-radius: var(--border-radius-circle);
+@avatar-prefix-cls: ~'@{prefix}-avatar';
+
+
+/*********** backtop ***********/
+
+@backtop-margin-bottom: 24px;
+@backtop-margin-right: 24px;
+@backtop-button-size-width: 40px;
+@backtop-button-size-font: 12px;
+@backtop-button-size-icon: 14px;
+@backtop-button-color-bg: rgb(var(--primary-6));
+@backtop-button-color-bg_hover: rgb(var(--primary-5));
+@backtop-button-border-radius: var(--border-radius-circle);
+@backtop-button-color-text: var(--color-white);
+@backtop-prefix-cls: ~'@{prefix}-backtop';
+
+
+/*********** badge ***********/
+
+@badge-size-count-height: 20px;
+@badge-padding-count-horizontal: 6px;
+@badge-margin-status-text-left: 8px;
+@badge-font-count-size: 12px;
+@badge-font-status-text-size: 14px;
+@badge-color-count-text: var(--color-white);
+@badge-color-status-text: var(--color-text-1);
+@badge-color-count-bg: rgb(var(--danger-6));
+@badge-size-dot-width: 6px;
+@badge-color-dot-bg_default: var(--color-fill-4);
+@badge-color-dot-bg_processing: rgb(var(--primary-6));
+@badge-color-dot-bg_success: rgb(var(--success-6));
+@badge-color-dot-bg_warning: rgb(var(--warning-6));
+@badge-color-dot-bg_error: rgb(var(--danger-6));
+@badge-font-count-weight: 500;
+@badge-red-color-dot-bg: rgb(var(--danger-6));
+@badge-orangered-color-dot-bg: #f77234;
+@badge-orange-color-dot-bg: rgb(var(--orange-6));
+@badge-lime-color-dot-bg: rgb(var(--lime-6));
+@badge-gold-color-dot-bg: rgb(var(--gold-6));
+@badge-green-color-dot-bg: rgb(var(--success-6));
+@badge-cyan-color-dot-bg: rgb(var(--cyan-6));
+@badge-arcoblue-color-dot-bg: rgb(var(--primary-6));
+@badge-pinkpurple-color-dot-bg: rgb(var(--pinkpurple-6));
+@badge-purple-color-dot-bg: rgb(var(--purple-6));
+@badge-magenta-color-dot-bg: rgb(var(--magenta-6));
+@badge-gray-color-dot-bg: rgb(var(--gray-4));
+@badge-prefix-cls: ~'@{prefix}-badge';
+
+
+/*********** breadcrumb ***********/
+
+@breadcrumb-color-text: var(--color-text-2);
+@breadcrumb-color-text_active: var(--color-text-1);
+@breadcrumb-color-link-text: var(--color-text-2);
+@breadcrumb-color-separator: var(--color-text-4);
+@breadcrumb-color-bg: transparent;
+@breadcrumb-color-bg_hover: var(--color-fill-2);
+@breadcrumb-margin-separator-horizontal: 4px;
+@breadcrumb-margin-dropdown-icon-left: 4px;
+@breadcrumb-padding-text-horizontal: 4px;
+@breadcrumb-border-text-radius_hover: var(--border-radius-small);
+@breadcrumb-size-text-height: 24px;
+@breadcrumb-size-dropdown-icon: 12px;
+@breadcrumb-size-font-size: 14px;
+@breadcrumb-font-weight_active: 500;
+@breadcrumb-color-icon: var(--color-text-3);
+@breadcrumb-color-link-text_hover: rgb(var(--link-6));
+@breadcrumb-color-dropdown-icon: var(--color-text-2);
+@breadcrumb-color-box-shadow: rgb(var(--primary-6));
+@breadcrumb-prefix-cls: ~'@{prefix}-breadcrumb';
+
+
+/*********** btn ***********/
+
+@btn-font-weight: 400;
+@btn-border-radius: var(--border-radius-small);
+@btn-border-width: 1px;
+@btn-size-mini-height: 24px;
+@btn-size-small-height: 28px;
+@btn-size-default-height: 32px;
+@btn-size-large-height: 36px;
+@btn-size-mini-radius: var(--border-radius-small);
+@btn-size-small-radius: var(--border-radius-small);
+@btn-size-default-radius: var(--border-radius-small);
+@btn-size-large-radius: var(--border-radius-small);
+@btn-size-mini-border-width: 1px;
+@btn-size-small-border-width: 1px;
+@btn-size-default-border-width: 1px;
+@btn-size-large-border-width: 1px;
+@btn-size-mini-icon-spacing: 4px;
+@btn-size-small-icon-spacing: 6px;
+@btn-size-default-icon-spacing: 8px;
+@btn-size-large-icon-spacing: 8px;
+@btn-size-mini-icon-vertical-align: -2px;
+@btn-size-small-icon-vertical-align: -2px;
+@btn-size-default-icon-vertical-align: -2px;
+@btn-size-large-icon-vertical-align: -2px;
+@btn-size-mini-padding-horizontal: 11px;
+@btn-size-small-padding-horizontal: 15px;
+@btn-size-default-padding-horizontal: 15px;
+@btn-size-large-padding-horizontal: 19px;
+@btn-size-mini-font-size: 12px;
+@btn-size-small-font-size: 14px;
+@btn-size-default-font-size: 14px;
+@btn-size-large-font-size: 14px;
+@btn-outline-color-text: rgb(var(--primary-6));
+@btn-outline-color-text_disabled: var(--color-primary-light-3);
+@btn-outline-color-text_hover: rgb(var(--primary-5));
+@btn-outline-color-text_active: rgb(var(--primary-7));
+@btn-outline-color-bg: transparent;
+@btn-outline-color-bg_disabled: transparent;
+@btn-outline-color-bg_hover: transparent;
+@btn-outline-color-bg_active: transparent;
+@btn-outline-color-border: rgb(var(--primary-6));
+@btn-outline-color-border_disabled: var(--color-primary-light-3);
+@btn-outline-color-border_hover: rgb(var(--primary-5));
+@btn-outline-color-border_active: rgb(var(--primary-7));
+@btn-outline-color-text_warning: rgb(var(--warning-6));
+@btn-outline-color-text_warning_disabled: var(--color-warning-light-3);
+@btn-outline-color-text_warning_hover: rgb(var(--warning-5));
+@btn-outline-color-text_warning_active: rgb(var(--warning-7));
+@btn-outline-color-bg_warning: transparent;
+@btn-outline-color-bg_warning_disabled: transparent;
+@btn-outline-color-bg_warning_hover: transparent;
+@btn-outline-color-bg_warning_active: transparent;
+@btn-outline-color-border_warning: rgb(var(--warning-6));
+@btn-outline-color-border_warning_disabled: var(--color-warning-light-3);
+@btn-outline-color-border_warning_hover: rgb(var(--warning-5));
+@btn-outline-color-border_warning_active: rgb(var(--warning-7));
+@btn-outline-color-text_danger: rgb(var(--danger-6));
+@btn-outline-color-text_danger_disabled: var(--color-danger-light-3);
+@btn-outline-color-text_danger_hover: rgb(var(--danger-5));
+@btn-outline-color-text_danger_active: rgb(var(--danger-7));
+@btn-outline-color-bg_danger: transparent;
+@btn-outline-color-bg_danger_disabled: transparent;
+@btn-outline-color-bg_danger_hover: transparent;
+@btn-outline-color-bg_danger_active: transparent;
+@btn-outline-color-border_danger: rgb(var(--danger-6));
+@btn-outline-color-border_danger_disabled: var(--color-danger-light-3);
+@btn-outline-color-border_danger_hover: rgb(var(--danger-5));
+@btn-outline-color-border_danger_active: rgb(var(--danger-7));
+@btn-outline-color-text_success: rgb(var(--success-6));
+@btn-outline-color-text_success_disabled: var(--color-success-light-3);
+@btn-outline-color-text_success_hover: rgb(var(--success-5));
+@btn-outline-color-text_success_active: rgb(var(--success-7));
+@btn-outline-color-bg_success: transparent;
+@btn-outline-color-bg_success_disabled: transparent;
+@btn-outline-color-bg_success_hover: transparent;
+@btn-outline-color-bg_success_active: transparent;
+@btn-outline-color-border_success: rgb(var(--success-6));
+@btn-outline-color-border_success_disabled: var(--color-success-light-3);
+@btn-outline-color-border_success_hover: rgb(var(--success-5));
+@btn-outline-color-border_success_active: rgb(var(--success-7));
+@btn-outline-border-style: solid;
+@btn-primary-color-text: #fff;
+@btn-primary-color-text_disabled: #fff;
+@btn-primary-color-text_hover: #fff;
+@btn-primary-color-text_active: #fff;
+@btn-primary-color-bg: rgb(var(--primary-6));
+@btn-primary-color-bg_disabled: var(--color-primary-light-3);
+@btn-primary-color-bg_hover: rgb(var(--primary-5));
+@btn-primary-color-bg_active: rgb(var(--primary-7));
+@btn-primary-color-border: transparent;
+@btn-primary-color-border_disabled: transparent;
+@btn-primary-color-border_hover: transparent;
+@btn-primary-color-border_active: transparent;
+@btn-primary-color-text_warning: #fff;
+@btn-primary-color-text_warning_disabled: #fff;
+@btn-primary-color-text_warning_hover: #fff;
+@btn-primary-color-text_warning_active: #fff;
+@btn-primary-color-bg_warning: rgb(var(--warning-6));
+@btn-primary-color-bg_warning_disabled: var(--color-warning-light-3);
+@btn-primary-color-bg_warning_hover: rgb(var(--warning-5));
+@btn-primary-color-bg_warning_active: rgb(var(--warning-7));
+@btn-primary-color-border_warning: transparent;
+@btn-primary-color-border_warning_disabled: transparent;
+@btn-primary-color-border_warning_hover: transparent;
+@btn-primary-color-border_warning_active: transparent;
+@btn-primary-color-text_danger: #fff;
+@btn-primary-color-text_danger_disabled: #fff;
+@btn-primary-color-text_danger_hover: #fff;
+@btn-primary-color-text_danger_active: #fff;
+@btn-primary-color-bg_danger: rgb(var(--danger-6));
+@btn-primary-color-bg_danger_disabled: var(--color-danger-light-3);
+@btn-primary-color-bg_danger_hover: rgb(var(--danger-5));
+@btn-primary-color-bg_danger_active: rgb(var(--danger-7));
+@btn-primary-color-border_danger: transparent;
+@btn-primary-color-border_danger_disabled: transparent;
+@btn-primary-color-border_danger_hover: transparent;
+@btn-primary-color-border_danger_active: transparent;
+@btn-primary-color-text_success: #fff;
+@btn-primary-color-text_success_disabled: #fff;
+@btn-primary-color-text_success_hover: #fff;
+@btn-primary-color-text_success_active: #fff;
+@btn-primary-color-bg_success: rgb(var(--success-6));
+@btn-primary-color-bg_success_disabled: var(--color-success-light-3);
+@btn-primary-color-bg_success_hover: rgb(var(--success-5));
+@btn-primary-color-bg_success_active: rgb(var(--success-7));
+@btn-primary-color-border_success: transparent;
+@btn-primary-color-border_success_disabled: transparent;
+@btn-primary-color-border_success_hover: transparent;
+@btn-primary-color-border_success_active: transparent;
+@btn-primary-border-style: solid;
+@btn-secondary-color-text: var(--color-text-2);
+@btn-secondary-color-text_disabled: var(--color-text-4);
+@btn-secondary-color-text_hover: var(--color-text-2);
+@btn-secondary-color-text_active: var(--color-text-2);
+@btn-secondary-color-bg: var(--color-secondary);
+@btn-secondary-color-bg_disabled: var(--color-secondary-disabled);
+@btn-secondary-color-bg_hover: var(--color-secondary-hover);
+@btn-secondary-color-bg_active: var(--color-secondary-active);
+@btn-secondary-color-border: transparent;
+@btn-secondary-color-border_disabled: transparent;
+@btn-secondary-color-border_hover: transparent;
+@btn-secondary-color-border_active: transparent;
+@btn-secondary-color-text_warning: rgb(var(--warning-6));
+@btn-secondary-color-text_warning_disabled: var(--color-warning-light-3);
+@btn-secondary-color-text_warning_hover: rgb(var(--warning-6));
+@btn-secondary-color-text_warning_active: rgb(var(--warning-6));
+@btn-secondary-color-bg_warning: var(--color-warning-light-1);
+@btn-secondary-color-bg_warning_disabled: var(--color-warning-light-1);
+@btn-secondary-color-bg_warning_hover: var(--color-warning-light-2);
+@btn-secondary-color-bg_warning_active: var(--color-warning-light-3);
+@btn-secondary-color-border_warning: transparent;
+@btn-secondary-color-border_warning_disabled: transparent;
+@btn-secondary-color-border_warning_hover: transparent;
+@btn-secondary-color-border_warning_active: transparent;
+@btn-secondary-color-text_danger: rgb(var(--danger-6));
+@btn-secondary-color-text_danger_disabled: var(--color-danger-light-3);
+@btn-secondary-color-text_danger_hover: rgb(var(--danger-6));
+@btn-secondary-color-text_danger_active: rgb(var(--danger-6));
+@btn-secondary-color-bg_danger: var(--color-danger-light-1);
+@btn-secondary-color-bg_danger_disabled: var(--color-danger-light-1);
+@btn-secondary-color-bg_danger_hover: var(--color-danger-light-2);
+@btn-secondary-color-bg_danger_active: var(--color-danger-light-3);
+@btn-secondary-color-border_danger: transparent;
+@btn-secondary-color-border_danger_disabled: transparent;
+@btn-secondary-color-border_danger_hover: transparent;
+@btn-secondary-color-border_danger_active: transparent;
+@btn-secondary-color-text_success: rgb(var(--success-6));
+@btn-secondary-color-text_success_disabled: var(--color-success-light-3);
+@btn-secondary-color-text_success_hover: rgb(var(--success-6));
+@btn-secondary-color-text_success_active: rgb(var(--success-6));
+@btn-secondary-color-bg_success: var(--color-success-light-1);
+@btn-secondary-color-bg_success_disabled: var(--color-success-light-1);
+@btn-secondary-color-bg_success_hover: var(--color-success-light-2);
+@btn-secondary-color-bg_success_active: var(--color-success-light-3);
+@btn-secondary-color-border_success: transparent;
+@btn-secondary-color-border_success_disabled: transparent;
+@btn-secondary-color-border_success_hover: transparent;
+@btn-secondary-color-border_success_active: transparent;
+@btn-secondary-border-style: solid;
+@btn-dashed-color-text: var(--color-text-2);
+@btn-dashed-color-text_disabled: var(--color-text-4);
+@btn-dashed-color-text_hover: var(--color-text-2);
+@btn-dashed-color-text_active: var(--color-text-2);
+@btn-dashed-color-bg: var(--color-fill-2);
+@btn-dashed-color-bg_disabled: var(--color-fill-2);
+@btn-dashed-color-bg_hover: var(--color-fill-3);
+@btn-dashed-color-bg_active: var(--color-fill-4);
+@btn-dashed-color-border: var(--color-neutral-3);
+@btn-dashed-color-border_disabled: var(--color-neutral-3);
+@btn-dashed-color-border_hover: var(--color-neutral-4);
+@btn-dashed-color-border_active: var(--color-neutral-5);
+@btn-dashed-color-text_warning: rgb(var(--warning-6));
+@btn-dashed-color-text_warning_disabled: var(--color-warning-light-3);
+@btn-dashed-color-text_warning_hover: rgb(var(--warning-6));
+@btn-dashed-color-text_warning_active: rgb(var(--warning-6));
+@btn-dashed-color-bg_warning: var(--color-warning-light-1);
+@btn-dashed-color-bg_warning_disabled: var(--color-warning-light-1);
+@btn-dashed-color-bg_warning_hover: var(--color-warning-light-2);
+@btn-dashed-color-bg_warning_active: var(--color-warning-light-3);
+@btn-dashed-color-border_warning: var(--color-warning-light-2);
+@btn-dashed-color-border_warning_disabled: var(--color-warning-light-2);
+@btn-dashed-color-border_warning_hover: var(--color-warning-light-3);
+@btn-dashed-color-border_warning_active: var(--color-warning-light-4);
+@btn-dashed-color-text_danger: rgb(var(--danger-6));
+@btn-dashed-color-text_danger_disabled: var(--color-danger-light-3);
+@btn-dashed-color-text_danger_hover: rgb(var(--danger-6));
+@btn-dashed-color-text_danger_active: rgb(var(--danger-6));
+@btn-dashed-color-bg_danger: var(--color-danger-light-1);
+@btn-dashed-color-bg_danger_disabled: var(--color-danger-light-1);
+@btn-dashed-color-bg_danger_hover: var(--color-danger-light-2);
+@btn-dashed-color-bg_danger_active: var(--color-danger-light-3);
+@btn-dashed-color-border_danger: var(--color-danger-light-2);
+@btn-dashed-color-border_danger_disabled: var(--color-danger-light-2);
+@btn-dashed-color-border_danger_hover: var(--color-danger-light-3);
+@btn-dashed-color-border_danger_active: var(--color-danger-light-4);
+@btn-dashed-color-text_success: rgb(var(--success-6));
+@btn-dashed-color-text_success_disabled: var(--color-success-light-3);
+@btn-dashed-color-text_success_hover: rgb(var(--success-6));
+@btn-dashed-color-text_success_active: rgb(var(--success-6));
+@btn-dashed-color-bg_success: var(--color-success-light-1);
+@btn-dashed-color-bg_success_disabled: var(--color-success-light-1);
+@btn-dashed-color-bg_success_hover: var(--color-success-light-2);
+@btn-dashed-color-bg_success_active: var(--color-success-light-3);
+@btn-dashed-color-border_success: var(--color-success-light-2);
+@btn-dashed-color-border_success_disabled: var(--color-success-light-2);
+@btn-dashed-color-border_success_hover: var(--color-success-light-3);
+@btn-dashed-color-border_success_active: var(--color-success-light-4);
+@btn-dashed-border-style: dashed;
+@btn-text-color-text: rgb(var(--primary-6));
+@btn-text-color-text_disabled: var(--color-primary-light-3);
+@btn-text-color-text_hover: rgb(var(--primary-6));
+@btn-text-color-text_active: rgb(var(--primary-6));
+@btn-text-color-bg: transparent;
+@btn-text-color-bg_disabled: transparent;
+@btn-text-color-bg_hover: var(--color-fill-2);
+@btn-text-color-bg_active: var(--color-fill-3);
+@btn-text-color-border: transparent;
+@btn-text-color-border_disabled: transparent;
+@btn-text-color-border_hover: transparent;
+@btn-text-color-border_active: transparent;
+@btn-text-color-text_warning: rgb(var(--warning-6));
+@btn-text-color-text_warning_disabled: var(--color-warning-light-3);
+@btn-text-color-text_warning_hover: rgb(var(--warning-6));
+@btn-text-color-text_warning_active: rgb(var(--warning-6));
+@btn-text-color-bg_warning: transparent;
+@btn-text-color-bg_warning_disabled: transparent;
+@btn-text-color-bg_warning_hover: var(--color-fill-2);
+@btn-text-color-bg_warning_active: var(--color-fill-3);
+@btn-text-color-border_warning: transparent;
+@btn-text-color-border_warning_disabled: transparent;
+@btn-text-color-border_warning_hover: transparent;
+@btn-text-color-border_warning_active: transparent;
+@btn-text-color-text_danger: rgb(var(--danger-6));
+@btn-text-color-text_danger_disabled: var(--color-danger-light-3);
+@btn-text-color-text_danger_hover: rgb(var(--danger-6));
+@btn-text-color-text_danger_active: rgb(var(--danger-6));
+@btn-text-color-bg_danger: transparent;
+@btn-text-color-bg_danger_disabled: transparent;
+@btn-text-color-bg_danger_hover: var(--color-fill-2);
+@btn-text-color-bg_danger_active: var(--color-fill-3);
+@btn-text-color-border_danger: transparent;
+@btn-text-color-border_danger_disabled: transparent;
+@btn-text-color-border_danger_hover: transparent;
+@btn-text-color-border_danger_active: transparent;
+@btn-text-color-text_success: rgb(var(--success-6));
+@btn-text-color-text_success_disabled: var(--color-success-light-3);
+@btn-text-color-text_success_hover: rgb(var(--success-6));
+@btn-text-color-text_success_active: rgb(var(--success-6));
+@btn-text-color-bg_success: transparent;
+@btn-text-color-bg_success_disabled: transparent;
+@btn-text-color-bg_success_hover: var(--color-fill-2);
+@btn-text-color-bg_success_active: var(--color-fill-3);
+@btn-text-color-border_success: transparent;
+@btn-text-color-border_success_disabled: transparent;
+@btn-text-color-border_success_hover: transparent;
+@btn-text-color-border_success_active: transparent;
+@btn-text-border-style: solid;
+@btn-box-shadow-radius: 2px;
+@btn-primary-color-box-shadow: rgb(var(--primary-3));
+@btn-outline-color-box-shadow: rgb(var(--primary-3));
+@btn-secondary-color-box-shadow: var(--color-neutral-4);
+@btn-dashed-color-box-shadow: var(--color-neutral-4);
+@btn-text-color-box-shadow: var(--color-neutral-4);
+@btn-color-box-shadow_warning: rgb(var(--warning-3));
+@btn-color-box-shadow_danger: rgb(var(--danger-3));
+@btn-color-box-shadow_success: rgb(var(--success-3));
+@btn-prefix-cls: ~'@{prefix}-btn';
+
+
+/*********** calendar ***********/
+
+@calendar-color-border: var(--color-neutral-3);
+@calendar-header-padding-horizontal: 24px;
+@calendar-header-padding-vertical: 24px;
+@calendar-panel-date-cell-padding-vertical: 4px;
+@calendar-panel-date-cell-circle-height: 24px;
+@calendar-panel-year-cell-padding-vertical: 4px;
+@calendar-panel-year-cell-circle-height: 24px;
+@calendar-color-switch-icon: var(--color-text-2);
+@calendar-color-bg-switch-icon: var(--color-bg-5);
+@calendar-color-bg-switch-icon_hover: var(--color-fill-3);
+@calendar-color-text-title: var(--color-text-1);
+@calendar-color-cell-text-in-view: var(--color-text-1);
+@calendar-color-cell-text-not-in-view: var(--color-text-4);
+@calendar-color-bg-circle_selected: rgb(var(--primary-6));
+@calendar-color-bg-cell-in-range: var(--color-primary-light-1);
+@calendar-color-bg-cell-disabled: var(--color-fill-1);
+@calendar-color-text-cell-range-boundary: var(--color-white);
+@calendar-color-bg-cell-range-boundary: rgb(var(--primary-6));
+@calendar-color-bg-cell-hover-in-range: var(--color-primary-light-1);
+@calendar-color-text-cell-hover-range-boundary: var(--color-text-1);
+@calendar-color-bg-cell-hover-range-boundary: var(--color-primary-light-2);
+@calendar-panel-color-text-cell_hover: rgb(var(--primary-6));
+@calendar-panel-color-bg-cell_hover: var(--color-primary-light-1);
+@calendar-panel-color-text-cell_selected: var(--color-white);
+@calendar-panel-color-bg-cell_selected: rgb(var(--primary-6));
+@calendar-panel-color-current-time-dot: rgb(var(--primary-6));
+@calendar-panel-cell-boundary-border-radius: 16px;
+@calendar-color-box-shadow: var(--color-primary-light-3);
+@calendar-prefix-cls: ~'@{prefix}-calendar';
+
+
+/*********** card ***********/
+
+@card-size-small-height-title: 40px;
+@card-size-small-font-size-title: 16px;
+@card-size-small-font-size-title-extra: 14px;
+@card-size-small-font-size: 14px;
+@card-size-small-padding-horizontal-title: 16px;
+@card-size-small-padding-horizontal-body: 16px;
+@card-size-small-padding-vertical-body: 12px;
+@card-size-default-height-title: 46px;
+@card-size-default-font-size-title: 16px;
+@card-size-default-font-size-title-extra: 14px;
+@card-size-default-font-size: 14px;
+@card-size-default-padding-horizontal-title: 16px;
+@card-size-default-padding-horizontal-body: 16px;
+@card-size-default-padding-vertical-body: 16px;
+@card-line-height: 1.5715;
+@card-font-weight-title: 500;
+@card-margin-top-meta-footer: 20px;
+@card-margin-top-meta-description: 4px;
+@card-margin-right-action-item: 12px;
+@card-color-bg: var(--color-bg-2);
+@card-color-border: var(--color-neutral-3);
+@card-color-title: var(--color-text-1);
+@card-color-title-extra: rgb(var(--primary-6));
+@card-color-body: var(--color-text-2);
+@card-color-action: var(--color-text-2);
+@card-color-action_hover: rgb(var(--primary-6));
+@card-color-box-shadow: rgb(var(--gray-2));
+@card-color-box-shadow_dark: rgba(var(--gray-1), 40%);
+@card-border-width: 1px;
+@card-border-width-title-bottom: 1px;
+@card-border-radius: var(--border-radius-small);
+@card-border-radius-no-border: var(--border-radius-none);
+@card-prefix-cls: ~'@{prefix}-card';
+
+
+/*********** carousel ***********/
+
+@carousel-content-border-radius: 0;
+@carousel-arrow-position: 12px;
+@carousel-arrow-size: 24px;
+@carousel-arrow-font-size: 14px;
+@carousel-arrow-color-icon: var(--color-white);
+@carousel-arrow-color-bg: rgba(255, 255, 255, 0.3);
+@carousel-arrow-color-bg_hover: rgba(255, 255, 255, 0.5);
+@carousel-arrow-color-box-shadow: var(--color-primary-light-3);
+@carousel-indicator-size-wrapper: 48px;
+@carousel-indicator-color-bg-wrapper: rgba(0, 0, 0, 0.15);
+@carousel-indicator-dot-size: 6px;
+@carousel-indicator-line-size-width: 12px;
+@carousel-indicator-line-size-height: 4px;
+@carousel-indicator-slider-size-width: 48px;
+@carousel-indicator-slider-size-height: 4px;
+@carousel-indicator-position: 12px;
+@carousel-indicator-gap: 8px;
+@carousel-indicator-border-radius: var(--border-radius-medium);
+@carousel-indicator-color_default: rgba(255, 255, 255, 0.3);
+@carousel-indicator-color_active: var(--color-white);
+@carousel-indicator-outer-border-radius: 20px;
+@carousel-indicator-outer-padding: 4px;
+@carousel-indicator-outer-color_default: rgba(var(--gray-4), 0.5);
+@carousel-indicator-outer-color_active: var(--color-fill-4);
+@carousel-indicator-outer-color-bg: transparent;
+@carousel-prefix-cls: ~'@{prefix}-carousel';
+
+
+/*********** cascader ***********/
+
+@cascader-size-item-height: 36px;
+@cascader-size-item-width: 120px;
+@cascader-font-item-size: 14px;
+@cascader-margin-item-icon-left: 12px;
+@cascader-color-item-text: var(--color-text-1);
+@cascader-color-item-icon: var(--color-text-2);
+@cascader-padding-item-left: 12px;
+@cascader-padding-item-right: 10px;
+@cascader-size-item-icon: 12px;
+@cascader-color-item-text_hover: var(--color-text-1);
+@cascader-color-item-text_active: var(--color-text-1);
+@cascader-color-item-text_disabled: var(--color-text-4);
+@cascader-color-item-text_disabled_active: var(--color-text-4);
+@cascader-font-item-weight_active: 500;
+@cascader-color-item-bg_active: var(--color-fill-2);
+@cascader-color-item-bg_hover: var(--color-fill-2);
+@cascader-color-item-bg_disabled: transparent;
+@cascader-color-item-bg_disabled_active: var(--color-fill-2);
+@cascader-color-checkbox-bg_hover: var(--color-fill-3);
+@cascader-margin-checkbox-right: 8px;
+@cascader-prefix-cls: ~'@{prefix}-cascader';
+@cascader-prefix-cls-rtl: ~'@{prefix}-cascader-rtl';
+
+
+/*********** checkbox ***********/
+
+@checkbox-prefix-cls: ~'@{prefix}-checkbox';
+@checkbox-mask-border-width: 2px;
+@checkbox-mask-border-style: solid;
+@checkbox-mask-border-radius: var(--border-radius-small);
+@checkbox-mask-height: 14px;
+@checkbox-mask-bg-height: 24px;
+@checkbox-mask-bg-color-bg: var(--color-fill-2);
+@checkbox-mask-color-bg: var(--color-bg-2);
+@checkbox-mask-color-bg_checked: rgb(var(--primary-6));
+@checkbox-mask-color-bg_disabled: var(--color-fill-2);
+@checkbox-mask-color-bg_checked_disabled: var(--color-primary-light-3);
+@checkbox-mask-color-border: var(--color-fill-3);
+@checkbox-mask-color-border_hover: var(--color-fill-4);
+@checkbox-mask-color-border_checked: transparent;
+@checkbox-mask-color-border_checked_disabled: transparent;
+@checkbox-mask-color-border_disabled: var(--color-fill-3);
+@checkbox-color-text: var(--color-text-1);
+@checkbox-color-text_disabled: var(--color-text-4);
+@checkbox-group-spacing: 16px;
+@checkbox-text-mask-spacing: 8px;
+@checkbox-text-font-size: 14px;
+@checkbox-group-size-line-height_vertical: 32px;
+@checkbox-size-check-icon: 8px;
+@checkbox-color-check-icon: var(--color-white);
+@checkbox-color-check-icon_disabled: var(--color-fill-2);
+@checkbox-color-indeterminate-icon-width: 6px;
+@checkbox-color-indeterminate-icon-height: 2px;
+@checkbox-color-indeterminate-icon: var(--color-white);
+
+
+/*********** collapse ***********/
+
+@collapse-border-width: 1px;
+@collapse-border-radius: var(--border-radius-medium);
+@collapse-color-border: var(--color-neutral-3);
+@collapse-line-height: 1.5715;
+@collapse-title-line-height: 24px;
+@collapse-title-border-width: 1px;
+@collapse-title-color-border: var(--color-neutral-3);
+@collapse-title-font-size: 14px;
+@collapse-title-padding-horizontal: 13px;
+@collapse-title-padding-vertical: 8px;
+@collapse-title-color-bg: var(--color-bg-2);
+@collapse-title-color-bg_active: var(--color-bg-2);
+@collapse-title-color-bg_disabled: var(--color-bg-2);
+@collapse-title-color-text: var(--color-text-1);
+@collapse-title-color-text_disabled: var(--color-text-4);
+@collapse-title-font-weight_active: 500;
+@collapse-content-color-text: var(--color-text-1);
+@collapse-content-color-text_disabled: var(--color-text-1);
+@collapse-content-font-size: 14px;
+@collapse-content-padding-vertical: 8px;
+@collapse-content-color-bg: var(--color-fill-1);
+@collapse-expand-icon-size: 14px;
+@collapse-expand-icon-size-bg: 16px;
+@collapse-expand-icon-color-bg: var(--color-fill-2);
+@collapse-expand-icon-spacing-text: 5px;
+@collapse-color-expand-icon: var(--color-neutral-7);
+@collapse-item-color-border: var(--color-neutral-3);
+@collapse-item-border-width: 1px;
+@collapse-color-box-shadow: var(--color-primary-light-3);
+@collapse-prefix-cls: ~'@{prefix}-collapse';
+
+
+/*********** comment ***********/
+
+@comment-color-author-text: var(--color-text-2);
+@comment-color-datetime-text: var(--color-text-3);
+@comment-color-content-text: var(--color-text-1);
+@comment-color-actions-text: var(--color-text-2);
+@comment-font-size: 14px;
+@comment-font-action-size: 14px;
+@comment-font-author-size: 14px;
+@comment-font-datetime-size: 12px;
+@comment-margin-avatar-right: 12px;
+@comment-margin-author-right: 8px;
+@comment-margin-actions-top: 8px;
+@comment-margin-bottom: 20px;
+@comment-margin-actions-right: 8px;
+@comment-size-avatar-width: 32px;
+@comment-prefix-cls: ~'@{prefix}-comment';
+
+
+/*********** timepicker ***********/
+
+@timepicker-wrapper-border-radius: var(--border-radius-medium);
+@timepicker-column-width: 64px;
+@timepicker-column-height: 224px;
+@timepicker-cell-height: 24px;
+@timepicker-cell-spacing: 8px;
+@timepicker-cell-font-size: 14px;
+@timepicker-color-border: var(--color-neutral-3);
+@timepicker-color-cell-border: var(--color-neutral-3);
+@timepicker-color-text-cell: var(--color-text-1);
+@timepicker-color-bg-cell_hover: var(--color-fill-2);
+@timepicker-color-bg-cell_active: var(--color-fill-2);
+@timepicker-color-text-cell_disabled: var(--color-text-4);
+@timepicker-font-weight-cell: 500;
+@timepicker-font-weight-cell_active: 500;
+@timepicker-color-extra-text: var(--color-text-1);
+@timepicker-font-extra-size: 12px;
+@timepicker-extra-padding-horizontal: 8px;
+@timepicker-extra-padding-vertical: 8px;
+@timepicker-footer-padding-horizontal: 8px;
+@timepicker-footer-padding-vertical: 8px;
+
+
+/*********** date ***********/
+
+@date-panel-prefix-cls: ~'@{prefix}-panel-date';
+@date-picker-prefix-cls: ~'@{prefix}-datepicker';
+
+
+/*********** time ***********/
+
+@time-picker-prefix-cls: ~'@{prefix}-timepicker';
+
+
+/*********** datepicker ***********/
+
+@datepicker-timepicker-height: 276px;
+
+
+/*********** month ***********/
+
+@month-panel-prefix-cls: ~'@{prefix}-panel-month';
+
+
+/*********** quarter ***********/
+
+@quarter-panel-prefix-cls: ~'@{prefix}-panel-quarter';
+
+
+/*********** year ***********/
+
+@year-panel-prefix-cls: ~'@{prefix}-panel-year';
+
+
+/*********** week ***********/
+
+@week-panel-prefix-cls: ~'@{prefix}-panel-week';
+
+
+/*********** range ***********/
+
+@range-picker-prefix-cls: ~'@{prefix}-picker-range';
+
+
+/*********** descriptions ***********/
+
+@descriptions-border-width: 1px;
+@descriptions-border-style: solid;
+@descriptions-color-border: var(--color-neutral-3);
+@descriptions-border-radius: var(--border-radius-medium);
+@descriptions-font-size-title: 16px;
+@descriptions-size-mini-title-margin-bottom: 6px;
+@descriptions-size-small-title-margin-bottom: 8px;
+@descriptions-size-medium-title-margin-bottom: 12px;
+@descriptions-size-default-title-margin-bottom: 16px;
+@descriptions-size-large-title-margin-bottom: 20px;
+@descriptions-size-mini-font-size-text: 12px;
+@descriptions-size-small-font-size-text: 14px;
+@descriptions-size-medium-font-size-text: 14px;
+@descriptions-size-default-font-size-text: 14px;
+@descriptions-size-large-font-size-text: 14px;
+@descriptions-color-title: var(--color-text-1);
+@descriptions-color-text-label: var(--color-text-3);
+@descriptions-color-text-value: var(--color-text-1);
+@descriptions-font-weight-title: 500;
+@descriptions-font-weight-text-label: 500;
+@descriptions-font-weight-text-value: 400;
+@descriptions-border-color-bg-label: var(--color-fill-1);
+@descriptions-item-size-mini-spacing-bottom: 2px;
+@descriptions-item-size-small-spacing-bottom: 4px;
+@descriptions-item-size-medium-spacing-bottom: 8px;
+@descriptions-item-size-default-spacing-bottom: 12px;
+@descriptions-item-size-large-spacing-bottom: 16px;
+@descriptions-border-item-size-mini-padding-horizontal: 20px;
+@descriptions-border-item-size-mini-padding-vertical: 3px;
+@descriptions-border-item-size-small-padding-horizontal: 20px;
+@descriptions-border-item-size-small-padding-vertical: 3px;
+@descriptions-border-item-size-medium-padding-horizontal: 20px;
+@descriptions-border-item-size-medium-padding-vertical: 5px;
+@descriptions-border-item-size-default-padding-horizontal: 20px;
+@descriptions-border-item-size-default-padding-vertical: 7px;
+@descriptions-border-item-size-large-padding-horizontal: 20px;
+@descriptions-border-item-size-large-padding-vertical: 9px;
+@descriptions-prefix-cls: ~'@{prefix}-descriptions';
+
+
+/*********** divider ***********/
+
+@divider-margin-horizontal: 12px;
+@divider-margin-vertical: 20px;
+@divider-margin-vertical_text: 20px;
+@divider-margin-text: 16px;
+@divider-position-text-left: 24px;
+@divider-position-text-right: 24px;
+@divider-font-size: 14px;
+@divider-font-weight: 500;
+@divider-size: 1px;
+@divider-line-style: solid;
+@divider-color-bg: var(--color-neutral-3);
+@divider-color-text: var(--color-text-1);
+@divider-prefix-cls: ~'@{prefix}-divider';
+
+
+/*********** drawer ***********/
+
+@drawer-size-header-height: 48px;
+@drawer-margin-footer-button-left: 12px;
+@drawer-font-header-size: 16px;
+@drawer-font-header-weight: 500;
+@drawer-padding-horizontal: 16px;
+@drawer-padding-footer-vertical: 16px;
+@drawer-padding-content-vertical: 12px;
+@drawer-color-border: var(--color-neutral-3);
+@drawer-color-header-text: var(--color-text-1);
+@drawer-color-content-text: var(--color-text-1);
+@drawer-position-close-icon-right: 16px;
+@drawer-font-size-close-icon: 12px;
+@drawer-prefix-cls: ~'@{prefix}-drawer';
+
+
+/*********** dropdown ***********/
+
+@dropdown-max-height: 200px;
+@dropdown-border-radius: var(--border-radius-medium);
+@dropdown-padding-vertical: 4px;
+@dropdown-font-size: 14px;
+@dropdown-color-bg: var(--color-bg-popup);
+@dropdown-color-border: var(--color-fill-3);
+@dropdown-box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+@dropdown-option-height: 36px;
+@dropdown-option-padding-horizontal: 12px;
+@dropdown-option-font-weight_selected: 500;
+@dropdown-option-color-bg_default: transparent;
+@dropdown-option-color-bg_hover: var(--color-fill-2);
+@dropdown-option-color-bg_selected: transparent;
+@dropdown-option-color-bg_disabled: transparent;
+@dropdown-option-color-text_default: var(--color-text-1);
+@dropdown-option-color-text_hover: var(--color-text-1);
+@dropdown-option-color-text_selected: var(--color-text-1);
+@dropdown-option-color-text_disabled: var(--color-text-4);
+@dropdown-group-title-height: 20px;
+@dropdown-group-title-padding-horizontal: 12px;
+@dropdown-group-title-margin-top: 8px;
+@dropdown-group-title-font-size: 12px;
+@dropdown-group-title-color-text: var(--color-text-3);
+@dropdown-margin-left-suffix-icon: 12px;
+@dropdown-dark-color-bg: var(--color-menu-dark-bg);
+@dropdown-dark-color-border: var(--color-menu-dark-bg);
+@dropdown-dark-option-color-bg_default: transparent;
+@dropdown-dark-option-color-bg_hover: var(--color-menu-dark-hover);
+@dropdown-dark-option-color-bg_selected: transparent;
+@dropdown-dark-option-color-bg_disabled: transparent;
+@dropdown-dark-option-color-text_default: var(--color-text-4);
+@dropdown-dark-option-color-text_hover: var(--color-text-4);
+@dropdown-dark-option-color-text_selected: var(--color-white);
+@dropdown-dark-option-color-text_disabled: var(--color-text-2);
+@dropdown-dark-group-title-color-text: var(--color-text-3);
+@dropdown-prefix-cls: ~'@{prefix}-dropdown';
+
+
+/*********** empty ***********/
+
+@empty-spacing-padding: 10px;
+@empty-color-icon: rgb(var(--gray-5));
+@empty-color-text: rgb(var(--gray-5));
+@empty-font-size-image: 48px;
+@empty-font-size-text: 14px;
+@empty-spacing-image-margin-bottom: 4px;
+@empty-size-img-height: 80px;
+@empty-prefix-cls: ~'@{prefix}-empty';
+
+
+/*********** form ***********/
+
+@form-size-mini-margin-item-bottom: 16px;
+@form-size-small-margin-item-bottom: 20px;
+@form-size-default-margin-item-bottom: 20px;
+@form-size-large-margin-item-bottom: 20px;
+@form-size-mini-size-item-height: 24px;
+@form-size-small-size-item-height: 28px;
+@form-size-default-size-item-height: 32px;
+@form-size-large-size-item-height: 36px;
+@form-size-mini-font-label-size: 12px;
+@form-size-small-font-label-size: 14px;
+@form-size-large-font-label-size: 14px;
+@form-size-default-font-label-size: 14px;
+@form-font-extra-text-size: 12px;
+@form-font-error-text-size: 12px;
+@form-margin-label-right: 16px;
+@form-margin-extra-bottom: 4px;
+@form-margin-extra-top: 4px;
+@form-inline-margin-item-right: 24px;
+@form-inline-margin-item-bottom: 8px;
+@form-vertical-margin-label-bottom: 8px;
+@form-color-extra-text: var(--color-text-3);
+@form-color-text-label: var(--color-text-2);
+@form-color-text-tooltip: var(--color-text-4);
+@form-margin-tooltip-left: 4px;
+@form-color-bg_warning: var(--color-warning-light-1);
+@form-color-bg_warning_hover: var(--color-warning-light-2);
+@form-color-bg_warning_focus: var(--color-bg-2);
+@form-color-border_warning: transparent;
+@form-color-border_warning_focus: rgb(var(--warning-6));
+@form-color-border_warning_hover: transparent;
+@form-size-shadow_warning_focus: 0;
+@form-color-shadow_warning_focus: var(--color-warning-light-2);
+@form-color-bg_success: var(--color-fill-2);
+@form-color-bg_success_hover: var(--color-fill-3);
+@form-color-bg_success_focus: var(--color-bg-2);
+@form-color-border_success: transparent;
+@form-color-border_success_focus: rgb(var(--success-6));
+@form-color-border_success_hover: transparent;
+@form-size-shadow_success_focus: 0;
+@form-color-shadow_success_focus: var(--color-success-light-2);
+@form-color-bg_error: var(--color-danger-light-1);
+@form-color-bg_error_hover: var(--color-danger-light-2);
+@form-color-bg_error_focus: var(--color-bg-2);
+@form-color-border_error: transparent;
+@form-color-border_error_focus: rgb(var(--danger-6));
+@form-color-border_error_hover: transparent;
+@form-size-shadow_error_focus: 0;
+@form-color-shadow_error_focus: var(--color-danger-light-2);
+@form-color-bg_validating: var(--color-fill-2);
+@form-color-bg_validating_hover: var(--color-fill-3);
+@form-color-bg_validating_focus: var(--color-bg-2);
+@form-color-border_validating: transparent;
+@form-color-border_validating_focus: rgb(var(--primary-6));
+@form-color-border_validating_hover: transparent;
+@form-size-shadow_validating_focus: 0;
+@form-color-shadow_validating_focus: var(--color-primary-light-2);
+@form-color-tip-text_success: rgb(var(--success-6));
+@form-color-tip-icon-text_success: rgb(var(--success-6));
+@form-color-tip-text_error: rgb(var(--danger-6));
+@form-color-tip-icon-text_error: rgb(var(--danger-6));
+@form-color-tip-text_warning: rgb(var(--warning-6));
+@form-color-tip-icon-text_warning: rgb(var(--warning-6));
+@form-color-tip-text_validating: rgb(var(--primary-6));
+@form-color-tip-icon-text_validating: rgb(var(--primary-6));
+@form-prefix-cls: ~'@{prefix}-form';
+
+
+/*********** row ***********/
+
+@row-prefix-cls: ~'@{prefix}-row';
+
+
+/*********** col ***********/
+
+@col-prefix-cls: ~'@{prefix}-col';
+
+
+/*********** grid ***********/
+
+@grid-prefix-cls: ~'@{prefix}-grid';
+
+
+/*********** image ***********/
+
+@image-radius: var(--border-radius-small);
+@image-font-size-title: 16px;
+@image-font-weight-title: 500;
+@image-font-size-description: 14px;
+@image-color-title_footer_outer-text: var(--color-text-1);
+@image-color-title_footer_inner-text: var(--color-white);
+@image-color-description_footer_inner-text: var(--color-white);
+@image-color-description_footer_outer-text: var(--color-neutral-6);
+@image-spacing-actions-left: 12px;
+@image-font-size-actions-item: 14px;
+@image-padding-actions-item-vertical: 0;
+@image-padding-actions-item-horizontal: 0;
+@image-spacing-actions-item-left: 12px;
+@image-radius-actions-item: var(--border-radius-small);
+@image-color-actions-item_footer_inner_hover-bg: rgba(0, 0, 0, 0.5);
+@image-color-actions-item_footer_outer_hover-bg: var(--color-neutral-2);
+@image-color-actions-item_trigger-text: var(--color-neutral-8);
+@image-color-actions-item_trigger_hover-bg: var(--color-neutral-2);
+@image-spacing-actions-trigger-item-vertical: 5px;
+@image-spacing-actions-trigger-item-horizontal: 4px;
+@image-color-footer_inner-bg: linear-gradient(360deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0) 100%);
+@image-color-footer_inner-text: var(--color-white);
+@image-padding-footer_inner_vertical: 9px;
+@image-padding-footer_inner_horizontal: 16px;
+@image-spacing-footer_inner_simple-vertical: 12px;
+@image-spacing-footer_inner_simple-horizontal: 16px;
+@image-color-footer_outer-text: var(--color-neutral-8);
+@image-spacing-footer-top: 4px;
+@image-color-error-bg: var(--color-neutral-1);
+@image-color-error-text: var(--color-neutral-4);
+@image-font-size-error-icon: 60px;
+@image-font-size-error-text: 12px;
+@image-line-height-error-text: 1.6667;
+@image-size-error-min-height: 100px;
+@image-spacing-error-padding: 16px;
+@image-color-loader-bg: var(--color-neutral-1);
+@image-size-loader-min-height: 100px;
+@image-font-size-loader-spin: 32px;
+@image-color-loader-spin-text: rgb(var(--primary-6));
+@image-color-loader-spin-text-text: var(--color-neutral-6);
+@image-font-size-loader-spin-text: 16px;
+@image-preview-color-mask-bg: var(--color-mask-bg);
+@image-preview-size-scale-value-height: 32px;
+@image-preview-spacing-scale-value-vertical: 7px;
+@image-preview-spacing-scale-value-horizontal: 10px;
+@image-preview-font-size-scale-value: 12px;
+@image-preview-color-scale-value-text: var(--color-white);
+@image-preview-color-scale-value-bg: rgba(255, 255, 255, 0.08);
+@image-preview-color-toolbar-bg: var(--color-bg-2);
+@image-preview-radius-toolbar: var(--border-radius-medium);
+@image-preview-spacing-toolbar-vertical: 4px;
+@image-preview-spacing-toolbar-horizontal: 16px;
+@image-preview-spacing-toolbar-horizontal_simple: 4px;
+@image-preview-spacing-toolbar-vertical_simple: 4px;
+@image-preview-position-toolbar-bottom: 46px;
+@image-preview-font-size-action: 14px;
+@image-preview-color-action-text: var(--color-neutral-8);
+@image-preview-radius-action: var(--border-radius-small);
+@image-preview-color-action-bg: transparent;
+@image-preview-color-action_hover-bg: var(--color-neutral-2);
+@image-preview-color-action_hover-text: rgb(var(--primary-6));
+@image-preview-color-action_disabled-bg: transparent;
+@image-preview-color-action_disabled-text: var(--color-text-4);
+@image-preview-font-size-action-name: 12px;
+@image-preview-spacing-action-name-right: 12px;
+@image-preview-padding-action-content: 13px;
+@image-preview-margin-action-right: 0;
+@image-preview-spacing-trigger-padding-vertical: 12px;
+@image-preview-spacing-trigger-padding-horizontal: 16px;
+@image-preview-margin-action-bottom: 0;
+@image-preview-color-loading-text: rgb(var(--primary-6));
+@image-preview-color-loading-bg: #232324;
+@image-preview-font-size-loading: 18px;
+@image-preview-spacing-loading-padding: 10px;
+@image-preview-size-loading-width: 48px;
+@image-preview-size-loading-height: 48px;
+@image-preview-radius-loading: var(--border-radius-medium);
+@image-preview-size-close-btn-width: 32px;
+@image-preview-size-close-icon: 14px;
+@image-preview-color-close-btn-bg: rgba(0, 0, 0, 0.5);
+@image-preview-color-close-btn-text: var(--color-white);
+@image-preview-position-close-btn-right: 36px;
+@image-preview-position-close-btn-top: 36px;
+@image-preview-arrow-position: 20px;
+@image-preview-arrow-size: 32px;
+@image-preview-arrow-font-size: 16px;
+@image-preview-arrow-color-icon: var(--color-white);
+@image-preview-arrow-color-icon_disabled: rgba(255, 255, 255, 0.3);
+@image-preview-arrow-color-bg: rgba(255, 255, 255, 0.3);
+@image-preview-arrow-color-bg_hover: rgba(255, 255, 255, 0.5);
+@image-preview-arrow-color-bg_disabled: rgba(255, 255, 255, 0.2);
+@image-trigger-spacing-padding-vertical: 6px;
+@image-trigger-spacing-padding-horizontal: 4px;
+@image-trigger-color-bg: var(--color-bg-5);
+@image-trigger-color-border: var(--color-neutral-3);
+@image-trigger-size-border: 1px;
+@image-trigger-radius: 4px;
+@image-color-box-shadow: rgb(var(--primary-6));
+@image-trigger-prefix-cls: ~'@{prefix}-image-trigger';
+@image-prefix-cls: ~'@{prefix}-image';
+
+
+/*********** preview ***********/
+
+@preview-prefix-cls: ~'@{prefix}-image-preview';
+
+
+/*********** layout ***********/
+
+@layout-trigger-height: 48px;
+@layout-sider-background: var(--color-menu-dark-bg);
+@layout-font-color-dark: var(--color-white);
+@layout-font-color: var(--color-text-1);
+@layout-trigger-dark-color: rgba(255, 255, 255, 0.2);
+@layout-sider-background-light: var(--color-menu-light-bg);
+@layout-trigger-light-color-border: var(--color-bg-5);
+@layout-prefix-cls: ~'@{prefix}-layout';
+
+
+/*********** list ***********/
+
+@list-border-width: 1px;
+@list-border-color: var(--color-neutral-3);
+@list-border-radius: var(--border-radius-medium);
+@list-color-text: var(--color-text-1);
+@list-font-size: 14px;
+@list-line-height: 1.5715;
+@list-color-text-header: var(--color-text-1);
+@list-color-bg-item-hover: var(--color-fill-1);
+@list-font-size-header: 16px;
+@list-font-weight-header: 500;
+@list-line-height-header: 1.5;
+@list-size-small-padding-vertical-header: 8px;
+@list-size-small-padding-horizontal-header: 20px;
+@list-size-small-padding-vertical-item: 9px;
+@list-size-small-padding-horizontal-item: 20px;
+@list-size-default-padding-vertical-header: 12px;
+@list-size-default-padding-horizontal-header: 20px;
+@list-size-default-padding-vertical-item: 13px;
+@list-size-default-padding-horizontal-item: 20px;
+@list-size-large-padding-vertical-header: 16px;
+@list-size-large-padding-horizontal-header: 20px;
+@list-size-large-padding-vertical-item: 17px;
+@list-size-large-padding-horizontal-item: 20px;
+@list-meta-font-weight-title: 500;
+@list-meta-color-title: var(--color-text-1);
+@list-mete-color-description: var(--color-text-2);
+@list-meta-margin-right-avatar: 16px;
+@list-meta-margin-bottom-title: 2px;
+@list-meta-padding-horizontal: 0;
+@list-meta-padding-vertical: 4px;
+@list-action-gap: 20px;
+@list-action-margin-top: 4px;
+@list-pagination-margin-top: 24px;
+@list-prefix-cls: ~'@{prefix}-list';
+
+
+/*********** mentions ***********/
+
+@mentions-padding-horizontal: 12px;
+@mentions-padding-vertical: 4px;
+@mentions-font-size: 14px;
+@mentions-line-height: 1.5715;
+@mentions-prefix-cls: ~'@{prefix}-mentions';
+
+
+/*********** menu ***********/
+
+@menu-font-size: 14px;
+@menu-line-height: 1.5715;
+@menu-border-radius: var(--border-radius-small);
+@menu-font-weight-item-selected: 500;
+@menu-color-label-item-selected: rgb(var(--primary-6));
+@menu-height-label-item-selected: 3px;
+@menu-margin-left-item-suffix-icon: 6px;
+@menu-margin-right-item-prefix-icon: 16px;
+@menu-horizontal-margin-right-item-prefix-icon: 8px;
+@menu-item-gap: 4px;
+@menu-item-indent-spacing: 20px;
+@menu-width-collapse-button: 24px;
+@menu-height-collapse-button: 24px;
+@menu-border-radius-collapse-button: var(--border-radius-small);
+@menu-light-color-bg: var(--color-menu-light-bg);
+@menu-light-color-bg-item_default: var(--color-menu-light-bg);
+@menu-light-color-bg-item_hover: var(--color-fill-2);
+@menu-light-color-bg-item_selected: var(--color-fill-2);
+@menu-light-color-bg-item_disabled: var(--color-menu-light-bg);
+@menu-light-color-item_default: var(--color-text-2);
+@menu-light-color-item_hover: var(--color-text-2);
+@menu-light-color-item_selected: rgb(var(--primary-6));
+@menu-light-color-submenu_selected: rgb(var(--primary-6));
+@menu-light-color-bg-submenu_selected_hover: var(--color-fill-2);
+@menu-light-color-item_disabled: var(--color-text-4);
+@menu-light-color-icon_default: var(--color-text-3);
+@menu-light-color-icon_hover: var(--color-text-3);
+@menu-light-color-icon_selected: rgb(var(--primary-6));
+@menu-light-color-icon_disabled: var(--color-text-4);
+@menu-light-color-group-title: var(--color-text-3);
+@menu-dark-color-bg: var(--color-menu-dark-bg);
+@menu-dark-color-bg-item_default: var(--color-menu-dark-bg);
+@menu-dark-color-bg-item_hover: var(--color-menu-dark-hover);
+@menu-dark-color-bg-item_selected: var(--color-menu-dark-hover);
+@menu-dark-color-bg-item_disabled: var(--color-menu-dark-bg);
+@menu-dark-color-submenu_selected: rgb(var(--primary-6));
+@menu-dark-color-bg-submenu_selected_hover: var(--color-menu-dark-hover);
+@menu-dark-color-item_default: var(--color-text-4);
+@menu-dark-color-item_hover: var(--color-text-4);
+@menu-dark-color-item_selected: var(--color-white);
+@menu-dark-color-item_disabled: var(--color-text-2);
+@menu-dark-color-icon_default: var(--color-text-3);
+@menu-dark-color-icon_hover: var(--color-text-3);
+@menu-dark-color-icon_selected: var(--color-white);
+@menu-dark-color-icon_disabled: var(--color-text-2);
+@menu-dark-color-group-title: var(--color-text-3);
+@menu-color-border-popup: var(--color-neutral-3);
+@menu-light-color-bg-button: var(--color-fill-1);
+@menu-light-color-bg-button_hover: var(--color-fill-3);
+@menu-light-color-button: var(--color-text-3);
+@menu-dark-color-bg-button: rgb(var(--primary-6));
+@menu-dark-color-bg-button_hover: rgb(var(--primary-7));
+@menu-dark-color-button: var(--color-white);
+@menu-horizontal-padding-vertical: 14px;
+@menu-horizontal-padding-horizontal: 20px;
+@menu-horizontal-item-gap: 12px;
+@menu-horizontal-item-height: 30px;
+@menu-horizontal-item-padding-horizontal: 12px;
+@menu-vertical-padding-vertical: 4px;
+@menu-vertical-padding-horizontal: 8px;
+@menu-vertical-item-height: 40px;
+@menu-vertical-item-padding-horizontal: 12px;
+@menu-collapse-width: 48px;
+@menu-collapse-padding-vertical: 4px;
+@menu-collapse-padding-horizontal: 4px;
+@menu-pop-button-size: 40px;
+@menu-pop-button-margin-bottom: 16px;
+@menu-pop-button-box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+@menu-pop-button-border-color: transparent;
+@menu-prefix-cls: ~'@{prefix}-menu';
+
+
+/*********** message ***********/
+
+@message-wrapper-margin-top: 40px;
+@message-wrapper-margin-bottom: 40px;
+@message-padding-top: 10px;
+@message-padding-bottom: 10px;
+@message-padding-left: 16px;
+@message-padding-right: 16px;
+@message-margin-bottom: 16px;
+@message-border-radius: var(--border-radius-small);
+@message-font-size-icon: 20px;
+@message-font-size-content: 14px;
+@message-icon-margin-right: 8px;
+@message-border-width: 1px;
+@message-border-style: solid;
+@message-box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+@message-color-close-icon: var(--color-text-1);
+@message-close-icon-font-size: 12px;
+@message-close-icon-top: 14px;
+@message-close-icon-right: 12px;
+@message-close-icon-left: 14px;
+@message-normal-color-bg: var(--color-bg-popup);
+@message-normal-color-icon: var(--color-text-1);
+@message-normal-color-content: var(--color-text-1);
+@message-normal-color-border: var(--color-neutral-3);
+@message-info-color-bg: var(--color-bg-popup);
+@message-info-color-icon: rgb(var(--primary-6));
+@message-info-color-content: var(--color-text-1);
+@message-info-color-border: var(--color-neutral-3);
+@message-success-color-bg: var(--color-bg-popup);
+@message-success-color-icon: rgb(var(--success-6));
+@message-success-color-content: var(--color-text-1);
+@message-success-color-border: var(--color-neutral-3);
+@message-warning-color-bg: var(--color-bg-popup);
+@message-warning-color-icon: rgb(var(--warning-6));
+@message-warning-color-content: var(--color-text-1);
+@message-warning-color-border: var(--color-neutral-3);
+@message-error-color-bg: var(--color-bg-popup);
+@message-error-color-icon: rgb(var(--danger-6));
+@message-error-color-content: var(--color-text-1);
+@message-error-color-border: var(--color-neutral-3);
+@message-loading-color-bg: var(--color-bg-popup);
+@message-loading-color-icon: rgb(var(--primary-6));
+@message-loading-color-content: var(--color-text-1);
+@message-loading-color-border: var(--color-neutral-3);
+@message-prefix-cls: ~'@{prefix}-message';
+
+
+/*********** modal ***********/
+
+@modal-border-radius: var(--border-radius-medium);
+@modal-size-tip-icon: 18px;
+@modal-margin-top: 100px;
+@modal-margin-tip-icon-right: 10px;
+@modal-margin-footer-button-left: 12px;
+@modal-color-border: var(--color-neutral-3);
+@modal-border-width: 0;
+@modal-box-shadow: none;
+@modal-color-header-text: var(--color-text-1);
+@modal-color-content-text: var(--color-text-1);
+@modal-font-header-size: 16px;
+@modal-font-header-weight: 500;
+@modal-font-content-size: 14px;
+@modal-default-align-header: center;
+@modal-simple-align-header: center;
+@modal-default-align-footer: right;
+@modal-simple-align-footer: center;
+@modal-default-padding-horizontal: 20px;
+@modal-default-size-header-height: 48px;
+@modal-default-padding-content-vertical: 24px;
+@modal-default-padding-footer-vertical: 16px;
+@modal-default-size-width: 520px;
+@modal-simple-size-width: 464px;
+@modal-simple-padding-horizontal: 32px;
+@modal-simple-padding-top: 24px;
+@modal-simple-padding-bottom: 32px;
+@modal-simple-margin-footer-top: 32px;
+@modal-simple-margin-content-top: 24px;
+@modal-position-close-icon-right: 16px;
+@modal-font-size-close-icon: 12px;
+@modal-color-close-icon: var(--color-text-1);
+@modal-prefix-cls: ~'@{prefix}-modal';
+
+
+/*********** notification ***********/
+
+@notification-wrapper-margin-top: 20px;
+@notification-wrapper-margin-bottom: 20px;
+@notification-wrapper-margin-left: 20px;
+@notification-wrapper-margin-right: 20px;
+@notification-border-radius: var(--border-radius-medium);
+@notification-margin-bottom: 20px;
+@notification-width: 300px;
+@notification-padding-top: 20px;
+@notification-padding-bottom: 20px;
+@notification-padding-left: 20px;
+@notification-padding-right: 20px;
+@notification-font-size-icon: 24px;
+@notification-font-size-title: 16px;
+@notification-font-size-content: 14px;
+@notification-icon-margin-right: 16px;
+@notification-title-margin-bottom: 4px;
+@notification-btn-wrapper-margin-top: 16px;
+@notification-border-width: 1px;
+@notification-border-style: solid;
+@notification-color-close-icon: var(--color-text-1);
+@notification-close-icon-font-size: 12px;
+@notification-close-icon-top: 12px;
+@notification-close-icon-right: 12px;
+@notification-normal-color-bg: var(--color-bg-popup);
+@notification-normal-color-icon: var(--color-text-1);
+@notification-normal-color-text-title: var(--color-text-1);
+@notification-normal-color-text-content: var(--color-text-1);
+@notification-normal-color-border: var(--color-neutral-3);
+@notification-info-color-bg: var(--color-bg-popup);
+@notification-info-color-icon: rgb(var(--primary-6));
+@notification-info-color-text-title: var(--color-text-1);
+@notification-info-color-text-content: var(--color-text-1);
+@notification-info-color-border: var(--color-neutral-3);
+@notification-success-color-bg: var(--color-bg-popup);
+@notification-success-color-icon: rgb(var(--success-6));
+@notification-success-color-text-title: var(--color-text-1);
+@notification-success-color-text-content: var(--color-text-1);
+@notification-success-color-border: var(--color-neutral-3);
+@notification-warning-color-bg: var(--color-bg-popup);
+@notification-warning-color-icon: rgb(var(--warning-6));
+@notification-warning-color-text-title: var(--color-text-1);
+@notification-warning-color-text-content: var(--color-text-1);
+@notification-warning-color-border: var(--color-neutral-3);
+@notification-error-color-bg: var(--color-bg-popup);
+@notification-error-color-icon: rgb(var(--danger-6));
+@notification-error-color-text-title: var(--color-text-1);
+@notification-error-color-text-content: var(--color-text-1);
+@notification-error-color-border: var(--color-neutral-3);
+@notification-prefix-cls: ~'@{prefix}-notification';
+
+
+/*********** page ***********/
+
+@page-header-padding-left: 24px;
+@page-header-padding-right: 20px;
+@page-header-padding-vertical: 16px;
+@page-header-padding-vertical_breadcrumb: 12px;
+@page-header-color-back-icon: var(--color-text-2);
+@page-header-size-back-icon: 14px;
+@page-header-color-back-icon-box-shadow: var(--color-primary-light-3);
+@page-header-margin-back-icon-right: 12px;
+@page-header-line-height: 28px;
+@page-header-color-title-text: var(--color-text-1);
+@page-header-weight-title-text: 600;
+@page-header-color-back-icon-bg_hover: var(--color-fill-2);
+@page-header-size-back-icon-bg_hover: 30px;
+@page-header-size-title-text: 20px;
+@page-header-color-divider-bg: var(--color-fill-3);
+@page-header-size-divider-height: 16px;
+@page-header-size-divider-width: 1px;
+@page-header-margin-divider-left: 12px;
+@page-header-margin-divider-right: 12px;
+@page-header-color-sub-title-text: var(--color-text-3);
+@page-header-size-sub-title-text: 14px;
+@page-header-color-header-border: var(--color-neutral-3);
+@page-header-border-header-width: 1px;
+@page-header-border-header-style: solid;
+@page-header-padding-content-vertical: 20px;
+@page-header-padding-content-horizontal: 32px;
+@page-header-margin-breadcrumb-bottom: 4px;
+@page-header-prefix-cls: ~'@{prefix}-page-header';
+
+
+/*********** pagination ***********/
+
+@pagination-prefix-cls: ~'@{prefix}-pagination';
+@pagination-item-border-radius: var(--border-radius-small);
+@pagination-item-spacing: 8px;
+@pagination-margin-total-spacing: 8px;
+@pagination-margin-option-left: 8px;
+@pagination-margin-jumper-left: 8px;
+@pagination-size-mini: 24px;
+@pagination-size-small: 28px;
+@pagination-size-default: 32px;
+@pagination-size-large: 36px;
+@pagination-size-mini-font-size: 12px;
+@pagination-size-small-font-size: 14px;
+@pagination-size-default-font-size: 14px;
+@pagination-size-large-font-size: 14px;
+@pagination-size-icon-arrow_mini: 12px;
+@pagination-size-icon-arrow_small: 12px;
+@pagination-size-icon-arrow_default: 12px;
+@pagination-size-icon-arrow_large: 14px;
+@pagination-size-icon-ellipsis: 16px;
+@pagination-border-width: 0;
+@pagination-color-bg-item: transparent;
+@pagination-color-bg-item_active: var(--color-primary-light-1);
+@pagination-color-bg-item_hover: var(--color-fill-1);
+@pagination-color-bg-item_disabled: transparent;
+@pagination-color-bg-item_active_disabled: var(--color-fill-1);
+@pagination-color-item-text: var(--color-text-2);
+@pagination-color-item-text_hover: var(--color-text-2);
+@pagination-color-item-text_active: rgb(var(--primary-6));
+@pagination-color-item-text_disabled: var(--color-text-4);
+@pagination-color-item-text_active_disabled: var(--color-primary-light-3);
+@pagination-color-item-border: transparent;
+@pagination-color-item-border_active: transparent;
+@pagination-color-item-border_hover: transparent;
+@pagination-color-item-border_disabled: transparent;
+@pagination-color-item-border_active_disabled: transparent;
+@pagination-color-icon-arrow: var(--color-text-2);
+@pagination-color-icon-arrow-bg: transparent;
+@pagination-color-icon-arrow-bg_hover: var(--color-fill-1);
+@pagination-color-icon-arrow-bg_disabled: transparent;
+@pagination-color-icon-arrow-text_hover: rgb(var(--primary-6));
+@pagination-color-icon-arrow-text_disabled: var(--color-text-4);
+@pagination-simple-input-width: 40px;
+@pagination-simple-color-icon-arrow: var(--color-text-2);
+@pagination-simple-color-icon-arrow-bg: transparent;
+@pagination-simple-color-icon-arrow-bg_hover: var(--color-fill-1);
+@pagination-simple-color-icon-arrow-bg_disabled: transparent;
+@pagination-simple-color-icon-arrow-text_hover: rgb(var(--primary-6));
+@pagination-simple-color-icon-arrow-text_disabled: var(--color-text-4);
+@pagination-simple-margin-prev-right: 4px;
+@pagination-simple-margin-next-left: 12px;
+@pagination-simple-margin-separator-left: 12px;
+@pagination-simple-margin-separator-right: 12px;
+@pagination-color-jumper-goto: var(--color-text-2);
+@pagination-color-text-total: var(--color-text-1);
+
+
+/*********** patination ***********/
+
+@patination-jumper-input-width: 40px;
+
+
+/*********** popconfirm ***********/
+
+@popconfirm-padding-horizontal: 16px;
+@popconfirm-padding-vertical: 16px;
+@popconfirm-margin-title-bottom: 16px;
+@popconfirm-margin-content-top: 4px;
+@popconfirm-margin-content-bottom: 16px;
+@popconfirm-size-title-icon: 18px;
+@popconfirm-margin-icon-right: 8px;
+@popconfirm-margin-button-left: 8px;
+@popconfirm-font-title-size: 14px;
+@popconfirm-color-title-text: var(--color-text-1);
+@popconfirm-prefix-cls: ~'@{prefix}-popconfirm';
+
+
+/*********** popover ***********/
+
+@popover-prefix-cls: ~'@{prefix}-popover';
+
+
+/*********** progress ***********/
+
+@progress-line-color-line-bg: var(--color-fill-3);
+@progress-line-color-inner-bg: rgb(var(--primary-6));
+@progress-line-color-inner-bg_success: rgb(var(--success-6));
+@progress-line-color-inner-bg_error: rgb(var(--danger-6));
+@progress-line-color-buffer-bg: var(--color-primary-light-3);
+@progress-line-size-large-font-size: 16px;
+@progress-line-size-small-font-size: 12px;
+@progress-line-size-default-font-size: 12px;
+@progress-line-size-large-margin-text-left: 16px;
+@progress-line-size-small-margin-text-left: 16px;
+@progress-line-color-icon_success: rgb(var(--success-6));
+@progress-line-color-icon_error: rgb(var(--danger-6));
+@progress-line-margin-text-left: 16px;
+@progress-line-margin-icon-left: 4px;
+@progress-line-color-text: var(--color-text-2);
+@progress-line-color-icon_normal: var(--color-text-2);
+@progress-line-size-default-icon-size: 12px;
+@progress-line-size-small-icon-size: 12px;
+@progress-line-size-large-icon-size: 14px;
+@progress-circle-size-small-font-size: 13px;
+@progress-circle-size-default-font-size: 14px;
+@progress-circle-size-large-font-size: 16px;
+@progress-circle-size-small-icon-size: 14px;
+@progress-circle-size-default-icon-size: 16px;
+@progress-circle-size-large-icon-size: 16px;
+@progress-circle-color-text: var(--color-text-3);
+@progress-circle-color-mask-stroke: var(--color-fill-3);
+@progress-circle-size-mini-color-mask-stroke: var(--color-primary-light-3);
+@progress-circle-size-mini-color-mask-stroke_success: var(--color-success-light-3);
+@progress-circle-size-mini-color-mask-stroke_error: var(--color-danger-light-3);
+@progress-circle-color-path-stroke: rgb(var(--primary-6));
+@progress-circle-color-path-stroke_success: rgb(var(--success-6));
+@progress-circle-color-path-stroke_error: rgb(var(--danger-6));
+@progress-circle-color-icon_success: rgb(var(--success-6));
+@progress-circle-color-icon_error: rgb(var(--danger-6));
+@progress-steps-size-small-steps-item-width: 2px;
+@progress-steps-margin-steps-item-right: 3px;
+@progress-steps-margin-steps-item-right_small: 3px;
+@progress-steps-color-item-bg: var(--color-fill-3);
+@progress-steps-color-item-bg_normal: rgb(var(--primary-6));
+@progress-steps-color-item-bg_success: rgb(var(--success-6));
+@progress-steps-color-item-bg_error: rgb(var(--danger-6));
+@progress-steps-color-item-bg_warning: rgb(var(--warning-6));
+@progress-steps-margin-text-left: 8px;
+@progress-line-color-inner-bg_warning: rgb(var(--warning-6));
+@progress-line-color-icon_warning: rgb(var(--warning-6));
+@progress-circle-size-mini-color-mask-stroke_warning: var(--color-warning-light-3);
+@progress-circle-color-path-stroke_warning: rgb(var(--warning-6));
+@progress-circle-color-icon_warning: rgb(var(--warning-6));
+@progress-prefix-cls: ~'@{prefix}-progress';
+
+
+/*********** radio ***********/
+
+@radio-color-border: var(--color-neutral-3);
+@radio-border-width: 2px;
+@radio-layout-height: 14px;
+@radio-color-border_hover: var(--color-neutral-3);
+@radio-color-border_disabled: var(--color-neutral-3);
+@radio-color-bg: var(--color-bg-2);
+@radio-color-bg_checked: rgb(var(--primary-6));
+@radio-border-radius: var(--border-radius-circle);
+@radio-color-bg_disabled: var(--color-fill-2);
+@radio-color-bg_checked_disabled: var(--color-primary-light-3);
+@radio-color-dot-bg_checked_disabled: var(--color-fill-2);
+@radio-color-text: var(--color-text-1);
+@radio-color-text_disabled: var(--color-text-4);
+@radio-color-text_checked_disabled: var(--color-text-4);
+@radio-font-text-size: 14px;
+@radio-font-text-size_large: 14px;
+@radio-font-text-size_mini: 12px;
+@radio-font-text-size_small: 14px;
+@radio-margin-text-left: 8px;
+@radio-size-mask-height: 24px;
+@radio-mask-bg-color-bg: var(--color-fill-2);
+@radio-group-margin-right: 20px;
+@radio-group-button-color-bg: var(--color-fill-2);
+@radio-group-button-color-bg_dark: var(--color-bg-3);
+@radio-button-padding-horizontal: 12px;
+@radio-button-spacing: 3px;
+@radio-button-color-bg_active: var(--color-bg-5);
+@radio-button-color-bg_active_dark: var(--color-fill-3);
+@radio-button-color-bg_hover: var(--color-bg-5);
+@radio-button-color-text_active: rgb(var(--primary-6));
+@radio-button-font-text-weight_active: 500;
+@radio-button-color-text_hover: var(--color-text-1);
+@radio-button-border-radius: var(--border-radius-small);
+@radio-button-bg-border-radius: var(--border-radius-small);
+@radio-button-color-bg: transparent;
+@radio-button-color-text: var(--color-text-2);
+@radio-button-color-bg_disabled: transparent;
+@radio-button-color-text_disabled: var(--color-text-4);
+@radio-button-color-bg_checked_disabled: var(--color-bg-5);
+@radio-button-color-text_checked_disabled: var(--color-primary-light-3);
+@radio-button-color-separator-bg: var(--color-neutral-3);
+@radio-button-size-separator-width: 1px;
+@radio-button-size-separator-height: 14px;
+@radio-size-default-height: 32px;
+@radio-size-small-height: 28px;
+@radio-size-mini-height: 24px;
+@radio-size-large-height: 36px;
+@radio-group-size-line-height_vertical: 32px;
+@radio-color-box-shadow: rgb(var(--primary-6));
+@radio-prefix-cls: ~'@{prefix}-radio';
+
+
+/*********** rate ***********/
+
+@rate-min-height: 32px;
+@rate-gap-size: 8px;
+@rate-font-size: 24px;
+@rate-scale_active: 1.2;
+@rate-color-bg_active: rgb(var(--gold-6));
+@rate-color-bg_default: var(--color-fill-3);
+@rate-color-bg_hover: rgb(var(--gold-5));
+@rate-prefix-cls: ~'@{prefix}-rate';
+
+
+/*********** prefixCls ***********/
+
+@prefixCls: ~'@{prefix}-rate';
+
+
+/*********** resizeBox ***********/
+
+@resizeBox-trigger-color-background: var(--color-neutral-3);
+@resizeBox-trigger-size-icon-wrapper: 6px;
+@resizeBox-trigger-font-size-icon: 12px;
+@resizeBox-trigger-color-icon: var(--color-text-1);
+@resizeBox-trigger-icon-empty: 18px;
+
+
+/*********** resizebox ***********/
+
+@resizebox-prefix-cls: ~'@{prefix}-resizebox';
+@resizebox-split-prefix-cls: ~'@{prefix}-resizebox-split';
+@resizebox-split-group-prefix-cls: ~'@{prefix}-resizebox-split-group';
+@resizebox-trigger-prefix-cls: ~'@{prefix}-resizebox-trigger';
+
+
+/*********** result ***********/
+
+@result-padding-top: 24px;
+@result-padding-top_icon: 32px;
+@result-padding-bottom: 24px;
+@result-padding-horizontal: 32px;
+@result-margin-icon-bottom: 16px;
+@result-margin-extra-top: 20px;
+@result-margin-content-top: 20px;
+@result-font-title-size: 14px;
+@result-font-title-weight: 500;
+@result-font-subtitle-size: 14px;
+@result-color-title-text: var(--color-text-1);
+@result-color-subtitle-text: var(--color-text-2);
+@result-size-icon: 20px;
+@result-size-icon-wrapper: 45px;
+@result-size-image-width: 92px;
+@result-size-icon_custom: 45px;
+@result-color-icon_default: inherit;
+@result-color-icon_success: rgb(var(--success-6));
+@result-color-icon-bg_success: var(--color-success-light-1);
+@result-color-icon_error: rgb(var(--danger-6));
+@result-color-icon-bg_error: var(--color-danger-light-1);
+@result-color-icon_warning: rgb(var(--warning-6));
+@result-color-icon-bg_warning: var(--color-warning-light-1);
+@result-color-icon_info: rgb(var(--primary-6));
+@result-color-icon-bg_info: var(--color-primary-light-1);
+@result-prefix-cls: ~'@{prefix}-result';
+
+
+/*********** skeleton ***********/
+
+@skeleton-color-bg-base: var(--color-fill-2);
+@skeleton-radius-image-border: var(--border-radius-small);
+@skeleton-size-image_default: 48px;
+@skeleton-size-image_small: 36px;
+@skeleton-size-image_large: 60px;
+@skeleton-spacing-image_left-margin-right: 16px;
+@skeleton-spacing-image_right-margin-left: 16px;
+@skeleton-size-row-height: 16px;
+@skeleton-spacing-last_row-margin-bottom: 16px;
+@skeleton-color-animate-bg: var(--color-fill-3);
+@skeleton-prefix-cls: ~'@{prefix}-skeleton';
+
+
+/*********** slider ***********/
+
+@slider-size-road-width: 2px;
+@slider-color-road-bg: var(--color-fill-3);
+@slider-color-road-bg_disabled: var(--color-fill-2);
+@slider-color-bar-bg: rgb(var(--primary-6));
+@slider-color-bar-bg_disabled: var(--color-fill-3);
+@slider-color-button-bg: var(--color-bg-2);
+@slider-border-size-button: 2px;
+@slider-color-button-border: rgb(var(--primary-6));
+@slider-color-button-border_disabled: var(--color-fill-3);
+@slider-shadow-button_active: 0 2px 5px rgba(0, 0, 0, 0.1);
+@slider-color-box-shadow-button_focus: var(--color-primary-light-3);
+@slider-size-button-width: 12px;
+@slider-size-button-width_active: 14px;
+@slider-color-dot-bg: var(--color-bg-2);
+@slider-border-size-dot: 2px;
+@slider-font-size-dot: 12px;
+@slider-size-dot-width: 8px;
+@slider-spacing-margin-bottom_with-mark: 24px;
+@slider-spacing-padding_width-mark: 20px;
+@slider-spacing-padding_width-mark_vertical: 20px;
+@slider-font-size-mark: 14px;
+@slider-color-mark-font: var(--color-text-3);
+@slider-size-tick-width: 1px;
+@slider-size-tick-height: 3px;
+@slider-spacing-input-margin-left: 20px;
+@slider-size-input-width: 60px;
+@slider-size-input-height: 32px;
+@slider-size-input_range-width: 20px;
+@slider-size-input_range-height: 32px;
+@slider-size-input_range_content-width: 8px;
+@slider-size-input_range_content-height: 2px;
+@slider-color-input_range_content-bg: rgb(var(--gray-6));
+@slider-size-height_vertical: 200px;
+@slider-spacing-mark-left: 3px;
+@slider-prefix: ~'@{prefix}-slider';
+
+
+/*********** space ***********/
+
+@space-prefix-cls: ~'@{prefix}-space';
+
+
+/*********** spin ***********/
+
+@spin-font-size-text: 14px;
+@spin-font-size-icon: 20px;
+@spin-font-weight: 500;
+@spin-margin-top-tip: 6px;
+@spin-color-text: rgb(var(--primary-6));
+@spin-color-icon: rgb(var(--primary-6));
+@spin-dot-color-icon_default: rgb(var(--primary-6));
+@spin-dot-color-icon_second: rgb(var(--primary-5));
+@spin-dot-color-icon_third: rgb(var(--primary-4));
+@spin-dot-color-icon_forth: rgb(var(--primary-4));
+@spin-dot-color-icon_last: rgb(var(--primary-2));
+@spin-dot-size-width: 8px;
+@spin-prefix-cls: ~'@{prefix}-spin';
+
+
+/*********** statistic ***********/
+
+@statistic-font-title-size: 14px;
+@statistic-margin-title-bottom: 8px;
+@statistic-margin-extra-top: 8px;
+@statistic-font-int-size: 26px;
+@statistic-font-decimal-size: 26px;
+@statistic-font-value-weight: 500;
+@statistic-color-value-text: var(--color-text-1);
+@statistic-color-text: var(--color-text-2);
+@statistic-color-title-text: var(--color-text-2);
+@statistic-color-extra-text: var(--color-text-2);
+@statistic-size-value-icon: 14px;
+@statistic-font-suffix-size: 14px;
+@statistic-margin-prefix-right: 4px;
+@statistic-margin-suffix-left: 4px;
+@statistic-prefix-cls: ~'@{prefix}-statistic';
+
+
+/*********** steps ***********/
+
+@steps-size-default: 28px;
+@steps-size-small: 24px;
+@steps-size-default-arrow: 72px;
+@steps-size-small-arrow: 40px;
+@steps-size-default-font-size-icon: 16px;
+@steps-size-default-font-size-title: 16px;
+@steps-size-default-font-size-description: 12px;
+@steps-size-small-font-size-icon: 14px;
+@steps-size-small-font-size-title: 14px;
+@steps-size-small-font-size-description: 12px;
+@steps-label-vertical-content-width: 140px;
+@steps-direction-horizontal-description-width: 140px;
+@steps-circle-size-item-tail: 1px;
+@steps-circle-size-item-icon-gap: 12px;
+@steps-circle-font-weight-item-title_active: 500;
+@steps-circle-border-radius-item-icon: var(--border-radius-circle);
+@steps-circle-horizontal-item-description-margin-top: 2px;
+@steps-circle-vertical-item-description-margin-top: 2px;
+@steps-circle-vertical-spacing-tail-top: 6px;
+@steps-circle-vertical-spacing-tail-bottom: 6px;
+@steps-circle-color-item-bg_wait: var(--color-fill-2);
+@steps-circle-color-item-border_wait: transparent;
+@steps-circle-color-item-icon-text_wait: var(--color-text-2);
+@steps-circle-color-item-tail_wait: var(--color-neutral-3);
+@steps-circle-color-item-title_wait: var(--color-text-2);
+@steps-circle-color-item-description_wait: var(--color-text-3);
+@steps-circle-color-item-bg_process: rgb(var(--primary-6));
+@steps-circle-color-item-border_process: transparent;
+@steps-circle-color-item-icon-text_process: var(--color-white);
+@steps-circle-color-item-tail_process: rgb(var(--primary-6));
+@steps-circle-color-item-title_process: var(--color-text-1);
+@steps-circle-color-item-description_process: var(--color-text-3);
+@steps-circle-color-item-bg_finish: var(--color-primary-light-1);
+@steps-circle-color-item-border_finish: transparent;
+@steps-circle-color-item-icon-text_finish: rgb(var(--primary-6));
+@steps-circle-color-item-title_finish: var(--color-text-1);
+@steps-circle-color-item-description_finish: var(--color-text-3);
+@steps-circle-color-item-bg_error: rgb(var(--danger-6));
+@steps-circle-color-item-border_error: transparent;
+@steps-circle-color-item-icon-text_error: var(--color-white);
+@steps-circle-color-item-tail_error: rgb(var(--danger-6));
+@steps-circle-color-item-title_error: var(--color-text-1);
+@steps-circle-color-item-description_error: var(--color-text-3);
+@steps-dot-horizontal-item-title-margin-top: 4px;
+@steps-dot-horizontal-item-description-margin-top: 4px;
+@steps-dot-vertical-item-dot-margin-top: 8px;
+@steps-dot-vertical-item-description-margin-top: 4px;
+@steps-dot-vertical-spacing-tail-top: 4px;
+@steps-dot-vertical-spacing-tail-bottom: 4px;
+@steps-dot-size-item-icon: 8px;
+@steps-dot-size-item-icon-active: 10px;
+@steps-dot-size-item-icon-gap: 4px;
+@steps-dot-size-item-tail: 1px;
+@steps-dot-vertical-item-icon-margin-right: 16px;
+@steps-dot-font-weight-item-title_active: 500;
+@steps-dot-border-radius-item-icon: var(--border-radius-circle);
+@steps-dot-color-item-bg_wait: var(--color-fill-4);
+@steps-dot-color-item-border_wait: var(--color-fill-4);
+@steps-dot-color-item-tail_wait: var(--color-neutral-3);
+@steps-dot-color-item-title_wait: var(--color-text-2);
+@steps-dot-color-item-description_wait: var(--color-text-3);
+@steps-dot-color-item-bg_process: rgb(var(--primary-6));
+@steps-dot-color-item-border_process: rgb(var(--primary-6));
+@steps-dot-color-item-tail_process: rgb(var(--primary-6));
+@steps-dot-color-item-title_process: var(--color-text-1);
+@steps-dot-color-item-description_process: var(--color-text-3);
+@steps-dot-color-item-bg_finish: rgb(var(--primary-6));
+@steps-dot-color-item-border_finish: rgb(var(--primary-6));
+@steps-dot-color-item-title_finish: var(--color-text-1);
+@steps-dot-color-item-description_finish: var(--color-text-3);
+@steps-dot-color-item-bg_error: rgb(var(--danger-6));
+@steps-dot-color-item-border_error: rgb(var(--danger-6));
+@steps-dot-color-item-tail_error: rgb(var(--danger-6));
+@steps-dot-color-item-title_error: var(--color-text-1);
+@steps-dot-color-item-description_error: var(--color-text-3);
+@steps-arrow-size-item-gap: 4px;
+@steps-arrow-size-default-title-padding-left: 16px;
+@steps-arrow-size-small-title-padding-left: 20px;
+@steps-arrow-item-description-margin-top: 0;
+@steps-arrow-font-weight-item-title_active: 500;
+@steps-arrow-color-item-bg_wait: var(--color-fill-1);
+@steps-arrow-color-item-title_wait: var(--color-text-2);
+@steps-arrow-color-item-description_wait: var(--color-text-3);
+@steps-arrow-color-item-bg_process: rgb(var(--primary-6));
+@steps-arrow-color-item-title_process: var(--color-white);
+@steps-arrow-color-item-description_process: var(--color-white);
+@steps-arrow-color-item-bg_finish: var(--color-primary-light-1);
+@steps-arrow-color-item-title_finish: var(--color-text-1);
+@steps-arrow-color-item-description_finish: var(--color-text-3);
+@steps-arrow-color-item-bg_error: rgb(var(--danger-6));
+@steps-arrow-color-item-title_error: var(--color-white);
+@steps-arrow-color-item-description_error: var(--color-white);
+@steps-navigation-color-arrow: var(--color-text-4);
+@steps-navigation-size-arrow: 6px;
+@steps-navigation-size-arrow-line-width: 2px;
+@steps-navigation-size-arrow-top: 10px;
+@steps-navigation-padding-left: 20px;
+@steps-navigation-margin-right: 32px;
+@steps-navigation-spacing-arrow-right: 10px;
+@steps-navigation-spacing-ink-left: 0;
+@steps-navigation-spacing-ink-right: 30px;
+@steps-size-default-item-icon-margin-left: 56px;
+@steps-size-small-item-icon-margin-left: 58px;
+@steps-dot-item-icon-margin-left: 66px;
+@steps-dot-vertical-item-dot-margin-top-active: 6px;
+@steps-prefix-cls: ~'@{prefix}-steps';
+
+
+/*********** switch ***********/
+
+@switch-size-default: 24px;
+@switch-size-small: 16px;
+@switch-font-size-text: 12px;
+@switch-size-dot-default: 16px;
+@switch-size-dot-small: 12px;
+@switch-line-size-dot-default: 20px;
+@switch-line-size-dot-small: 16px;
+@switch-circle-default-width: 40px;
+@switch-circle-small-width: 28px;
+@switch-round-default-width: 40px;
+@switch-round-small-width: 28px;
+@switch-line-default-width: 36px;
+@switch-line-small-width: 28px;
+@switch-line-height-bg-line: 6px;
+@switch-line-color-dot-shadow: var(--color-neutral-6);
+@switch-color-bg_on: rgb(var(--primary-6));
+@switch-color-bg_off: var(--color-fill-4);
+@switch-color-bg_on_disabled: var(--color-primary-light-3);
+@switch-color-bg_off_disabled: var(--color-fill-2);
+@switch-color-bg_on_loading: var(--color-primary-light-3);
+@switch-color-bg_off_loading: var(--color-fill-2);
+@switch-color-dot-bg: var(--color-bg-white);
+@switch-color-text_on: var(--color-white);
+@switch-color-text_off: var(--color-white);
+@switch-color-text_on_disabled: var(--color-white);
+@switch-color-text_off_disabled: var(--color-white);
+@switch-color-text_on_loading: var(--color-primary-light-1);
+@switch-color-text_off_loading: var(--color-white);
+@switch-color-dot-icon_on: rgb(var(--primary-6));
+@switch-color-dot-icon_off: var(--color-neutral-3);
+@switch-color-dot-icon_on_disabled: var(--color-primary-light-3);
+@switch-color-dot-icon_off_disabled: var(--color-fill-2);
+@switch-color-dot-icon_on_loading: var(--color-primary-light-3);
+@switch-color-dot-icon_off_loading: var(--color-neutral-3);
+@switch-color-box-shadow_checked: var(--color-primary-light-3);
+@switch-color-box-shadow_default: rgb(var(--gray-6));
+@switch-prefix-cls: ~'@{prefix}-switch';
+@switch-size-default-gap: 4px;
+@switch-size-small-gap: 2px;
+@switch-size-default-line-gap: 2px;
+@switch-size-small-line-gap: 0px;
+
+
+/*********** table ***********/
+
+@table-prefix-cls: ~'@{prefix}-table';
+@table-size-default-padding-horizontal: 16px;
+@table-size-default-padding-vertical: 9px;
+@table-size-middle-padding-horizontal: 16px;
+@table-size-middle-padding-vertical: 7px;
+@table-size-small-padding-horizontal: 16px;
+@table-size-small-padding-vertical: 5px;
+@table-size-mini-padding-horizontal: 16px;
+@table-size-mini-padding-vertical: 2px;
+@table-size-default-font-size: 14px;
+@table-size-middle-font-size: 14px;
+@table-size-small-font-size: 14px;
+@table-size-mini-font-size: 12px;
+@table-size-default-font-header-size: 14px;
+@table-size-middle-font-header-size: 14px;
+@table-size-small-font-header-size: 14px;
+@table-size-mini-font-header-size: 12px;
+@table-border-width: 1px;
+@table-border-style: solid;
+@table-size-expand-button: 14px;
+@table-spacing-expand-button-margin-right: 4px;
+@table-font-size-expand-button: 12px;
+@table-border-radius-expand-button: 2px;
+@table-color-border: var(--color-neutral-3);
+@table-border-radius: var(--border-radius-medium);
+@table-color-text-header-cell: rgb(var(--gray-10));
+@table-color-bg-header-cell: var(--color-neutral-2);
+@table-color-bg-header-sorted-cell: var(--color-neutral-3);
+@table-color-bg-header-sorted-cell_hover: rgba(var(--gray-4), 0.5);
+@table-color-header-filters-icon: var(--color-text-2);
+@table-color-header-filters-icon_active: rgb(var(--primary-6));
+@table-color-bg-header-filters-icon_hover: var(--color-neutral-4);
+@table-font-size-filters-icon: 16px;
+@table-size-filters-width: 24px;
+@table-font-weight-header-text: 500;
+@table-color-text-body-cell: rgb(var(--gray-10));
+@table-color-bg-body-cell: var(--color-bg-2);
+@table-color-bg-body-sorted-cell: var(--color-fill-1);
+@table-color-bg-body-stripe-row: var(--color-fill-1);
+@table-color-bg-body-stripe-row_dark: var(--color-bg-3);
+@table-color-bg-body-row_hover: var(--color-fill-1);
+@table-color-bg-body-row_active: var(--color-fill-1);
+@table-color-expand-icon: var(--color-text-2);
+@table-color-expand-icon-border: transparent;
+@table-color-expand-icon-border_hover: transparent;
+@table-color-expand-icon_hover: var(--color-text-1);
+@table-color-bg-expand-icon: var(--color-neutral-3);
+@table-color-bg-expand-icon_hover: var(--color-neutral-4);
+@table-color-bg-expand-content: var(--color-fill-1);
+@table-color-bg-expand-content_hover: var(--color-fill-1);
+@table-border-expand-icon-width: 1px;
+@table-spacing-header-sorter-icon-margin-left: 8px;
+@table-color-header-sorter-icon: var(--color-neutral-5);
+@table-color-header-sorter-icon_next: var(--color-neutral-6);
+@table-color-header-sorter-icon_active: rgb(var(--primary-6));
+@table-size-header-sorter-icon-height: 8px;
+@table-font-size-header-sorter-icon: 12px;
+@table-position-header-sorter-icon-up-top: -2px;
+@table-position-header-sorter-icon-down-top: -3px;
+@table-color-bg-filters-popup: var(--color-bg-5);
+@table-color-border-filters-popup: var(--color-neutral-3);
+@table-popup-min-width: 100px;
+@table-popup-max-height: 200px;
+@table-popup-border-radius: var(--border-radius-medium);
+@table-shadow-left: inset 6px 0 8px -3px rgba(0, 0, 0, 0.15);
+@table-shadow-right: inset -6px 0 8px -3px rgba(0, 0, 0, 0.15);
+@table-size-shadow-wrapper-width: 10px;
+@table-color-editable-body-cell-border: var(--color-white);
+@table-spacing-pagination-margin: 12px;
+@table-size-selection-col-width: 40px;
+@table-size-expand-icon-col-width: 40px;
+@table-color-body-background: var(--color-bg-2);
+@table-color-bg-tfoot: var(--color-neutral-2);
+@table-cls-tr: ~'@{prefix}-table-tr';
+@table-cls-th: ~'@{prefix}-table-th';
+@table-cls-td: ~'@{prefix}-table-td';
+
+
+/*********** tabs ***********/
+
+@tabs-size-mini-font-size: 12px;
+@tabs-size-small-font-size: 14px;
+@tabs-size-default-font-size: 14px;
+@tabs-size-large-font-size: 14px;
+@tabs-size-mini-font-size_card: 12px;
+@tabs-size-small-font-size_card: 14px;
+@tabs-size-default-font-size_card: 14px;
+@tabs-size-large-font-size_card: 14px;
+@tabs-size-default-font-size_text: 14px;
+@tabs-size-mini-font-size_rounded: 12px;
+@tabs-size-small-font-size_rounded: 14px;
+@tabs-size-default-font-size_rounded: 14px;
+@tabs-size-large-font-size_rounded: 14px;
+@tabs-size-mini-font-size_capsule: 12px;
+@tabs-size-small-font-size_capsule: 14px;
+@tabs-size-default-font-size_capsule: 14px;
+@tabs-size-large-font-size_capsule: 14px;
+@tabs-size-mini-header-height_line: 32px;
+@tabs-size-small-header-height_line: 36px;
+@tabs-size-default-header-height_line: 40px;
+@tabs-size-large-header-height_line: 44px;
+@tabs-size-mini-header-height: 24px;
+@tabs-size-small-header-height: 28px;
+@tabs-size-default-header-height: 32px;
+@tabs-size-large-header-height: 36px;
+@tabs-size-mini-header-height_capsule: 24px;
+@tabs-size-small-header-height_capsule: 28px;
+@tabs-size-default-header-height_capsule: 32px;
+@tabs-size-large-header-height_capsule: 36px;
+@tabs-size-default-header-height_text: 32px;
+@tabs-size-mini-header-height_rounded: 24px;
+@tabs-size-small-header-height_rounded: 28px;
+@tabs-size-default-header-height_rounded: 32px;
+@tabs-size-large-header-height_rounded: 36px;
+@tabs-padding-title-text-vertical: 1px;
+@tabs-padding-title-text-horizontal: 8px;
+@tabs-color-title-text: var(--color-text-2);
+@tabs-color-title-text_active: rgb(var(--primary-6));
+@tabs-color-title-text_hover: var(--color-text-2);
+@tabs-color-title-text_disabled: var(--color-text-4);
+@tabs-color-title-text_disabled_active: var(--color-primary-light-3);
+@tabs-line-size-header-border: 1px;
+@tabs-line-color-header-border: var(--color-neutral-3);
+@tabs-line-size-ink-stroke: 2px;
+@tabs-line-color-ink-bg: rgb(var(--primary-6));
+@tabs-line-color-ink-bg_disabled: var(--color-primary-light-3);
+@tabs-line-font-title-text-weight_active: 500;
+@tabs-line-margin-title-horizontal: 32px;
+@tabs-line-margin-title-horizontal_first: 16px;
+@tabs-line-margin-title-vertical: 12px;
+@tabs-line-padding-title-horizontal_vertical: 20px;
+@tabs-line-color-title-bg: transparent;
+@tabs-line-color-title-bg_active: transparent;
+@tabs-line-color-title-bg_hover: var(--color-fill-2);
+@tabs-line-font-title-text-weight_hover: 400;
+@tabs-line-border-radius: var(--border-radius-small);
+@tabs-card-border-width: 1px;
+@tabs-card-color-title-border: var(--color-neutral-3);
+@tabs-card-padding-title-horizontal: 16px;
+@tabs-card-padding-title-right_editable: 12px;
+@tabs-card-border-radius: var(--border-radius-small);
+@tabs-card-color-title-bg: transparent;
+@tabs-card-color-title-bg_hover: var(--color-fill-3);
+@tabs-card-color-title-bg_disabled: transparent;
+@tabs-card-color-title-bg_active: transparent;
+@tabs-card-border-content-width: 1px;
+@tabs-card-gutter-spacing-horizontal: 4px;
+@tabs-card-gutter-color-title-bg: var(--color-fill-1);
+@tabs-card-gutter-color-title-bg_hover: var(--color-fill-3);
+@tabs-card-gutter-color-title-bg_active: transparent;
+@tabs-card-gutter-color-title-bg_disabled: var(--color-fill-1);
+@tabs-text-size-separator-height: 12px;
+@tabs-text-size-separator-width: 2px;
+@tabs-text-color-separator-bg: var(--color-fill-3);
+@tabs-text-margin-title-horizontal: 8px;
+@tabs-text-color-title-bg: transparent;
+@tabs-text-color-title-bg_active: transparent;
+@tabs-text-color-title-bg_disabled: transparent;
+@tabs-text-color-title-bg_disabled_active: var(--color-primary-light-3);
+@tabs-text-color-title-bg_hover: var(--color-fill-2);
+@tabs-rounded-padding-title-horizontal: 16px;
+@tabs-rounded-margin-title-horizontal: 12px;
+@tabs-rounded-color-title-bg: transparent;
+@tabs-rounded-color-title-bg_active: var(--color-fill-2);
+@tabs-rounded-color-title-bg_disabled: transparent;
+@tabs-rounded-color-title-bg_hover: var(--color-fill-2);
+@tabs-capsule-color-header-bg: var(--color-fill-2);
+@tabs-capsule-margin-title-horizontal: 3px;
+@tabs-capsule-padding-title-horizontal: 12px;
+@tabs-capsule-padding-header-vertical: 3px;
+@tabs-capsule-padding-header-horizontal: 3px;
+@tabs-capsule-border-header-radius: var(--border-radius-small);
+@tabs-capsule-border-title-radius: var(--border-radius-small);
+@tabs-capsule-color-title-bg: transparent;
+@tabs-capsule-color-title-bg_active: var(--color-bg-2);
+@tabs-capsule-color-title-bg_hover: var(--color-bg-2);
+@tabs-capsule-size-separator-width: 1px;
+@tabs-capsule-size-separator-height: 14px;
+@tabs-capsule-color-separator-bg: var(--color-fill-3);
+@tabs-margin-close-icon-left: 8px;
+@tabs-size-icon: 12px;
+@tabs-size-icon-bg: 16px;
+@tabs-card-color-close-icon-bg_hover: var(--color-fill-4);
+@tabs-margin-add-icon-left: 8px;
+@tabs-color-icon: var(--color-text-2);
+@tabs-color-icon_disabled: var(--color-text-4);
+@tabs-spacing-nav-icon-header: 6px;
+@tabs-padding-header-wrapper-horizontal: 10px;
+@tabs-padding-header-wrapper-vertical: 6px;
+@tabs-content-padding: 16px;
+@tabs-box-shadow-radius: 2px;
+@tabs-color-box-shadow: var(--color-primary-light-3);
+@tabs-prefix-cls: ~'@{prefix}-tabs';
+@tabs-prefix-cls-vertical: ~'@{prefix}-tabs-vertical';
+
+
+/*********** sizes ***********/
+
+@sizes: mini, small, large;
+
+
+/*********** tag ***********/
+
+@tag-border-width: 1px;
+@tag-border-type: solid;
+@tag-padding-horizontal: 8px;
+@tag-padding-vertical: 0;
+@tag-icon-margin-right: 4px;
+@tag-text-font-weight: 500;
+@tag-border-radius: var(--border-radius-small);
+@tag-size-small: 20px;
+@tag-size-default: 24px;
+@tag-size-medium: 28px;
+@tag-size-large: 32px;
+@tag-size-small-font-size: 12px;
+@tag-size-default-font-size: 12px;
+@tag-size-medium-font-size: 14px;
+@tag-size-large-font-size: 14px;
+@tag-color-bg-not-checked_hover: var(--color-fill-2);
+@tag-custom-color-text: var(--color-white);
+@tag-custom-color-icon-bg_hover: rgba(255, 255, 255, 0.2);
+@tag-default-color-bg: var(--color-fill-2);
+@tag-default-color-bg_hover: var(--color-fill-3);
+@tag-default-color-icon: var(--color-text-2);
+@tag-default-color-text: var(--color-text-1);
+@tag-default-color-border: transparent;
+@tag-default-bordered-color-border: var(--color-border-2);
+@tag-default-color-border_hover: transparent;
+@tag-red-color-bg: rgb(var(--red-1));
+@tag-red-color-bg_hover: rgb(var(--red-2));
+@tag-red-color-border: transparent;
+@tag-red-bordered-color-border: rgb(var(--red-6));
+@tag-red-color-border_hover: transparent;
+@tag-red-color-icon: rgb(var(--red-6));
+@tag-red-color-icon-bg_hover: rgb(var(--red-2));
+@tag-red-color-text: rgb(var(--red-6));
+@tag-orangered-color-bg: rgb(var(--orangered-1));
+@tag-orangered-color-bg_hover: rgb(var(--orangered-2));
+@tag-orangered-color-border: transparent;
+@tag-orangered-bordered-color-border: rgb(var(--orangered-6));
+@tag-orangered-color-border_hover: transparent;
+@tag-orangered-color-icon: rgb(var(--orangered-6));
+@tag-orangered-color-icon-bg_hover: rgb(var(--orangered-2));
+@tag-orangered-color-text: rgb(var(--orangered-6));
+@tag-orange-color-bg: rgb(var(--orange-1));
+@tag-orange-color-bg_hover: rgb(var(--orange-2));
+@tag-orange-color-border: transparent;
+@tag-orange-bordered-color-border: rgb(var(--orange-6));
+@tag-orange-color-border_hover: transparent;
+@tag-orange-color-icon: rgb(var(--orange-6));
+@tag-orange-color-icon-bg_hover: rgb(var(--orange-2));
+@tag-orange-color-text: rgb(var(--orange-6));
+@tag-gold-color-bg: rgb(var(--gold-1));
+@tag-gold-color-bg_hover: rgb(var(--gold-3));
+@tag-gold-color-border: transparent;
+@tag-gold-bordered-color-border: rgb(var(--gold-6));
+@tag-gold-color-border_hover: transparent;
+@tag-gold-color-icon: rgb(var(--gold-6));
+@tag-gold-color-icon-bg_hover: rgb(var(--gold-2));
+@tag-gold-color-text: rgb(var(--gold-6));
+@tag-lime-color-bg: rgb(var(--lime-1));
+@tag-lime-color-bg_hover: rgb(var(--lime-2));
+@tag-lime-color-border: transparent;
+@tag-lime-bordered-color-border: rgb(var(--lime-6));
+@tag-lime-color-border_hover: transparent;
+@tag-lime-color-icon: rgb(var(--lime-6));
+@tag-lime-color-icon-bg_hover: rgb(var(--lime-2));
+@tag-lime-color-text: rgb(var(--lime-6));
+@tag-green-color-bg: rgb(var(--green-1));
+@tag-green-color-bg_hover: rgb(var(--green-2));
+@tag-green-color-border: transparent;
+@tag-green-bordered-color-border: rgb(var(--green-6));
+@tag-green-color-border_hover: transparent;
+@tag-green-color-icon: rgb(var(--green-6));
+@tag-green-color-icon-bg_hover: rgb(var(--green-2));
+@tag-green-color-text: rgb(var(--green-6));
+@tag-cyan-color-bg: rgb(var(--cyan-1));
+@tag-cyan-color-bg_hover: rgb(var(--cyan-2));
+@tag-cyan-color-border: transparent;
+@tag-cyan-bordered-color-border: rgb(var(--cyan-6));
+@tag-cyan-color-border_hover: transparent;
+@tag-cyan-color-icon: rgb(var(--cyan-6));
+@tag-cyan-color-icon-bg_hover: rgb(var(--cyan-2));
+@tag-cyan-color-text: rgb(var(--cyan-6));
+@tag-blue-color-bg: rgb(var(--blue-1));
+@tag-blue-color-bg_hover: rgb(var(--blue-2));
+@tag-blue-color-border: transparent;
+@tag-blue-bordered-color-border: rgb(var(--blue-6));
+@tag-blue-color-border_hover: transparent;
+@tag-blue-color-icon: rgb(var(--blue-6));
+@tag-blue-color-icon-bg_hover: rgb(var(--blue-2));
+@tag-blue-color-text: rgb(var(--blue-6));
+@tag-arcoblue-color-bg: rgb(var(--arcoblue-1));
+@tag-arcoblue-color-bg_hover: rgb(var(--arcoblue-2));
+@tag-arcoblue-color-border: transparent;
+@tag-arcoblue-bordered-color-border: rgb(var(--arcoblue-6));
+@tag-arcoblue-color-border_hover: transparent;
+@tag-arcoblue-color-icon: rgb(var(--arcoblue-6));
+@tag-arcoblue-color-icon-bg_hover: rgb(var(--arcoblue-2));
+@tag-arcoblue-color-text: rgb(var(--arcoblue-6));
+@tag-purple-color-bg: rgb(var(--purple-1));
+@tag-purple-color-bg_hover: rgb(var(--purple-2));
+@tag-purple-color-border: transparent;
+@tag-purple-bordered-color-border: rgb(var(--purple-6));
+@tag-purple-color-border_hover: transparent;
+@tag-purple-color-icon: rgb(var(--purple-6));
+@tag-purple-color-icon-bg_hover: rgb(var(--purple-2));
+@tag-purple-color-text: rgb(var(--purple-6));
+@tag-pinkpurple-color-bg: rgb(var(--pinkpurple-1));
+@tag-pinkpurple-color-bg_hover: rgb(var(--pinkpurple-2));
+@tag-pinkpurple-color-border: transparent;
+@tag-pinkpurple-bordered-color-border: rgb(var(--pinkpurple-6));
+@tag-pinkpurple-color-border_hover: transparent;
+@tag-pinkpurple-color-icon: rgb(var(--pinkpurple-6));
+@tag-pinkpurple-color-icon-bg_hover: rgb(var(--pinkpurple-2));
+@tag-pinkpurple-color-text: rgb(var(--pinkpurple-6));
+@tag-magenta-color-bg: rgb(var(--magenta-1));
+@tag-magenta-color-bg_hover: rgb(var(--magenta-2));
+@tag-magenta-color-border: transparent;
+@tag-magenta-bordered-color-border: rgb(var(--magenta-6));
+@tag-magenta-color-border_hover: transparent;
+@tag-magenta-color-icon: rgb(var(--magenta-6));
+@tag-magenta-color-icon-bg_hover: rgb(var(--magenta-2));
+@tag-magenta-color-text: rgb(var(--magenta-6));
+@tag-gray-color-bg: rgb(var(--gray-2));
+@tag-gray-color-bg_hover: rgb(var(--gray-3));
+@tag-gray-color-border: transparent;
+@tag-gray-bordered-color-border: rgb(var(--gray-6));
+@tag-gray-color-border_hover: transparent;
+@tag-gray-color-icon: rgb(var(--gray-6));
+@tag-gray-color-icon-bg_hover: rgb(var(--gray-3));
+@tag-gray-color-text: rgb(var(--gray-6));
+@tag-prefix-cls: ~'@{prefix}-tag';
+
+
+/*********** colors ***********/
+
+@colors: red, orangered, orange, gold, lime, green, cyan, blue, arcoblue, purple, pinkpurple, magenta, gray;
+
+
+/*********** timeline ***********/
+
+@timeline-color-content-text: var(--color-text-1);
+@timeline-color-label-text: var(--color-text-3);
+@timeline-color-dot-bg: rgb(var(--primary-6));
+@timeline-color-line-bg: var(--color-neutral-3);
+@timeline-font-content-size: 14px;
+@timeline-font-label-size: 12px;
+@timeline-item-min-height: 78px;
+@timeline-dot-size-width: 6px;
+@timeline-dot-border-radius: var(--border-radius-circle);
+@timeline-dot-border-width_hollow: 2px;
+@timeline-color-dot-bg_hollow: var(--color-bg-2);
+@timeline-horizontal-margin-content-spacing: 16px;
+@timeline-horizontal-margin-line-left: 6px;
+@timeline-horizontal-margin-line-right: 4px;
+@timeline-vertical-margin-content-bottom: 4px;
+@timeline-vertical-margin-content-left: 16px;
+@timeline-vertical-margin-line-top: 4px;
+@timeline-vertical-margin-line-bottom: 4px;
+@timeline-size-line-width: 1px;
+@timeline-prefix-cls: ~'@{prefix}-timeline';
+@timeline-item-prefix-cls: ~'@{prefix}-timeline-item';
+
+
+/*********** tooltip ***********/
+
+@tooltip-padding-horizontal: 12px;
+@tooltip-padding-vertical: 8px;
+@tooltip-mini-padding-horizontal: 12px;
+@tooltip-mini-padding-vertical: 4px;
+@tooltip-mini-font-size: 14px;
+@tooltip-font-size: 14px;
+@tooltip-border-radius: var(--border-radius-small);
+@tooltip-color-text: #fff;
+@tooltip-color-bg: var(--color-tooltip-bg);
+@tooltip-prefix-cls: ~'@{prefix}-tooltip';
+
+
+/*********** transfer ***********/
+
+@transfer-width: 200px;
+@transfer-height: 224px;
+@transfer-height-title: 40px;
+@transfer-height-footer: 40px;
+@transfer-padding-horizontal-footer: 8px;
+@transfer-border-color: var(--color-neutral-3);
+@transfer-border-width: 1px;
+@transfer-border-radius: var(--border-radius-small);
+@transfer-font-size-header: 14px;
+@transfer-font-size-header-unit: 12px;
+@transfer-font-size-icon: 12px;
+@transfer-font-weight-header: 500;
+@transfer-color-text-header: var(--color-text-1);
+@transfer-color-text-header-unit: var(--color-text-3);
+@transfer-color-icon: var(--color-text-2);
+@transfer-color-bg-icon: var(--color-fill-3);
+@transfer-color-bg-header: var(--color-fill-1);
+@transfer-search-padding-left: 12px;
+@transfer-search-padding-right: 12px;
+@transfer-search-padding-top: 8px;
+@transfer-search-padding-bottom: 4px;
+@transfer-item-color-bg_default: transparent;
+@transfer-item-color-bg_hover: var(--color-fill-2);
+@transfer-item-color-bg_disabled: transparent;
+@transfer-item-color_default: var(--color-text-1);
+@transfer-item-color_hover: var(--color-text-1);
+@transfer-item-color_disabled: var(--color-text-4);
+@transfer-item-height: 36px;
+@transfer-item-padding-horizontal: 10px;
+@transfer-item-font-size: 14px;
+@transfer-item-draggable-height-gap: 2px;
+@transfer-item-draggable-color-bg-gap: rgb(var(--primary-6));
+@transfer-item-draggable-color-bg_dragging: var(--color-fill-1);
+@transfer-item-draggable-color_dragging: var(--color-text-4);
+@transfer-item-draggable-color_blink: var(--color-primary-light-1);
+@transfer-pagination-width-input: 24px;
+@transfer-pagination-gap-separator: 8px;
+@transfer-operation-padding-horizontal: 20px;
+@transfer-operation-gap-buttons: 12px;
+@transfer-color-icon-box-shadow: rgb(var(--primary-6));
+@transfer-prefix-cls: ~'@{prefix}-transfer';
+
+
+/*********** tree ***********/
+
+@tree-color-title-text: var(--color-text-1);
+@tree-color-title-text_hover: var(--color-text-1);
+@tree-color-title-text_active: rgb(var(--primary-6));
+@tree-color-title-text_disabled: var(--color-text-4);
+@tree-color-title-text_active_disabled: var(--color-primary-light-3);
+@tree-color-title-bg_hover: var(--color-fill-2);
+@tree-color-title-bg_highlight: var(--color-primary-light-1);
+@tree-color-title-text_highlight: var(--color-text-1);
+@tree-color-title-bg_dragging: var(--color-fill-1);
+@tree-color-title-text_dragging: var(--color-text-4);
+@tree-color-loading-icon: rgb(var(--primary-6));
+@tree-color-switcher-icon: var(--color-text-2);
+@tree-color-drag-icon: rgb(var(--primary-6));
+@tree-node-border-radius: var(--border-radius-small);
+@tree-margin-checkbox-right: 10px;
+@tree-margin-switcher-icon-right: 10px;
+@tree-margin-custom-icon-right: 10px;
+@tree-padding-title-horizontal: 4px;
+@tree-spacing-drag-icon-right: 12px;
+@tree-spacing-drag-icon-text: 120px;
+@tree-draggable-color-gap-bg: rgb(var(--primary-6));
+@tree-draggable-size-gap-height: 2px;
+@tree-showline-color-line-bg: var(--color-neutral-3);
+@tree-showline-color-plus-icon-bg: var(--color-fill-2);
+@tree-showline-color-plus-icon-border: transparent;
+@tree-showline-plus-icon-border-width: 1px;
+@tree-showline-size-plus-icon-stroke: 2px;
+@tree-showline-size-plus-icon-width: 6px;
+@tree-showline-size-line-width: 1px;
+@tree-showline-style-line: solid;
+@tree-showline-size-switcher-icon: 14px;
+@tree-showline-spacing-line-vertical: 4px;
+@tree-showline-border-plus-icon-radius: var(--border-radius-small);
+@tree-size-mini-icon-size: 12px;
+@tree-size-small-icon-size: 12px;
+@tree-size-default-icon-size: 12px;
+@tree-size-large-icon-size: 12px;
+@tree-size-expand-icon-bg_hover: 16px;
+@tree-size-mini-font-size: 12px;
+@tree-size-small-font-size: 14px;
+@tree-size-default-font-size: 14px;
+@tree-size-large-font-size: 14px;
+@tree-size-mini-line-height: 24px;
+@tree-size-small-line-height: 28px;
+@tree-size-default-line-height: 32px;
+@tree-size-large-line-height: 36px;
+@tree-prefix-cls: ~'@{prefix}-tree';
+@tree-node-prefix-cls: ~'@{prefix}-tree-node';
+@tree-select-padding-popup-left: 10px;
+@tree-select-padding-popup-right: 4px;
+@tree-select-padding-popup-vertical: 4px;
+@tree-select-prefix-cls: ~'@{prefix}-tree-select';
+@tree-select-prefix-cls-rtl: ~'@{prefix}-tree-select-rtl';
+
+
+/*********** trigger ***********/
+
+@trigger-color-arrow-bg: var(--color-bg-5);
+@trigger-size-arrow-width: 8px;
+@trigger-border-arrow-radius: 2px;
+@trigger-prefix-cls: ~'@{prefix}-trigger';
+
+
+/*********** typography ***********/
+
+@typography-font-size-h1: 36px;
+@typography-font-size-h2: 32px;
+@typography-font-size-h3: 28px;
+@typography-font-size-h4: 24px;
+@typography-font-size-h5: 20px;
+@typography-font-size-h6: 16px;
+@typography-heading-margin-top: 1em;
+@typography-heading-margin-bottom: 0.5em;
+@typography-heading-font-weight: 500;
+@typography-color-text: var(--color-text-1);
+@typography-text-color-text-primary: rgb(var(--primary-6));
+@typography-text-color-text-secondary: var(--color-text-2);
+@typography-text-color-text-success: rgb(var(--success-6));
+@typography-text-color-text-warning: rgb(var(--warning-6));
+@typography-text-color-text-error: rgb(var(--danger-6));
+@typography-text-color-text_disabled: var(--color-text-4);
+@typography-text-color-bg-mark: rgb(var(--yellow-4));
+@typography-text-font-weight-bold: 500;
+@typography-text-color-code: var(--color-text-2);
+@typography-text-color-code-border: var(--color-neutral-3);
+@typography-text-color-code-bg: var(--color-neutral-2);
+@typography-text-padding-code-vertical: 2px;
+@typography-text-padding-code-horizontal: 8px;
+@typography-text-margin-code-horizontal: 2px;
+@typography-paragraph-line-height: 1.5715;
+@typography-paragraph-line-height-close: 1.3;
+@typography-operation-margin-left: 2px;
+@typography-color-icon-copy: var(--color-text-2);
+@typography-color-bg-icon-copy: transparent;
+@typography-color-icon-copy_hover: var(--color-text-2);
+@typography-color-bg-icon-copy_hover: var(--color-fill-2);
+@typography-color-icon-copy_copied: rgb(var(--success-6));
+@typography-color-icon-edit: var(--color-text-2);
+@typography-color-bg-icon-edit: transparent;
+@typography-color-icon-edit_hover: var(--color-text-2);
+@typography-color-bg-icon-edit_hover: var(--color-fill-2);
+@typography-color-expand-text: rgb(var(--primary-6));
+@typography-color-expand-text_hover: rgb(var(--primary-5));
+@typography-color-blockquote-border-width: 2px;
+@typography-color-blockquote-border-left: var(--color-neutral-6);
+@typography-color-blockquote-bg: var(--color-bg-2);
+@typography-color-box-shadow: var(--color-primary-light-3);
+@typography-prefix-cls: ~'@{prefix}-typography';
+
+
+/*********** ellipsis ***********/
+
+@ellipsis-action-text-color: rgb(var(--primary-6));
+@ellipsis-action-text-color_hover: rgb(var(--primary-5));
+@ellipsis-cls: arco-ellipsis;
+
+
+/*********** upload ***********/
+
+@upload-tip-color-text: var(--color-text-3);
+@upload-tip-margin-top: 4px;
+@upload-tip-font-size: 12px;
+@upload-list-margin-top: 24px;
+@upload-picture-item-width: 80px;
+@upload-picture-color-bg: var(--color-fill-2);
+@upload-picture-border-radius: var(--border-radius-small);
+@upload-picture-border-width: 1px;
+@upload-picture-border-style: dashed;
+@upload-picture-color-border: var(--color-neutral-3);
+@upload-picture-color-border_disabled: var(--color-neutral-4);
+@upload-picture-color-border_hover: var(--color-neutral-4);
+@upload-picture-color-bg_hover: var(--color-fill-3);
+@upload-picture-color-bg_disabled: var(--color-fill-1);
+@upload-picture-color-text: var(--color-text-2);
+@upload-picture-color-text_hover: var(--color-text-2);
+@upload-picture-color-text_disabled: var(--color-text-4);
+@upload-drag-font-size: 14px;
+@upload-drag-border-radius: var(--border-radius-small);
+@upload-drag-tip-margin-top: 0;
+@upload-drag-color-text: var(--color-text-1);
+@upload-drag-border-style: dashed;
+@upload-drag-border-width: 1px;
+@upload-drag-padding-vertical: 50px;
+@upload-drag-margin-icon-bottom: 24px;
+@upload-drag-color-bg: var(--color-fill-1);
+@upload-drag-color-bg_hover: var(--color-fill-3);
+@upload-drag-color-bg_active: var(--color-primary-light-1);
+@upload-drag-color-bg_disabled: var(--color-fill-1);
+@upload-drag-color-border: var(--color-neutral-3);
+@upload-drag-color-border_active: rgb(var(--primary-6));
+@upload-drag-color-border_hover: var(--color-neutral-4);
+@upload-drag-color-border_disabled: var(--color-text-4);
+@upload-drag-color-icon: var(--color-text-2);
+@upload-drag-color-icon_hover: var(--color-text-2);
+@upload-drag-color-icon_active: rgb(var(--primary-6));
+@upload-drag-color-text_hover: var(--color-text-1);
+@upload-drag-color-text_active: var(--color-text-1);
+@upload-drag-color-text_disabled: var(--color-text-4);
+@upload-text-item-size-operation-icon: 12px;
+@upload-text-item-margin-top: 12px;
+@upload-text-item-font-size: 14px;
+@upload-text-item-color-text: var(--color-text-1);
+@upload-text-item-padding-left: 12px;
+@upload-text-item-color-bg: var(--color-fill-1);
+@upload-text-item-padding-vertical: 8px;
+@upload-text-item-margin-remove-icon-left: 12px;
+@upload-text-item-color-remove-icon: var(--color-text-2);
+@upload-text-item-color-status-icon: var(--color-white);
+@upload-text-item-color-file-icon_success: rgb(var(--primary-6));
+@upload-text-item-color-progress-bg_hover: rgba(var(--gray-10), 0.2);
+@upload-text-item-color-progress-bg_hover_active: rgb(var(--primary-7));
+@upload-text-item-size-file-icon: 16px;
+@upload-text-item-margin-file-icon-right: 12px;
+@upload-text-item-color-file-icon: rgb(var(--primary-6));
+@upload-text-item-padding-right: 10px;
+@upload-text-item-color-link: rgb(var(--link-6));
+@upload-text-item-color-reupload-icon: rgb(var(--primary-6));
+@upload-text-item-color-reupload-icon_hover: rgb(var(--primary-7));
+@upload-text-item-size-status-icon: 12px;
+@upload-text-item-color-error-icon: rgb(var(--danger-6));
+@upload-text-item-color-success-icon: rgb(var(--success-6));
+@upload-text-item-border-radius: var(--border-radius-small);
+@upload-text-item-margin-error-icon-left: 4px;
+@upload-text-item-margin-status-left: 10px;
+@upload-text-item-thumbnail-width: 40px;
+@upload-text-item-margin-thumbnail-right: 12px;
+@upload-text-item-color-text_error: rgb(var(--danger-6));
+@upload-text-item-color-text_success: unset;
+@upload-text-item-color-text_uploading: unset;
+@upload-picture-item-margin-preview-icon-right: 20px;
+@upload-picture-item-size-width: 80px;
+@upload-picture-item-border-radius: var(--border-radius-small);
+@upload-picture-item-margin-right: 8px;
+@upload-picture-item-margin-bottom: 8px;
+@upload-picture-item-color-bg: var(--color-fill-2);
+@upload-picture-item-color-operation_bg: rgba(0, 0, 0, 0.5);
+@upload-picture-item-color-operation-icon: var(--color-white);
+@upload-picture-item-color-error-icon: var(--color-white);
+@upload-picture-text-item-color-bg_error: var(--color-danger-light-1);
+@upload-picture-text-item-color-text_error: rgb(var(--danger-6));
+@upload-picture-text-item-color-text_success: unset;
+@upload-picture-text-item-color-text_uploading: unset;
+@upload-picture-text-item-padding-vertical: 8px;
+@upload-drag-size-icon: 14px;
+@upload-picture-item-size-mask-icon: 16px;
+@upload-picture-item-size-error-icon: 26px;
+@upload-text-item-size-reupload-icon: 14px;
+@upload-text-item-size-success-icon: 14px;
+@upload-text-item-size-error-icon: 14px;
+@upload-picture-item-size-operation-icon: 14px;
+@upload-color-icon-box-shadow_default: var(--color-primary-light-3);
+@upload-color-icon-box-shadow_active: rgb(var(--primary-6));
+@upload-prefix-cls: ~'@{prefix}-upload';
+
+
+/*********** verification ***********/
+
+@verification-code-prefix-cls: ~'@{prefix}-verification-code';
+
+
+/*********** watermark ***********/
+
+@watermark-prefix-cls: ~'@{prefix}-watermark';
diff --git a/packages/plugin-vite-react/CHANGELOG.md b/packages/plugin-vite-react/CHANGELOG.md
new file mode 100644
index 0000000..2a553d4
--- /dev/null
+++ b/packages/plugin-vite-react/CHANGELOG.md
@@ -0,0 +1,7 @@
+## 1.0.5
+
+`2022-01-21`
+
+### 💎 Optimization
+
+- Improve icons' loading speed
diff --git a/packages/plugin-vite-react/CHANGLOG.zh-CN.md b/packages/plugin-vite-react/CHANGLOG.zh-CN.md
new file mode 100644
index 0000000..ba91d30
--- /dev/null
+++ b/packages/plugin-vite-react/CHANGLOG.zh-CN.md
@@ -0,0 +1,7 @@
+## 1.0.5
+
+`2022-01-21`
+
+### 💎 功能优化
+
+- 提升图标的加载速度
diff --git a/packages/plugin-vite-react/README.md b/packages/plugin-vite-react/README.md
new file mode 100644
index 0000000..717f385
--- /dev/null
+++ b/packages/plugin-vite-react/README.md
@@ -0,0 +1,70 @@
+# @arco-plugins/vite-react
+
+## Feature
+
+1. `Style lazy load`
+2. `Theme import`
+3. `Icon replacement`
+
+> `Style lazy load` doesn't work during development for better experience.
+
+## Install
+
+```bash
+npm i @arco-plugins/vite-react -D
+```
+
+## Usage
+
+```js
+// vite.config.js
+
+import { vitePluginForArco } from '@arco-plugins/vite-react'
+
+export default {
+ ...
+ plugins: [
+ vitePluginForArco(options),
+ ],
+}
+```
+
+```tsx
+// react
+import { Button } from '@arco-design/web-react';
+
+export default () => (
+
+
+
+
+);
+```
+
+## Options
+
+The plugin supports the following parameters:
+
+| Params | Type | Default Value | Description |
+| :--------------: | :----------------: | :-----------: | :------------------------ |
+| **`theme`** | `{String}` | `''` | Theme package name |
+| **`iconBox`** | `{String}` | `''` | Icon library package name |
+| **`modifyVars`** | `{Object}` | `{}` | Less variables |
+| **`style`** | `{'css'\|Boolean}` | `true` | Style import method |
+|**`varsInjectScope`**|`{(string\|RegExp)[]}`|`[]`| Scope of injection of less variables (modifyVars and the theme package's variables) |
+
+**Style import methods **
+
+`style: true` will import less file
+
+```js
+import '@arco-design/web-react/Affix/style';
+```
+
+`style: 'css'` will import css file
+
+```js
+import '@arco-design/web-react/Affix/style/css';
+```
+
+`style: false` will not import any style file
diff --git a/packages/plugin-vite-react/README.zh-CN.md b/packages/plugin-vite-react/README.zh-CN.md
new file mode 100644
index 0000000..0e858c9
--- /dev/null
+++ b/packages/plugin-vite-react/README.zh-CN.md
@@ -0,0 +1,70 @@
+# @arco-plugins/vite-react
+
+## 特性
+
+1. `样式按需加载`
+2. `主题引入`
+3. `图标替换`
+
+> 为了开发体验,开发环境下样式为全量引入
+
+## 安装
+
+```bash
+npm i @arco-plugins/vite-react -D
+```
+
+## 用法
+
+```js
+// vite.config.js
+
+import { vitePluginForArco } from '@arco-plugins/vite-react'
+
+export default {
+ ...
+ plugins: [
+ vitePluginForArco(options),
+ ],
+}
+```
+
+```tsx
+// react
+import { Button } from '@arco-design/web-react';
+
+export default () => (
+
+
+
+
+);
+```
+
+## 参数
+
+插件支持以下参数:
+
+| 参数名 | 类型 | 默认值 | 描述 |
+| :--------------: | :----------------: | :----: | :----------- |
+| **`theme`** | `{String}` | `''` | 主题包名 |
+| **`iconBox`** | `{String}` | `''` | 图标库包名 |
+| **`modifyVars`** | `{Object}` | `{}` | Less 变量 |
+| **`style`** | `{'css'\|Boolean}` | `true` | 样式引入方式 |
+|**`varsInjectScope`**|`string[]`|`[]`| less 变量(modifyVars 和主题包的变量)注入的范围 |
+
+**样式引入方式 **
+
+`style: true` 将引入 less 文件
+
+```js
+import '@arco-design/web-react/Affix/style';
+```
+
+`style: 'css'` 将引入 css 文件
+
+```js
+import '@arco-design/web-react/Affix/style/css';
+```
+
+`style: false` 不引入样式
diff --git a/packages/plugin-vite-react/package.json b/packages/plugin-vite-react/package.json
new file mode 100644
index 0000000..2e0fe41
--- /dev/null
+++ b/packages/plugin-vite-react/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@refly/arco-vite-plugin-react",
+ "version": "1.3.3",
+ "description": "For Vite build, load Arco Design styles on demand",
+ "main": "lib/index.js",
+ "types": "types/index.d.ts",
+ "files": [
+ "lib",
+ "types"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git@github.com:arco-design/arco-plugins.git",
+ "directory": "packages/plugin-vite-react"
+ },
+ "keywords": [
+ "arco",
+ "arco-design",
+ "arco-plugin",
+ "plugin",
+ "vite"
+ ],
+ "scripts": {
+ "prebuild": "rm -fr lib types",
+ "build": "npx tsc"
+ },
+ "author": "arco-design",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/generator": "^7.12.11",
+ "@babel/helper-module-imports": "^7.12.5",
+ "@babel/parser": "^7.12.11",
+ "@babel/traverse": "^7.12.12",
+ "@babel/types": "^7.12.12"
+ },
+ "devDependencies": {
+ "vite": "2.6.14",
+ "esbuild": "^0.13.2"
+ }
+}
diff --git a/packages/plugin-vite-react/src/arco-design-plugin/config.ts b/packages/plugin-vite-react/src/arco-design-plugin/config.ts
new file mode 100644
index 0000000..c21444f
--- /dev/null
+++ b/packages/plugin-vite-react/src/arco-design-plugin/config.ts
@@ -0,0 +1,30 @@
+type Matchers = [string, number?];
+
+export const libraryName = '@arco-design/web-react';
+
+export const iconCjsListMatchers: Matchers = [`${libraryName}/icon/index\\.js[^/]*$`];
+
+export const iconComponentMatchers: Matchers = [
+ `${libraryName}/icon/react-icon/([^/]+)/index\\.js[^/]*$`,
+ 1,
+];
+
+export const lessMatchers: Matchers = [`${libraryName}/.+\\.less[^/]*$`];
+
+export const fullLessMatchers: Matchers = [`${libraryName}/dist/css/index\\.less[^/]*$`];
+
+export const globalLessMatchers: Matchers = [`${libraryName}/(es|lib)/style/index\\.less[^/]*$`];
+
+export const componentLessMatchers: Matchers = [
+ `${libraryName}/(es|lib)/([^/]+)/style/index\\.less[^/]*$`,
+ 2,
+];
+
+export const fullCssMatchers: Matchers = [`${libraryName}/dist/css/arco\\.css[^/]*$`];
+
+export const globalCssMatchers: Matchers = [`${libraryName}/(es|lib)/style/index\\.css[^/]*$`];
+
+export const componentCssMatchers: Matchers = [
+ `${libraryName}/(es|lib)/([^/]+)/style/index\\.css[^/]*$`,
+ 2,
+];
diff --git a/packages/plugin-vite-react/src/arco-design-plugin/icon.ts b/packages/plugin-vite-react/src/arco-design-plugin/icon.ts
new file mode 100644
index 0000000..d299db9
--- /dev/null
+++ b/packages/plugin-vite-react/src/arco-design-plugin/icon.ts
@@ -0,0 +1,54 @@
+import type { UserConfig } from 'vite';
+import type { PluginBuild, OnLoadArgs } from 'esbuild';
+
+import { iconCjsListMatchers, iconComponentMatchers } from './config';
+import { pathMatch } from './utils';
+
+const filter = new RegExp(`(${iconCjsListMatchers[0]})|(${iconComponentMatchers[0]})`);
+
+export function loadIcon(id: string, iconBox: string, iconBoxLib: any) {
+ if (!iconBox || !iconBoxLib) {
+ return;
+ }
+
+ // cjs -> es
+ if (pathMatch(id, iconCjsListMatchers)) {
+ return `export * from './index.es.js'`;
+ }
+
+ const componentName = pathMatch(id, iconComponentMatchers);
+ if (componentName && iconBoxLib[componentName]) {
+ return `export { default } from '${iconBox}/esm/${componentName}/index.js'`;
+ }
+}
+
+export function modifyIconConfig(config: UserConfig, iconBox: string, iconBoxLib: any) {
+ if (!iconBox || !iconBoxLib) {
+ return;
+ }
+ // Pre-Bundling
+ config.optimizeDeps = config.optimizeDeps || {};
+ config.optimizeDeps.esbuildOptions = config.optimizeDeps.esbuildOptions || {};
+ config.optimizeDeps.esbuildOptions.plugins = config.optimizeDeps.esbuildOptions.plugins || [];
+ config.optimizeDeps.esbuildOptions.plugins.push({
+ name: 'arcoIconReplace',
+ setup(build: PluginBuild) {
+ build.onLoad(
+ {
+ namespace: 'file',
+ filter,
+ },
+ ({ path: id }: OnLoadArgs) => {
+ const contents = loadIcon(id, iconBox, iconBoxLib);
+ if (contents) {
+ return {
+ contents,
+ loader: 'js',
+ };
+ }
+ return null;
+ }
+ );
+ },
+ });
+}
diff --git a/packages/plugin-vite-react/src/arco-design-plugin/index.ts b/packages/plugin-vite-react/src/arco-design-plugin/index.ts
new file mode 100644
index 0000000..66f6749
--- /dev/null
+++ b/packages/plugin-vite-react/src/arco-design-plugin/index.ts
@@ -0,0 +1,98 @@
+import type { Plugin, ResolvedConfig, UserConfig, ConfigEnv } from 'vite';
+import { modifyCssConfig } from './less';
+import { modifyIconConfig, loadIcon } from './icon';
+import { emptyTransformJsFiles, transformCssFile, transformJsFiles } from './transform';
+
+const pkg = require('../../package.json');
+
+type Vars = Record;
+type Style = boolean | 'css';
+
+interface PluginOption {
+ theme?: string; // Theme package name
+ iconBox?: string; // Icon library package name
+ modifyVars?: Vars; // less modifyVars
+ style?: Style; // Style lazy load
+ filePatterns?: (string | RegExp)[]; // File to transform
+ varsInjectScope?: (string | RegExp)[]; // Less vars inject
+ sourceMaps?: boolean;
+}
+
+export default function vitePluginArcoImport(options: PluginOption = {}): Plugin {
+ const { theme = '', iconBox = '', modifyVars = {}, style = true, varsInjectScope = [] } = options;
+ let iconBoxLib: any;
+ let resolvedConfig: ResolvedConfig;
+ let isDevelopment = false;
+
+ if (iconBox) {
+ try {
+ iconBoxLib = require(iconBox); // eslint-disable-line
+ } catch (e) {
+ throw new Error(`IconBox ${iconBox} not existed`);
+ }
+ }
+ return {
+ name: pkg.name,
+ config(config: UserConfig, { command }: ConfigEnv) {
+ // dev mode
+ isDevelopment = command === 'serve';
+
+ // css preprocessorOptions
+ modifyCssConfig(pkg.name, config, theme, modifyVars, varsInjectScope);
+
+ // iconbox
+ modifyIconConfig(config, iconBox, iconBoxLib);
+ },
+ async load(id: string) {
+ const res = loadIcon(id, iconBox, iconBoxLib);
+ if (res !== undefined) {
+ return res;
+ }
+ // other ids should be handled as usually
+ return null;
+ },
+ configResolved(config: ResolvedConfig) {
+ resolvedConfig = config;
+ // console.log('viteConfig', resolvedConfig)
+ },
+ transform(code, id) {
+ let shouldTransform = false;
+
+ for (const pattern of options.filePatterns) {
+ if (id.match(pattern)) {
+ shouldTransform = true;
+ }
+ }
+
+ // Do not transform packages in this monorepo!
+ if (!shouldTransform) {
+ return emptyTransformJsFiles({
+ id,
+ code,
+ isDevelopment,
+ sourceMaps: options.sourceMaps || isDevelopment || Boolean(resolvedConfig?.build?.sourcemap),
+ });
+ }
+
+ // transform css files
+ const res = transformCssFile({
+ code,
+ id,
+ theme,
+ });
+ if (res !== undefined) {
+ return res;
+ }
+
+ // css lazy load
+ return transformJsFiles({
+ code,
+ id,
+ theme,
+ style,
+ isDevelopment,
+ sourceMaps: options.sourceMaps || isDevelopment || Boolean(resolvedConfig?.build?.sourcemap),
+ });
+ },
+ };
+}
diff --git a/packages/plugin-vite-react/src/arco-design-plugin/less.ts b/packages/plugin-vite-react/src/arco-design-plugin/less.ts
new file mode 100644
index 0000000..c90f311
--- /dev/null
+++ b/packages/plugin-vite-react/src/arco-design-plugin/less.ts
@@ -0,0 +1,81 @@
+import type { UserConfig } from 'vite';
+import { writeFileSync } from 'fs';
+import { lessMatchers, globalLessMatchers, componentLessMatchers } from './config';
+import { getThemeComponentList, pathMatch, readFileStrSync, parseInclude2RegExp } from './utils';
+
+type Vars = Record;
+
+// eslint-disable-next-line import/prefer-default-export
+export function modifyCssConfig(
+ pkgName: string,
+ config: UserConfig,
+ theme: string,
+ modifyVars: Vars,
+ varsInjectScope: (string | RegExp)[]
+) {
+ let modifyLess: string | boolean = '';
+ if (theme) {
+ modifyLess = readFileStrSync(`${theme}/tokens.less`);
+ if (modifyLess === false) {
+ throw new Error(`Theme ${theme} not existed`);
+ }
+ }
+ Object.entries(modifyVars).forEach(([k, v]) => {
+ modifyLess += `@${k}:${v};`;
+ });
+
+ config.css = config.css || {};
+ config.css.preprocessorOptions = config.css.preprocessorOptions || {};
+
+ const { preprocessorOptions } = config.css;
+ preprocessorOptions.less = preprocessorOptions.less || {};
+ preprocessorOptions.less.javascriptEnabled = true;
+ if (modifyLess) {
+ writeFileSync(`${__dirname}/../../.tokens.less`, modifyLess, {
+ flag: 'w',
+ });
+ const modifyLessFile = `${pkgName}/.tokens.less`;
+ const includeRegExp = parseInclude2RegExp(varsInjectScope);
+ preprocessorOptions.less.plugins = preprocessorOptions.less.plugins || [];
+ preprocessorOptions.less.plugins.push({
+ install(_lessObj: any, pluginManager: any) {
+ pluginManager.addPreProcessor(
+ {
+ process(src: string, extra: any) {
+ const {
+ fileInfo: { filename },
+ } = extra;
+
+ // arco less vars inject
+ const varsInjectMatch =
+ pathMatch(filename, lessMatchers) ||
+ (includeRegExp && pathMatch(filename, [includeRegExp]));
+ if (!varsInjectMatch) return src;
+
+ if (theme) {
+ // arco global style
+ const globalMatch = pathMatch(filename, globalLessMatchers);
+ if (globalMatch) {
+ src += `; @import '${theme}/theme.less';`;
+ }
+
+ // arco component style
+ const componentName = pathMatch(filename, componentLessMatchers);
+ if (componentName) {
+ if (getThemeComponentList(theme).includes(componentName)) {
+ src += `; @import '${theme}/components/${componentName}/index.less';`;
+ }
+ }
+ }
+
+ src += `; @import '${modifyLessFile}';`;
+
+ return src;
+ },
+ },
+ 1000
+ );
+ },
+ });
+ }
+}
diff --git a/packages/plugin-vite-react/src/arco-design-plugin/transform.ts b/packages/plugin-vite-react/src/arco-design-plugin/transform.ts
new file mode 100644
index 0000000..7f2219a
--- /dev/null
+++ b/packages/plugin-vite-react/src/arco-design-plugin/transform.ts
@@ -0,0 +1,127 @@
+import * as parser from '@babel/parser';
+import * as types from '@babel/types';
+import traverse, { NodePath } from '@babel/traverse';
+import generate from '@babel/generator';
+import { addSideEffect } from '@babel/helper-module-imports';
+
+import { libraryName, fullCssMatchers } from './config';
+import { isModExist, pathMatch, readFileStrSync } from './utils';
+
+interface Specifier {
+ imported: {
+ name: string;
+ };
+}
+
+type TransformedResult = undefined | { code: string; map: any };
+
+type Style = boolean | 'css';
+
+export function transformCssFile({ id, theme }: { code: string; id: string; theme: string }): TransformedResult {
+ if (theme) {
+ const matches = pathMatch(id, fullCssMatchers);
+ if (matches) {
+ const themeCode = readFileStrSync(`${theme}/css/arco.css`);
+ if (themeCode !== false) {
+ return {
+ code: themeCode,
+ map: null,
+ };
+ }
+ }
+ }
+ return undefined;
+}
+
+export function emptyTransformJsFiles({
+ id,
+ code,
+ isDevelopment,
+ sourceMaps,
+}: {
+ code: string;
+ id: string;
+ isDevelopment: boolean;
+ sourceMaps: boolean;
+}): TransformedResult {
+ if (!/\.(js|jsx|ts|tsx)$/.test(id)) {
+ return undefined;
+ }
+
+ if (isDevelopment) {
+ return {
+ code,
+ map: null,
+ };
+ }
+
+ const ast = parser.parse(code, {
+ sourceType: 'module',
+ plugins: ['jsx', 'classProperties', 'classPrivateProperties', 'classPrivateMethods'],
+ }) as any;
+
+ return generate(ast, { sourceMaps, sourceFileName: id });
+}
+
+export function transformJsFiles({
+ code,
+ id,
+ theme,
+ style,
+ isDevelopment,
+ sourceMaps,
+}: {
+ code: string;
+ id: string;
+ theme?: string;
+ style: Style;
+ isDevelopment: boolean;
+ sourceMaps: boolean;
+}): TransformedResult {
+ if (style === false || !/\.(js|jsx|ts|tsx)$/.test(id)) {
+ return undefined;
+ }
+
+ const fullStyleFile = `${libraryName}/dist/css/${style === 'css' ? 'arco.css' : 'index.less'}`;
+
+ // edge case && dev faster
+ if (isDevelopment) {
+ return {
+ code: `${code}\nimport '${fullStyleFile}';`,
+ map: null,
+ };
+ }
+
+ const ast = parser.parse(code, {
+ sourceType: 'module',
+ plugins: ['jsx', 'classProperties', 'classPrivateProperties', 'classPrivateMethods'],
+ }) as any;
+
+ traverse(ast, {
+ enter(path: NodePath) {
+ const { node } = path;
+ // import { Button, InputNumber, TimeLine } from '@arco-design/web-react'
+ if (types.isImportDeclaration(node)) {
+ const { value } = node.source;
+ if (value === libraryName) {
+ // lazy load (css files don't support lazy load with theme)
+ if (style !== 'css' || !theme) {
+ node.specifiers.forEach((spec) => {
+ if (types.isImportSpecifier(spec)) {
+ const importedName = (spec as Specifier).imported.name;
+ const stylePath = `${libraryName}/es/${importedName}/style/${style === 'css' ? 'css.js' : 'index.js'}`;
+ if (isModExist(stylePath)) addSideEffect(path, stylePath);
+ }
+ });
+ }
+ // import full less/css bundle file
+ else {
+ addSideEffect(path, fullStyleFile);
+ }
+ }
+ }
+ },
+ });
+
+ return generate(ast, { sourceMaps, sourceFileName: id });
+}
diff --git a/packages/plugin-vite-react/src/arco-design-plugin/typings.d.ts b/packages/plugin-vite-react/src/arco-design-plugin/typings.d.ts
new file mode 100644
index 0000000..910ab18
--- /dev/null
+++ b/packages/plugin-vite-react/src/arco-design-plugin/typings.d.ts
@@ -0,0 +1,2 @@
+declare module '*.json';
+declare module '@babel/helper-module-imports';
diff --git a/packages/plugin-vite-react/src/arco-design-plugin/utils.ts b/packages/plugin-vite-react/src/arco-design-plugin/utils.ts
new file mode 100644
index 0000000..ebc93e7
--- /dev/null
+++ b/packages/plugin-vite-react/src/arco-design-plugin/utils.ts
@@ -0,0 +1,89 @@
+import { readFileSync, readdirSync } from 'fs';
+import { dirname, extname, resolve, sep, win32, posix } from 'path';
+
+// read file content
+export function readFileStrSync(path: string): false | string {
+ try {
+ const resolvedPath = require.resolve(path);
+ return readFileSync(resolvedPath).toString();
+ } catch (error) {
+ return false;
+ }
+}
+
+// check if a module existed
+const modExistObj: Record = {};
+export function isModExist(path: string) {
+ if (modExistObj[path] === undefined) {
+ try {
+ require.resolve(path);
+ modExistObj[path] = true;
+ } catch (error) {
+ modExistObj[path] = false;
+ }
+ }
+ return modExistObj[path];
+}
+
+// the theme package's component list
+const componentsListObj: Record = {};
+export function getThemeComponentList(theme: string) {
+ if (!theme) return [];
+ if (!componentsListObj[theme]) {
+ try {
+ const packageRootDir = dirname(require.resolve(`${theme}/package.json`));
+ const dirPath = `${packageRootDir}/components`;
+ componentsListObj[theme] = readdirSync(dirPath) || [];
+ } catch (error) {
+ componentsListObj[theme] = [];
+ }
+ }
+ return componentsListObj[theme];
+}
+
+export const parse2PosixPath = (path: string) =>
+ sep === win32.sep ? path.replaceAll(win32.sep, posix.sep) : path;
+
+// filePath match
+export function pathMatch(path: string, conf: [string | RegExp, number?]): false | string {
+ const [regStr, order = 0] = conf;
+ const reg = new RegExp(regStr);
+ const posixPath = parse2PosixPath(path);
+ const matches = posixPath.match(reg);
+ if (!matches) return false;
+ return matches[order];
+}
+
+export function parseInclude2RegExp(include: (string | RegExp)[] = [], context?: string) {
+ if (include.length === 0) return false;
+ context = context || process.cwd();
+ const regStrList = [];
+ const folders = include
+ .map((el) => {
+ if (el instanceof RegExp) {
+ const regStr = el.toString();
+ if (regStr.slice(-1) === '/') {
+ regStrList.push(`(${regStr.slice(1, -1)})`);
+ }
+ return false;
+ }
+ const absolutePath = parse2PosixPath(resolve(context, el));
+ const idx = absolutePath.indexOf('/node_modules/');
+ const len = '/node_modules/'.length;
+ const isFolder = extname(absolutePath) === '';
+ if (idx > -1) {
+ const prexPath = absolutePath.slice(0, idx + len);
+ const packagePath = absolutePath.slice(idx + len);
+ return `(${prexPath}(\\.pnpm/.+/)?${packagePath}${isFolder ? '/' : ''})`;
+ }
+ return `(${absolutePath}${isFolder ? '/' : ''})`;
+ })
+ .filter((el) => el !== false);
+ if (folders.length) {
+ regStrList.push(`(^${folders.join('|')})`);
+ }
+ if (regStrList.length > 0) {
+ return new RegExp(regStrList.join('|'));
+ }
+ return false;
+}
diff --git a/packages/plugin-vite-react/src/index.ts b/packages/plugin-vite-react/src/index.ts
new file mode 100644
index 0000000..1311d94
--- /dev/null
+++ b/packages/plugin-vite-react/src/index.ts
@@ -0,0 +1,5 @@
+import vitePluginForArco from './arco-design-plugin';
+
+export default vitePluginForArco;
+
+export { vitePluginForArco };
diff --git a/packages/plugin-vite-react/tsconfig.json b/packages/plugin-vite-react/tsconfig.json
new file mode 100644
index 0000000..0cd0ce4
--- /dev/null
+++ b/packages/plugin-vite-react/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./lib",
+ "declarationDir": "./types",
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "noUnusedParameters": true,
+ "noUnusedLocals": true,
+ "module": "commonjs",
+ "target": "esnext",
+ "declaration": true,
+ "lib": ["esnext", "dom"],
+ "types": ["node"],
+ "resolveJsonModule": true,
+ "sourceMap": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/plugin-vite-watcher/.gitignore b/packages/plugin-vite-watcher/.gitignore
new file mode 100644
index 0000000..3058771
--- /dev/null
+++ b/packages/plugin-vite-watcher/.gitignore
@@ -0,0 +1,2 @@
+/lib
+/types
\ No newline at end of file
diff --git a/packages/plugin-vite-watcher/.tokens.less b/packages/plugin-vite-watcher/.tokens.less
new file mode 100644
index 0000000..ba67957
--- /dev/null
+++ b/packages/plugin-vite-watcher/.tokens.less
@@ -0,0 +1,3972 @@
+
+
+/*********** font ***********/
+
+@font-family: Inter, -apple-system, BlinkMacSystemFont, PingFang SC, Hiragino Sans GB, noto sans, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
+@font-size-display-3: 56px;
+@font-size-display-2: 48px;
+@font-size-display-1: 36px;
+@font-size-title-3: 24px;
+@font-size-title-2: 20px;
+@font-size-title-1: 16px;
+@font-size-body-3: 14px;
+@font-size-body-2: 13px;
+@font-size-body-1: 12px;
+@font-size-caption: 12px;
+@font-weight-100: 100;
+@font-weight-200: 200;
+@font-weight-300: 300;
+@font-weight-400: 400;
+@font-weight-500: 500;
+@font-weight-600: 600;
+@font-weight-700: 700;
+@font-weight-800: 800;
+@font-weight-900: 900;
+@font-size-body: 14px;
+
+
+/*********** red ***********/
+
+@red-1: #FFECE8;
+@red-2: #FDCDC5;
+@red-3: #FBACA3;
+@red-4: #F98981;
+@red-5: #F76560;
+@red-6: #f53f3f;
+@red-7: #CB272D;
+@red-8: #A1151E;
+@red-9: #770813;
+@red-10: #4D000A;
+
+
+/*********** orangered ***********/
+
+@orangered-1: #FFF3E8;
+@orangered-2: #FDDDC3;
+@orangered-3: #FCC59F;
+@orangered-4: #FAAC7B;
+@orangered-5: #F99057;
+@orangered-6: #f77234;
+@orangered-7: #CC5120;
+@orangered-8: #A23511;
+@orangered-9: #771F06;
+@orangered-10: #4D0E00;
+
+
+/*********** orange ***********/
+
+@orange-1: #FFF7E8;
+@orange-2: #FFE4BA;
+@orange-3: #FFCF8B;
+@orange-4: #FFB65D;
+@orange-5: #FF9A2E;
+@orange-6: #ff7d00;
+@orange-7: #D25F00;
+@orange-8: #A64500;
+@orange-9: #792E00;
+@orange-10: #4D1B00;
+
+
+/*********** gold ***********/
+
+@gold-1: #FFFCE8;
+@gold-2: #FDF4BF;
+@gold-3: #FCE996;
+@gold-4: #FADC6D;
+@gold-5: #F9CC45;
+@gold-6: #f7ba1e;
+@gold-7: #CC9213;
+@gold-8: #A26D0A;
+@gold-9: #774B04;
+@gold-10: #4D2D00;
+
+
+/*********** yellow ***********/
+
+@yellow-1: #FEFFE8;
+@yellow-2: #FEFEBE;
+@yellow-3: #FDFA94;
+@yellow-4: #FCF26B;
+@yellow-5: #FBE842;
+@yellow-6: #fadc19;
+@yellow-7: #CFAF0F;
+@yellow-8: #A38408;
+@yellow-9: #785D03;
+@yellow-10: #4D3800;
+
+
+/*********** lime ***********/
+
+@lime-1: #FCFFE8;
+@lime-2: #EDF8BB;
+@lime-3: #DCF190;
+@lime-4: #C9E968;
+@lime-5: #B5E241;
+@lime-6: #9fdb1d;
+@lime-7: #7EB712;
+@lime-8: #5F940A;
+@lime-9: #437004;
+@lime-10: #2A4D00;
+
+
+/*********** green ***********/
+
+@green-1: #E8FFEA;
+@green-2: #AFF0B5;
+@green-3: #7BE188;
+@green-4: #4CD263;
+@green-5: #23C343;
+@green-6: #00b42a;
+@green-7: #009A29;
+@green-8: #008026;
+@green-9: #006622;
+@green-10: #004D1C;
+
+
+/*********** cyan ***********/
+
+@cyan-1: #E8FFFB;
+@cyan-2: #B7F4EC;
+@cyan-3: #89E9E0;
+@cyan-4: #5EDFD6;
+@cyan-5: #37D4CF;
+@cyan-6: #14c9c9;
+@cyan-7: #0DA5AA;
+@cyan-8: #07828B;
+@cyan-9: #03616C;
+@cyan-10: #00424D;
+
+
+/*********** blue ***********/
+
+@blue-1: #E8F7FF;
+@blue-2: #C3E7FE;
+@blue-3: #9FD4FD;
+@blue-4: #7BC0FC;
+@blue-5: #57A9FB;
+@blue-6: #3491fa;
+@blue-7: #206CCF;
+@blue-8: #114BA3;
+@blue-9: #063078;
+@blue-10: #001A4D;
+
+
+/*********** arcoblue ***********/
+
+@arcoblue-1: #E8F3FF;
+@arcoblue-2: #BEDAFF;
+@arcoblue-3: #94BFFF;
+@arcoblue-4: #6AA1FF;
+@arcoblue-5: #4080FF;
+@arcoblue-6: #165dff;
+@arcoblue-7: #0E42D2;
+@arcoblue-8: #072CA6;
+@arcoblue-9: #031A79;
+@arcoblue-10: #000D4D;
+
+
+/*********** purple ***********/
+
+@purple-1: #F5E8FF;
+@purple-2: #DDBEF6;
+@purple-3: #C396ED;
+@purple-4: #A871E3;
+@purple-5: #8D4EDA;
+@purple-6: #722ed1;
+@purple-7: #551DB0;
+@purple-8: #3C108F;
+@purple-9: #27066E;
+@purple-10: #16004D;
+
+
+/*********** pinkpurple ***********/
+
+@pinkpurple-1: #FFE8FB;
+@pinkpurple-2: #F7BAEF;
+@pinkpurple-3: #F08EE6;
+@pinkpurple-4: #E865DF;
+@pinkpurple-5: #E13EDB;
+@pinkpurple-6: #d91ad9;
+@pinkpurple-7: #B010B6;
+@pinkpurple-8: #8A0993;
+@pinkpurple-9: #650370;
+@pinkpurple-10: #42004D;
+
+
+/*********** magenta ***********/
+
+@magenta-1: #FFE8F1;
+@magenta-2: #FDC2DB;
+@magenta-3: #FB9DC7;
+@magenta-4: #F979B7;
+@magenta-5: #F754A8;
+@magenta-6: #f5319d;
+@magenta-7: #CB1E83;
+@magenta-8: #A11069;
+@magenta-9: #77064F;
+@magenta-10: #4D0034;
+
+
+/*********** gray ***********/
+
+@gray-1: #f7f8fa;
+@gray-2: #f2f3f5;
+@gray-3: #e5e6eb;
+@gray-4: #c9cdd4;
+@gray-5: #a9aeb8;
+@gray-6: #86909c;
+@gray-7: #6b7785;
+@gray-8: #4e5969;
+@gray-9: #272e3b;
+@gray-10: #1d2129;
+
+
+/*********** dark ***********/
+
+@dark-red-1: #4D000A;
+@dark-red-2: #770611;
+@dark-red-3: #A1161F;
+@dark-red-4: #CB2E34;
+@dark-red-5: #F54E4E;
+@dark-red-6: #F76965;
+@dark-red-7: #F98D86;
+@dark-red-8: #FBB0A7;
+@dark-red-9: #FDD1CA;
+@dark-red-10: #FFF0EC;
+@dark-orangered-1: #4D0E00;
+@dark-orangered-2: #771E05;
+@dark-orangered-3: #A23714;
+@dark-orangered-4: #CC5729;
+@dark-orangered-5: #F77E45;
+@dark-orangered-6: #F9925A;
+@dark-orangered-7: #FAAD7D;
+@dark-orangered-8: #FCC6A1;
+@dark-orangered-9: #FDDEC5;
+@dark-orangered-10: #FFF4EB;
+@dark-orange-1: #4D1B00;
+@dark-orange-2: #793004;
+@dark-orange-3: #A64B0A;
+@dark-orange-4: #D26913;
+@dark-orange-5: #FF8D1F;
+@dark-orange-6: #FF9626;
+@dark-orange-7: #FFB357;
+@dark-orange-8: #FFCD87;
+@dark-orange-9: #FFE3B8;
+@dark-orange-10: #FFF7E8;
+@dark-gold-1: #4D2D00;
+@dark-gold-2: #774B04;
+@dark-gold-3: #A26F0F;
+@dark-gold-4: #CC961F;
+@dark-gold-5: #F7C034;
+@dark-gold-6: #F9CC44;
+@dark-gold-7: #FADC6C;
+@dark-gold-8: #FCE995;
+@dark-gold-9: #FDF4BE;
+@dark-gold-10: #FFFCE8;
+@dark-yellow-1: #4D3800;
+@dark-yellow-2: #785E07;
+@dark-yellow-3: #A38614;
+@dark-yellow-4: #CFB325;
+@dark-yellow-5: #FAE13C;
+@dark-yellow-6: #FBE94B;
+@dark-yellow-7: #FCF374;
+@dark-yellow-8: #FDFA9D;
+@dark-yellow-9: #FEFEC6;
+@dark-yellow-10: #FEFFF0;
+@dark-lime-1: #2A4D00;
+@dark-lime-2: #447006;
+@dark-lime-3: #629412;
+@dark-lime-4: #84B723;
+@dark-lime-5: #A8DB39;
+@dark-lime-6: #B8E24B;
+@dark-lime-7: #CBE970;
+@dark-lime-8: #DEF198;
+@dark-lime-9: #EEF8C2;
+@dark-lime-10: #FDFFEE;
+@dark-green-1: #004D1C;
+@dark-green-2: #046625;
+@dark-green-3: #0A802D;
+@dark-green-4: #129A37;
+@dark-green-5: #1DB440;
+@dark-green-6: #27C346;
+@dark-green-7: #50D266;
+@dark-green-8: #7EE18B;
+@dark-green-9: #B2F0B7;
+@dark-green-10: #EBFFEC;
+@dark-cyan-1: #00424D;
+@dark-cyan-2: #06616C;
+@dark-cyan-3: #11838B;
+@dark-cyan-4: #1FA6AA;
+@dark-cyan-5: #30C9C9;
+@dark-cyan-6: #3FD4CF;
+@dark-cyan-7: #66DFD7;
+@dark-cyan-8: #90E9E1;
+@dark-cyan-9: #BEF4ED;
+@dark-cyan-10: #F0FFFC;
+@dark-blue-1: #001A4D;
+@dark-blue-2: #052F78;
+@dark-blue-3: #134CA3;
+@dark-blue-4: #2971CF;
+@dark-blue-5: #469AFA;
+@dark-blue-6: #5AAAFB;
+@dark-blue-7: #7DC1FC;
+@dark-blue-8: #A1D5FD;
+@dark-blue-9: #C6E8FE;
+@dark-blue-10: #EAF8FF;
+@dark-arcoblue-1: #000D4D;
+@dark-arcoblue-2: #041B79;
+@dark-arcoblue-3: #0E32A6;
+@dark-arcoblue-4: #1D4DD2;
+@dark-arcoblue-5: #306FFF;
+@dark-arcoblue-6: #3C7EFF;
+@dark-arcoblue-7: #689FFF;
+@dark-arcoblue-8: #93BEFF;
+@dark-arcoblue-9: #BEDAFF;
+@dark-arcoblue-10: #EAF4FF;
+@dark-purple-1: #16004D;
+@dark-purple-2: #27066E;
+@dark-purple-3: #3E138F;
+@dark-purple-4: #5A25B0;
+@dark-purple-5: #7B3DD1;
+@dark-purple-6: #8E51DA;
+@dark-purple-7: #A974E3;
+@dark-purple-8: #C59AED;
+@dark-purple-9: #DFC2F6;
+@dark-purple-10: #F7EDFF;
+@dark-pinkpurple-1: #42004D;
+@dark-pinkpurple-2: #650370;
+@dark-pinkpurple-3: #8A0D93;
+@dark-pinkpurple-4: #B01BB6;
+@dark-pinkpurple-5: #D92ED9;
+@dark-pinkpurple-6: #E13DDB;
+@dark-pinkpurple-7: #E866DF;
+@dark-pinkpurple-8: #F092E6;
+@dark-pinkpurple-9: #F7C1F0;
+@dark-pinkpurple-10: #FFF2FD;
+@dark-magenta-1: #4D0034;
+@dark-magenta-2: #770850;
+@dark-magenta-3: #A1176C;
+@dark-magenta-4: #CB2B88;
+@dark-magenta-5: #F545A6;
+@dark-magenta-6: #F756A9;
+@dark-magenta-7: #F97AB8;
+@dark-magenta-8: #FB9EC8;
+@dark-magenta-9: #FDC3DB;
+@dark-magenta-10: #FFE8F1;
+@dark-gray-1: #17171a;
+@dark-gray-2: #2e2e30;
+@dark-gray-3: #484849;
+@dark-gray-4: #5f5f60;
+@dark-gray-5: #78787a;
+@dark-gray-6: #929293;
+@dark-gray-7: #ababac;
+@dark-gray-8: #c5c5c5;
+@dark-gray-9: #dfdfdf;
+@dark-gray-10: #f6f6f6;
+@dark-primary-1: #00464d;
+@dark-primary-2: #045a5f;
+@dark-primary-3: #096f71;
+@dark-primary-4: #108481;
+@dark-primary-5: #189690;
+@dark-primary-6: #22ab9f;
+@dark-primary-7: #49c0b2;
+@dark-primary-8: #77d5c7;
+@dark-primary-9: #adeadf;
+@dark-primary-10: #ebfffb;
+@dark-success-1: rgb(var(--green-1));
+@dark-success-2: rgb(var(--green-2));
+@dark-success-3: rgb(var(--green-3));
+@dark-success-4: rgb(var(--green-4));
+@dark-success-5: rgb(var(--green-5));
+@dark-success-6: rgb(var(--green-6));
+@dark-success-7: rgb(var(--green-7));
+@dark-success-8: rgb(var(--green-8));
+@dark-success-9: rgb(var(--green-9));
+@dark-success-10: rgb(var(--green-10));
+@dark-danger-1: rgb(var(--red-1));
+@dark-danger-2: rgb(var(--red-2));
+@dark-danger-3: rgb(var(--red-3));
+@dark-danger-4: rgb(var(--red-4));
+@dark-danger-5: rgb(var(--red-5));
+@dark-danger-6: rgb(var(--red-6));
+@dark-danger-7: rgb(var(--red-7));
+@dark-danger-8: rgb(var(--red-8));
+@dark-danger-9: rgb(var(--red-9));
+@dark-danger-10: rgb(var(--red-10));
+@dark-warning-1: rgb(var(--orange-1));
+@dark-warning-2: rgb(var(--orange-2));
+@dark-warning-3: rgb(var(--orange-3));
+@dark-warning-4: rgb(var(--orange-4));
+@dark-warning-5: rgb(var(--orange-5));
+@dark-warning-6: rgb(var(--orange-6));
+@dark-warning-7: rgb(var(--orange-7));
+@dark-warning-8: rgb(var(--orange-8));
+@dark-warning-9: rgb(var(--orange-9));
+@dark-warning-10: rgb(var(--orange-10));
+@dark-link-1: rgb(var(--arcoblue-1));
+@dark-link-2: rgb(var(--arcoblue-2));
+@dark-link-3: rgb(var(--arcoblue-3));
+@dark-link-4: rgb(var(--arcoblue-4));
+@dark-link-5: rgb(var(--arcoblue-5));
+@dark-link-6: rgb(var(--arcoblue-6));
+@dark-link-7: rgb(var(--arcoblue-7));
+@dark-link-8: rgb(var(--arcoblue-8));
+@dark-link-9: rgb(var(--arcoblue-9));
+@dark-link-10: rgb(var(--arcoblue-10));
+@dark-color-white: rgba(255, 255, 255, 0.9);
+@dark-color-black: #000000;
+@dark-mask-color-bg: rgba(23, 23, 26, 0.6);
+@dark-color-tooltip-bg: #373739;
+@dark-color-spin-layer-bg: rgba(51, 51, 51, 0.6);
+@dark-color-menu-dark-hover: var(--color-fill-2);
+@dark-color-border: #333335;
+@dark-color-bg-1: #17171A;
+@dark-color-bg-2: #232324;
+@dark-color-bg-3: #2a2a2b;
+@dark-color-bg-4: #313132;
+@dark-color-bg-5: #373739;
+@dark-color-bg-white: #f6f6f6;
+@dark-color-text-1: rgba(255, 255, 255, 0.9);
+@dark-color-text-2: rgba(255, 255, 255, 0.7);
+@dark-color-text-3: rgba(255, 255, 255, 0.5);
+@dark-color-text-4: rgba(255, 255, 255, 0.3);
+@dark-color-fill-1: rgba(255, 255, 255, 0.04);
+@dark-color-fill-2: rgba(255, 255, 255, 0.08);
+@dark-color-fill-3: rgba(255, 255, 255, 0.12);
+@dark-color-fill-4: rgba(255, 255, 255, 0.16);
+@dark-color-primary-light-1: rgba(var(--primary-6), 0.2);
+@dark-color-primary-light-2: rgba(var(--primary-6), 0.35);
+@dark-color-primary-light-3: rgba(var(--primary-6), 0.5);
+@dark-color-primary-light-4: rgba(var(--primary-6), 0.65);
+@dark-color-secondary: rgba(var(--gray-9), 0.08);
+@dark-color-secondary-hover: rgba(var(--gray-8), 0.16);
+@dark-color-secondary-active: rgba(var(--gray-7), 0.24);
+@dark-color-secondary-disabled: rgba(var(--gray-9), 0.08);
+@dark-color-danger-light-1: rgba(var(--danger-6), 0.2);
+@dark-color-danger-light-2: rgba(var(--danger-6), 0.35);
+@dark-color-danger-light-3: rgba(var(--danger-6), 0.5);
+@dark-color-danger-light-4: rgba(var(--danger-6), 0.65);
+@dark-color-success-light-1: rgb(var(--success-6), 0.2);
+@dark-color-success-light-2: rgb(var(--success-6), 0.35);
+@dark-color-success-light-3: rgb(var(--success-6), 0.5);
+@dark-color-success-light-4: rgb(var(--success-6), 0.65);
+@dark-color-warning-light-1: rgb(var(--warning-6), 0.2);
+@dark-color-warning-light-2: rgb(var(--warning-6), 0.35);
+@dark-color-warning-light-3: rgb(var(--warning-6), 0.5);
+@dark-color-warning-light-4: rgb(var(--warning-6), 0.65);
+@dark-color-link-light-1: rgba(var(--link-6), 0.2);
+@dark-color-link-light-2: rgba(var(--link-6), 0.35);
+@dark-color-link-light-3: rgba(var(--link-6), 0.5);
+@dark-color-link-light-4: rgba(var(--link-6), 0.65);
+@dark-color-border-1: #2e2e30;
+@dark-color-border-2: #484849;
+@dark-color-border-4: #929293;
+@dark-color-border-3: #5f5f60;
+
+
+/*********** primary ***********/
+
+@primary-1: #e8fffa;
+@primary-2: #aaeade;
+@primary-3: #74d5c6;
+@primary-4: #46c0b2;
+@primary-5: #1fab9f;
+@primary-6: #00968f;
+@primary-7: #008481;
+@primary-8: #006f71;
+@primary-9: #005a5f;
+@primary-10: #00464d;
+
+
+/*********** success ***********/
+
+@success-1: rgb(var(--green-1));
+@success-2: rgb(var(--green-2));
+@success-3: rgb(var(--green-3));
+@success-4: rgb(var(--green-4));
+@success-5: rgb(var(--green-5));
+@success-6: rgb(var(--green-6));
+@success-7: rgb(var(--green-7));
+@success-8: rgb(var(--green-8));
+@success-9: rgb(var(--green-9));
+@success-10: rgb(var(--green-10));
+
+
+/*********** danger ***********/
+
+@danger-1: rgb(var(--red-1));
+@danger-2: rgb(var(--red-2));
+@danger-3: rgb(var(--red-3));
+@danger-4: rgb(var(--red-4));
+@danger-5: rgb(var(--red-5));
+@danger-6: rgb(var(--red-6));
+@danger-7: rgb(var(--red-7));
+@danger-8: rgb(var(--red-8));
+@danger-9: rgb(var(--red-9));
+@danger-10: rgb(var(--red-10));
+
+
+/*********** warning ***********/
+
+@warning-1: rgb(var(--orange-1));
+@warning-2: rgb(var(--orange-2));
+@warning-3: rgb(var(--orange-3));
+@warning-4: rgb(var(--orange-4));
+@warning-5: rgb(var(--orange-5));
+@warning-6: rgb(var(--orange-6));
+@warning-7: rgb(var(--orange-7));
+@warning-8: rgb(var(--orange-8));
+@warning-9: rgb(var(--orange-9));
+@warning-10: rgb(var(--orange-10));
+
+
+/*********** link ***********/
+
+@link-1: rgb(var(--arcoblue-1));
+@link-2: rgb(var(--arcoblue-2));
+@link-3: rgb(var(--arcoblue-3));
+@link-4: rgb(var(--arcoblue-4));
+@link-5: rgb(var(--arcoblue-5));
+@link-6: rgb(var(--arcoblue-6));
+@link-7: rgb(var(--arcoblue-7));
+@link-8: rgb(var(--arcoblue-8));
+@link-9: rgb(var(--arcoblue-9));
+@link-10: rgb(var(--arcoblue-10));
+@link-font-size: 14px;
+@link-line-height: 1.5715;
+@link-color-bg_hover: var(--color-fill-2);
+@link-color-bg_active: var(--color-fill-3);
+@link-padding-horizontal: 4px;
+@link-color-text: rgb(var(--link-6));
+@link-color-text_hover: rgb(var(--link-6));
+@link-color-text_active: rgb(var(--link-6));
+@link-color-text_disabled: var(--color-link-light-3);
+@link-color-text_success: rgb(var(--success-6));
+@link-color-text_success_hover: rgb(var(--success-6));
+@link-color-text_success_active: rgb(var(--success-6));
+@link-color-text_success_disabled: var(--color-success-light-3);
+@link-color-text_error: rgb(var(--danger-6));
+@link-color-text_error_active: rgb(var(--danger-6));
+@link-color-text_error_hover: rgb(var(--danger-6));
+@link-color-text_error_disabled: var(--color-danger-light-3);
+@link-color-text_warning: rgb(var(--warning-6));
+@link-color-text_warning_hover: rgb(var(--warning-6));
+@link-color-text_warning_active: rgb(var(--warning-6));
+@link-color-text_warning_disabled: var(--color-warning-light-2);
+@link-margin-icon-right: 6px;
+@link-padding-vertical: 1px;
+@link-size-icon: 12px;
+@link-border-radius: var(--border-radius-small);
+@link-color-box-shadow: var(--color-link-light-3);
+@link-prefix-cls: ~'@{prefix}-link';
+
+
+/*********** global ***********/
+
+@data-1: rgb(var(--arcoblue-5));
+@data-2: rgb(var(--arcoblue-2));
+@data-3: #55c5fd;
+@data-4: #9cdcfc;
+@data-5: rgb(var(--orange-6));
+@data-6: rgb(var(--orange-3));
+@data-7: rgb(var(--green-4));
+@data-8: rgb(var(--green-2));
+@data-9: rgb(var(--purple-4));
+@data-10: rgb(var(--purple-2));
+@data-11: rgb(var(--gold-6));
+@data-12: rgb(var(--gold-4));
+@data-13: rgb(var(--lime-6));
+@data-14: rgb(var(--lime-4));
+@data-15: rgb(var(--magenta-4));
+@data-16: rgb(var(--magenta-3));
+@data-17: rgb(var(--cyan-6));
+@data-18: rgb(var(--cyan-3));
+@data-19: rgb(var(--pinkpurple-4));
+@data-20: rgb(var(--pinkpurple-2));
+@dark-data-1: rgb(var(--arcoblue-5));
+@dark-data-2: rgb(var(--arcoblue-3));
+@dark-data-3: rgb(var(--blue-5));
+@dark-data-4: rgb(var(--blue-3));
+@dark-data-5: rgb(var(--orange-6));
+@dark-data-6: rgb(var(--orange-3));
+@dark-data-7: rgb(var(--green-4));
+@dark-data-8: rgb(var(--green-3));
+@dark-data-9: rgb(var(--purple-4));
+@dark-data-10: rgb(var(--purple-3));
+@dark-data-11: rgb(var(--gold-6));
+@dark-data-12: rgb(var(--gold-4));
+@dark-data-13: rgb(var(--lime-6));
+@dark-data-14: rgb(var(--lime-4));
+@dark-data-15: rgb(var(--magenta-4));
+@dark-data-16: rgb(var(--magenta-3));
+@dark-data-17: rgb(var(--cyan-6));
+@dark-data-18: rgb(var(--cyan-3));
+@dark-data-19: rgb(var(--pinkpurple-4));
+@dark-data-20: rgb(var(--pinkpurple-2));
+@color-data-1: rgb(var(--arcoblue-5));
+@color-data-2: rgb(var(--arcoblue-3));
+@color-data-3: rgb(var(--blue-5));
+@color-data-4: rgb(var(--blue-3));
+@color-data-5: rgb(var(--orange-6));
+@color-data-6: rgb(var(--orange-3));
+@color-data-7: rgb(var(--green-4));
+@color-data-8: rgb(var(--green-3));
+@color-data-9: rgb(var(--purple-4));
+@color-data-10: rgb(var(--purple-3));
+@color-data-11: rgb(var(--gold-6));
+@color-data-12: rgb(var(--gold-4));
+@color-data-13: rgb(var(--lime-6));
+@color-data-14: rgb(var(--lime-4));
+@color-data-15: rgb(var(--magenta-4));
+@color-data-16: rgb(var(--magenta-3));
+@color-data-17: rgb(var(--cyan-6));
+@color-data-18: rgb(var(--cyan-3));
+@color-data-19: rgb(var(--pinkpurple-4));
+@color-data-20: rgb(var(--pinkpurple-2));
+
+
+/*********** border ***********/
+
+@border-none: 0;
+@border-1: 1px;
+@border-2: 2px;
+@border-3: 3px;
+@border-4: 4px;
+@border-5: 5px;
+@border-solid: solid;
+@border-dashed: dashed;
+@border-dotted: dotted;
+@border-radius-none: 0;
+@border-radius-small: 2px;
+@border-radius-medium: 4px;
+@border-radius-large: 8px;
+@border-radius-circle: 50%;
+
+
+/*********** shadow ***********/
+
+@shadow-distance-none: 0;
+@shadow-distance-1: 1px;
+@shadow-distance-2: 2px;
+@shadow-distance-3: 3px;
+@shadow-distance-4: 4px;
+@shadow-none: none;
+@shadow-special: 0 0 1px rgba(0, 0, 0, 0.3);
+
+
+/*********** size ***********/
+
+@size-none: 0;
+@size-1: 4px;
+@size-2: 8px;
+@size-3: 12px;
+@size-4: 16px;
+@size-5: 20px;
+@size-6: 24px;
+@size-7: 28px;
+@size-8: 32px;
+@size-9: 36px;
+@size-10: 40px;
+@size-11: 44px;
+@size-12: 48px;
+@size-13: 52px;
+@size-14: 56px;
+@size-15: 60px;
+@size-16: 64px;
+@size-17: 68px;
+@size-18: 72px;
+@size-19: 76px;
+@size-20: 80px;
+@size-21: 84px;
+@size-22: 88px;
+@size-23: 92px;
+@size-24: 96px;
+@size-25: 100px;
+@size-26: 104px;
+@size-27: 108px;
+@size-28: 112px;
+@size-29: 116px;
+@size-30: 120px;
+@size-31: 124px;
+@size-32: 128px;
+@size-33: 132px;
+@size-34: 136px;
+@size-35: 140px;
+@size-36: 144px;
+@size-37: 148px;
+@size-38: 152px;
+@size-39: 156px;
+@size-40: 160px;
+@size-41: 164px;
+@size-42: 168px;
+@size-43: 172px;
+@size-44: 176px;
+@size-45: 180px;
+@size-46: 184px;
+@size-47: 188px;
+@size-48: 192px;
+@size-49: 196px;
+@size-50: 200px;
+@size-mini: 24px;
+@size-small: 28px;
+@size-default: 32px;
+@size-large: 36px;
+
+
+/*********** spacing ***********/
+
+@spacing-none: 0;
+@spacing-1: 2px;
+@spacing-2: 4px;
+@spacing-3: 6px;
+@spacing-4: 8px;
+@spacing-5: 10px;
+@spacing-6: 12px;
+@spacing-7: 16px;
+@spacing-8: 20px;
+@spacing-9: 24px;
+@spacing-10: 32px;
+@spacing-11: 36px;
+@spacing-12: 40px;
+@spacing-13: 48px;
+@spacing-14: 56px;
+@spacing-15: 60px;
+@spacing-16: 64px;
+@spacing-17: 72px;
+@spacing-18: 80px;
+@spacing-19: 84px;
+@spacing-20: 96px;
+@spacing-21: 100px;
+@spacing-22: 120px;
+
+
+/*********** color ***********/
+
+@color-transparent: transparent;
+@color-primary-1: rgb(var(--primary-1));
+@color-primary-2: rgb(var(--primary-2));
+@color-primary-3: rgb(var(--primary-3));
+@color-primary-4: rgb(var(--primary-4));
+@color-primary-5: rgb(var(--primary-5));
+@color-primary-6: rgb(var(--primary-6));
+@color-primary-7: rgb(var(--primary-7));
+@color-primary-8: rgb(var(--primary-8));
+@color-primary-9: rgb(var(--primary-9));
+@color-primary-10: rgb(var(--primary-10));
+@color-success-1: rgb(var(--success-1));
+@color-success-2: rgb(var(--success-2));
+@color-success-3: rgb(var(--success-3));
+@color-success-4: rgb(var(--success-4));
+@color-success-5: rgb(var(--success-5));
+@color-success-6: rgb(var(--success-6));
+@color-success-7: rgb(var(--success-7));
+@color-success-8: rgb(var(--success-8));
+@color-success-9: rgb(var(--success-9));
+@color-success-10: rgb(var(--success-10));
+@color-warning-1: rgb(var(--warning-1));
+@color-warning-2: rgb(var(--warning-2));
+@color-warning-3: rgb(var(--warning-3));
+@color-warning-4: rgb(var(--warning-4));
+@color-warning-5: rgb(var(--warning-5));
+@color-warning-6: rgb(var(--warning-6));
+@color-warning-7: rgb(var(--warning-7));
+@color-warning-8: rgb(var(--warning-8));
+@color-warning-9: rgb(var(--warning-9));
+@color-warning-10: rgb(var(--warning-10));
+@color-danger-1: rgb(var(--danger-1));
+@color-danger-2: rgb(var(--danger-2));
+@color-danger-3: rgb(var(--danger-3));
+@color-danger-4: rgb(var(--danger-4));
+@color-danger-5: rgb(var(--danger-5));
+@color-danger-6: rgb(var(--danger-6));
+@color-danger-7: rgb(var(--danger-7));
+@color-danger-8: rgb(var(--danger-8));
+@color-danger-9: rgb(var(--danger-9));
+@color-danger-10: rgb(var(--danger-10));
+@color-link-1: rgb(var(--link-1));
+@color-link-2: rgb(var(--link-2));
+@color-link-3: rgb(var(--link-3));
+@color-link-4: rgb(var(--link-4));
+@color-link-5: rgb(var(--link-5));
+@color-link-6: rgb(var(--link-6));
+@color-link-7: rgb(var(--link-7));
+@color-link-8: rgb(var(--link-8));
+@color-link-9: rgb(var(--link-9));
+@color-link-10: rgb(var(--link-10));
+@color-white: #ffffff;
+@color-black: #000000;
+@color-menu-dark-bg: #232324;
+@color-menu-light-bg: #ffffff;
+@color-spin-layer-bg: rgba(255, 255, 255, 0.6);
+@color-menu-dark-hover: rgba(255, 255, 255, 0.04);
+@color-tooltip-bg: rgb(var(--gray-10));
+@color-border: rgb(var(--gray-3));
+@color-bg-popup: var(--color-bg-5);
+@color-bg-1: #ffffff;
+@color-bg-2: #ffffff;
+@color-bg-3: #ffffff;
+@color-bg-4: #ffffff;
+@color-bg-5: #ffffff;
+@color-bg-white: #ffffff;
+@color-text-1: var(--color-neutral-10);
+@color-text-2: var(--color-neutral-8);
+@color-text-3: var(--color-neutral-6);
+@color-text-4: var(--color-neutral-4);
+@color-fill-1: var(--color-neutral-1);
+@color-fill-2: var(--color-neutral-2);
+@color-fill-3: var(--color-neutral-3);
+@color-fill-4: var(--color-neutral-4);
+@color-border-1: var(--color-neutral-2);
+@color-border-2: var(--color-neutral-3);
+@color-border-3: var(--color-neutral-4);
+@color-border-4: var(--color-neutral-6);
+@color-primary-light-1: rgb(var(--primary-1));
+@color-primary-light-2: rgb(var(--primary-2));
+@color-primary-light-3: rgb(var(--primary-3));
+@color-primary-light-4: rgb(var(--primary-4));
+@color-secondary: var(--color-neutral-2);
+@color-secondary-hover: var(--color-neutral-3);
+@color-secondary-active: var(--color-neutral-4);
+@color-secondary-disabled: var(--color-neutral-1);
+@color-danger-light-1: rgb(var(--danger-1));
+@color-danger-light-2: rgb(var(--danger-2));
+@color-danger-light-3: rgb(var(--danger-3));
+@color-danger-light-4: rgb(var(--danger-4));
+@color-success-light-1: rgb(var(--success-1));
+@color-success-light-2: rgb(var(--success-2));
+@color-success-light-3: rgb(var(--success-3));
+@color-success-light-4: rgb(var(--success-4));
+@color-warning-light-1: rgb(var(--warning-1));
+@color-warning-light-2: rgb(var(--warning-2));
+@color-warning-light-3: rgb(var(--warning-3));
+@color-warning-light-4: rgb(var(--warning-4));
+@color-link-light-1: rgb(var(--link-1));
+@color-link-light-2: rgb(var(--link-2));
+@color-link-light-3: rgb(var(--link-3));
+@color-link-light-4: rgb(var(--link-4));
+@color-input-size-mini-padding-horizontal: 4px;
+@color-input-size-small-padding-horizontal: 4px;
+@color-input-size-default-padding-horizontal: 4px;
+@color-input-size-large-padding-horizontal: 5px;
+@color-preview-size-mini: 16px;
+@color-preview-size-small: 22px;
+@color-preview-size-default: 24px;
+@color-preview-size-large: 26px;
+@color-input-bg-color: var(--color-fill-2);
+@color-value-margin-left: 4px;
+@color-value-font-color: var(--color-text-1);
+@color-value-font-color_disabled: var(--color-text-4);
+@color-value-size-mini-font-size: 12px;
+@color-value-size-small-font-size: 14px;
+@color-value-size-default-font-size: 14px;
+@color-value-size-large-font-size: 14px;
+@color-input-border-radius: 2px;
+@color-preview-border-size: 1px;
+@color-preview-border-color: var(--color-border-2);
+@color-value-font-size: 400;
+@color-panel-width: 260px;
+@color-panel-padding: 12px;
+@color-panel-border-radius: 2px;
+@color-panel-bg-color: var(--color-bg-1);
+@color-panel-box-shadow: 0 8px 20px 0 rgba(0, 0, 0, 0.1);
+@color-palette-height: 178px;
+@color-palette-handle-size: 16px;
+@color-palette-handle-border-size: 2px;
+@color-control-bar-width: 182px;
+@color-control-bar-height: 14px;
+@color-control-bar-handle-size: 16px;
+@color-control-bar-alpha-margin-top: 12px;
+@color-panel-input-margin-top: 12px;
+@color-panel-input-group-margin-left: 12px;
+@color-panel-preview-size: 40px;
+@color-panel-format-select-width: 58px;
+@color-panel-alpha-input-width: 52px;
+@color-panel-section-title-font-size: 12px;
+@color-panel-empty-font-size: 12px;
+@color-panel-block-size: 16px;
+@color-panel-block-margin: 6px;
+@color-panel-block-border-radius: 2px;
+@color-panel-border-color: var(--color-border-2);
+@color-picker-prefix-cls: ~'@{prefix}-color-picker';
+
+
+/*********** shadow1 ***********/
+
+@shadow1-center: 0 0 5px rgba(0, 0, 0, 0.1);
+@shadow1-up: 0 -2px 5px rgba(0, 0, 0, 0.1);
+@shadow1-down: 0 2px 5px rgba(0, 0, 0, 0.1);
+@shadow1-left: -2px 0 5px rgba(0, 0, 0, 0.1);
+@shadow1-right: 2px 0 5px rgba(0, 0, 0, 0.1);
+@shadow1-left-up: -2px -2px 5px rgba(0, 0, 0, 0.1);
+@shadow1-left-down: -2px 2px 5px rgba(0, 0, 0, 0.1);
+@shadow1-right-up: 2px -2px 5px rgba(0, 0, 0, 0.1);
+@shadow1-right-down: 2px 2px 5px rgba(0, 0, 0, 0.1);
+
+
+/*********** shadow2 ***********/
+
+@shadow2-center: 0 0 10px rgba(0, 0, 0, 0.1);
+@shadow2-up: 0 -4px 10px rgba(0, 0, 0, 0.1);
+@shadow2-down: 0 4px 10px rgba(0, 0, 0, 0.1);
+@shadow2-left: -4px 0 10px rgba(0, 0, 0, 0.1);
+@shadow2-right: 4px 0 10px rgba(0, 0, 0, 0.1);
+@shadow2-left-up: -4px -4px 10px rgba(0, 0, 0, 0.1);
+@shadow2-left-down: -4px 4px 10px rgba(0, 0, 0, 0.1);
+@shadow2-right-up: 4px -4px 10px rgba(0, 0, 0, 0.1);
+@shadow2-right-down: 4px 4px 10px rgba(0, 0, 0, 0.1);
+
+
+/*********** shadow3 ***********/
+
+@shadow3-center: 0 0 20px rgba(0, 0, 0, 0.1);
+@shadow3-up: 0 -8px 20px rgba(0, 0, 0, 0.1);
+@shadow3-down: 0 8px 20px rgba(0, 0, 0, 0.1);
+@shadow3-left: -8px 0 20px rgba(0, 0, 0, 0.1);
+@shadow3-right: 8px 0 20px rgba(0, 0, 0, 0.1);
+@shadow3-left-up: -8px -8px 20px rgba(0, 0, 0, 0.1);
+@shadow3-left-down: -8px 8px 20px rgba(0, 0, 0, 0.1);
+@shadow3-right-up: 8px -8px 20px rgba(0, 0, 0, 0.1);
+@shadow3-right-down: 8px 8px 20px rgba(0, 0, 0, 0.1);
+
+
+/*********** opacity ***********/
+
+@opacity-none: 0;
+@opacity-1: 10%;
+@opacity-2: 20%;
+@opacity-3: 30%;
+@opacity-4: 40%;
+@opacity-5: 50%;
+@opacity-6: 60%;
+@opacity-7: 70%;
+@opacity-8: 80%;
+@opacity-9: 90%;
+@opacity-10: 100%;
+
+
+/*********** radius ***********/
+
+@radius-none: var(--border-radius-none);
+@radius-small: var(--border-radius-small);
+@radius-medium: var(--border-radius-medium);
+@radius-large: var(--border-radius-large);
+@radius-circle: var(--border-radius-circle);
+
+
+/*********** mask ***********/
+
+@mask-bg-opacity: 60%;
+@mask-color-bg: rgba(29, 33, 41, 0.6);
+
+
+/*********** icon ***********/
+
+@icon-hover-border-radius: var(--border-radius-circle);
+@icon-hover-color-bg: var(--color-fill-2);
+@icon-hover-size-default-height: 20px;
+@icon-hover-size-small-height: 20px;
+@icon-hover-size-mini-height: 20px;
+@icon-hover-size-large-height: 24px;
+@icon-hover-size-huge-height: 24px;
+@icon-hover-size-small-icon: 12px;
+@icon-hover-size-mini-icon: 12px;
+@icon-hover-size-default-icon: 12px;
+@icon-hover-size-large-icon: 12px;
+@icon-hover-size-huge-icon: 12px;
+
+
+/*********** prefix ***********/
+
+@prefix: arco;
+
+
+/*********** arco ***********/
+
+@arco-theme-tag: body;
+@arco-cssvars-prefix: -;
+@arco-draggable-prefix-cls: ~'@{prefix}-draggable';
+@arco-vars-prefix: ~'';
+
+
+/*********** code ***********/
+
+@code-family: Consolas, Menlo;
+
+
+/*********** transition ***********/
+
+@transition-duration-1: 0.1s;
+@transition-duration-2: 0.2s;
+@transition-duration-3: 0.3s;
+@transition-duration-4: 0.4s;
+@transition-duration-5: 0.5s;
+@transition-duration-loading: 1s;
+@transition-timing-function-linear: cubic-bezier(0, 0, 1, 1);
+@transition-timing-function-standard: cubic-bezier(0.34, 0.69, 0.1, 1);
+@transition-timing-function-overshoot: cubic-bezier(0.3, 1.3, 0.3, 1);
+@transition-timing-function-decelerate: cubic-bezier(0.4, 0.8, 0.74, 1);
+@transition-timing-function-accelerate: cubic-bezier(0.26, 0, 0.6, 0.2);
+
+
+/*********** z ***********/
+
+@z-index-popup-base: 1000;
+@z-index-affix: 999;
+@z-index-popup: 1000;
+@z-index-drawer: 1001;
+@z-index-modal: 1001;
+@z-index-message: 1003;
+@z-index-notification: 1003;
+@z-index-image-preview: 1001;
+
+
+/*********** line ***********/
+
+@line-height-base: 1.5715;
+
+
+/*********** popup ***********/
+
+@popup-box-shadow-base: 0 2px 5px rgba(0, 0, 0, 0.1);
+@popup-color-content-text: var(--color-text-2);
+@popup-color-content-bg: var(--color-bg-popup);
+@popup-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+@popup-padding-horizontal: 16px;
+@popup-padding-vertical: 12px;
+@popup-color-title-text: var(--color-text-1);
+@popup-font-title-size: 16px;
+@popup-margin-content-top: 4px;
+@popup-color-border: var(--color-neutral-3);
+@popup-font-size: 14px;
+@popup-border-radius: var(--border-radius-medium);
+
+
+/*********** input ***********/
+
+@input-color-bg: var(--color-fill-2);
+@input-color-bg_hover: var(--color-fill-3);
+@input-color-bg_focus: var(--color-bg-2);
+@input-color-bg_disabled: var(--color-fill-2);
+@input-color-addon-bg: var(--color-fill-2);
+@input-color-addon-border: var(--color-neutral-3);
+@input-border-addon-separator-width: 1px;
+@input-color-border_focus: rgb(var(--primary-6));
+@input-color-shadow_focus: var(--color-primary-light-2);
+@input-size-shadow_focus: 0;
+@input-color-addon-border_default: transparent;
+@input-color-text: var(--color-text-1);
+@input-color-placeholder-text: var(--color-text-3);
+@input-color-text_disabled: var(--color-text-4);
+@input-color-addon-text: var(--color-text-1);
+@input-color-bg_error: var(--color-danger-light-1);
+@input-color-bg_error_hover: var(--color-danger-light-2);
+@input-color-bg_error_focus: var(--color-bg-2);
+@input-color-border_error_focus: rgb(var(--danger-6));
+@input-color-shadow_error_focus: var(--color-danger-light-2);
+@input-size-shadow_error_focus: 0;
+@input-color-bg_warning: var(--color-warning-light-1);
+@input-color-bg_warning_hover: var(--color-warning-light-2);
+@input-color-bg_warning_focus: var(--color-bg-2);
+@input-color-border_warning_focus: rgb(var(--warning-6));
+@input-color-shadow_warning_focus: var(--color-warning-light-2);
+@input-size-shadow_warning_focus: 0;
+@input-border-radius: var(--border-radius-small);
+@input-size-default-height: 32px;
+@input-size-mini-height: 24px;
+@input-size-small-height: 28px;
+@input-size-large-height: 36px;
+@input-border-width: 1px;
+@input-color-border: transparent;
+@input-color-border_disabled: transparent;
+@input-color-border_hover: transparent;
+@input-color-border_error: transparent;
+@input-color-border_error_hover: transparent;
+@input-color-border_warning: transparent;
+@input-color-border_warning_hover: transparent;
+@input-size-default-font-size: 14px;
+@input-size-small-font-size: 14px;
+@input-size-large-font-size: 14px;
+@input-size-mini-font-size: 12px;
+@input-font-tip-size: 12px;
+@input-size-mini-icon-suffix-size: 12px;
+@input-size-small-icon-suffix-size: 14px;
+@input-size-default-icon-suffix-size: 14px;
+@input-size-large-icon-suffix-size: 14px;
+@input-size-mini-icon-addon-size: 12px;
+@input-size-small-icon-addon-size: 14px;
+@input-size-default-icon-addon-size: 14px;
+@input-size-large-icon-addon-size: 14px;
+@input-size-icon-clear: 12px;
+@input-color-prefix-text: var(--color-text-2);
+@input-color-suffix-text: var(--color-text-2);
+@input-color-tip-text: var(--color-text-3);
+@input-color-icon-clear: var(--color-text-2);
+@input-color-icon-clear-bg_hover: var(--color-fill-4);
+@input-padding-horizontal: 12px;
+@input-size-mini-padding-horizontal: 8px;
+@input-size-small-padding-horizontal: 12px;
+@input-size-large-padding-horizontal: 16px;
+@input-spacing-clear-icon-right: 8px;
+@input-padding-word-limit-left: 8px;
+@input-group-border-radius_compact: var(--border-radius-small);
+@input-group-border-separator-width: 1px;
+@input-group-color-separator-border: var(--color-neutral-3);
+@input-tag-size-mini-height: 24px;
+@input-tag-size-small-height: 28px;
+@input-tag-size-default-height: 32px;
+@input-tag-size-large-height: 36px;
+@input-tag-size-mini-tag-height: 20px;
+@input-tag-size-small-tag-height: 20px;
+@input-tag-size-default-tag-height: 24px;
+@input-tag-size-large-tag-height: 28px;
+@input-tag-size-mini-font-size: 12px;
+@input-tag-size-small-font-size: 14px;
+@input-tag-size-default-font-size: 14px;
+@input-tag-size-large-font-size: 16px;
+@input-tag-size-mini-padding_no_tag: 8px;
+@input-tag-size-small-padding_no_tag: 12px;
+@input-tag-size-default-padding_no_tag: 12px;
+@input-tag-size-large-padding_no_tag: 16px;
+@input-tag-color-text_default: var(--color-text-1);
+@input-tag-color-text_error: var(--color-text-1);
+@input-tag-color-text_disabled: var(--color-text-4);
+@input-tag-color-placeholder: var(--color-text-3);
+@input-tag-color-icon-clear: var(--color-text-2);
+@input-tag-color-icon-clear-bg_hover: var(--color-fill-4);
+@input-tag-color-border_default: transparent;
+@input-tag-color-border_default_hover: transparent;
+@input-tag-color-border_default_focus: rgb(var(--primary-6));
+@input-tag-color-border_error: transparent;
+@input-tag-color-border_error_hover: transparent;
+@input-tag-color-border_error_focus: rgb(var(--danger-6));
+@input-tag-color-border_disabled: transparent;
+@input-tag-color-border_disabled_hover: transparent;
+@input-tag-color-border_disabled_focus: transparent;
+@input-tag-color-bg_default: var(--color-fill-2);
+@input-tag-color-bg_default_hover: var(--color-fill-3);
+@input-tag-color-bg_default_focus: var(--color-bg-2);
+@input-tag-color-bg_error: rgb(var(--danger-1));
+@input-tag-color-bg_error_hover: rgb(var(--danger-2));
+@input-tag-color-bg_error_focus: var(--color-bg-2);
+@input-tag-color-bg_disabled: var(--color-fill-2);
+@input-tag-color-bg_disabled_hover: var(--color-fill-2);
+@input-tag-color-shadow_default_focus: rgb(var(--primary-2));
+@input-tag-color-shadow_error_focus: rgb(var(--danger-2));
+@input-tag-size-shadow_error_focus: 0;
+@input-tag-size-shadow_default_focus: 0;
+@input-tag-tag-margin-right: 4px;
+@input-tag-tag-margin-vertical: 2px;
+@input-tag-padding-horizontal: 4px;
+@input-tag-border-radius: var(--border-radius-small);
+@input-tag-border-width: 1px;
+@input-tag-size-icon-clear: 12px;
+@input-tag-size-icon-clear_hover: 20px;
+@input-tag-tag-font-size: 12px;
+@input-tag-tag-color-bg: var(--color-bg-2);
+@input-tag-tag-color-bg_focus: var(--color-fill-2);
+@input-tag-tag-color-bg_disabled: var(--color-fill-2);
+@input-tag-tag-color-border: var(--color-fill-3);
+@input-tag-tag-color-border_disabled: var(--color-fill-3);
+@input-tag-tag-color-border_focus: var(--color-fill-2);
+@input-tag-tag-remove-icon-color-bg: var(--color-fill-2);
+@input-tag-tag-remove-icon-color-bg_focus: var(--color-fill-3);
+@input-tag-color-text_warning: var(--color-text-1);
+@input-tag-color-border_warning: transparent;
+@input-tag-color-border_warning_hover: transparent;
+@input-tag-color-border_warning_focus: rgb(var(--warning-6));
+@input-tag-color-bg_warning: var(--color-warning-light-1);
+@input-tag-color-bg_warning_hover: var(--color-warning-light-2);
+@input-tag-color-bg_warning_focus: var(--color-bg-2);
+@input-tag-color-shadow_warning_focus: var(--color-warning-light-2);
+@input-tag-size-shadow_warning_focus: 0;
+@input-tag-addon-padding-horizontal: 12px;
+@input-tag-color-addon-bg: var(--color-fill-2);
+@input-tag-color-addon-border: var(--color-border-2);
+@input-tag-color-addon-border_default: transparent;
+@input-tag-border-addon-separator-width: 1px;
+@input-tag-color-addon-text: var(--color-text-1);
+@input-tag-prefix-cls: ~'@{prefix}-input-tag';
+@input-prefix-cls: ~'@{prefix}-input';
+@input-number-border-radius: var(--border-radius-small);
+@input-number-step-layer-border-radius: 1px;
+@input-number-size-mini-step-button-width: 24px;
+@input-number-size-small-step-button-width: 28px;
+@input-number-size-default-step-button-width: 32px;
+@input-number-size-large-step-button-width: 36px;
+@input-number-step-button-color: var(--color-text-2);
+@input-number-step-button-color_disabled: var(--color-text-4);
+@input-number-step-button-color-border: var(--color-neutral-3);
+@input-number-step-button-color-bg_default: var(--color-fill-2);
+@input-number-step-button-color-bg_default_hover: var(--color-fill-3);
+@input-number-step-button-color-bg_default_active: var(--color-fill-4);
+@input-number-step-button-color-bg_disabled: var(--color-fill-2);
+@input-number-step-button-color-bg_disabled_hover: var(--color-fill-2);
+@input-number-step-button-color-bg_disabled_active: var(--color-fill-2);
+@input-number-color-illegal_value: rgb(var(--danger-6));
+@input-number-prefix-cls: ~'@{prefix}-input-number';
+
+
+/*********** textarea ***********/
+
+@textarea-color-tip-text: var(--color-text-3);
+@textarea-padding-horizontal: 12px;
+@textarea-padding-vertical: 4px;
+@textarea-font-size: 14px;
+@textarea-font-tip-size: 12px;
+@textarea-layout-tip-right: 10px;
+@textarea-layout-tip-bottom: 6px;
+@textarea-size-min-height: 32px;
+@textarea-size-icon-clear: 12px;
+@textarea-layout-top-icon-clear: 10px;
+@textarea-prefix-cls: ~'@{prefix}-textarea';
+
+
+/*********** search ***********/
+
+@search-color-icon: var(--color-text-2);
+@search-button-color-text: var(--color-white);
+@search-size-icon: 14px;
+@search-button-padding-horizontal: 8px;
+
+
+/*********** password ***********/
+
+@password-color-eye-icon: var(--color-text-2);
+@password-size-eye-icon: 12px;
+
+
+/*********** picker ***********/
+
+@picker-size-mini: 24px;
+@picker-size-small: 28px;
+@picker-size-default: 32px;
+@picker-size-large: 36px;
+@picker-size-mini-font-size-text: 12px;
+@picker-size-small-font-size-text: 14px;
+@picker-size-default-font-size-text: 14px;
+@picker-size-large-font-size-text: 14px;
+@picker-input-border-radius: var(--border-radius-small);
+@picker-color-shadow_focus: var(--color-primary-light-2);
+@picker-size-shadow_focus: 0;
+@picker-color-shadow_error_focus: var(--color-danger-light-2);
+@picker-size-shadow_error_focus: 0;
+@picker-color-bg: var(--color-fill-2);
+@picker-color-bg_hover: var(--color-fill-3);
+@picker-color-bg_focus: var(--color-bg-2);
+@picker-color-bg_disabled: var(--color-fill-2);
+@picker-color-bg_error: var(--color-danger-light-1);
+@picker-color-bg_error_hover: var(--color-danger-light-2);
+@picker-color-bg_error_focus: var(--color-bg-2);
+@picker-color-border: transparent;
+@picker-color-border_hover: transparent;
+@picker-color-border_focus: rgb(var(--primary-6));
+@picker-color-border_disabled: transparent;
+@picker-color-border_error: transparent;
+@picker-color-border_error_hover: transparent;
+@picker-color-border_error_focus: rgb(var(--danger-6));
+@picker-color-placeholder: var(--color-text-3);
+@picker-color-placeholder_disabled: var(--color-text-4);
+@picker-color-text: var(--color-text-1);
+@picker-color-text_disabled: var(--color-text-4);
+@picker-color-icon: var(--color-text-2);
+@picker-color-icon_disabled: var(--color-text-4);
+@picker-color-separator: var(--color-text-3);
+@picker-color-separator_disabled: var(--color-text-4);
+@picker-range-color-bg-input_focus: var(--color-primary-light-1);
+@picker-padding-horizontal: 4px;
+@picker-input-padding-horizontal: 8px;
+@picker-color-shadow_warning_focus: var(--color-warning-light-2);
+@picker-size-shadow_warning_focus: 0;
+@picker-color-bg_warning: var(--color-warning-light-1);
+@picker-color-bg_warning_hover: var(--color-warning-light-2);
+@picker-color-bg_warning_focus: var(--color-bg-2);
+@picker-color-border_warning: transparent;
+@picker-color-border_warning_hover: transparent;
+@picker-color-border_warning_focus: rgb(var(--warning-6));
+@picker-container-border-radius: var(--border-radius-medium);
+@picker-header-color-text: var(--color-text-1);
+@picker-header-font-weight-text: 500;
+@picker-header-font-size: 14px;
+@picker-header-padding-horizontal: 24px;
+@picker-header-padding-vertical: 24px;
+@picker-panel-border-width: 1px;
+@picker-panel-date-width: 265px;
+@picker-panel-month-width: 265px;
+@picker-panel-year-width: 265px;
+@picker-panel-week-width: 298px;
+@picker-panel-quarter-width: 265px;
+@picker-panel-time-cell-width: 36px;
+@picker-panel-time-cell-spacing-horizontal: 4px;
+@picker-panel-time-padding-horizontal: 10px;
+@picker-panel-cell-padding-vertical: 4px;
+@picker-panel-cell-circle-height: 24px;
+@picker-panel-row-padding-vertical: 2px;
+@picker-color-switch-icon: var(--color-text-2);
+@picker-color-bg-switch-icon: var(--color-bg-popup);
+@picker-color-bg-switch-icon_hover: var(--color-fill-3);
+@picker-cell-font-weight-in-view: 500;
+@picker-color-cell-text-in-view: var(--color-text-1);
+@picker-cell-font-weight-not-in-view: 500;
+@picker-color-cell-text-not-in-view: var(--color-text-4);
+@picker-color-bg-circle_selected: rgb(var(--primary-6));
+@picker-color-bg-cell-in-range: var(--color-primary-light-1);
+@picker-color-bg-cell-disabled: var(--color-fill-1);
+@picker-color-text-cell-range-boundary: var(--color-white);
+@picker-color-bg-cell-range-boundary: rgb(var(--primary-6));
+@picker-color-bg-cell-hover-in-range: var(--color-primary-light-2);
+@picker-color-text-cell-hover-range-boundary: var(--color-text-1);
+@picker-color-bg-cell-hover-range-boundary: var(--color-primary-light-2);
+@picker-color-text-week-list-item: var(--color-text-2);
+@picker-font-weight-week-list-item: 500;
+@picker-panel-color-border: var(--color-neutral-3);
+@picker-panel-color-text-cell_hover: var(--color-text-1);
+@picker-panel-color-bg-cell_hover: var(--color-fill-3);
+@picker-panel-color-text-cell_selected: var(--color-white);
+@picker-panel-color-bg-cell_selected: rgb(var(--primary-6));
+@picker-panel-color-current-time-dot: rgb(var(--primary-6));
+@picker-panel-color-text-holder: var(--color-text-3);
+@picker-panel-color-text-holder_active: var(--color-text-1);
+@picker-panel-color-bg-label_hover: var(--color-fill-3);
+@picker-panel-border-radius-cell_selected: 24px;
+@picker-panel-cell-boundary-border-radius: 24px;
+@picker-prefix-cls: ~'@{prefix}-picker';
+
+
+/*********** affix ***********/
+
+@affix-prefix-cls: ~'@{prefix}-affix';
+
+
+/*********** alert ***********/
+
+@alert-border-width: 1px;
+@alert-margin-close-icon-left: 8px;
+@alert-margin-icon-right: 8px;
+@alert-margin-action-right: 8px;
+@alert-margin-action-left: 8px;
+@alert-border-radius: var(--border-radius-small);
+@alert-line-height: 1.5715;
+@alert-title-line-height: 1.5;
+@alert-title-margin-bottom: 4px;
+@alert-padding-horizontal: 16px;
+@alert-padding-vertical: 9px;
+@alert-padding-horizontal_with_title: 16px;
+@alert-padding-vertical_with_title: 16px;
+@alert-font-weight-title: 500;
+@alert-font-size-text-title: 16px;
+@alert-font-size-text-content: 14px;
+@alert-font-size-icon: 16px;
+@alert-font-size-icon_with_title: 18px;
+@alert-font-size-close-icon: 12px;
+@alert-color-close-icon: var(--color-text-2);
+@alert-color-close-icon_hover: var(--color-text-1);
+@alert-info-color-bg: var(--color-primary-light-1);
+@alert-info-color-border: transparent;
+@alert-info-color-icon: rgb(var(--primary-6));
+@alert-info-color-text-title: var(--color-text-1);
+@alert-info-color-text-content: var(--color-text-1);
+@alert-info-color-text-content_title: var(--color-text-2);
+@alert-warning-color-bg: var(--color-warning-light-1);
+@alert-warning-color-border: transparent;
+@alert-warning-color-icon: rgb(var(--warning-6));
+@alert-warning-color-text-title: var(--color-text-1);
+@alert-warning-color-text-content: var(--color-text-1);
+@alert-warning-color-text-content_title: var(--color-text-2);
+@alert-error-color-bg: var(--color-danger-light-1);
+@alert-error-color-border: transparent;
+@alert-error-color-icon: rgb(var(--danger-6));
+@alert-error-color-text-title: var(--color-text-1);
+@alert-error-color-text-content: var(--color-text-1);
+@alert-error-color-text-content_title: var(--color-text-2);
+@alert-success-color-bg: var(--color-success-light-1);
+@alert-success-color-border: transparent;
+@alert-success-color-icon: rgb(var(--success-6));
+@alert-success-color-text-title: var(--color-text-1);
+@alert-success-color-text-content: var(--color-text-1);
+@alert-success-color-text-content_title: var(--color-text-2);
+@alert-prefix-cls: ~'@{prefix}-alert';
+
+
+/*********** anchor ***********/
+
+@anchor-width: 150px;
+@anchor-line-width: 2px;
+@anchor-line-slider-height: 12px;
+@anchor-line-margin-right: 12px;
+@anchor-color-bg-line: var(--color-fill-3);
+@anchor-color-bg-line_active: rgb(var(--primary-6));
+@anchor-border-radius-title-hover: var(--border-radius-small);
+@anchor-item-inner-margin-left: 16px;
+@anchor-title-padding-horizontal: 8px;
+@anchor-title-padding-vertical: 4px;
+@anchor-title-margin-bottom: 2px;
+@anchor-title-margin-left_horizontal: 16px;
+@anchor-color-title: var(--color-text-2);
+@anchor-color-title_hover: var(--color-text-1);
+@anchor-color-title_active: var(--color-text-1);
+@anchor-font-weight-title_hover: 500;
+@anchor-font-weight-title_horizontal_hover: 400;
+@anchor-font-weight-title_active: 500;
+@anchor-color-bg-title_hover: var(--color-fill-2);
+@anchor-font-size-title: 14px;
+@anchor-lineless-color-title_active: rgb(var(--primary-6));
+@anchor-lineless-bg-title_active: var(--color-fill-2);
+@anchor-lineless-font-weight-title_active: 500;
+@anchor-color-box-shadow: rgb(var(--primary-6));
+@anchor-prefix-cls: ~'@{prefix}-anchor';
+
+
+/*********** auto ***********/
+
+@auto-complete-popup-max-height: 200px;
+@auto-complete-popup-border-radius: var(--border-radius-medium);
+@auto-complete-popup-padding-vertical: 4px;
+@auto-complete-popup-font-size: 14px;
+@auto-complete-popup-color-border: var(--color-fill-3);
+@auto-complete-popup-box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+@auto-complete-option-height: 36px;
+@auto-complete-option-font-weight_selected: 500;
+@auto-complete-option-padding-horizontal: 12px;
+@auto-complete-option-color-bg_default: var(--color-bg-popup);
+@auto-complete-option-color-bg_hover: var(--color-fill-2);
+@auto-complete-option-color-bg_selected: var(--color-bg-popup);
+@auto-complete-option-color-bg_disabled: var(--color-bg-popup);
+@auto-complete-option-color-text_default: var(--color-text-1);
+@auto-complete-option-color-text_hover: var(--color-text-1);
+@auto-complete-option-color-text_selected: var(--color-text-1);
+@auto-complete-option-color-text_disabled: var(--color-text-4);
+@auto-complete-prefix-cls: ~'@{prefix}-autocomplete';
+
+
+/*********** select ***********/
+
+@select-prefix-cls: ~'@{prefix}-select';
+@select-size-mini-height: 24px;
+@select-size-small-height: 28px;
+@select-size-default-height: 32px;
+@select-size-large-height: 36px;
+@select-size-mini-font-size: 12px;
+@select-size-small-font-size: 14px;
+@select-size-default-font-size: 14px;
+@select-size-large-font-size: 14px;
+@select-signal-size-mini-padding: 8px;
+@select-signal-size-small-padding: 12px;
+@select-signal-size-default-padding: 12px;
+@select-signal-size-large-padding: 16px;
+@select-multi-padding: 4px;
+@select-size-icon: 12px;
+@select-size-icon-bg: 16px;
+@select-border-width: 1px;
+@select-border-radius: var(--border-radius-small);
+@select-color-text_default: var(--color-text-1);
+@select-color-text_disabled: var(--color-text-4);
+@select-color-text_focused: var(--color-text-1);
+@select-color-placeholder_default: var(--color-text-3);
+@select-color-placeholder_disabled: var(--color-text-4);
+@select-color-placeholder_focused: var(--color-text-3);
+@select-color-icon_default: var(--color-text-2);
+@select-color-icon_disabled: var(--color-text-4);
+@select-color-icon_focused: var(--color-text-2);
+@select-color-icon-bg_hover: var(--color-fill-4);
+@select-color-bg_default: var(--color-fill-2);
+@select-color-bg_default_hover: var(--color-fill-3);
+@select-color-bg_default_focus: var(--color-bg-2);
+@select-color-bg_error_focus: var(--color-bg-2);
+@select-color-bg_error: var(--color-danger-light-1);
+@select-color-bg_error_hover: var(--color-danger-light-2);
+@select-color-bg_disabled: var(--color-fill-2);
+@select-color-bg_disabled_hover: var(--color-fill-2);
+@select-color-border_default: transparent;
+@select-color-border_default_hover: transparent;
+@select-color-border_default_focus: rgb(var(--primary-6));
+@select-color-border_error: transparent;
+@select-color-border_error_hover: transparent;
+@select-color-border_error_focus: rgb(var(--danger-6));
+@select-color-border_disabled: transparent;
+@select-color-border_disabled_hover: transparent;
+@select-shadow-distance_default_focus: 0;
+@select-shadow-distance_error_focus: 0;
+@select-color-shadow_default_focus: var(--color-primary-light-2);
+@select-color-shadow_error_focus: var(--color-danger-light-2);
+@select-popup-max-height: 200px;
+@select-popup-border-radius: var(--border-radius-medium);
+@select-popup-padding-vertical: 4px;
+@select-popup-padding-horizontal: 0;
+@select-popup-font-size: 14px;
+@select-popup-color-bg: var(--color-bg-popup);
+@select-popup-color-border: var(--color-fill-3);
+@select-popup-box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+@select-popup-option-height: 36px;
+@select-popup-option-font-weight_selected: 500;
+@select-signal-popup-option-padding-horizontal: 12px;
+@select-multi-popup-option-padding-horizontal: 4px;
+@select-popup-option-border-radius: 0;
+@select-popup-option-color-bg_default: var(--color-bg-popup);
+@select-popup-option-color-bg_hover: var(--color-fill-2);
+@select-popup-option-color-bg_selected: var(--color-bg-popup);
+@select-popup-option-color-bg_disabled: var(--color-bg-popup);
+@select-popup-option-color-text_default: var(--color-text-1);
+@select-popup-option-color-text_hover: var(--color-text-1);
+@select-popup-option-color-text_selected: var(--color-text-1);
+@select-popup-option-color-text_disabled: var(--color-text-4);
+@select-popup-option-color-hightlight-text: var(--color-text-1);
+@select-popup-option-font-hightlight-weight: 500;
+@select-popup-group-title-height: 20px;
+@select-popup-group-title-padding-horizontal: 12px;
+@select-popup-group-title-padding-top: 8px;
+@select-popup-group-title-font-size: 12px;
+@select-popup-group-title-color-text: var(--color-text-3);
+@select-addon-padding-horizontal: 12px;
+@select-color-addon-bg: var(--color-fill-2);
+@select-color-addon-border: var(--color-border-2);
+@select-color-addon-border_default: transparent;
+@select-border-addon-separator-width: 1px;
+@select-color-addon-text: var(--color-text-1);
+@select-color-bg_warning_focus: var(--color-bg-2);
+@select-color-bg_warning: var(--color-warning-light-1);
+@select-color-bg_warning_hover: var(--color-warning-light-2);
+@select-color-border_warning: transparent;
+@select-color-border_warning_hover: transparent;
+@select-color-border_warning_focus: rgb(var(--warning-6));
+@select-color-shadow_warning_focus: var(--color-warning-light-2);
+@select-shadow-distance_warning_focus: 0;
+@select-prefix-cls-rtl: ~'@{prefix}-select-rtl';
+
+
+/*********** avatar ***********/
+
+@avatar-size-default: 40px;
+@avatar-color-text: var(--color-white);
+@avatar-color-bg: var(--color-fill-4);
+@avatar-color-group-item-border: var(--color-bg-2);
+@avatar-group-item-border-width: 2px;
+@avatar-group-item-margin-left: -10px;
+@avatar-group-popover-item-spacing: 4px;
+@avatar-font-weight-text: 500;
+@avatar-font-size-text: 20px;
+@avatar-circle-border-radius: var(--border-radius-circle);
+@avatar-square-border-radius: var(--border-radius-medium);
+@avatar-font-size-max-count: 20px;
+@avatar-color-max-count-text: var(--color-white);
+@avatar-size-trigger-button: 20px;
+@avatar-spacing-trigger-button-right: 4px;
+@avatar-spacing-trigger-button-bottom: 4px;
+@avatar-color-trigger-button-bg: var(--color-neutral-2);
+@avatar-color-trigger-button-bg_hover: var(--color-neutral-3);
+@avatar-color-trigger-mask-icon: var(--color-white);
+@avatar-opacity-trigger-mask-bg: 60%;
+@avatar-color-trigger-icon-button: var(--color-fill-4);
+@avatar-size-trigger-icon: 12px;
+@avatar-border-trigger-button-radius: var(--border-radius-circle);
+@avatar-prefix-cls: ~'@{prefix}-avatar';
+
+
+/*********** backtop ***********/
+
+@backtop-margin-bottom: 24px;
+@backtop-margin-right: 24px;
+@backtop-button-size-width: 40px;
+@backtop-button-size-font: 12px;
+@backtop-button-size-icon: 14px;
+@backtop-button-color-bg: rgb(var(--primary-6));
+@backtop-button-color-bg_hover: rgb(var(--primary-5));
+@backtop-button-border-radius: var(--border-radius-circle);
+@backtop-button-color-text: var(--color-white);
+@backtop-prefix-cls: ~'@{prefix}-backtop';
+
+
+/*********** badge ***********/
+
+@badge-size-count-height: 20px;
+@badge-padding-count-horizontal: 6px;
+@badge-margin-status-text-left: 8px;
+@badge-font-count-size: 12px;
+@badge-font-status-text-size: 14px;
+@badge-color-count-text: var(--color-white);
+@badge-color-status-text: var(--color-text-1);
+@badge-color-count-bg: rgb(var(--danger-6));
+@badge-size-dot-width: 6px;
+@badge-color-dot-bg_default: var(--color-fill-4);
+@badge-color-dot-bg_processing: rgb(var(--primary-6));
+@badge-color-dot-bg_success: rgb(var(--success-6));
+@badge-color-dot-bg_warning: rgb(var(--warning-6));
+@badge-color-dot-bg_error: rgb(var(--danger-6));
+@badge-font-count-weight: 500;
+@badge-red-color-dot-bg: rgb(var(--danger-6));
+@badge-orangered-color-dot-bg: #f77234;
+@badge-orange-color-dot-bg: rgb(var(--orange-6));
+@badge-lime-color-dot-bg: rgb(var(--lime-6));
+@badge-gold-color-dot-bg: rgb(var(--gold-6));
+@badge-green-color-dot-bg: rgb(var(--success-6));
+@badge-cyan-color-dot-bg: rgb(var(--cyan-6));
+@badge-arcoblue-color-dot-bg: rgb(var(--primary-6));
+@badge-pinkpurple-color-dot-bg: rgb(var(--pinkpurple-6));
+@badge-purple-color-dot-bg: rgb(var(--purple-6));
+@badge-magenta-color-dot-bg: rgb(var(--magenta-6));
+@badge-gray-color-dot-bg: rgb(var(--gray-4));
+@badge-prefix-cls: ~'@{prefix}-badge';
+
+
+/*********** breadcrumb ***********/
+
+@breadcrumb-color-text: var(--color-text-2);
+@breadcrumb-color-text_active: var(--color-text-1);
+@breadcrumb-color-link-text: var(--color-text-2);
+@breadcrumb-color-separator: var(--color-text-4);
+@breadcrumb-color-bg: transparent;
+@breadcrumb-color-bg_hover: var(--color-fill-2);
+@breadcrumb-margin-separator-horizontal: 4px;
+@breadcrumb-margin-dropdown-icon-left: 4px;
+@breadcrumb-padding-text-horizontal: 4px;
+@breadcrumb-border-text-radius_hover: var(--border-radius-small);
+@breadcrumb-size-text-height: 24px;
+@breadcrumb-size-dropdown-icon: 12px;
+@breadcrumb-size-font-size: 14px;
+@breadcrumb-font-weight_active: 500;
+@breadcrumb-color-icon: var(--color-text-3);
+@breadcrumb-color-link-text_hover: rgb(var(--link-6));
+@breadcrumb-color-dropdown-icon: var(--color-text-2);
+@breadcrumb-color-box-shadow: rgb(var(--primary-6));
+@breadcrumb-prefix-cls: ~'@{prefix}-breadcrumb';
+
+
+/*********** btn ***********/
+
+@btn-font-weight: 400;
+@btn-border-radius: var(--border-radius-small);
+@btn-border-width: 1px;
+@btn-size-mini-height: 24px;
+@btn-size-small-height: 28px;
+@btn-size-default-height: 32px;
+@btn-size-large-height: 36px;
+@btn-size-mini-radius: var(--border-radius-small);
+@btn-size-small-radius: var(--border-radius-small);
+@btn-size-default-radius: var(--border-radius-small);
+@btn-size-large-radius: var(--border-radius-small);
+@btn-size-mini-border-width: 1px;
+@btn-size-small-border-width: 1px;
+@btn-size-default-border-width: 1px;
+@btn-size-large-border-width: 1px;
+@btn-size-mini-icon-spacing: 4px;
+@btn-size-small-icon-spacing: 6px;
+@btn-size-default-icon-spacing: 8px;
+@btn-size-large-icon-spacing: 8px;
+@btn-size-mini-icon-vertical-align: -2px;
+@btn-size-small-icon-vertical-align: -2px;
+@btn-size-default-icon-vertical-align: -2px;
+@btn-size-large-icon-vertical-align: -2px;
+@btn-size-mini-padding-horizontal: 11px;
+@btn-size-small-padding-horizontal: 15px;
+@btn-size-default-padding-horizontal: 15px;
+@btn-size-large-padding-horizontal: 19px;
+@btn-size-mini-font-size: 12px;
+@btn-size-small-font-size: 14px;
+@btn-size-default-font-size: 14px;
+@btn-size-large-font-size: 14px;
+@btn-outline-color-text: rgb(var(--primary-6));
+@btn-outline-color-text_disabled: var(--color-primary-light-3);
+@btn-outline-color-text_hover: rgb(var(--primary-5));
+@btn-outline-color-text_active: rgb(var(--primary-7));
+@btn-outline-color-bg: transparent;
+@btn-outline-color-bg_disabled: transparent;
+@btn-outline-color-bg_hover: transparent;
+@btn-outline-color-bg_active: transparent;
+@btn-outline-color-border: rgb(var(--primary-6));
+@btn-outline-color-border_disabled: var(--color-primary-light-3);
+@btn-outline-color-border_hover: rgb(var(--primary-5));
+@btn-outline-color-border_active: rgb(var(--primary-7));
+@btn-outline-color-text_warning: rgb(var(--warning-6));
+@btn-outline-color-text_warning_disabled: var(--color-warning-light-3);
+@btn-outline-color-text_warning_hover: rgb(var(--warning-5));
+@btn-outline-color-text_warning_active: rgb(var(--warning-7));
+@btn-outline-color-bg_warning: transparent;
+@btn-outline-color-bg_warning_disabled: transparent;
+@btn-outline-color-bg_warning_hover: transparent;
+@btn-outline-color-bg_warning_active: transparent;
+@btn-outline-color-border_warning: rgb(var(--warning-6));
+@btn-outline-color-border_warning_disabled: var(--color-warning-light-3);
+@btn-outline-color-border_warning_hover: rgb(var(--warning-5));
+@btn-outline-color-border_warning_active: rgb(var(--warning-7));
+@btn-outline-color-text_danger: rgb(var(--danger-6));
+@btn-outline-color-text_danger_disabled: var(--color-danger-light-3);
+@btn-outline-color-text_danger_hover: rgb(var(--danger-5));
+@btn-outline-color-text_danger_active: rgb(var(--danger-7));
+@btn-outline-color-bg_danger: transparent;
+@btn-outline-color-bg_danger_disabled: transparent;
+@btn-outline-color-bg_danger_hover: transparent;
+@btn-outline-color-bg_danger_active: transparent;
+@btn-outline-color-border_danger: rgb(var(--danger-6));
+@btn-outline-color-border_danger_disabled: var(--color-danger-light-3);
+@btn-outline-color-border_danger_hover: rgb(var(--danger-5));
+@btn-outline-color-border_danger_active: rgb(var(--danger-7));
+@btn-outline-color-text_success: rgb(var(--success-6));
+@btn-outline-color-text_success_disabled: var(--color-success-light-3);
+@btn-outline-color-text_success_hover: rgb(var(--success-5));
+@btn-outline-color-text_success_active: rgb(var(--success-7));
+@btn-outline-color-bg_success: transparent;
+@btn-outline-color-bg_success_disabled: transparent;
+@btn-outline-color-bg_success_hover: transparent;
+@btn-outline-color-bg_success_active: transparent;
+@btn-outline-color-border_success: rgb(var(--success-6));
+@btn-outline-color-border_success_disabled: var(--color-success-light-3);
+@btn-outline-color-border_success_hover: rgb(var(--success-5));
+@btn-outline-color-border_success_active: rgb(var(--success-7));
+@btn-outline-border-style: solid;
+@btn-primary-color-text: #fff;
+@btn-primary-color-text_disabled: #fff;
+@btn-primary-color-text_hover: #fff;
+@btn-primary-color-text_active: #fff;
+@btn-primary-color-bg: rgb(var(--primary-6));
+@btn-primary-color-bg_disabled: var(--color-primary-light-3);
+@btn-primary-color-bg_hover: rgb(var(--primary-5));
+@btn-primary-color-bg_active: rgb(var(--primary-7));
+@btn-primary-color-border: transparent;
+@btn-primary-color-border_disabled: transparent;
+@btn-primary-color-border_hover: transparent;
+@btn-primary-color-border_active: transparent;
+@btn-primary-color-text_warning: #fff;
+@btn-primary-color-text_warning_disabled: #fff;
+@btn-primary-color-text_warning_hover: #fff;
+@btn-primary-color-text_warning_active: #fff;
+@btn-primary-color-bg_warning: rgb(var(--warning-6));
+@btn-primary-color-bg_warning_disabled: var(--color-warning-light-3);
+@btn-primary-color-bg_warning_hover: rgb(var(--warning-5));
+@btn-primary-color-bg_warning_active: rgb(var(--warning-7));
+@btn-primary-color-border_warning: transparent;
+@btn-primary-color-border_warning_disabled: transparent;
+@btn-primary-color-border_warning_hover: transparent;
+@btn-primary-color-border_warning_active: transparent;
+@btn-primary-color-text_danger: #fff;
+@btn-primary-color-text_danger_disabled: #fff;
+@btn-primary-color-text_danger_hover: #fff;
+@btn-primary-color-text_danger_active: #fff;
+@btn-primary-color-bg_danger: rgb(var(--danger-6));
+@btn-primary-color-bg_danger_disabled: var(--color-danger-light-3);
+@btn-primary-color-bg_danger_hover: rgb(var(--danger-5));
+@btn-primary-color-bg_danger_active: rgb(var(--danger-7));
+@btn-primary-color-border_danger: transparent;
+@btn-primary-color-border_danger_disabled: transparent;
+@btn-primary-color-border_danger_hover: transparent;
+@btn-primary-color-border_danger_active: transparent;
+@btn-primary-color-text_success: #fff;
+@btn-primary-color-text_success_disabled: #fff;
+@btn-primary-color-text_success_hover: #fff;
+@btn-primary-color-text_success_active: #fff;
+@btn-primary-color-bg_success: rgb(var(--success-6));
+@btn-primary-color-bg_success_disabled: var(--color-success-light-3);
+@btn-primary-color-bg_success_hover: rgb(var(--success-5));
+@btn-primary-color-bg_success_active: rgb(var(--success-7));
+@btn-primary-color-border_success: transparent;
+@btn-primary-color-border_success_disabled: transparent;
+@btn-primary-color-border_success_hover: transparent;
+@btn-primary-color-border_success_active: transparent;
+@btn-primary-border-style: solid;
+@btn-secondary-color-text: var(--color-text-2);
+@btn-secondary-color-text_disabled: var(--color-text-4);
+@btn-secondary-color-text_hover: var(--color-text-2);
+@btn-secondary-color-text_active: var(--color-text-2);
+@btn-secondary-color-bg: var(--color-secondary);
+@btn-secondary-color-bg_disabled: var(--color-secondary-disabled);
+@btn-secondary-color-bg_hover: var(--color-secondary-hover);
+@btn-secondary-color-bg_active: var(--color-secondary-active);
+@btn-secondary-color-border: transparent;
+@btn-secondary-color-border_disabled: transparent;
+@btn-secondary-color-border_hover: transparent;
+@btn-secondary-color-border_active: transparent;
+@btn-secondary-color-text_warning: rgb(var(--warning-6));
+@btn-secondary-color-text_warning_disabled: var(--color-warning-light-3);
+@btn-secondary-color-text_warning_hover: rgb(var(--warning-6));
+@btn-secondary-color-text_warning_active: rgb(var(--warning-6));
+@btn-secondary-color-bg_warning: var(--color-warning-light-1);
+@btn-secondary-color-bg_warning_disabled: var(--color-warning-light-1);
+@btn-secondary-color-bg_warning_hover: var(--color-warning-light-2);
+@btn-secondary-color-bg_warning_active: var(--color-warning-light-3);
+@btn-secondary-color-border_warning: transparent;
+@btn-secondary-color-border_warning_disabled: transparent;
+@btn-secondary-color-border_warning_hover: transparent;
+@btn-secondary-color-border_warning_active: transparent;
+@btn-secondary-color-text_danger: rgb(var(--danger-6));
+@btn-secondary-color-text_danger_disabled: var(--color-danger-light-3);
+@btn-secondary-color-text_danger_hover: rgb(var(--danger-6));
+@btn-secondary-color-text_danger_active: rgb(var(--danger-6));
+@btn-secondary-color-bg_danger: var(--color-danger-light-1);
+@btn-secondary-color-bg_danger_disabled: var(--color-danger-light-1);
+@btn-secondary-color-bg_danger_hover: var(--color-danger-light-2);
+@btn-secondary-color-bg_danger_active: var(--color-danger-light-3);
+@btn-secondary-color-border_danger: transparent;
+@btn-secondary-color-border_danger_disabled: transparent;
+@btn-secondary-color-border_danger_hover: transparent;
+@btn-secondary-color-border_danger_active: transparent;
+@btn-secondary-color-text_success: rgb(var(--success-6));
+@btn-secondary-color-text_success_disabled: var(--color-success-light-3);
+@btn-secondary-color-text_success_hover: rgb(var(--success-6));
+@btn-secondary-color-text_success_active: rgb(var(--success-6));
+@btn-secondary-color-bg_success: var(--color-success-light-1);
+@btn-secondary-color-bg_success_disabled: var(--color-success-light-1);
+@btn-secondary-color-bg_success_hover: var(--color-success-light-2);
+@btn-secondary-color-bg_success_active: var(--color-success-light-3);
+@btn-secondary-color-border_success: transparent;
+@btn-secondary-color-border_success_disabled: transparent;
+@btn-secondary-color-border_success_hover: transparent;
+@btn-secondary-color-border_success_active: transparent;
+@btn-secondary-border-style: solid;
+@btn-dashed-color-text: var(--color-text-2);
+@btn-dashed-color-text_disabled: var(--color-text-4);
+@btn-dashed-color-text_hover: var(--color-text-2);
+@btn-dashed-color-text_active: var(--color-text-2);
+@btn-dashed-color-bg: var(--color-fill-2);
+@btn-dashed-color-bg_disabled: var(--color-fill-2);
+@btn-dashed-color-bg_hover: var(--color-fill-3);
+@btn-dashed-color-bg_active: var(--color-fill-4);
+@btn-dashed-color-border: var(--color-neutral-3);
+@btn-dashed-color-border_disabled: var(--color-neutral-3);
+@btn-dashed-color-border_hover: var(--color-neutral-4);
+@btn-dashed-color-border_active: var(--color-neutral-5);
+@btn-dashed-color-text_warning: rgb(var(--warning-6));
+@btn-dashed-color-text_warning_disabled: var(--color-warning-light-3);
+@btn-dashed-color-text_warning_hover: rgb(var(--warning-6));
+@btn-dashed-color-text_warning_active: rgb(var(--warning-6));
+@btn-dashed-color-bg_warning: var(--color-warning-light-1);
+@btn-dashed-color-bg_warning_disabled: var(--color-warning-light-1);
+@btn-dashed-color-bg_warning_hover: var(--color-warning-light-2);
+@btn-dashed-color-bg_warning_active: var(--color-warning-light-3);
+@btn-dashed-color-border_warning: var(--color-warning-light-2);
+@btn-dashed-color-border_warning_disabled: var(--color-warning-light-2);
+@btn-dashed-color-border_warning_hover: var(--color-warning-light-3);
+@btn-dashed-color-border_warning_active: var(--color-warning-light-4);
+@btn-dashed-color-text_danger: rgb(var(--danger-6));
+@btn-dashed-color-text_danger_disabled: var(--color-danger-light-3);
+@btn-dashed-color-text_danger_hover: rgb(var(--danger-6));
+@btn-dashed-color-text_danger_active: rgb(var(--danger-6));
+@btn-dashed-color-bg_danger: var(--color-danger-light-1);
+@btn-dashed-color-bg_danger_disabled: var(--color-danger-light-1);
+@btn-dashed-color-bg_danger_hover: var(--color-danger-light-2);
+@btn-dashed-color-bg_danger_active: var(--color-danger-light-3);
+@btn-dashed-color-border_danger: var(--color-danger-light-2);
+@btn-dashed-color-border_danger_disabled: var(--color-danger-light-2);
+@btn-dashed-color-border_danger_hover: var(--color-danger-light-3);
+@btn-dashed-color-border_danger_active: var(--color-danger-light-4);
+@btn-dashed-color-text_success: rgb(var(--success-6));
+@btn-dashed-color-text_success_disabled: var(--color-success-light-3);
+@btn-dashed-color-text_success_hover: rgb(var(--success-6));
+@btn-dashed-color-text_success_active: rgb(var(--success-6));
+@btn-dashed-color-bg_success: var(--color-success-light-1);
+@btn-dashed-color-bg_success_disabled: var(--color-success-light-1);
+@btn-dashed-color-bg_success_hover: var(--color-success-light-2);
+@btn-dashed-color-bg_success_active: var(--color-success-light-3);
+@btn-dashed-color-border_success: var(--color-success-light-2);
+@btn-dashed-color-border_success_disabled: var(--color-success-light-2);
+@btn-dashed-color-border_success_hover: var(--color-success-light-3);
+@btn-dashed-color-border_success_active: var(--color-success-light-4);
+@btn-dashed-border-style: dashed;
+@btn-text-color-text: rgb(var(--primary-6));
+@btn-text-color-text_disabled: var(--color-primary-light-3);
+@btn-text-color-text_hover: rgb(var(--primary-6));
+@btn-text-color-text_active: rgb(var(--primary-6));
+@btn-text-color-bg: transparent;
+@btn-text-color-bg_disabled: transparent;
+@btn-text-color-bg_hover: var(--color-fill-2);
+@btn-text-color-bg_active: var(--color-fill-3);
+@btn-text-color-border: transparent;
+@btn-text-color-border_disabled: transparent;
+@btn-text-color-border_hover: transparent;
+@btn-text-color-border_active: transparent;
+@btn-text-color-text_warning: rgb(var(--warning-6));
+@btn-text-color-text_warning_disabled: var(--color-warning-light-3);
+@btn-text-color-text_warning_hover: rgb(var(--warning-6));
+@btn-text-color-text_warning_active: rgb(var(--warning-6));
+@btn-text-color-bg_warning: transparent;
+@btn-text-color-bg_warning_disabled: transparent;
+@btn-text-color-bg_warning_hover: var(--color-fill-2);
+@btn-text-color-bg_warning_active: var(--color-fill-3);
+@btn-text-color-border_warning: transparent;
+@btn-text-color-border_warning_disabled: transparent;
+@btn-text-color-border_warning_hover: transparent;
+@btn-text-color-border_warning_active: transparent;
+@btn-text-color-text_danger: rgb(var(--danger-6));
+@btn-text-color-text_danger_disabled: var(--color-danger-light-3);
+@btn-text-color-text_danger_hover: rgb(var(--danger-6));
+@btn-text-color-text_danger_active: rgb(var(--danger-6));
+@btn-text-color-bg_danger: transparent;
+@btn-text-color-bg_danger_disabled: transparent;
+@btn-text-color-bg_danger_hover: var(--color-fill-2);
+@btn-text-color-bg_danger_active: var(--color-fill-3);
+@btn-text-color-border_danger: transparent;
+@btn-text-color-border_danger_disabled: transparent;
+@btn-text-color-border_danger_hover: transparent;
+@btn-text-color-border_danger_active: transparent;
+@btn-text-color-text_success: rgb(var(--success-6));
+@btn-text-color-text_success_disabled: var(--color-success-light-3);
+@btn-text-color-text_success_hover: rgb(var(--success-6));
+@btn-text-color-text_success_active: rgb(var(--success-6));
+@btn-text-color-bg_success: transparent;
+@btn-text-color-bg_success_disabled: transparent;
+@btn-text-color-bg_success_hover: var(--color-fill-2);
+@btn-text-color-bg_success_active: var(--color-fill-3);
+@btn-text-color-border_success: transparent;
+@btn-text-color-border_success_disabled: transparent;
+@btn-text-color-border_success_hover: transparent;
+@btn-text-color-border_success_active: transparent;
+@btn-text-border-style: solid;
+@btn-box-shadow-radius: 2px;
+@btn-primary-color-box-shadow: rgb(var(--primary-3));
+@btn-outline-color-box-shadow: rgb(var(--primary-3));
+@btn-secondary-color-box-shadow: var(--color-neutral-4);
+@btn-dashed-color-box-shadow: var(--color-neutral-4);
+@btn-text-color-box-shadow: var(--color-neutral-4);
+@btn-color-box-shadow_warning: rgb(var(--warning-3));
+@btn-color-box-shadow_danger: rgb(var(--danger-3));
+@btn-color-box-shadow_success: rgb(var(--success-3));
+@btn-prefix-cls: ~'@{prefix}-btn';
+
+
+/*********** calendar ***********/
+
+@calendar-color-border: var(--color-neutral-3);
+@calendar-header-padding-horizontal: 24px;
+@calendar-header-padding-vertical: 24px;
+@calendar-panel-date-cell-padding-vertical: 4px;
+@calendar-panel-date-cell-circle-height: 24px;
+@calendar-panel-year-cell-padding-vertical: 4px;
+@calendar-panel-year-cell-circle-height: 24px;
+@calendar-color-switch-icon: var(--color-text-2);
+@calendar-color-bg-switch-icon: var(--color-bg-5);
+@calendar-color-bg-switch-icon_hover: var(--color-fill-3);
+@calendar-color-text-title: var(--color-text-1);
+@calendar-color-cell-text-in-view: var(--color-text-1);
+@calendar-color-cell-text-not-in-view: var(--color-text-4);
+@calendar-color-bg-circle_selected: rgb(var(--primary-6));
+@calendar-color-bg-cell-in-range: var(--color-primary-light-1);
+@calendar-color-bg-cell-disabled: var(--color-fill-1);
+@calendar-color-text-cell-range-boundary: var(--color-white);
+@calendar-color-bg-cell-range-boundary: rgb(var(--primary-6));
+@calendar-color-bg-cell-hover-in-range: var(--color-primary-light-1);
+@calendar-color-text-cell-hover-range-boundary: var(--color-text-1);
+@calendar-color-bg-cell-hover-range-boundary: var(--color-primary-light-2);
+@calendar-panel-color-text-cell_hover: rgb(var(--primary-6));
+@calendar-panel-color-bg-cell_hover: var(--color-primary-light-1);
+@calendar-panel-color-text-cell_selected: var(--color-white);
+@calendar-panel-color-bg-cell_selected: rgb(var(--primary-6));
+@calendar-panel-color-current-time-dot: rgb(var(--primary-6));
+@calendar-panel-cell-boundary-border-radius: 16px;
+@calendar-color-box-shadow: var(--color-primary-light-3);
+@calendar-prefix-cls: ~'@{prefix}-calendar';
+
+
+/*********** card ***********/
+
+@card-size-small-height-title: 40px;
+@card-size-small-font-size-title: 16px;
+@card-size-small-font-size-title-extra: 14px;
+@card-size-small-font-size: 14px;
+@card-size-small-padding-horizontal-title: 16px;
+@card-size-small-padding-horizontal-body: 16px;
+@card-size-small-padding-vertical-body: 12px;
+@card-size-default-height-title: 46px;
+@card-size-default-font-size-title: 16px;
+@card-size-default-font-size-title-extra: 14px;
+@card-size-default-font-size: 14px;
+@card-size-default-padding-horizontal-title: 16px;
+@card-size-default-padding-horizontal-body: 16px;
+@card-size-default-padding-vertical-body: 16px;
+@card-line-height: 1.5715;
+@card-font-weight-title: 500;
+@card-margin-top-meta-footer: 20px;
+@card-margin-top-meta-description: 4px;
+@card-margin-right-action-item: 12px;
+@card-color-bg: var(--color-bg-2);
+@card-color-border: var(--color-neutral-3);
+@card-color-title: var(--color-text-1);
+@card-color-title-extra: rgb(var(--primary-6));
+@card-color-body: var(--color-text-2);
+@card-color-action: var(--color-text-2);
+@card-color-action_hover: rgb(var(--primary-6));
+@card-color-box-shadow: rgb(var(--gray-2));
+@card-color-box-shadow_dark: rgba(var(--gray-1), 40%);
+@card-border-width: 1px;
+@card-border-width-title-bottom: 1px;
+@card-border-radius: var(--border-radius-small);
+@card-border-radius-no-border: var(--border-radius-none);
+@card-prefix-cls: ~'@{prefix}-card';
+
+
+/*********** carousel ***********/
+
+@carousel-content-border-radius: 0;
+@carousel-arrow-position: 12px;
+@carousel-arrow-size: 24px;
+@carousel-arrow-font-size: 14px;
+@carousel-arrow-color-icon: var(--color-white);
+@carousel-arrow-color-bg: rgba(255, 255, 255, 0.3);
+@carousel-arrow-color-bg_hover: rgba(255, 255, 255, 0.5);
+@carousel-arrow-color-box-shadow: var(--color-primary-light-3);
+@carousel-indicator-size-wrapper: 48px;
+@carousel-indicator-color-bg-wrapper: rgba(0, 0, 0, 0.15);
+@carousel-indicator-dot-size: 6px;
+@carousel-indicator-line-size-width: 12px;
+@carousel-indicator-line-size-height: 4px;
+@carousel-indicator-slider-size-width: 48px;
+@carousel-indicator-slider-size-height: 4px;
+@carousel-indicator-position: 12px;
+@carousel-indicator-gap: 8px;
+@carousel-indicator-border-radius: var(--border-radius-medium);
+@carousel-indicator-color_default: rgba(255, 255, 255, 0.3);
+@carousel-indicator-color_active: var(--color-white);
+@carousel-indicator-outer-border-radius: 20px;
+@carousel-indicator-outer-padding: 4px;
+@carousel-indicator-outer-color_default: rgba(var(--gray-4), 0.5);
+@carousel-indicator-outer-color_active: var(--color-fill-4);
+@carousel-indicator-outer-color-bg: transparent;
+@carousel-prefix-cls: ~'@{prefix}-carousel';
+
+
+/*********** cascader ***********/
+
+@cascader-size-item-height: 36px;
+@cascader-size-item-width: 120px;
+@cascader-font-item-size: 14px;
+@cascader-margin-item-icon-left: 12px;
+@cascader-color-item-text: var(--color-text-1);
+@cascader-color-item-icon: var(--color-text-2);
+@cascader-padding-item-left: 12px;
+@cascader-padding-item-right: 10px;
+@cascader-size-item-icon: 12px;
+@cascader-color-item-text_hover: var(--color-text-1);
+@cascader-color-item-text_active: var(--color-text-1);
+@cascader-color-item-text_disabled: var(--color-text-4);
+@cascader-color-item-text_disabled_active: var(--color-text-4);
+@cascader-font-item-weight_active: 500;
+@cascader-color-item-bg_active: var(--color-fill-2);
+@cascader-color-item-bg_hover: var(--color-fill-2);
+@cascader-color-item-bg_disabled: transparent;
+@cascader-color-item-bg_disabled_active: var(--color-fill-2);
+@cascader-color-checkbox-bg_hover: var(--color-fill-3);
+@cascader-margin-checkbox-right: 8px;
+@cascader-prefix-cls: ~'@{prefix}-cascader';
+@cascader-prefix-cls-rtl: ~'@{prefix}-cascader-rtl';
+
+
+/*********** checkbox ***********/
+
+@checkbox-prefix-cls: ~'@{prefix}-checkbox';
+@checkbox-mask-border-width: 2px;
+@checkbox-mask-border-style: solid;
+@checkbox-mask-border-radius: var(--border-radius-small);
+@checkbox-mask-height: 14px;
+@checkbox-mask-bg-height: 24px;
+@checkbox-mask-bg-color-bg: var(--color-fill-2);
+@checkbox-mask-color-bg: var(--color-bg-2);
+@checkbox-mask-color-bg_checked: rgb(var(--primary-6));
+@checkbox-mask-color-bg_disabled: var(--color-fill-2);
+@checkbox-mask-color-bg_checked_disabled: var(--color-primary-light-3);
+@checkbox-mask-color-border: var(--color-fill-3);
+@checkbox-mask-color-border_hover: var(--color-fill-4);
+@checkbox-mask-color-border_checked: transparent;
+@checkbox-mask-color-border_checked_disabled: transparent;
+@checkbox-mask-color-border_disabled: var(--color-fill-3);
+@checkbox-color-text: var(--color-text-1);
+@checkbox-color-text_disabled: var(--color-text-4);
+@checkbox-group-spacing: 16px;
+@checkbox-text-mask-spacing: 8px;
+@checkbox-text-font-size: 14px;
+@checkbox-group-size-line-height_vertical: 32px;
+@checkbox-size-check-icon: 8px;
+@checkbox-color-check-icon: var(--color-white);
+@checkbox-color-check-icon_disabled: var(--color-fill-2);
+@checkbox-color-indeterminate-icon-width: 6px;
+@checkbox-color-indeterminate-icon-height: 2px;
+@checkbox-color-indeterminate-icon: var(--color-white);
+
+
+/*********** collapse ***********/
+
+@collapse-border-width: 1px;
+@collapse-border-radius: var(--border-radius-medium);
+@collapse-color-border: var(--color-neutral-3);
+@collapse-line-height: 1.5715;
+@collapse-title-line-height: 24px;
+@collapse-title-border-width: 1px;
+@collapse-title-color-border: var(--color-neutral-3);
+@collapse-title-font-size: 14px;
+@collapse-title-padding-horizontal: 13px;
+@collapse-title-padding-vertical: 8px;
+@collapse-title-color-bg: var(--color-bg-2);
+@collapse-title-color-bg_active: var(--color-bg-2);
+@collapse-title-color-bg_disabled: var(--color-bg-2);
+@collapse-title-color-text: var(--color-text-1);
+@collapse-title-color-text_disabled: var(--color-text-4);
+@collapse-title-font-weight_active: 500;
+@collapse-content-color-text: var(--color-text-1);
+@collapse-content-color-text_disabled: var(--color-text-1);
+@collapse-content-font-size: 14px;
+@collapse-content-padding-vertical: 8px;
+@collapse-content-color-bg: var(--color-fill-1);
+@collapse-expand-icon-size: 14px;
+@collapse-expand-icon-size-bg: 16px;
+@collapse-expand-icon-color-bg: var(--color-fill-2);
+@collapse-expand-icon-spacing-text: 5px;
+@collapse-color-expand-icon: var(--color-neutral-7);
+@collapse-item-color-border: var(--color-neutral-3);
+@collapse-item-border-width: 1px;
+@collapse-color-box-shadow: var(--color-primary-light-3);
+@collapse-prefix-cls: ~'@{prefix}-collapse';
+
+
+/*********** comment ***********/
+
+@comment-color-author-text: var(--color-text-2);
+@comment-color-datetime-text: var(--color-text-3);
+@comment-color-content-text: var(--color-text-1);
+@comment-color-actions-text: var(--color-text-2);
+@comment-font-size: 14px;
+@comment-font-action-size: 14px;
+@comment-font-author-size: 14px;
+@comment-font-datetime-size: 12px;
+@comment-margin-avatar-right: 12px;
+@comment-margin-author-right: 8px;
+@comment-margin-actions-top: 8px;
+@comment-margin-bottom: 20px;
+@comment-margin-actions-right: 8px;
+@comment-size-avatar-width: 32px;
+@comment-prefix-cls: ~'@{prefix}-comment';
+
+
+/*********** timepicker ***********/
+
+@timepicker-wrapper-border-radius: var(--border-radius-medium);
+@timepicker-column-width: 64px;
+@timepicker-column-height: 224px;
+@timepicker-cell-height: 24px;
+@timepicker-cell-spacing: 8px;
+@timepicker-cell-font-size: 14px;
+@timepicker-color-border: var(--color-neutral-3);
+@timepicker-color-cell-border: var(--color-neutral-3);
+@timepicker-color-text-cell: var(--color-text-1);
+@timepicker-color-bg-cell_hover: var(--color-fill-2);
+@timepicker-color-bg-cell_active: var(--color-fill-2);
+@timepicker-color-text-cell_disabled: var(--color-text-4);
+@timepicker-font-weight-cell: 500;
+@timepicker-font-weight-cell_active: 500;
+@timepicker-color-extra-text: var(--color-text-1);
+@timepicker-font-extra-size: 12px;
+@timepicker-extra-padding-horizontal: 8px;
+@timepicker-extra-padding-vertical: 8px;
+@timepicker-footer-padding-horizontal: 8px;
+@timepicker-footer-padding-vertical: 8px;
+
+
+/*********** date ***********/
+
+@date-panel-prefix-cls: ~'@{prefix}-panel-date';
+@date-picker-prefix-cls: ~'@{prefix}-datepicker';
+
+
+/*********** time ***********/
+
+@time-picker-prefix-cls: ~'@{prefix}-timepicker';
+
+
+/*********** datepicker ***********/
+
+@datepicker-timepicker-height: 276px;
+
+
+/*********** month ***********/
+
+@month-panel-prefix-cls: ~'@{prefix}-panel-month';
+
+
+/*********** quarter ***********/
+
+@quarter-panel-prefix-cls: ~'@{prefix}-panel-quarter';
+
+
+/*********** year ***********/
+
+@year-panel-prefix-cls: ~'@{prefix}-panel-year';
+
+
+/*********** week ***********/
+
+@week-panel-prefix-cls: ~'@{prefix}-panel-week';
+
+
+/*********** range ***********/
+
+@range-picker-prefix-cls: ~'@{prefix}-picker-range';
+
+
+/*********** descriptions ***********/
+
+@descriptions-border-width: 1px;
+@descriptions-border-style: solid;
+@descriptions-color-border: var(--color-neutral-3);
+@descriptions-border-radius: var(--border-radius-medium);
+@descriptions-font-size-title: 16px;
+@descriptions-size-mini-title-margin-bottom: 6px;
+@descriptions-size-small-title-margin-bottom: 8px;
+@descriptions-size-medium-title-margin-bottom: 12px;
+@descriptions-size-default-title-margin-bottom: 16px;
+@descriptions-size-large-title-margin-bottom: 20px;
+@descriptions-size-mini-font-size-text: 12px;
+@descriptions-size-small-font-size-text: 14px;
+@descriptions-size-medium-font-size-text: 14px;
+@descriptions-size-default-font-size-text: 14px;
+@descriptions-size-large-font-size-text: 14px;
+@descriptions-color-title: var(--color-text-1);
+@descriptions-color-text-label: var(--color-text-3);
+@descriptions-color-text-value: var(--color-text-1);
+@descriptions-font-weight-title: 500;
+@descriptions-font-weight-text-label: 500;
+@descriptions-font-weight-text-value: 400;
+@descriptions-border-color-bg-label: var(--color-fill-1);
+@descriptions-item-size-mini-spacing-bottom: 2px;
+@descriptions-item-size-small-spacing-bottom: 4px;
+@descriptions-item-size-medium-spacing-bottom: 8px;
+@descriptions-item-size-default-spacing-bottom: 12px;
+@descriptions-item-size-large-spacing-bottom: 16px;
+@descriptions-border-item-size-mini-padding-horizontal: 20px;
+@descriptions-border-item-size-mini-padding-vertical: 3px;
+@descriptions-border-item-size-small-padding-horizontal: 20px;
+@descriptions-border-item-size-small-padding-vertical: 3px;
+@descriptions-border-item-size-medium-padding-horizontal: 20px;
+@descriptions-border-item-size-medium-padding-vertical: 5px;
+@descriptions-border-item-size-default-padding-horizontal: 20px;
+@descriptions-border-item-size-default-padding-vertical: 7px;
+@descriptions-border-item-size-large-padding-horizontal: 20px;
+@descriptions-border-item-size-large-padding-vertical: 9px;
+@descriptions-prefix-cls: ~'@{prefix}-descriptions';
+
+
+/*********** divider ***********/
+
+@divider-margin-horizontal: 12px;
+@divider-margin-vertical: 20px;
+@divider-margin-vertical_text: 20px;
+@divider-margin-text: 16px;
+@divider-position-text-left: 24px;
+@divider-position-text-right: 24px;
+@divider-font-size: 14px;
+@divider-font-weight: 500;
+@divider-size: 1px;
+@divider-line-style: solid;
+@divider-color-bg: var(--color-neutral-3);
+@divider-color-text: var(--color-text-1);
+@divider-prefix-cls: ~'@{prefix}-divider';
+
+
+/*********** drawer ***********/
+
+@drawer-size-header-height: 48px;
+@drawer-margin-footer-button-left: 12px;
+@drawer-font-header-size: 16px;
+@drawer-font-header-weight: 500;
+@drawer-padding-horizontal: 16px;
+@drawer-padding-footer-vertical: 16px;
+@drawer-padding-content-vertical: 12px;
+@drawer-color-border: var(--color-neutral-3);
+@drawer-color-header-text: var(--color-text-1);
+@drawer-color-content-text: var(--color-text-1);
+@drawer-position-close-icon-right: 16px;
+@drawer-font-size-close-icon: 12px;
+@drawer-prefix-cls: ~'@{prefix}-drawer';
+
+
+/*********** dropdown ***********/
+
+@dropdown-max-height: 200px;
+@dropdown-border-radius: var(--border-radius-medium);
+@dropdown-padding-vertical: 4px;
+@dropdown-font-size: 14px;
+@dropdown-color-bg: var(--color-bg-popup);
+@dropdown-color-border: var(--color-fill-3);
+@dropdown-box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+@dropdown-option-height: 36px;
+@dropdown-option-padding-horizontal: 12px;
+@dropdown-option-font-weight_selected: 500;
+@dropdown-option-color-bg_default: transparent;
+@dropdown-option-color-bg_hover: var(--color-fill-2);
+@dropdown-option-color-bg_selected: transparent;
+@dropdown-option-color-bg_disabled: transparent;
+@dropdown-option-color-text_default: var(--color-text-1);
+@dropdown-option-color-text_hover: var(--color-text-1);
+@dropdown-option-color-text_selected: var(--color-text-1);
+@dropdown-option-color-text_disabled: var(--color-text-4);
+@dropdown-group-title-height: 20px;
+@dropdown-group-title-padding-horizontal: 12px;
+@dropdown-group-title-margin-top: 8px;
+@dropdown-group-title-font-size: 12px;
+@dropdown-group-title-color-text: var(--color-text-3);
+@dropdown-margin-left-suffix-icon: 12px;
+@dropdown-dark-color-bg: var(--color-menu-dark-bg);
+@dropdown-dark-color-border: var(--color-menu-dark-bg);
+@dropdown-dark-option-color-bg_default: transparent;
+@dropdown-dark-option-color-bg_hover: var(--color-menu-dark-hover);
+@dropdown-dark-option-color-bg_selected: transparent;
+@dropdown-dark-option-color-bg_disabled: transparent;
+@dropdown-dark-option-color-text_default: var(--color-text-4);
+@dropdown-dark-option-color-text_hover: var(--color-text-4);
+@dropdown-dark-option-color-text_selected: var(--color-white);
+@dropdown-dark-option-color-text_disabled: var(--color-text-2);
+@dropdown-dark-group-title-color-text: var(--color-text-3);
+@dropdown-prefix-cls: ~'@{prefix}-dropdown';
+
+
+/*********** empty ***********/
+
+@empty-spacing-padding: 10px;
+@empty-color-icon: rgb(var(--gray-5));
+@empty-color-text: rgb(var(--gray-5));
+@empty-font-size-image: 48px;
+@empty-font-size-text: 14px;
+@empty-spacing-image-margin-bottom: 4px;
+@empty-size-img-height: 80px;
+@empty-prefix-cls: ~'@{prefix}-empty';
+
+
+/*********** form ***********/
+
+@form-size-mini-margin-item-bottom: 16px;
+@form-size-small-margin-item-bottom: 20px;
+@form-size-default-margin-item-bottom: 20px;
+@form-size-large-margin-item-bottom: 20px;
+@form-size-mini-size-item-height: 24px;
+@form-size-small-size-item-height: 28px;
+@form-size-default-size-item-height: 32px;
+@form-size-large-size-item-height: 36px;
+@form-size-mini-font-label-size: 12px;
+@form-size-small-font-label-size: 14px;
+@form-size-large-font-label-size: 14px;
+@form-size-default-font-label-size: 14px;
+@form-font-extra-text-size: 12px;
+@form-font-error-text-size: 12px;
+@form-margin-label-right: 16px;
+@form-margin-extra-bottom: 4px;
+@form-margin-extra-top: 4px;
+@form-inline-margin-item-right: 24px;
+@form-inline-margin-item-bottom: 8px;
+@form-vertical-margin-label-bottom: 8px;
+@form-color-extra-text: var(--color-text-3);
+@form-color-text-label: var(--color-text-2);
+@form-color-text-tooltip: var(--color-text-4);
+@form-margin-tooltip-left: 4px;
+@form-color-bg_warning: var(--color-warning-light-1);
+@form-color-bg_warning_hover: var(--color-warning-light-2);
+@form-color-bg_warning_focus: var(--color-bg-2);
+@form-color-border_warning: transparent;
+@form-color-border_warning_focus: rgb(var(--warning-6));
+@form-color-border_warning_hover: transparent;
+@form-size-shadow_warning_focus: 0;
+@form-color-shadow_warning_focus: var(--color-warning-light-2);
+@form-color-bg_success: var(--color-fill-2);
+@form-color-bg_success_hover: var(--color-fill-3);
+@form-color-bg_success_focus: var(--color-bg-2);
+@form-color-border_success: transparent;
+@form-color-border_success_focus: rgb(var(--success-6));
+@form-color-border_success_hover: transparent;
+@form-size-shadow_success_focus: 0;
+@form-color-shadow_success_focus: var(--color-success-light-2);
+@form-color-bg_error: var(--color-danger-light-1);
+@form-color-bg_error_hover: var(--color-danger-light-2);
+@form-color-bg_error_focus: var(--color-bg-2);
+@form-color-border_error: transparent;
+@form-color-border_error_focus: rgb(var(--danger-6));
+@form-color-border_error_hover: transparent;
+@form-size-shadow_error_focus: 0;
+@form-color-shadow_error_focus: var(--color-danger-light-2);
+@form-color-bg_validating: var(--color-fill-2);
+@form-color-bg_validating_hover: var(--color-fill-3);
+@form-color-bg_validating_focus: var(--color-bg-2);
+@form-color-border_validating: transparent;
+@form-color-border_validating_focus: rgb(var(--primary-6));
+@form-color-border_validating_hover: transparent;
+@form-size-shadow_validating_focus: 0;
+@form-color-shadow_validating_focus: var(--color-primary-light-2);
+@form-color-tip-text_success: rgb(var(--success-6));
+@form-color-tip-icon-text_success: rgb(var(--success-6));
+@form-color-tip-text_error: rgb(var(--danger-6));
+@form-color-tip-icon-text_error: rgb(var(--danger-6));
+@form-color-tip-text_warning: rgb(var(--warning-6));
+@form-color-tip-icon-text_warning: rgb(var(--warning-6));
+@form-color-tip-text_validating: rgb(var(--primary-6));
+@form-color-tip-icon-text_validating: rgb(var(--primary-6));
+@form-prefix-cls: ~'@{prefix}-form';
+
+
+/*********** row ***********/
+
+@row-prefix-cls: ~'@{prefix}-row';
+
+
+/*********** col ***********/
+
+@col-prefix-cls: ~'@{prefix}-col';
+
+
+/*********** grid ***********/
+
+@grid-prefix-cls: ~'@{prefix}-grid';
+
+
+/*********** image ***********/
+
+@image-radius: var(--border-radius-small);
+@image-font-size-title: 16px;
+@image-font-weight-title: 500;
+@image-font-size-description: 14px;
+@image-color-title_footer_outer-text: var(--color-text-1);
+@image-color-title_footer_inner-text: var(--color-white);
+@image-color-description_footer_inner-text: var(--color-white);
+@image-color-description_footer_outer-text: var(--color-neutral-6);
+@image-spacing-actions-left: 12px;
+@image-font-size-actions-item: 14px;
+@image-padding-actions-item-vertical: 0;
+@image-padding-actions-item-horizontal: 0;
+@image-spacing-actions-item-left: 12px;
+@image-radius-actions-item: var(--border-radius-small);
+@image-color-actions-item_footer_inner_hover-bg: rgba(0, 0, 0, 0.5);
+@image-color-actions-item_footer_outer_hover-bg: var(--color-neutral-2);
+@image-color-actions-item_trigger-text: var(--color-neutral-8);
+@image-color-actions-item_trigger_hover-bg: var(--color-neutral-2);
+@image-spacing-actions-trigger-item-vertical: 5px;
+@image-spacing-actions-trigger-item-horizontal: 4px;
+@image-color-footer_inner-bg: linear-gradient(360deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0) 100%);
+@image-color-footer_inner-text: var(--color-white);
+@image-padding-footer_inner_vertical: 9px;
+@image-padding-footer_inner_horizontal: 16px;
+@image-spacing-footer_inner_simple-vertical: 12px;
+@image-spacing-footer_inner_simple-horizontal: 16px;
+@image-color-footer_outer-text: var(--color-neutral-8);
+@image-spacing-footer-top: 4px;
+@image-color-error-bg: var(--color-neutral-1);
+@image-color-error-text: var(--color-neutral-4);
+@image-font-size-error-icon: 60px;
+@image-font-size-error-text: 12px;
+@image-line-height-error-text: 1.6667;
+@image-size-error-min-height: 100px;
+@image-spacing-error-padding: 16px;
+@image-color-loader-bg: var(--color-neutral-1);
+@image-size-loader-min-height: 100px;
+@image-font-size-loader-spin: 32px;
+@image-color-loader-spin-text: rgb(var(--primary-6));
+@image-color-loader-spin-text-text: var(--color-neutral-6);
+@image-font-size-loader-spin-text: 16px;
+@image-preview-color-mask-bg: var(--color-mask-bg);
+@image-preview-size-scale-value-height: 32px;
+@image-preview-spacing-scale-value-vertical: 7px;
+@image-preview-spacing-scale-value-horizontal: 10px;
+@image-preview-font-size-scale-value: 12px;
+@image-preview-color-scale-value-text: var(--color-white);
+@image-preview-color-scale-value-bg: rgba(255, 255, 255, 0.08);
+@image-preview-color-toolbar-bg: var(--color-bg-2);
+@image-preview-radius-toolbar: var(--border-radius-medium);
+@image-preview-spacing-toolbar-vertical: 4px;
+@image-preview-spacing-toolbar-horizontal: 16px;
+@image-preview-spacing-toolbar-horizontal_simple: 4px;
+@image-preview-spacing-toolbar-vertical_simple: 4px;
+@image-preview-position-toolbar-bottom: 46px;
+@image-preview-font-size-action: 14px;
+@image-preview-color-action-text: var(--color-neutral-8);
+@image-preview-radius-action: var(--border-radius-small);
+@image-preview-color-action-bg: transparent;
+@image-preview-color-action_hover-bg: var(--color-neutral-2);
+@image-preview-color-action_hover-text: rgb(var(--primary-6));
+@image-preview-color-action_disabled-bg: transparent;
+@image-preview-color-action_disabled-text: var(--color-text-4);
+@image-preview-font-size-action-name: 12px;
+@image-preview-spacing-action-name-right: 12px;
+@image-preview-padding-action-content: 13px;
+@image-preview-margin-action-right: 0;
+@image-preview-spacing-trigger-padding-vertical: 12px;
+@image-preview-spacing-trigger-padding-horizontal: 16px;
+@image-preview-margin-action-bottom: 0;
+@image-preview-color-loading-text: rgb(var(--primary-6));
+@image-preview-color-loading-bg: #232324;
+@image-preview-font-size-loading: 18px;
+@image-preview-spacing-loading-padding: 10px;
+@image-preview-size-loading-width: 48px;
+@image-preview-size-loading-height: 48px;
+@image-preview-radius-loading: var(--border-radius-medium);
+@image-preview-size-close-btn-width: 32px;
+@image-preview-size-close-icon: 14px;
+@image-preview-color-close-btn-bg: rgba(0, 0, 0, 0.5);
+@image-preview-color-close-btn-text: var(--color-white);
+@image-preview-position-close-btn-right: 36px;
+@image-preview-position-close-btn-top: 36px;
+@image-preview-arrow-position: 20px;
+@image-preview-arrow-size: 32px;
+@image-preview-arrow-font-size: 16px;
+@image-preview-arrow-color-icon: var(--color-white);
+@image-preview-arrow-color-icon_disabled: rgba(255, 255, 255, 0.3);
+@image-preview-arrow-color-bg: rgba(255, 255, 255, 0.3);
+@image-preview-arrow-color-bg_hover: rgba(255, 255, 255, 0.5);
+@image-preview-arrow-color-bg_disabled: rgba(255, 255, 255, 0.2);
+@image-trigger-spacing-padding-vertical: 6px;
+@image-trigger-spacing-padding-horizontal: 4px;
+@image-trigger-color-bg: var(--color-bg-5);
+@image-trigger-color-border: var(--color-neutral-3);
+@image-trigger-size-border: 1px;
+@image-trigger-radius: 4px;
+@image-color-box-shadow: rgb(var(--primary-6));
+@image-trigger-prefix-cls: ~'@{prefix}-image-trigger';
+@image-prefix-cls: ~'@{prefix}-image';
+
+
+/*********** preview ***********/
+
+@preview-prefix-cls: ~'@{prefix}-image-preview';
+
+
+/*********** layout ***********/
+
+@layout-trigger-height: 48px;
+@layout-sider-background: var(--color-menu-dark-bg);
+@layout-font-color-dark: var(--color-white);
+@layout-font-color: var(--color-text-1);
+@layout-trigger-dark-color: rgba(255, 255, 255, 0.2);
+@layout-sider-background-light: var(--color-menu-light-bg);
+@layout-trigger-light-color-border: var(--color-bg-5);
+@layout-prefix-cls: ~'@{prefix}-layout';
+
+
+/*********** list ***********/
+
+@list-border-width: 1px;
+@list-border-color: var(--color-neutral-3);
+@list-border-radius: var(--border-radius-medium);
+@list-color-text: var(--color-text-1);
+@list-font-size: 14px;
+@list-line-height: 1.5715;
+@list-color-text-header: var(--color-text-1);
+@list-color-bg-item-hover: var(--color-fill-1);
+@list-font-size-header: 16px;
+@list-font-weight-header: 500;
+@list-line-height-header: 1.5;
+@list-size-small-padding-vertical-header: 8px;
+@list-size-small-padding-horizontal-header: 20px;
+@list-size-small-padding-vertical-item: 9px;
+@list-size-small-padding-horizontal-item: 20px;
+@list-size-default-padding-vertical-header: 12px;
+@list-size-default-padding-horizontal-header: 20px;
+@list-size-default-padding-vertical-item: 13px;
+@list-size-default-padding-horizontal-item: 20px;
+@list-size-large-padding-vertical-header: 16px;
+@list-size-large-padding-horizontal-header: 20px;
+@list-size-large-padding-vertical-item: 17px;
+@list-size-large-padding-horizontal-item: 20px;
+@list-meta-font-weight-title: 500;
+@list-meta-color-title: var(--color-text-1);
+@list-mete-color-description: var(--color-text-2);
+@list-meta-margin-right-avatar: 16px;
+@list-meta-margin-bottom-title: 2px;
+@list-meta-padding-horizontal: 0;
+@list-meta-padding-vertical: 4px;
+@list-action-gap: 20px;
+@list-action-margin-top: 4px;
+@list-pagination-margin-top: 24px;
+@list-prefix-cls: ~'@{prefix}-list';
+
+
+/*********** mentions ***********/
+
+@mentions-padding-horizontal: 12px;
+@mentions-padding-vertical: 4px;
+@mentions-font-size: 14px;
+@mentions-line-height: 1.5715;
+@mentions-prefix-cls: ~'@{prefix}-mentions';
+
+
+/*********** menu ***********/
+
+@menu-font-size: 14px;
+@menu-line-height: 1.5715;
+@menu-border-radius: var(--border-radius-small);
+@menu-font-weight-item-selected: 500;
+@menu-color-label-item-selected: rgb(var(--primary-6));
+@menu-height-label-item-selected: 3px;
+@menu-margin-left-item-suffix-icon: 6px;
+@menu-margin-right-item-prefix-icon: 16px;
+@menu-horizontal-margin-right-item-prefix-icon: 8px;
+@menu-item-gap: 4px;
+@menu-item-indent-spacing: 20px;
+@menu-width-collapse-button: 24px;
+@menu-height-collapse-button: 24px;
+@menu-border-radius-collapse-button: var(--border-radius-small);
+@menu-light-color-bg: var(--color-menu-light-bg);
+@menu-light-color-bg-item_default: var(--color-menu-light-bg);
+@menu-light-color-bg-item_hover: var(--color-fill-2);
+@menu-light-color-bg-item_selected: var(--color-fill-2);
+@menu-light-color-bg-item_disabled: var(--color-menu-light-bg);
+@menu-light-color-item_default: var(--color-text-2);
+@menu-light-color-item_hover: var(--color-text-2);
+@menu-light-color-item_selected: rgb(var(--primary-6));
+@menu-light-color-submenu_selected: rgb(var(--primary-6));
+@menu-light-color-bg-submenu_selected_hover: var(--color-fill-2);
+@menu-light-color-item_disabled: var(--color-text-4);
+@menu-light-color-icon_default: var(--color-text-3);
+@menu-light-color-icon_hover: var(--color-text-3);
+@menu-light-color-icon_selected: rgb(var(--primary-6));
+@menu-light-color-icon_disabled: var(--color-text-4);
+@menu-light-color-group-title: var(--color-text-3);
+@menu-dark-color-bg: var(--color-menu-dark-bg);
+@menu-dark-color-bg-item_default: var(--color-menu-dark-bg);
+@menu-dark-color-bg-item_hover: var(--color-menu-dark-hover);
+@menu-dark-color-bg-item_selected: var(--color-menu-dark-hover);
+@menu-dark-color-bg-item_disabled: var(--color-menu-dark-bg);
+@menu-dark-color-submenu_selected: rgb(var(--primary-6));
+@menu-dark-color-bg-submenu_selected_hover: var(--color-menu-dark-hover);
+@menu-dark-color-item_default: var(--color-text-4);
+@menu-dark-color-item_hover: var(--color-text-4);
+@menu-dark-color-item_selected: var(--color-white);
+@menu-dark-color-item_disabled: var(--color-text-2);
+@menu-dark-color-icon_default: var(--color-text-3);
+@menu-dark-color-icon_hover: var(--color-text-3);
+@menu-dark-color-icon_selected: var(--color-white);
+@menu-dark-color-icon_disabled: var(--color-text-2);
+@menu-dark-color-group-title: var(--color-text-3);
+@menu-color-border-popup: var(--color-neutral-3);
+@menu-light-color-bg-button: var(--color-fill-1);
+@menu-light-color-bg-button_hover: var(--color-fill-3);
+@menu-light-color-button: var(--color-text-3);
+@menu-dark-color-bg-button: rgb(var(--primary-6));
+@menu-dark-color-bg-button_hover: rgb(var(--primary-7));
+@menu-dark-color-button: var(--color-white);
+@menu-horizontal-padding-vertical: 14px;
+@menu-horizontal-padding-horizontal: 20px;
+@menu-horizontal-item-gap: 12px;
+@menu-horizontal-item-height: 30px;
+@menu-horizontal-item-padding-horizontal: 12px;
+@menu-vertical-padding-vertical: 4px;
+@menu-vertical-padding-horizontal: 8px;
+@menu-vertical-item-height: 40px;
+@menu-vertical-item-padding-horizontal: 12px;
+@menu-collapse-width: 48px;
+@menu-collapse-padding-vertical: 4px;
+@menu-collapse-padding-horizontal: 4px;
+@menu-pop-button-size: 40px;
+@menu-pop-button-margin-bottom: 16px;
+@menu-pop-button-box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+@menu-pop-button-border-color: transparent;
+@menu-prefix-cls: ~'@{prefix}-menu';
+
+
+/*********** message ***********/
+
+@message-wrapper-margin-top: 40px;
+@message-wrapper-margin-bottom: 40px;
+@message-padding-top: 10px;
+@message-padding-bottom: 10px;
+@message-padding-left: 16px;
+@message-padding-right: 16px;
+@message-margin-bottom: 16px;
+@message-border-radius: var(--border-radius-small);
+@message-font-size-icon: 20px;
+@message-font-size-content: 14px;
+@message-icon-margin-right: 8px;
+@message-border-width: 1px;
+@message-border-style: solid;
+@message-box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+@message-color-close-icon: var(--color-text-1);
+@message-close-icon-font-size: 12px;
+@message-close-icon-top: 14px;
+@message-close-icon-right: 12px;
+@message-close-icon-left: 14px;
+@message-normal-color-bg: var(--color-bg-popup);
+@message-normal-color-icon: var(--color-text-1);
+@message-normal-color-content: var(--color-text-1);
+@message-normal-color-border: var(--color-neutral-3);
+@message-info-color-bg: var(--color-bg-popup);
+@message-info-color-icon: rgb(var(--primary-6));
+@message-info-color-content: var(--color-text-1);
+@message-info-color-border: var(--color-neutral-3);
+@message-success-color-bg: var(--color-bg-popup);
+@message-success-color-icon: rgb(var(--success-6));
+@message-success-color-content: var(--color-text-1);
+@message-success-color-border: var(--color-neutral-3);
+@message-warning-color-bg: var(--color-bg-popup);
+@message-warning-color-icon: rgb(var(--warning-6));
+@message-warning-color-content: var(--color-text-1);
+@message-warning-color-border: var(--color-neutral-3);
+@message-error-color-bg: var(--color-bg-popup);
+@message-error-color-icon: rgb(var(--danger-6));
+@message-error-color-content: var(--color-text-1);
+@message-error-color-border: var(--color-neutral-3);
+@message-loading-color-bg: var(--color-bg-popup);
+@message-loading-color-icon: rgb(var(--primary-6));
+@message-loading-color-content: var(--color-text-1);
+@message-loading-color-border: var(--color-neutral-3);
+@message-prefix-cls: ~'@{prefix}-message';
+
+
+/*********** modal ***********/
+
+@modal-border-radius: var(--border-radius-medium);
+@modal-size-tip-icon: 18px;
+@modal-margin-top: 100px;
+@modal-margin-tip-icon-right: 10px;
+@modal-margin-footer-button-left: 12px;
+@modal-color-border: var(--color-neutral-3);
+@modal-border-width: 0;
+@modal-box-shadow: none;
+@modal-color-header-text: var(--color-text-1);
+@modal-color-content-text: var(--color-text-1);
+@modal-font-header-size: 16px;
+@modal-font-header-weight: 500;
+@modal-font-content-size: 14px;
+@modal-default-align-header: center;
+@modal-simple-align-header: center;
+@modal-default-align-footer: right;
+@modal-simple-align-footer: center;
+@modal-default-padding-horizontal: 20px;
+@modal-default-size-header-height: 48px;
+@modal-default-padding-content-vertical: 24px;
+@modal-default-padding-footer-vertical: 16px;
+@modal-default-size-width: 520px;
+@modal-simple-size-width: 464px;
+@modal-simple-padding-horizontal: 32px;
+@modal-simple-padding-top: 24px;
+@modal-simple-padding-bottom: 32px;
+@modal-simple-margin-footer-top: 32px;
+@modal-simple-margin-content-top: 24px;
+@modal-position-close-icon-right: 16px;
+@modal-font-size-close-icon: 12px;
+@modal-color-close-icon: var(--color-text-1);
+@modal-prefix-cls: ~'@{prefix}-modal';
+
+
+/*********** notification ***********/
+
+@notification-wrapper-margin-top: 20px;
+@notification-wrapper-margin-bottom: 20px;
+@notification-wrapper-margin-left: 20px;
+@notification-wrapper-margin-right: 20px;
+@notification-border-radius: var(--border-radius-medium);
+@notification-margin-bottom: 20px;
+@notification-width: 300px;
+@notification-padding-top: 20px;
+@notification-padding-bottom: 20px;
+@notification-padding-left: 20px;
+@notification-padding-right: 20px;
+@notification-font-size-icon: 24px;
+@notification-font-size-title: 16px;
+@notification-font-size-content: 14px;
+@notification-icon-margin-right: 16px;
+@notification-title-margin-bottom: 4px;
+@notification-btn-wrapper-margin-top: 16px;
+@notification-border-width: 1px;
+@notification-border-style: solid;
+@notification-color-close-icon: var(--color-text-1);
+@notification-close-icon-font-size: 12px;
+@notification-close-icon-top: 12px;
+@notification-close-icon-right: 12px;
+@notification-normal-color-bg: var(--color-bg-popup);
+@notification-normal-color-icon: var(--color-text-1);
+@notification-normal-color-text-title: var(--color-text-1);
+@notification-normal-color-text-content: var(--color-text-1);
+@notification-normal-color-border: var(--color-neutral-3);
+@notification-info-color-bg: var(--color-bg-popup);
+@notification-info-color-icon: rgb(var(--primary-6));
+@notification-info-color-text-title: var(--color-text-1);
+@notification-info-color-text-content: var(--color-text-1);
+@notification-info-color-border: var(--color-neutral-3);
+@notification-success-color-bg: var(--color-bg-popup);
+@notification-success-color-icon: rgb(var(--success-6));
+@notification-success-color-text-title: var(--color-text-1);
+@notification-success-color-text-content: var(--color-text-1);
+@notification-success-color-border: var(--color-neutral-3);
+@notification-warning-color-bg: var(--color-bg-popup);
+@notification-warning-color-icon: rgb(var(--warning-6));
+@notification-warning-color-text-title: var(--color-text-1);
+@notification-warning-color-text-content: var(--color-text-1);
+@notification-warning-color-border: var(--color-neutral-3);
+@notification-error-color-bg: var(--color-bg-popup);
+@notification-error-color-icon: rgb(var(--danger-6));
+@notification-error-color-text-title: var(--color-text-1);
+@notification-error-color-text-content: var(--color-text-1);
+@notification-error-color-border: var(--color-neutral-3);
+@notification-prefix-cls: ~'@{prefix}-notification';
+
+
+/*********** page ***********/
+
+@page-header-padding-left: 24px;
+@page-header-padding-right: 20px;
+@page-header-padding-vertical: 16px;
+@page-header-padding-vertical_breadcrumb: 12px;
+@page-header-color-back-icon: var(--color-text-2);
+@page-header-size-back-icon: 14px;
+@page-header-color-back-icon-box-shadow: var(--color-primary-light-3);
+@page-header-margin-back-icon-right: 12px;
+@page-header-line-height: 28px;
+@page-header-color-title-text: var(--color-text-1);
+@page-header-weight-title-text: 600;
+@page-header-color-back-icon-bg_hover: var(--color-fill-2);
+@page-header-size-back-icon-bg_hover: 30px;
+@page-header-size-title-text: 20px;
+@page-header-color-divider-bg: var(--color-fill-3);
+@page-header-size-divider-height: 16px;
+@page-header-size-divider-width: 1px;
+@page-header-margin-divider-left: 12px;
+@page-header-margin-divider-right: 12px;
+@page-header-color-sub-title-text: var(--color-text-3);
+@page-header-size-sub-title-text: 14px;
+@page-header-color-header-border: var(--color-neutral-3);
+@page-header-border-header-width: 1px;
+@page-header-border-header-style: solid;
+@page-header-padding-content-vertical: 20px;
+@page-header-padding-content-horizontal: 32px;
+@page-header-margin-breadcrumb-bottom: 4px;
+@page-header-prefix-cls: ~'@{prefix}-page-header';
+
+
+/*********** pagination ***********/
+
+@pagination-prefix-cls: ~'@{prefix}-pagination';
+@pagination-item-border-radius: var(--border-radius-small);
+@pagination-item-spacing: 8px;
+@pagination-margin-total-spacing: 8px;
+@pagination-margin-option-left: 8px;
+@pagination-margin-jumper-left: 8px;
+@pagination-size-mini: 24px;
+@pagination-size-small: 28px;
+@pagination-size-default: 32px;
+@pagination-size-large: 36px;
+@pagination-size-mini-font-size: 12px;
+@pagination-size-small-font-size: 14px;
+@pagination-size-default-font-size: 14px;
+@pagination-size-large-font-size: 14px;
+@pagination-size-icon-arrow_mini: 12px;
+@pagination-size-icon-arrow_small: 12px;
+@pagination-size-icon-arrow_default: 12px;
+@pagination-size-icon-arrow_large: 14px;
+@pagination-size-icon-ellipsis: 16px;
+@pagination-border-width: 0;
+@pagination-color-bg-item: transparent;
+@pagination-color-bg-item_active: var(--color-primary-light-1);
+@pagination-color-bg-item_hover: var(--color-fill-1);
+@pagination-color-bg-item_disabled: transparent;
+@pagination-color-bg-item_active_disabled: var(--color-fill-1);
+@pagination-color-item-text: var(--color-text-2);
+@pagination-color-item-text_hover: var(--color-text-2);
+@pagination-color-item-text_active: rgb(var(--primary-6));
+@pagination-color-item-text_disabled: var(--color-text-4);
+@pagination-color-item-text_active_disabled: var(--color-primary-light-3);
+@pagination-color-item-border: transparent;
+@pagination-color-item-border_active: transparent;
+@pagination-color-item-border_hover: transparent;
+@pagination-color-item-border_disabled: transparent;
+@pagination-color-item-border_active_disabled: transparent;
+@pagination-color-icon-arrow: var(--color-text-2);
+@pagination-color-icon-arrow-bg: transparent;
+@pagination-color-icon-arrow-bg_hover: var(--color-fill-1);
+@pagination-color-icon-arrow-bg_disabled: transparent;
+@pagination-color-icon-arrow-text_hover: rgb(var(--primary-6));
+@pagination-color-icon-arrow-text_disabled: var(--color-text-4);
+@pagination-simple-input-width: 40px;
+@pagination-simple-color-icon-arrow: var(--color-text-2);
+@pagination-simple-color-icon-arrow-bg: transparent;
+@pagination-simple-color-icon-arrow-bg_hover: var(--color-fill-1);
+@pagination-simple-color-icon-arrow-bg_disabled: transparent;
+@pagination-simple-color-icon-arrow-text_hover: rgb(var(--primary-6));
+@pagination-simple-color-icon-arrow-text_disabled: var(--color-text-4);
+@pagination-simple-margin-prev-right: 4px;
+@pagination-simple-margin-next-left: 12px;
+@pagination-simple-margin-separator-left: 12px;
+@pagination-simple-margin-separator-right: 12px;
+@pagination-color-jumper-goto: var(--color-text-2);
+@pagination-color-text-total: var(--color-text-1);
+
+
+/*********** patination ***********/
+
+@patination-jumper-input-width: 40px;
+
+
+/*********** popconfirm ***********/
+
+@popconfirm-padding-horizontal: 16px;
+@popconfirm-padding-vertical: 16px;
+@popconfirm-margin-title-bottom: 16px;
+@popconfirm-margin-content-top: 4px;
+@popconfirm-margin-content-bottom: 16px;
+@popconfirm-size-title-icon: 18px;
+@popconfirm-margin-icon-right: 8px;
+@popconfirm-margin-button-left: 8px;
+@popconfirm-font-title-size: 14px;
+@popconfirm-color-title-text: var(--color-text-1);
+@popconfirm-prefix-cls: ~'@{prefix}-popconfirm';
+
+
+/*********** popover ***********/
+
+@popover-prefix-cls: ~'@{prefix}-popover';
+
+
+/*********** progress ***********/
+
+@progress-line-color-line-bg: var(--color-fill-3);
+@progress-line-color-inner-bg: rgb(var(--primary-6));
+@progress-line-color-inner-bg_success: rgb(var(--success-6));
+@progress-line-color-inner-bg_error: rgb(var(--danger-6));
+@progress-line-color-buffer-bg: var(--color-primary-light-3);
+@progress-line-size-large-font-size: 16px;
+@progress-line-size-small-font-size: 12px;
+@progress-line-size-default-font-size: 12px;
+@progress-line-size-large-margin-text-left: 16px;
+@progress-line-size-small-margin-text-left: 16px;
+@progress-line-color-icon_success: rgb(var(--success-6));
+@progress-line-color-icon_error: rgb(var(--danger-6));
+@progress-line-margin-text-left: 16px;
+@progress-line-margin-icon-left: 4px;
+@progress-line-color-text: var(--color-text-2);
+@progress-line-color-icon_normal: var(--color-text-2);
+@progress-line-size-default-icon-size: 12px;
+@progress-line-size-small-icon-size: 12px;
+@progress-line-size-large-icon-size: 14px;
+@progress-circle-size-small-font-size: 13px;
+@progress-circle-size-default-font-size: 14px;
+@progress-circle-size-large-font-size: 16px;
+@progress-circle-size-small-icon-size: 14px;
+@progress-circle-size-default-icon-size: 16px;
+@progress-circle-size-large-icon-size: 16px;
+@progress-circle-color-text: var(--color-text-3);
+@progress-circle-color-mask-stroke: var(--color-fill-3);
+@progress-circle-size-mini-color-mask-stroke: var(--color-primary-light-3);
+@progress-circle-size-mini-color-mask-stroke_success: var(--color-success-light-3);
+@progress-circle-size-mini-color-mask-stroke_error: var(--color-danger-light-3);
+@progress-circle-color-path-stroke: rgb(var(--primary-6));
+@progress-circle-color-path-stroke_success: rgb(var(--success-6));
+@progress-circle-color-path-stroke_error: rgb(var(--danger-6));
+@progress-circle-color-icon_success: rgb(var(--success-6));
+@progress-circle-color-icon_error: rgb(var(--danger-6));
+@progress-steps-size-small-steps-item-width: 2px;
+@progress-steps-margin-steps-item-right: 3px;
+@progress-steps-margin-steps-item-right_small: 3px;
+@progress-steps-color-item-bg: var(--color-fill-3);
+@progress-steps-color-item-bg_normal: rgb(var(--primary-6));
+@progress-steps-color-item-bg_success: rgb(var(--success-6));
+@progress-steps-color-item-bg_error: rgb(var(--danger-6));
+@progress-steps-color-item-bg_warning: rgb(var(--warning-6));
+@progress-steps-margin-text-left: 8px;
+@progress-line-color-inner-bg_warning: rgb(var(--warning-6));
+@progress-line-color-icon_warning: rgb(var(--warning-6));
+@progress-circle-size-mini-color-mask-stroke_warning: var(--color-warning-light-3);
+@progress-circle-color-path-stroke_warning: rgb(var(--warning-6));
+@progress-circle-color-icon_warning: rgb(var(--warning-6));
+@progress-prefix-cls: ~'@{prefix}-progress';
+
+
+/*********** radio ***********/
+
+@radio-color-border: var(--color-neutral-3);
+@radio-border-width: 2px;
+@radio-layout-height: 14px;
+@radio-color-border_hover: var(--color-neutral-3);
+@radio-color-border_disabled: var(--color-neutral-3);
+@radio-color-bg: var(--color-bg-2);
+@radio-color-bg_checked: rgb(var(--primary-6));
+@radio-border-radius: var(--border-radius-circle);
+@radio-color-bg_disabled: var(--color-fill-2);
+@radio-color-bg_checked_disabled: var(--color-primary-light-3);
+@radio-color-dot-bg_checked_disabled: var(--color-fill-2);
+@radio-color-text: var(--color-text-1);
+@radio-color-text_disabled: var(--color-text-4);
+@radio-color-text_checked_disabled: var(--color-text-4);
+@radio-font-text-size: 14px;
+@radio-font-text-size_large: 14px;
+@radio-font-text-size_mini: 12px;
+@radio-font-text-size_small: 14px;
+@radio-margin-text-left: 8px;
+@radio-size-mask-height: 24px;
+@radio-mask-bg-color-bg: var(--color-fill-2);
+@radio-group-margin-right: 20px;
+@radio-group-button-color-bg: var(--color-fill-2);
+@radio-group-button-color-bg_dark: var(--color-bg-3);
+@radio-button-padding-horizontal: 12px;
+@radio-button-spacing: 3px;
+@radio-button-color-bg_active: var(--color-bg-5);
+@radio-button-color-bg_active_dark: var(--color-fill-3);
+@radio-button-color-bg_hover: var(--color-bg-5);
+@radio-button-color-text_active: rgb(var(--primary-6));
+@radio-button-font-text-weight_active: 500;
+@radio-button-color-text_hover: var(--color-text-1);
+@radio-button-border-radius: var(--border-radius-small);
+@radio-button-bg-border-radius: var(--border-radius-small);
+@radio-button-color-bg: transparent;
+@radio-button-color-text: var(--color-text-2);
+@radio-button-color-bg_disabled: transparent;
+@radio-button-color-text_disabled: var(--color-text-4);
+@radio-button-color-bg_checked_disabled: var(--color-bg-5);
+@radio-button-color-text_checked_disabled: var(--color-primary-light-3);
+@radio-button-color-separator-bg: var(--color-neutral-3);
+@radio-button-size-separator-width: 1px;
+@radio-button-size-separator-height: 14px;
+@radio-size-default-height: 32px;
+@radio-size-small-height: 28px;
+@radio-size-mini-height: 24px;
+@radio-size-large-height: 36px;
+@radio-group-size-line-height_vertical: 32px;
+@radio-color-box-shadow: rgb(var(--primary-6));
+@radio-prefix-cls: ~'@{prefix}-radio';
+
+
+/*********** rate ***********/
+
+@rate-min-height: 32px;
+@rate-gap-size: 8px;
+@rate-font-size: 24px;
+@rate-scale_active: 1.2;
+@rate-color-bg_active: rgb(var(--gold-6));
+@rate-color-bg_default: var(--color-fill-3);
+@rate-color-bg_hover: rgb(var(--gold-5));
+@rate-prefix-cls: ~'@{prefix}-rate';
+
+
+/*********** prefixCls ***********/
+
+@prefixCls: ~'@{prefix}-rate';
+
+
+/*********** resizeBox ***********/
+
+@resizeBox-trigger-color-background: var(--color-neutral-3);
+@resizeBox-trigger-size-icon-wrapper: 6px;
+@resizeBox-trigger-font-size-icon: 12px;
+@resizeBox-trigger-color-icon: var(--color-text-1);
+@resizeBox-trigger-icon-empty: 18px;
+
+
+/*********** resizebox ***********/
+
+@resizebox-prefix-cls: ~'@{prefix}-resizebox';
+@resizebox-split-prefix-cls: ~'@{prefix}-resizebox-split';
+@resizebox-split-group-prefix-cls: ~'@{prefix}-resizebox-split-group';
+@resizebox-trigger-prefix-cls: ~'@{prefix}-resizebox-trigger';
+
+
+/*********** result ***********/
+
+@result-padding-top: 24px;
+@result-padding-top_icon: 32px;
+@result-padding-bottom: 24px;
+@result-padding-horizontal: 32px;
+@result-margin-icon-bottom: 16px;
+@result-margin-extra-top: 20px;
+@result-margin-content-top: 20px;
+@result-font-title-size: 14px;
+@result-font-title-weight: 500;
+@result-font-subtitle-size: 14px;
+@result-color-title-text: var(--color-text-1);
+@result-color-subtitle-text: var(--color-text-2);
+@result-size-icon: 20px;
+@result-size-icon-wrapper: 45px;
+@result-size-image-width: 92px;
+@result-size-icon_custom: 45px;
+@result-color-icon_default: inherit;
+@result-color-icon_success: rgb(var(--success-6));
+@result-color-icon-bg_success: var(--color-success-light-1);
+@result-color-icon_error: rgb(var(--danger-6));
+@result-color-icon-bg_error: var(--color-danger-light-1);
+@result-color-icon_warning: rgb(var(--warning-6));
+@result-color-icon-bg_warning: var(--color-warning-light-1);
+@result-color-icon_info: rgb(var(--primary-6));
+@result-color-icon-bg_info: var(--color-primary-light-1);
+@result-prefix-cls: ~'@{prefix}-result';
+
+
+/*********** skeleton ***********/
+
+@skeleton-color-bg-base: var(--color-fill-2);
+@skeleton-radius-image-border: var(--border-radius-small);
+@skeleton-size-image_default: 48px;
+@skeleton-size-image_small: 36px;
+@skeleton-size-image_large: 60px;
+@skeleton-spacing-image_left-margin-right: 16px;
+@skeleton-spacing-image_right-margin-left: 16px;
+@skeleton-size-row-height: 16px;
+@skeleton-spacing-last_row-margin-bottom: 16px;
+@skeleton-color-animate-bg: var(--color-fill-3);
+@skeleton-prefix-cls: ~'@{prefix}-skeleton';
+
+
+/*********** slider ***********/
+
+@slider-size-road-width: 2px;
+@slider-color-road-bg: var(--color-fill-3);
+@slider-color-road-bg_disabled: var(--color-fill-2);
+@slider-color-bar-bg: rgb(var(--primary-6));
+@slider-color-bar-bg_disabled: var(--color-fill-3);
+@slider-color-button-bg: var(--color-bg-2);
+@slider-border-size-button: 2px;
+@slider-color-button-border: rgb(var(--primary-6));
+@slider-color-button-border_disabled: var(--color-fill-3);
+@slider-shadow-button_active: 0 2px 5px rgba(0, 0, 0, 0.1);
+@slider-color-box-shadow-button_focus: var(--color-primary-light-3);
+@slider-size-button-width: 12px;
+@slider-size-button-width_active: 14px;
+@slider-color-dot-bg: var(--color-bg-2);
+@slider-border-size-dot: 2px;
+@slider-font-size-dot: 12px;
+@slider-size-dot-width: 8px;
+@slider-spacing-margin-bottom_with-mark: 24px;
+@slider-spacing-padding_width-mark: 20px;
+@slider-spacing-padding_width-mark_vertical: 20px;
+@slider-font-size-mark: 14px;
+@slider-color-mark-font: var(--color-text-3);
+@slider-size-tick-width: 1px;
+@slider-size-tick-height: 3px;
+@slider-spacing-input-margin-left: 20px;
+@slider-size-input-width: 60px;
+@slider-size-input-height: 32px;
+@slider-size-input_range-width: 20px;
+@slider-size-input_range-height: 32px;
+@slider-size-input_range_content-width: 8px;
+@slider-size-input_range_content-height: 2px;
+@slider-color-input_range_content-bg: rgb(var(--gray-6));
+@slider-size-height_vertical: 200px;
+@slider-spacing-mark-left: 3px;
+@slider-prefix: ~'@{prefix}-slider';
+
+
+/*********** space ***********/
+
+@space-prefix-cls: ~'@{prefix}-space';
+
+
+/*********** spin ***********/
+
+@spin-font-size-text: 14px;
+@spin-font-size-icon: 20px;
+@spin-font-weight: 500;
+@spin-margin-top-tip: 6px;
+@spin-color-text: rgb(var(--primary-6));
+@spin-color-icon: rgb(var(--primary-6));
+@spin-dot-color-icon_default: rgb(var(--primary-6));
+@spin-dot-color-icon_second: rgb(var(--primary-5));
+@spin-dot-color-icon_third: rgb(var(--primary-4));
+@spin-dot-color-icon_forth: rgb(var(--primary-4));
+@spin-dot-color-icon_last: rgb(var(--primary-2));
+@spin-dot-size-width: 8px;
+@spin-prefix-cls: ~'@{prefix}-spin';
+
+
+/*********** statistic ***********/
+
+@statistic-font-title-size: 14px;
+@statistic-margin-title-bottom: 8px;
+@statistic-margin-extra-top: 8px;
+@statistic-font-int-size: 26px;
+@statistic-font-decimal-size: 26px;
+@statistic-font-value-weight: 500;
+@statistic-color-value-text: var(--color-text-1);
+@statistic-color-text: var(--color-text-2);
+@statistic-color-title-text: var(--color-text-2);
+@statistic-color-extra-text: var(--color-text-2);
+@statistic-size-value-icon: 14px;
+@statistic-font-suffix-size: 14px;
+@statistic-margin-prefix-right: 4px;
+@statistic-margin-suffix-left: 4px;
+@statistic-prefix-cls: ~'@{prefix}-statistic';
+
+
+/*********** steps ***********/
+
+@steps-size-default: 28px;
+@steps-size-small: 24px;
+@steps-size-default-arrow: 72px;
+@steps-size-small-arrow: 40px;
+@steps-size-default-font-size-icon: 16px;
+@steps-size-default-font-size-title: 16px;
+@steps-size-default-font-size-description: 12px;
+@steps-size-small-font-size-icon: 14px;
+@steps-size-small-font-size-title: 14px;
+@steps-size-small-font-size-description: 12px;
+@steps-label-vertical-content-width: 140px;
+@steps-direction-horizontal-description-width: 140px;
+@steps-circle-size-item-tail: 1px;
+@steps-circle-size-item-icon-gap: 12px;
+@steps-circle-font-weight-item-title_active: 500;
+@steps-circle-border-radius-item-icon: var(--border-radius-circle);
+@steps-circle-horizontal-item-description-margin-top: 2px;
+@steps-circle-vertical-item-description-margin-top: 2px;
+@steps-circle-vertical-spacing-tail-top: 6px;
+@steps-circle-vertical-spacing-tail-bottom: 6px;
+@steps-circle-color-item-bg_wait: var(--color-fill-2);
+@steps-circle-color-item-border_wait: transparent;
+@steps-circle-color-item-icon-text_wait: var(--color-text-2);
+@steps-circle-color-item-tail_wait: var(--color-neutral-3);
+@steps-circle-color-item-title_wait: var(--color-text-2);
+@steps-circle-color-item-description_wait: var(--color-text-3);
+@steps-circle-color-item-bg_process: rgb(var(--primary-6));
+@steps-circle-color-item-border_process: transparent;
+@steps-circle-color-item-icon-text_process: var(--color-white);
+@steps-circle-color-item-tail_process: rgb(var(--primary-6));
+@steps-circle-color-item-title_process: var(--color-text-1);
+@steps-circle-color-item-description_process: var(--color-text-3);
+@steps-circle-color-item-bg_finish: var(--color-primary-light-1);
+@steps-circle-color-item-border_finish: transparent;
+@steps-circle-color-item-icon-text_finish: rgb(var(--primary-6));
+@steps-circle-color-item-title_finish: var(--color-text-1);
+@steps-circle-color-item-description_finish: var(--color-text-3);
+@steps-circle-color-item-bg_error: rgb(var(--danger-6));
+@steps-circle-color-item-border_error: transparent;
+@steps-circle-color-item-icon-text_error: var(--color-white);
+@steps-circle-color-item-tail_error: rgb(var(--danger-6));
+@steps-circle-color-item-title_error: var(--color-text-1);
+@steps-circle-color-item-description_error: var(--color-text-3);
+@steps-dot-horizontal-item-title-margin-top: 4px;
+@steps-dot-horizontal-item-description-margin-top: 4px;
+@steps-dot-vertical-item-dot-margin-top: 8px;
+@steps-dot-vertical-item-description-margin-top: 4px;
+@steps-dot-vertical-spacing-tail-top: 4px;
+@steps-dot-vertical-spacing-tail-bottom: 4px;
+@steps-dot-size-item-icon: 8px;
+@steps-dot-size-item-icon-active: 10px;
+@steps-dot-size-item-icon-gap: 4px;
+@steps-dot-size-item-tail: 1px;
+@steps-dot-vertical-item-icon-margin-right: 16px;
+@steps-dot-font-weight-item-title_active: 500;
+@steps-dot-border-radius-item-icon: var(--border-radius-circle);
+@steps-dot-color-item-bg_wait: var(--color-fill-4);
+@steps-dot-color-item-border_wait: var(--color-fill-4);
+@steps-dot-color-item-tail_wait: var(--color-neutral-3);
+@steps-dot-color-item-title_wait: var(--color-text-2);
+@steps-dot-color-item-description_wait: var(--color-text-3);
+@steps-dot-color-item-bg_process: rgb(var(--primary-6));
+@steps-dot-color-item-border_process: rgb(var(--primary-6));
+@steps-dot-color-item-tail_process: rgb(var(--primary-6));
+@steps-dot-color-item-title_process: var(--color-text-1);
+@steps-dot-color-item-description_process: var(--color-text-3);
+@steps-dot-color-item-bg_finish: rgb(var(--primary-6));
+@steps-dot-color-item-border_finish: rgb(var(--primary-6));
+@steps-dot-color-item-title_finish: var(--color-text-1);
+@steps-dot-color-item-description_finish: var(--color-text-3);
+@steps-dot-color-item-bg_error: rgb(var(--danger-6));
+@steps-dot-color-item-border_error: rgb(var(--danger-6));
+@steps-dot-color-item-tail_error: rgb(var(--danger-6));
+@steps-dot-color-item-title_error: var(--color-text-1);
+@steps-dot-color-item-description_error: var(--color-text-3);
+@steps-arrow-size-item-gap: 4px;
+@steps-arrow-size-default-title-padding-left: 16px;
+@steps-arrow-size-small-title-padding-left: 20px;
+@steps-arrow-item-description-margin-top: 0;
+@steps-arrow-font-weight-item-title_active: 500;
+@steps-arrow-color-item-bg_wait: var(--color-fill-1);
+@steps-arrow-color-item-title_wait: var(--color-text-2);
+@steps-arrow-color-item-description_wait: var(--color-text-3);
+@steps-arrow-color-item-bg_process: rgb(var(--primary-6));
+@steps-arrow-color-item-title_process: var(--color-white);
+@steps-arrow-color-item-description_process: var(--color-white);
+@steps-arrow-color-item-bg_finish: var(--color-primary-light-1);
+@steps-arrow-color-item-title_finish: var(--color-text-1);
+@steps-arrow-color-item-description_finish: var(--color-text-3);
+@steps-arrow-color-item-bg_error: rgb(var(--danger-6));
+@steps-arrow-color-item-title_error: var(--color-white);
+@steps-arrow-color-item-description_error: var(--color-white);
+@steps-navigation-color-arrow: var(--color-text-4);
+@steps-navigation-size-arrow: 6px;
+@steps-navigation-size-arrow-line-width: 2px;
+@steps-navigation-size-arrow-top: 10px;
+@steps-navigation-padding-left: 20px;
+@steps-navigation-margin-right: 32px;
+@steps-navigation-spacing-arrow-right: 10px;
+@steps-navigation-spacing-ink-left: 0;
+@steps-navigation-spacing-ink-right: 30px;
+@steps-size-default-item-icon-margin-left: 56px;
+@steps-size-small-item-icon-margin-left: 58px;
+@steps-dot-item-icon-margin-left: 66px;
+@steps-dot-vertical-item-dot-margin-top-active: 6px;
+@steps-prefix-cls: ~'@{prefix}-steps';
+
+
+/*********** switch ***********/
+
+@switch-size-default: 24px;
+@switch-size-small: 16px;
+@switch-font-size-text: 12px;
+@switch-size-dot-default: 16px;
+@switch-size-dot-small: 12px;
+@switch-line-size-dot-default: 20px;
+@switch-line-size-dot-small: 16px;
+@switch-circle-default-width: 40px;
+@switch-circle-small-width: 28px;
+@switch-round-default-width: 40px;
+@switch-round-small-width: 28px;
+@switch-line-default-width: 36px;
+@switch-line-small-width: 28px;
+@switch-line-height-bg-line: 6px;
+@switch-line-color-dot-shadow: var(--color-neutral-6);
+@switch-color-bg_on: rgb(var(--primary-6));
+@switch-color-bg_off: var(--color-fill-4);
+@switch-color-bg_on_disabled: var(--color-primary-light-3);
+@switch-color-bg_off_disabled: var(--color-fill-2);
+@switch-color-bg_on_loading: var(--color-primary-light-3);
+@switch-color-bg_off_loading: var(--color-fill-2);
+@switch-color-dot-bg: var(--color-bg-white);
+@switch-color-text_on: var(--color-white);
+@switch-color-text_off: var(--color-white);
+@switch-color-text_on_disabled: var(--color-white);
+@switch-color-text_off_disabled: var(--color-white);
+@switch-color-text_on_loading: var(--color-primary-light-1);
+@switch-color-text_off_loading: var(--color-white);
+@switch-color-dot-icon_on: rgb(var(--primary-6));
+@switch-color-dot-icon_off: var(--color-neutral-3);
+@switch-color-dot-icon_on_disabled: var(--color-primary-light-3);
+@switch-color-dot-icon_off_disabled: var(--color-fill-2);
+@switch-color-dot-icon_on_loading: var(--color-primary-light-3);
+@switch-color-dot-icon_off_loading: var(--color-neutral-3);
+@switch-color-box-shadow_checked: var(--color-primary-light-3);
+@switch-color-box-shadow_default: rgb(var(--gray-6));
+@switch-prefix-cls: ~'@{prefix}-switch';
+@switch-size-default-gap: 4px;
+@switch-size-small-gap: 2px;
+@switch-size-default-line-gap: 2px;
+@switch-size-small-line-gap: 0px;
+
+
+/*********** table ***********/
+
+@table-prefix-cls: ~'@{prefix}-table';
+@table-size-default-padding-horizontal: 16px;
+@table-size-default-padding-vertical: 9px;
+@table-size-middle-padding-horizontal: 16px;
+@table-size-middle-padding-vertical: 7px;
+@table-size-small-padding-horizontal: 16px;
+@table-size-small-padding-vertical: 5px;
+@table-size-mini-padding-horizontal: 16px;
+@table-size-mini-padding-vertical: 2px;
+@table-size-default-font-size: 14px;
+@table-size-middle-font-size: 14px;
+@table-size-small-font-size: 14px;
+@table-size-mini-font-size: 12px;
+@table-size-default-font-header-size: 14px;
+@table-size-middle-font-header-size: 14px;
+@table-size-small-font-header-size: 14px;
+@table-size-mini-font-header-size: 12px;
+@table-border-width: 1px;
+@table-border-style: solid;
+@table-size-expand-button: 14px;
+@table-spacing-expand-button-margin-right: 4px;
+@table-font-size-expand-button: 12px;
+@table-border-radius-expand-button: 2px;
+@table-color-border: var(--color-neutral-3);
+@table-border-radius: var(--border-radius-medium);
+@table-color-text-header-cell: rgb(var(--gray-10));
+@table-color-bg-header-cell: var(--color-neutral-2);
+@table-color-bg-header-sorted-cell: var(--color-neutral-3);
+@table-color-bg-header-sorted-cell_hover: rgba(var(--gray-4), 0.5);
+@table-color-header-filters-icon: var(--color-text-2);
+@table-color-header-filters-icon_active: rgb(var(--primary-6));
+@table-color-bg-header-filters-icon_hover: var(--color-neutral-4);
+@table-font-size-filters-icon: 16px;
+@table-size-filters-width: 24px;
+@table-font-weight-header-text: 500;
+@table-color-text-body-cell: rgb(var(--gray-10));
+@table-color-bg-body-cell: var(--color-bg-2);
+@table-color-bg-body-sorted-cell: var(--color-fill-1);
+@table-color-bg-body-stripe-row: var(--color-fill-1);
+@table-color-bg-body-stripe-row_dark: var(--color-bg-3);
+@table-color-bg-body-row_hover: var(--color-fill-1);
+@table-color-bg-body-row_active: var(--color-fill-1);
+@table-color-expand-icon: var(--color-text-2);
+@table-color-expand-icon-border: transparent;
+@table-color-expand-icon-border_hover: transparent;
+@table-color-expand-icon_hover: var(--color-text-1);
+@table-color-bg-expand-icon: var(--color-neutral-3);
+@table-color-bg-expand-icon_hover: var(--color-neutral-4);
+@table-color-bg-expand-content: var(--color-fill-1);
+@table-color-bg-expand-content_hover: var(--color-fill-1);
+@table-border-expand-icon-width: 1px;
+@table-spacing-header-sorter-icon-margin-left: 8px;
+@table-color-header-sorter-icon: var(--color-neutral-5);
+@table-color-header-sorter-icon_next: var(--color-neutral-6);
+@table-color-header-sorter-icon_active: rgb(var(--primary-6));
+@table-size-header-sorter-icon-height: 8px;
+@table-font-size-header-sorter-icon: 12px;
+@table-position-header-sorter-icon-up-top: -2px;
+@table-position-header-sorter-icon-down-top: -3px;
+@table-color-bg-filters-popup: var(--color-bg-5);
+@table-color-border-filters-popup: var(--color-neutral-3);
+@table-popup-min-width: 100px;
+@table-popup-max-height: 200px;
+@table-popup-border-radius: var(--border-radius-medium);
+@table-shadow-left: inset 6px 0 8px -3px rgba(0, 0, 0, 0.15);
+@table-shadow-right: inset -6px 0 8px -3px rgba(0, 0, 0, 0.15);
+@table-size-shadow-wrapper-width: 10px;
+@table-color-editable-body-cell-border: var(--color-white);
+@table-spacing-pagination-margin: 12px;
+@table-size-selection-col-width: 40px;
+@table-size-expand-icon-col-width: 40px;
+@table-color-body-background: var(--color-bg-2);
+@table-color-bg-tfoot: var(--color-neutral-2);
+@table-cls-tr: ~'@{prefix}-table-tr';
+@table-cls-th: ~'@{prefix}-table-th';
+@table-cls-td: ~'@{prefix}-table-td';
+
+
+/*********** tabs ***********/
+
+@tabs-size-mini-font-size: 12px;
+@tabs-size-small-font-size: 14px;
+@tabs-size-default-font-size: 14px;
+@tabs-size-large-font-size: 14px;
+@tabs-size-mini-font-size_card: 12px;
+@tabs-size-small-font-size_card: 14px;
+@tabs-size-default-font-size_card: 14px;
+@tabs-size-large-font-size_card: 14px;
+@tabs-size-default-font-size_text: 14px;
+@tabs-size-mini-font-size_rounded: 12px;
+@tabs-size-small-font-size_rounded: 14px;
+@tabs-size-default-font-size_rounded: 14px;
+@tabs-size-large-font-size_rounded: 14px;
+@tabs-size-mini-font-size_capsule: 12px;
+@tabs-size-small-font-size_capsule: 14px;
+@tabs-size-default-font-size_capsule: 14px;
+@tabs-size-large-font-size_capsule: 14px;
+@tabs-size-mini-header-height_line: 32px;
+@tabs-size-small-header-height_line: 36px;
+@tabs-size-default-header-height_line: 40px;
+@tabs-size-large-header-height_line: 44px;
+@tabs-size-mini-header-height: 24px;
+@tabs-size-small-header-height: 28px;
+@tabs-size-default-header-height: 32px;
+@tabs-size-large-header-height: 36px;
+@tabs-size-mini-header-height_capsule: 24px;
+@tabs-size-small-header-height_capsule: 28px;
+@tabs-size-default-header-height_capsule: 32px;
+@tabs-size-large-header-height_capsule: 36px;
+@tabs-size-default-header-height_text: 32px;
+@tabs-size-mini-header-height_rounded: 24px;
+@tabs-size-small-header-height_rounded: 28px;
+@tabs-size-default-header-height_rounded: 32px;
+@tabs-size-large-header-height_rounded: 36px;
+@tabs-padding-title-text-vertical: 1px;
+@tabs-padding-title-text-horizontal: 8px;
+@tabs-color-title-text: var(--color-text-2);
+@tabs-color-title-text_active: rgb(var(--primary-6));
+@tabs-color-title-text_hover: var(--color-text-2);
+@tabs-color-title-text_disabled: var(--color-text-4);
+@tabs-color-title-text_disabled_active: var(--color-primary-light-3);
+@tabs-line-size-header-border: 1px;
+@tabs-line-color-header-border: var(--color-neutral-3);
+@tabs-line-size-ink-stroke: 2px;
+@tabs-line-color-ink-bg: rgb(var(--primary-6));
+@tabs-line-color-ink-bg_disabled: var(--color-primary-light-3);
+@tabs-line-font-title-text-weight_active: 500;
+@tabs-line-margin-title-horizontal: 32px;
+@tabs-line-margin-title-horizontal_first: 16px;
+@tabs-line-margin-title-vertical: 12px;
+@tabs-line-padding-title-horizontal_vertical: 20px;
+@tabs-line-color-title-bg: transparent;
+@tabs-line-color-title-bg_active: transparent;
+@tabs-line-color-title-bg_hover: var(--color-fill-2);
+@tabs-line-font-title-text-weight_hover: 400;
+@tabs-line-border-radius: var(--border-radius-small);
+@tabs-card-border-width: 1px;
+@tabs-card-color-title-border: var(--color-neutral-3);
+@tabs-card-padding-title-horizontal: 16px;
+@tabs-card-padding-title-right_editable: 12px;
+@tabs-card-border-radius: var(--border-radius-small);
+@tabs-card-color-title-bg: transparent;
+@tabs-card-color-title-bg_hover: var(--color-fill-3);
+@tabs-card-color-title-bg_disabled: transparent;
+@tabs-card-color-title-bg_active: transparent;
+@tabs-card-border-content-width: 1px;
+@tabs-card-gutter-spacing-horizontal: 4px;
+@tabs-card-gutter-color-title-bg: var(--color-fill-1);
+@tabs-card-gutter-color-title-bg_hover: var(--color-fill-3);
+@tabs-card-gutter-color-title-bg_active: transparent;
+@tabs-card-gutter-color-title-bg_disabled: var(--color-fill-1);
+@tabs-text-size-separator-height: 12px;
+@tabs-text-size-separator-width: 2px;
+@tabs-text-color-separator-bg: var(--color-fill-3);
+@tabs-text-margin-title-horizontal: 8px;
+@tabs-text-color-title-bg: transparent;
+@tabs-text-color-title-bg_active: transparent;
+@tabs-text-color-title-bg_disabled: transparent;
+@tabs-text-color-title-bg_disabled_active: var(--color-primary-light-3);
+@tabs-text-color-title-bg_hover: var(--color-fill-2);
+@tabs-rounded-padding-title-horizontal: 16px;
+@tabs-rounded-margin-title-horizontal: 12px;
+@tabs-rounded-color-title-bg: transparent;
+@tabs-rounded-color-title-bg_active: var(--color-fill-2);
+@tabs-rounded-color-title-bg_disabled: transparent;
+@tabs-rounded-color-title-bg_hover: var(--color-fill-2);
+@tabs-capsule-color-header-bg: var(--color-fill-2);
+@tabs-capsule-margin-title-horizontal: 3px;
+@tabs-capsule-padding-title-horizontal: 12px;
+@tabs-capsule-padding-header-vertical: 3px;
+@tabs-capsule-padding-header-horizontal: 3px;
+@tabs-capsule-border-header-radius: var(--border-radius-small);
+@tabs-capsule-border-title-radius: var(--border-radius-small);
+@tabs-capsule-color-title-bg: transparent;
+@tabs-capsule-color-title-bg_active: var(--color-bg-2);
+@tabs-capsule-color-title-bg_hover: var(--color-bg-2);
+@tabs-capsule-size-separator-width: 1px;
+@tabs-capsule-size-separator-height: 14px;
+@tabs-capsule-color-separator-bg: var(--color-fill-3);
+@tabs-margin-close-icon-left: 8px;
+@tabs-size-icon: 12px;
+@tabs-size-icon-bg: 16px;
+@tabs-card-color-close-icon-bg_hover: var(--color-fill-4);
+@tabs-margin-add-icon-left: 8px;
+@tabs-color-icon: var(--color-text-2);
+@tabs-color-icon_disabled: var(--color-text-4);
+@tabs-spacing-nav-icon-header: 6px;
+@tabs-padding-header-wrapper-horizontal: 10px;
+@tabs-padding-header-wrapper-vertical: 6px;
+@tabs-content-padding: 16px;
+@tabs-box-shadow-radius: 2px;
+@tabs-color-box-shadow: var(--color-primary-light-3);
+@tabs-prefix-cls: ~'@{prefix}-tabs';
+@tabs-prefix-cls-vertical: ~'@{prefix}-tabs-vertical';
+
+
+/*********** sizes ***********/
+
+@sizes: mini, small, large;
+
+
+/*********** tag ***********/
+
+@tag-border-width: 1px;
+@tag-border-type: solid;
+@tag-padding-horizontal: 8px;
+@tag-padding-vertical: 0;
+@tag-icon-margin-right: 4px;
+@tag-text-font-weight: 500;
+@tag-border-radius: var(--border-radius-small);
+@tag-size-small: 20px;
+@tag-size-default: 24px;
+@tag-size-medium: 28px;
+@tag-size-large: 32px;
+@tag-size-small-font-size: 12px;
+@tag-size-default-font-size: 12px;
+@tag-size-medium-font-size: 14px;
+@tag-size-large-font-size: 14px;
+@tag-color-bg-not-checked_hover: var(--color-fill-2);
+@tag-custom-color-text: var(--color-white);
+@tag-custom-color-icon-bg_hover: rgba(255, 255, 255, 0.2);
+@tag-default-color-bg: var(--color-fill-2);
+@tag-default-color-bg_hover: var(--color-fill-3);
+@tag-default-color-icon: var(--color-text-2);
+@tag-default-color-text: var(--color-text-1);
+@tag-default-color-border: transparent;
+@tag-default-bordered-color-border: var(--color-border-2);
+@tag-default-color-border_hover: transparent;
+@tag-red-color-bg: rgb(var(--red-1));
+@tag-red-color-bg_hover: rgb(var(--red-2));
+@tag-red-color-border: transparent;
+@tag-red-bordered-color-border: rgb(var(--red-6));
+@tag-red-color-border_hover: transparent;
+@tag-red-color-icon: rgb(var(--red-6));
+@tag-red-color-icon-bg_hover: rgb(var(--red-2));
+@tag-red-color-text: rgb(var(--red-6));
+@tag-orangered-color-bg: rgb(var(--orangered-1));
+@tag-orangered-color-bg_hover: rgb(var(--orangered-2));
+@tag-orangered-color-border: transparent;
+@tag-orangered-bordered-color-border: rgb(var(--orangered-6));
+@tag-orangered-color-border_hover: transparent;
+@tag-orangered-color-icon: rgb(var(--orangered-6));
+@tag-orangered-color-icon-bg_hover: rgb(var(--orangered-2));
+@tag-orangered-color-text: rgb(var(--orangered-6));
+@tag-orange-color-bg: rgb(var(--orange-1));
+@tag-orange-color-bg_hover: rgb(var(--orange-2));
+@tag-orange-color-border: transparent;
+@tag-orange-bordered-color-border: rgb(var(--orange-6));
+@tag-orange-color-border_hover: transparent;
+@tag-orange-color-icon: rgb(var(--orange-6));
+@tag-orange-color-icon-bg_hover: rgb(var(--orange-2));
+@tag-orange-color-text: rgb(var(--orange-6));
+@tag-gold-color-bg: rgb(var(--gold-1));
+@tag-gold-color-bg_hover: rgb(var(--gold-3));
+@tag-gold-color-border: transparent;
+@tag-gold-bordered-color-border: rgb(var(--gold-6));
+@tag-gold-color-border_hover: transparent;
+@tag-gold-color-icon: rgb(var(--gold-6));
+@tag-gold-color-icon-bg_hover: rgb(var(--gold-2));
+@tag-gold-color-text: rgb(var(--gold-6));
+@tag-lime-color-bg: rgb(var(--lime-1));
+@tag-lime-color-bg_hover: rgb(var(--lime-2));
+@tag-lime-color-border: transparent;
+@tag-lime-bordered-color-border: rgb(var(--lime-6));
+@tag-lime-color-border_hover: transparent;
+@tag-lime-color-icon: rgb(var(--lime-6));
+@tag-lime-color-icon-bg_hover: rgb(var(--lime-2));
+@tag-lime-color-text: rgb(var(--lime-6));
+@tag-green-color-bg: rgb(var(--green-1));
+@tag-green-color-bg_hover: rgb(var(--green-2));
+@tag-green-color-border: transparent;
+@tag-green-bordered-color-border: rgb(var(--green-6));
+@tag-green-color-border_hover: transparent;
+@tag-green-color-icon: rgb(var(--green-6));
+@tag-green-color-icon-bg_hover: rgb(var(--green-2));
+@tag-green-color-text: rgb(var(--green-6));
+@tag-cyan-color-bg: rgb(var(--cyan-1));
+@tag-cyan-color-bg_hover: rgb(var(--cyan-2));
+@tag-cyan-color-border: transparent;
+@tag-cyan-bordered-color-border: rgb(var(--cyan-6));
+@tag-cyan-color-border_hover: transparent;
+@tag-cyan-color-icon: rgb(var(--cyan-6));
+@tag-cyan-color-icon-bg_hover: rgb(var(--cyan-2));
+@tag-cyan-color-text: rgb(var(--cyan-6));
+@tag-blue-color-bg: rgb(var(--blue-1));
+@tag-blue-color-bg_hover: rgb(var(--blue-2));
+@tag-blue-color-border: transparent;
+@tag-blue-bordered-color-border: rgb(var(--blue-6));
+@tag-blue-color-border_hover: transparent;
+@tag-blue-color-icon: rgb(var(--blue-6));
+@tag-blue-color-icon-bg_hover: rgb(var(--blue-2));
+@tag-blue-color-text: rgb(var(--blue-6));
+@tag-arcoblue-color-bg: rgb(var(--arcoblue-1));
+@tag-arcoblue-color-bg_hover: rgb(var(--arcoblue-2));
+@tag-arcoblue-color-border: transparent;
+@tag-arcoblue-bordered-color-border: rgb(var(--arcoblue-6));
+@tag-arcoblue-color-border_hover: transparent;
+@tag-arcoblue-color-icon: rgb(var(--arcoblue-6));
+@tag-arcoblue-color-icon-bg_hover: rgb(var(--arcoblue-2));
+@tag-arcoblue-color-text: rgb(var(--arcoblue-6));
+@tag-purple-color-bg: rgb(var(--purple-1));
+@tag-purple-color-bg_hover: rgb(var(--purple-2));
+@tag-purple-color-border: transparent;
+@tag-purple-bordered-color-border: rgb(var(--purple-6));
+@tag-purple-color-border_hover: transparent;
+@tag-purple-color-icon: rgb(var(--purple-6));
+@tag-purple-color-icon-bg_hover: rgb(var(--purple-2));
+@tag-purple-color-text: rgb(var(--purple-6));
+@tag-pinkpurple-color-bg: rgb(var(--pinkpurple-1));
+@tag-pinkpurple-color-bg_hover: rgb(var(--pinkpurple-2));
+@tag-pinkpurple-color-border: transparent;
+@tag-pinkpurple-bordered-color-border: rgb(var(--pinkpurple-6));
+@tag-pinkpurple-color-border_hover: transparent;
+@tag-pinkpurple-color-icon: rgb(var(--pinkpurple-6));
+@tag-pinkpurple-color-icon-bg_hover: rgb(var(--pinkpurple-2));
+@tag-pinkpurple-color-text: rgb(var(--pinkpurple-6));
+@tag-magenta-color-bg: rgb(var(--magenta-1));
+@tag-magenta-color-bg_hover: rgb(var(--magenta-2));
+@tag-magenta-color-border: transparent;
+@tag-magenta-bordered-color-border: rgb(var(--magenta-6));
+@tag-magenta-color-border_hover: transparent;
+@tag-magenta-color-icon: rgb(var(--magenta-6));
+@tag-magenta-color-icon-bg_hover: rgb(var(--magenta-2));
+@tag-magenta-color-text: rgb(var(--magenta-6));
+@tag-gray-color-bg: rgb(var(--gray-2));
+@tag-gray-color-bg_hover: rgb(var(--gray-3));
+@tag-gray-color-border: transparent;
+@tag-gray-bordered-color-border: rgb(var(--gray-6));
+@tag-gray-color-border_hover: transparent;
+@tag-gray-color-icon: rgb(var(--gray-6));
+@tag-gray-color-icon-bg_hover: rgb(var(--gray-3));
+@tag-gray-color-text: rgb(var(--gray-6));
+@tag-prefix-cls: ~'@{prefix}-tag';
+
+
+/*********** colors ***********/
+
+@colors: red, orangered, orange, gold, lime, green, cyan, blue, arcoblue, purple, pinkpurple, magenta, gray;
+
+
+/*********** timeline ***********/
+
+@timeline-color-content-text: var(--color-text-1);
+@timeline-color-label-text: var(--color-text-3);
+@timeline-color-dot-bg: rgb(var(--primary-6));
+@timeline-color-line-bg: var(--color-neutral-3);
+@timeline-font-content-size: 14px;
+@timeline-font-label-size: 12px;
+@timeline-item-min-height: 78px;
+@timeline-dot-size-width: 6px;
+@timeline-dot-border-radius: var(--border-radius-circle);
+@timeline-dot-border-width_hollow: 2px;
+@timeline-color-dot-bg_hollow: var(--color-bg-2);
+@timeline-horizontal-margin-content-spacing: 16px;
+@timeline-horizontal-margin-line-left: 6px;
+@timeline-horizontal-margin-line-right: 4px;
+@timeline-vertical-margin-content-bottom: 4px;
+@timeline-vertical-margin-content-left: 16px;
+@timeline-vertical-margin-line-top: 4px;
+@timeline-vertical-margin-line-bottom: 4px;
+@timeline-size-line-width: 1px;
+@timeline-prefix-cls: ~'@{prefix}-timeline';
+@timeline-item-prefix-cls: ~'@{prefix}-timeline-item';
+
+
+/*********** tooltip ***********/
+
+@tooltip-padding-horizontal: 12px;
+@tooltip-padding-vertical: 8px;
+@tooltip-mini-padding-horizontal: 12px;
+@tooltip-mini-padding-vertical: 4px;
+@tooltip-mini-font-size: 14px;
+@tooltip-font-size: 14px;
+@tooltip-border-radius: var(--border-radius-small);
+@tooltip-color-text: #fff;
+@tooltip-color-bg: var(--color-tooltip-bg);
+@tooltip-prefix-cls: ~'@{prefix}-tooltip';
+
+
+/*********** transfer ***********/
+
+@transfer-width: 200px;
+@transfer-height: 224px;
+@transfer-height-title: 40px;
+@transfer-height-footer: 40px;
+@transfer-padding-horizontal-footer: 8px;
+@transfer-border-color: var(--color-neutral-3);
+@transfer-border-width: 1px;
+@transfer-border-radius: var(--border-radius-small);
+@transfer-font-size-header: 14px;
+@transfer-font-size-header-unit: 12px;
+@transfer-font-size-icon: 12px;
+@transfer-font-weight-header: 500;
+@transfer-color-text-header: var(--color-text-1);
+@transfer-color-text-header-unit: var(--color-text-3);
+@transfer-color-icon: var(--color-text-2);
+@transfer-color-bg-icon: var(--color-fill-3);
+@transfer-color-bg-header: var(--color-fill-1);
+@transfer-search-padding-left: 12px;
+@transfer-search-padding-right: 12px;
+@transfer-search-padding-top: 8px;
+@transfer-search-padding-bottom: 4px;
+@transfer-item-color-bg_default: transparent;
+@transfer-item-color-bg_hover: var(--color-fill-2);
+@transfer-item-color-bg_disabled: transparent;
+@transfer-item-color_default: var(--color-text-1);
+@transfer-item-color_hover: var(--color-text-1);
+@transfer-item-color_disabled: var(--color-text-4);
+@transfer-item-height: 36px;
+@transfer-item-padding-horizontal: 10px;
+@transfer-item-font-size: 14px;
+@transfer-item-draggable-height-gap: 2px;
+@transfer-item-draggable-color-bg-gap: rgb(var(--primary-6));
+@transfer-item-draggable-color-bg_dragging: var(--color-fill-1);
+@transfer-item-draggable-color_dragging: var(--color-text-4);
+@transfer-item-draggable-color_blink: var(--color-primary-light-1);
+@transfer-pagination-width-input: 24px;
+@transfer-pagination-gap-separator: 8px;
+@transfer-operation-padding-horizontal: 20px;
+@transfer-operation-gap-buttons: 12px;
+@transfer-color-icon-box-shadow: rgb(var(--primary-6));
+@transfer-prefix-cls: ~'@{prefix}-transfer';
+
+
+/*********** tree ***********/
+
+@tree-color-title-text: var(--color-text-1);
+@tree-color-title-text_hover: var(--color-text-1);
+@tree-color-title-text_active: rgb(var(--primary-6));
+@tree-color-title-text_disabled: var(--color-text-4);
+@tree-color-title-text_active_disabled: var(--color-primary-light-3);
+@tree-color-title-bg_hover: var(--color-fill-2);
+@tree-color-title-bg_highlight: var(--color-primary-light-1);
+@tree-color-title-text_highlight: var(--color-text-1);
+@tree-color-title-bg_dragging: var(--color-fill-1);
+@tree-color-title-text_dragging: var(--color-text-4);
+@tree-color-loading-icon: rgb(var(--primary-6));
+@tree-color-switcher-icon: var(--color-text-2);
+@tree-color-drag-icon: rgb(var(--primary-6));
+@tree-node-border-radius: var(--border-radius-small);
+@tree-margin-checkbox-right: 10px;
+@tree-margin-switcher-icon-right: 10px;
+@tree-margin-custom-icon-right: 10px;
+@tree-padding-title-horizontal: 4px;
+@tree-spacing-drag-icon-right: 12px;
+@tree-spacing-drag-icon-text: 120px;
+@tree-draggable-color-gap-bg: rgb(var(--primary-6));
+@tree-draggable-size-gap-height: 2px;
+@tree-showline-color-line-bg: var(--color-neutral-3);
+@tree-showline-color-plus-icon-bg: var(--color-fill-2);
+@tree-showline-color-plus-icon-border: transparent;
+@tree-showline-plus-icon-border-width: 1px;
+@tree-showline-size-plus-icon-stroke: 2px;
+@tree-showline-size-plus-icon-width: 6px;
+@tree-showline-size-line-width: 1px;
+@tree-showline-style-line: solid;
+@tree-showline-size-switcher-icon: 14px;
+@tree-showline-spacing-line-vertical: 4px;
+@tree-showline-border-plus-icon-radius: var(--border-radius-small);
+@tree-size-mini-icon-size: 12px;
+@tree-size-small-icon-size: 12px;
+@tree-size-default-icon-size: 12px;
+@tree-size-large-icon-size: 12px;
+@tree-size-expand-icon-bg_hover: 16px;
+@tree-size-mini-font-size: 12px;
+@tree-size-small-font-size: 14px;
+@tree-size-default-font-size: 14px;
+@tree-size-large-font-size: 14px;
+@tree-size-mini-line-height: 24px;
+@tree-size-small-line-height: 28px;
+@tree-size-default-line-height: 32px;
+@tree-size-large-line-height: 36px;
+@tree-prefix-cls: ~'@{prefix}-tree';
+@tree-node-prefix-cls: ~'@{prefix}-tree-node';
+@tree-select-padding-popup-left: 10px;
+@tree-select-padding-popup-right: 4px;
+@tree-select-padding-popup-vertical: 4px;
+@tree-select-prefix-cls: ~'@{prefix}-tree-select';
+@tree-select-prefix-cls-rtl: ~'@{prefix}-tree-select-rtl';
+
+
+/*********** trigger ***********/
+
+@trigger-color-arrow-bg: var(--color-bg-5);
+@trigger-size-arrow-width: 8px;
+@trigger-border-arrow-radius: 2px;
+@trigger-prefix-cls: ~'@{prefix}-trigger';
+
+
+/*********** typography ***********/
+
+@typography-font-size-h1: 36px;
+@typography-font-size-h2: 32px;
+@typography-font-size-h3: 28px;
+@typography-font-size-h4: 24px;
+@typography-font-size-h5: 20px;
+@typography-font-size-h6: 16px;
+@typography-heading-margin-top: 1em;
+@typography-heading-margin-bottom: 0.5em;
+@typography-heading-font-weight: 500;
+@typography-color-text: var(--color-text-1);
+@typography-text-color-text-primary: rgb(var(--primary-6));
+@typography-text-color-text-secondary: var(--color-text-2);
+@typography-text-color-text-success: rgb(var(--success-6));
+@typography-text-color-text-warning: rgb(var(--warning-6));
+@typography-text-color-text-error: rgb(var(--danger-6));
+@typography-text-color-text_disabled: var(--color-text-4);
+@typography-text-color-bg-mark: rgb(var(--yellow-4));
+@typography-text-font-weight-bold: 500;
+@typography-text-color-code: var(--color-text-2);
+@typography-text-color-code-border: var(--color-neutral-3);
+@typography-text-color-code-bg: var(--color-neutral-2);
+@typography-text-padding-code-vertical: 2px;
+@typography-text-padding-code-horizontal: 8px;
+@typography-text-margin-code-horizontal: 2px;
+@typography-paragraph-line-height: 1.5715;
+@typography-paragraph-line-height-close: 1.3;
+@typography-operation-margin-left: 2px;
+@typography-color-icon-copy: var(--color-text-2);
+@typography-color-bg-icon-copy: transparent;
+@typography-color-icon-copy_hover: var(--color-text-2);
+@typography-color-bg-icon-copy_hover: var(--color-fill-2);
+@typography-color-icon-copy_copied: rgb(var(--success-6));
+@typography-color-icon-edit: var(--color-text-2);
+@typography-color-bg-icon-edit: transparent;
+@typography-color-icon-edit_hover: var(--color-text-2);
+@typography-color-bg-icon-edit_hover: var(--color-fill-2);
+@typography-color-expand-text: rgb(var(--primary-6));
+@typography-color-expand-text_hover: rgb(var(--primary-5));
+@typography-color-blockquote-border-width: 2px;
+@typography-color-blockquote-border-left: var(--color-neutral-6);
+@typography-color-blockquote-bg: var(--color-bg-2);
+@typography-color-box-shadow: var(--color-primary-light-3);
+@typography-prefix-cls: ~'@{prefix}-typography';
+
+
+/*********** ellipsis ***********/
+
+@ellipsis-action-text-color: rgb(var(--primary-6));
+@ellipsis-action-text-color_hover: rgb(var(--primary-5));
+@ellipsis-cls: arco-ellipsis;
+
+
+/*********** upload ***********/
+
+@upload-tip-color-text: var(--color-text-3);
+@upload-tip-margin-top: 4px;
+@upload-tip-font-size: 12px;
+@upload-list-margin-top: 24px;
+@upload-picture-item-width: 80px;
+@upload-picture-color-bg: var(--color-fill-2);
+@upload-picture-border-radius: var(--border-radius-small);
+@upload-picture-border-width: 1px;
+@upload-picture-border-style: dashed;
+@upload-picture-color-border: var(--color-neutral-3);
+@upload-picture-color-border_disabled: var(--color-neutral-4);
+@upload-picture-color-border_hover: var(--color-neutral-4);
+@upload-picture-color-bg_hover: var(--color-fill-3);
+@upload-picture-color-bg_disabled: var(--color-fill-1);
+@upload-picture-color-text: var(--color-text-2);
+@upload-picture-color-text_hover: var(--color-text-2);
+@upload-picture-color-text_disabled: var(--color-text-4);
+@upload-drag-font-size: 14px;
+@upload-drag-border-radius: var(--border-radius-small);
+@upload-drag-tip-margin-top: 0;
+@upload-drag-color-text: var(--color-text-1);
+@upload-drag-border-style: dashed;
+@upload-drag-border-width: 1px;
+@upload-drag-padding-vertical: 50px;
+@upload-drag-margin-icon-bottom: 24px;
+@upload-drag-color-bg: var(--color-fill-1);
+@upload-drag-color-bg_hover: var(--color-fill-3);
+@upload-drag-color-bg_active: var(--color-primary-light-1);
+@upload-drag-color-bg_disabled: var(--color-fill-1);
+@upload-drag-color-border: var(--color-neutral-3);
+@upload-drag-color-border_active: rgb(var(--primary-6));
+@upload-drag-color-border_hover: var(--color-neutral-4);
+@upload-drag-color-border_disabled: var(--color-text-4);
+@upload-drag-color-icon: var(--color-text-2);
+@upload-drag-color-icon_hover: var(--color-text-2);
+@upload-drag-color-icon_active: rgb(var(--primary-6));
+@upload-drag-color-text_hover: var(--color-text-1);
+@upload-drag-color-text_active: var(--color-text-1);
+@upload-drag-color-text_disabled: var(--color-text-4);
+@upload-text-item-size-operation-icon: 12px;
+@upload-text-item-margin-top: 12px;
+@upload-text-item-font-size: 14px;
+@upload-text-item-color-text: var(--color-text-1);
+@upload-text-item-padding-left: 12px;
+@upload-text-item-color-bg: var(--color-fill-1);
+@upload-text-item-padding-vertical: 8px;
+@upload-text-item-margin-remove-icon-left: 12px;
+@upload-text-item-color-remove-icon: var(--color-text-2);
+@upload-text-item-color-status-icon: var(--color-white);
+@upload-text-item-color-file-icon_success: rgb(var(--primary-6));
+@upload-text-item-color-progress-bg_hover: rgba(var(--gray-10), 0.2);
+@upload-text-item-color-progress-bg_hover_active: rgb(var(--primary-7));
+@upload-text-item-size-file-icon: 16px;
+@upload-text-item-margin-file-icon-right: 12px;
+@upload-text-item-color-file-icon: rgb(var(--primary-6));
+@upload-text-item-padding-right: 10px;
+@upload-text-item-color-link: rgb(var(--link-6));
+@upload-text-item-color-reupload-icon: rgb(var(--primary-6));
+@upload-text-item-color-reupload-icon_hover: rgb(var(--primary-7));
+@upload-text-item-size-status-icon: 12px;
+@upload-text-item-color-error-icon: rgb(var(--danger-6));
+@upload-text-item-color-success-icon: rgb(var(--success-6));
+@upload-text-item-border-radius: var(--border-radius-small);
+@upload-text-item-margin-error-icon-left: 4px;
+@upload-text-item-margin-status-left: 10px;
+@upload-text-item-thumbnail-width: 40px;
+@upload-text-item-margin-thumbnail-right: 12px;
+@upload-text-item-color-text_error: rgb(var(--danger-6));
+@upload-text-item-color-text_success: unset;
+@upload-text-item-color-text_uploading: unset;
+@upload-picture-item-margin-preview-icon-right: 20px;
+@upload-picture-item-size-width: 80px;
+@upload-picture-item-border-radius: var(--border-radius-small);
+@upload-picture-item-margin-right: 8px;
+@upload-picture-item-margin-bottom: 8px;
+@upload-picture-item-color-bg: var(--color-fill-2);
+@upload-picture-item-color-operation_bg: rgba(0, 0, 0, 0.5);
+@upload-picture-item-color-operation-icon: var(--color-white);
+@upload-picture-item-color-error-icon: var(--color-white);
+@upload-picture-text-item-color-bg_error: var(--color-danger-light-1);
+@upload-picture-text-item-color-text_error: rgb(var(--danger-6));
+@upload-picture-text-item-color-text_success: unset;
+@upload-picture-text-item-color-text_uploading: unset;
+@upload-picture-text-item-padding-vertical: 8px;
+@upload-drag-size-icon: 14px;
+@upload-picture-item-size-mask-icon: 16px;
+@upload-picture-item-size-error-icon: 26px;
+@upload-text-item-size-reupload-icon: 14px;
+@upload-text-item-size-success-icon: 14px;
+@upload-text-item-size-error-icon: 14px;
+@upload-picture-item-size-operation-icon: 14px;
+@upload-color-icon-box-shadow_default: var(--color-primary-light-3);
+@upload-color-icon-box-shadow_active: rgb(var(--primary-6));
+@upload-prefix-cls: ~'@{prefix}-upload';
+
+
+/*********** verification ***********/
+
+@verification-code-prefix-cls: ~'@{prefix}-verification-code';
diff --git a/packages/plugin-vite-watcher/CHANGELOG.md b/packages/plugin-vite-watcher/CHANGELOG.md
new file mode 100644
index 0000000..2a553d4
--- /dev/null
+++ b/packages/plugin-vite-watcher/CHANGELOG.md
@@ -0,0 +1,7 @@
+## 1.0.5
+
+`2022-01-21`
+
+### 💎 Optimization
+
+- Improve icons' loading speed
diff --git a/packages/plugin-vite-watcher/CHANGLOG.zh-CN.md b/packages/plugin-vite-watcher/CHANGLOG.zh-CN.md
new file mode 100644
index 0000000..ba91d30
--- /dev/null
+++ b/packages/plugin-vite-watcher/CHANGLOG.zh-CN.md
@@ -0,0 +1,7 @@
+## 1.0.5
+
+`2022-01-21`
+
+### 💎 功能优化
+
+- 提升图标的加载速度
diff --git a/packages/plugin-vite-watcher/README.md b/packages/plugin-vite-watcher/README.md
new file mode 100644
index 0000000..717f385
--- /dev/null
+++ b/packages/plugin-vite-watcher/README.md
@@ -0,0 +1,70 @@
+# @arco-plugins/vite-react
+
+## Feature
+
+1. `Style lazy load`
+2. `Theme import`
+3. `Icon replacement`
+
+> `Style lazy load` doesn't work during development for better experience.
+
+## Install
+
+```bash
+npm i @arco-plugins/vite-react -D
+```
+
+## Usage
+
+```js
+// vite.config.js
+
+import { vitePluginForArco } from '@arco-plugins/vite-react'
+
+export default {
+ ...
+ plugins: [
+ vitePluginForArco(options),
+ ],
+}
+```
+
+```tsx
+// react
+import { Button } from '@arco-design/web-react';
+
+export default () => (
+
+
+
+
+);
+```
+
+## Options
+
+The plugin supports the following parameters:
+
+| Params | Type | Default Value | Description |
+| :--------------: | :----------------: | :-----------: | :------------------------ |
+| **`theme`** | `{String}` | `''` | Theme package name |
+| **`iconBox`** | `{String}` | `''` | Icon library package name |
+| **`modifyVars`** | `{Object}` | `{}` | Less variables |
+| **`style`** | `{'css'\|Boolean}` | `true` | Style import method |
+|**`varsInjectScope`**|`{(string\|RegExp)[]}`|`[]`| Scope of injection of less variables (modifyVars and the theme package's variables) |
+
+**Style import methods **
+
+`style: true` will import less file
+
+```js
+import '@arco-design/web-react/Affix/style';
+```
+
+`style: 'css'` will import css file
+
+```js
+import '@arco-design/web-react/Affix/style/css';
+```
+
+`style: false` will not import any style file
diff --git a/packages/plugin-vite-watcher/README.zh-CN.md b/packages/plugin-vite-watcher/README.zh-CN.md
new file mode 100644
index 0000000..0e858c9
--- /dev/null
+++ b/packages/plugin-vite-watcher/README.zh-CN.md
@@ -0,0 +1,70 @@
+# @arco-plugins/vite-react
+
+## 特性
+
+1. `样式按需加载`
+2. `主题引入`
+3. `图标替换`
+
+> 为了开发体验,开发环境下样式为全量引入
+
+## 安装
+
+```bash
+npm i @arco-plugins/vite-react -D
+```
+
+## 用法
+
+```js
+// vite.config.js
+
+import { vitePluginForArco } from '@arco-plugins/vite-react'
+
+export default {
+ ...
+ plugins: [
+ vitePluginForArco(options),
+ ],
+}
+```
+
+```tsx
+// react
+import { Button } from '@arco-design/web-react';
+
+export default () => (
+
+
+
+
+);
+```
+
+## 参数
+
+插件支持以下参数:
+
+| 参数名 | 类型 | 默认值 | 描述 |
+| :--------------: | :----------------: | :----: | :----------- |
+| **`theme`** | `{String}` | `''` | 主题包名 |
+| **`iconBox`** | `{String}` | `''` | 图标库包名 |
+| **`modifyVars`** | `{Object}` | `{}` | Less 变量 |
+| **`style`** | `{'css'\|Boolean}` | `true` | 样式引入方式 |
+|**`varsInjectScope`**|`string[]`|`[]`| less 变量(modifyVars 和主题包的变量)注入的范围 |
+
+**样式引入方式 **
+
+`style: true` 将引入 less 文件
+
+```js
+import '@arco-design/web-react/Affix/style';
+```
+
+`style: 'css'` 将引入 css 文件
+
+```js
+import '@arco-design/web-react/Affix/style/css';
+```
+
+`style: false` 不引入样式
diff --git a/packages/plugin-vite-watcher/package.json b/packages/plugin-vite-watcher/package.json
new file mode 100644
index 0000000..eb6363c
--- /dev/null
+++ b/packages/plugin-vite-watcher/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "@refly/plugin-vite-watcher",
+ "version": "1.3.3",
+ "description": "For Vite build, load Arco Design styles on demand",
+ "main": "lib/index.js",
+ "types": "types/index.d.ts",
+ "files": [
+ "lib",
+ "types"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git@github.com:arco-design/arco-plugins.git",
+ "directory": "packages/plugin-vite-react"
+ },
+ "keywords": [
+ "arco",
+ "arco-design",
+ "arco-plugin",
+ "plugin",
+ "vite"
+ ],
+ "scripts": {
+ "prebuild": "rm -fr lib types",
+ "build": "npx tsc"
+ },
+ "author": "arco-design",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/generator": "^7.12.11",
+ "@babel/helper-module-imports": "^7.12.5",
+ "@babel/parser": "^7.12.11",
+ "@babel/traverse": "^7.12.12",
+ "@babel/types": "^7.12.12",
+ "rollup": "~4.18.0"
+ },
+ "devDependencies": {
+ "@types/node": "^16.11.10",
+ "vite": "^2.6.14",
+ "esbuild": "^0.13.2",
+ "typescript": "^4.5.2"
+ }
+}
diff --git a/packages/plugin-vite-watcher/src/index.ts b/packages/plugin-vite-watcher/src/index.ts
new file mode 100644
index 0000000..58184db
--- /dev/null
+++ b/packages/plugin-vite-watcher/src/index.ts
@@ -0,0 +1,5 @@
+import pluginViteWatcher from './plugin-vite-watcher';
+
+export default pluginViteWatcher;
+
+export { pluginViteWatcher };
diff --git a/packages/plugin-vite-watcher/src/plugin-vite-watcher/index.ts b/packages/plugin-vite-watcher/src/plugin-vite-watcher/index.ts
new file mode 100644
index 0000000..29b5f1f
--- /dev/null
+++ b/packages/plugin-vite-watcher/src/plugin-vite-watcher/index.ts
@@ -0,0 +1,17 @@
+import type { Plugin } from 'vite';
+
+const pkg = require('../../package.json');
+
+interface PluginOption {
+ filesPath?: string[]; // File to transform
+}
+
+export default function vitePluginWatcher(options: PluginOption): Plugin {
+ return {
+ name: pkg.name,
+
+ buildStart() {
+ options.filesPath.forEach((filePath) => this.addWatchFile(filePath));
+ },
+ };
+}
diff --git a/packages/plugin-vite-watcher/src/plugin-vite-watcher/typings.d.ts b/packages/plugin-vite-watcher/src/plugin-vite-watcher/typings.d.ts
new file mode 100644
index 0000000..910ab18
--- /dev/null
+++ b/packages/plugin-vite-watcher/src/plugin-vite-watcher/typings.d.ts
@@ -0,0 +1,2 @@
+declare module '*.json';
+declare module '@babel/helper-module-imports';
diff --git a/packages/plugin-vite-watcher/src/plugin-vite-watcher/utils.ts b/packages/plugin-vite-watcher/src/plugin-vite-watcher/utils.ts
new file mode 100644
index 0000000..ebc93e7
--- /dev/null
+++ b/packages/plugin-vite-watcher/src/plugin-vite-watcher/utils.ts
@@ -0,0 +1,89 @@
+import { readFileSync, readdirSync } from 'fs';
+import { dirname, extname, resolve, sep, win32, posix } from 'path';
+
+// read file content
+export function readFileStrSync(path: string): false | string {
+ try {
+ const resolvedPath = require.resolve(path);
+ return readFileSync(resolvedPath).toString();
+ } catch (error) {
+ return false;
+ }
+}
+
+// check if a module existed
+const modExistObj: Record = {};
+export function isModExist(path: string) {
+ if (modExistObj[path] === undefined) {
+ try {
+ require.resolve(path);
+ modExistObj[path] = true;
+ } catch (error) {
+ modExistObj[path] = false;
+ }
+ }
+ return modExistObj[path];
+}
+
+// the theme package's component list
+const componentsListObj: Record = {};
+export function getThemeComponentList(theme: string) {
+ if (!theme) return [];
+ if (!componentsListObj[theme]) {
+ try {
+ const packageRootDir = dirname(require.resolve(`${theme}/package.json`));
+ const dirPath = `${packageRootDir}/components`;
+ componentsListObj[theme] = readdirSync(dirPath) || [];
+ } catch (error) {
+ componentsListObj[theme] = [];
+ }
+ }
+ return componentsListObj[theme];
+}
+
+export const parse2PosixPath = (path: string) =>
+ sep === win32.sep ? path.replaceAll(win32.sep, posix.sep) : path;
+
+// filePath match
+export function pathMatch(path: string, conf: [string | RegExp, number?]): false | string {
+ const [regStr, order = 0] = conf;
+ const reg = new RegExp(regStr);
+ const posixPath = parse2PosixPath(path);
+ const matches = posixPath.match(reg);
+ if (!matches) return false;
+ return matches[order];
+}
+
+export function parseInclude2RegExp(include: (string | RegExp)[] = [], context?: string) {
+ if (include.length === 0) return false;
+ context = context || process.cwd();
+ const regStrList = [];
+ const folders = include
+ .map((el) => {
+ if (el instanceof RegExp) {
+ const regStr = el.toString();
+ if (regStr.slice(-1) === '/') {
+ regStrList.push(`(${regStr.slice(1, -1)})`);
+ }
+ return false;
+ }
+ const absolutePath = parse2PosixPath(resolve(context, el));
+ const idx = absolutePath.indexOf('/node_modules/');
+ const len = '/node_modules/'.length;
+ const isFolder = extname(absolutePath) === '';
+ if (idx > -1) {
+ const prexPath = absolutePath.slice(0, idx + len);
+ const packagePath = absolutePath.slice(idx + len);
+ return `(${prexPath}(\\.pnpm/.+/)?${packagePath}${isFolder ? '/' : ''})`;
+ }
+ return `(${absolutePath}${isFolder ? '/' : ''})`;
+ })
+ .filter((el) => el !== false);
+ if (folders.length) {
+ regStrList.push(`(^${folders.join('|')})`);
+ }
+ if (regStrList.length > 0) {
+ return new RegExp(regStrList.join('|'));
+ }
+ return false;
+}
diff --git a/packages/plugin-vite-watcher/tsconfig.json b/packages/plugin-vite-watcher/tsconfig.json
new file mode 100644
index 0000000..9cadd07
--- /dev/null
+++ b/packages/plugin-vite-watcher/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./lib",
+ "declarationDir": "./types",
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "experimentalDecorators": true,
+ "allowSyntheticDefaultImports": true,
+ "noUnusedParameters": true,
+ "noUnusedLocals": true,
+ "module": "commonjs",
+ "target": "esnext",
+ "declaration": true,
+ "lib": ["esnext", "dom"],
+ "types": ["node"],
+ "resolveJsonModule": true,
+ "sourceMap": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/wxt/CHANGELOG.md b/packages/wxt/CHANGELOG.md
new file mode 100644
index 0000000..0bc99bd
--- /dev/null
+++ b/packages/wxt/CHANGELOG.md
@@ -0,0 +1,2214 @@
+# Changelog
+
+## v0.18.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.18.3...v0.18.4)
+
+### 🩹 Fixes
+
+- Allow zipping hidden files in sources by listing them explicitly in `includeSources` ([#674](https://github.com/wxt-dev/wxt/pull/674))
+- Properly invalidate content script context ([#683](https://github.com/wxt-dev/wxt/pull/683))
+
+### 📖 Documentation
+
+- Update contributing guidelines ([8125d74](https://github.com/wxt-dev/wxt/commit/8125d74))
+
+### ❤️ Contributors
+
+- Hujiulong ([@hujiulong](http://github.com/hujiulong))
+
+## v0.18.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.18.2...v0.18.3)
+
+### 🩹 Fixes
+
+- Automatically add dev server to sandbox CSP ([#663](https://github.com/wxt-dev/wxt/pull/663))
+- Remove `import * as` imports from entrypoints during build ([#671](https://github.com/wxt-dev/wxt/pull/671))
+- **security:** Upgrade tar to 6.2.1 ([215def7](https://github.com/wxt-dev/wxt/commit/215def7))
+
+### 📖 Documentation
+
+- Add YTBlock to homepage ([#666](https://github.com/wxt-dev/wxt/pull/666))
+
+### 🏡 Chore
+
+- Add missing tests for dev mode CSP ([#662](https://github.com/wxt-dev/wxt/pull/662))
+- Upgrade templates to v0.18 ([3b954cc](https://github.com/wxt-dev/wxt/commit/3b954cc))
+
+### 🤖 CI
+
+- Fix sync-releases workflow trigger ([5d8efef](https://github.com/wxt-dev/wxt/commit/5d8efef))
+
+### ❤️ Contributors
+
+- Edoan ([@EdoanR](http://github.com/EdoanR))
+
+## v0.18.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.18.1...v0.18.2)
+
+### 🚀 Enhancements
+
+- **runner:** Add `keepProfileChanges` option ([#655](https://github.com/wxt-dev/wxt/pull/655))
+
+### 🩹 Fixes
+
+- Automatically detect and add "sidePanel" permission ([5fcaf7c](https://github.com/wxt-dev/wxt/commit/5fcaf7c))
+
+### 📖 Documentation
+
+- Fix `wxt-vitest-plugin` reference ([#650](https://github.com/wxt-dev/wxt/pull/650))
+- Fix spelling mistake in remote-code.md ([#652](https://github.com/wxt-dev/wxt/pull/652))
+- Correct event handler name in handling-updates.md ([#653](https://github.com/wxt-dev/wxt/pull/653))
+- Fix iframe typos ([c74e530](https://github.com/wxt-dev/wxt/commit/c74e530))
+
+### ❤️ Contributors
+
+- Edoan ([@EdoanR](http://github.com/EdoanR))
+- Linus Norton ([@linusnorton](http://github.com/linusnorton))
+- Jeffrey Zang ([@jeffrey-zang](http://github.com/jeffrey-zang))
+- Emmanuel Ferdman ([@emmanuel-ferdman](https://github.com/emmanuel-ferdman))
+
+## v0.18.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.18.0...v0.18.1)
+
+### 🩹 Fixes
+
+- `_background` is not defined ([#649](https://github.com/wxt-dev/wxt/pull/649))
+
+### 🏡 Chore
+
+- Add root README back ([ec3dd52](https://github.com/wxt-dev/wxt/commit/ec3dd52))
+
+### 🤖 CI
+
+- Fix sync releases workflow ([dc5b55b](https://github.com/wxt-dev/wxt/commit/dc5b55b))
+
+## v0.18.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.12...v0.18.0)
+
+### 🚀 Enhancements
+
+- Add zip compression settings ([#605](https://github.com/wxt-dev/wxt/pull/605))
+- Support returning values from scripts executed with the scripting API ([#624](https://github.com/wxt-dev/wxt/pull/624))
+- **experimental:** Load entrypoint options with Vite Runtime API ([#648](https://github.com/wxt-dev/wxt/pull/648))
+
+### 🩹 Fixes
+
+- ⚠️ Automatically move `host_permissions` to `permissions` for MV2 ([#626](https://github.com/wxt-dev/wxt/pull/626))
+- **dep:** Upgrade `@webext-core/isolated-element` to v1.1.2 ([#625](https://github.com/wxt-dev/wxt/pull/625))
+
+### 📖 Documentation
+
+- Add Fluent Read to homepage ([#600](https://github.com/wxt-dev/wxt/pull/600))
+- Fix typo on example for wxt.config.ts. ([#609](https://github.com/wxt-dev/wxt/pull/609))
+- Tix typo in `entrypoints.md` ([#614](https://github.com/wxt-dev/wxt/pull/614))
+- Add Facebook Video Controls to homepage ([#615](https://github.com/wxt-dev/wxt/pull/615))
+- Fix typo in assets page ([a94d673](https://github.com/wxt-dev/wxt/commit/a94d673))
+- Add ElemSnap to homepage ([#621](https://github.com/wxt-dev/wxt/pull/621))
+- Update content script registration JSDoc ([e47519f](https://github.com/wxt-dev/wxt/commit/e47519f))
+- Add docs about handling updates ([acb7554](https://github.com/wxt-dev/wxt/commit/acb7554))
+- Add MS Edge TTS to homepage ([#647](https://github.com/wxt-dev/wxt/pull/647))
+- Document required permission for storage API ([#632](https://github.com/wxt-dev/wxt/pull/632))
+
+### 🏡 Chore
+
+- Update vue template config ([#607](https://github.com/wxt-dev/wxt/pull/607))
+- **deps-dev:** Bump lint-staged from 15.2.1 to 15.2.2 ([#637](https://github.com/wxt-dev/wxt/pull/637))
+- **deps-dev:** Bump publint from 0.2.6 to 0.2.7 ([#639](https://github.com/wxt-dev/wxt/pull/639))
+- **deps-dev:** Bump simple-git-hooks from 2.9.0 to 2.11.1 ([#640](https://github.com/wxt-dev/wxt/pull/640))
+- Refactor repo to a standard monorepo ([#646](https://github.com/wxt-dev/wxt/pull/646))
+- Fix formatting after monorepo refactor ([6ca3767](https://github.com/wxt-dev/wxt/commit/6ca3767))
+
+#### ⚠️ Breaking Changes
+
+- ⚠️ Automatically move `host_permissions` to `permissions` for MV2 ([#626](https://github.com/wxt-dev/wxt/pull/626))
+
+ Out of an abundance of caution, I've marked this as a breaking change because permission generation has changed. **_If you list `host_permissions` in your `wxt.config.ts`'s manifest and have released your extension_**, double check that your `permissions` and `host_permissions` have not changed for all browsers you target in your `.output/*/manifest.json` files. Permission changes can cause the extension to be disabled on update, and can cause a drop in users, so be sure to double check for differences compared to the previous manifest version.
+
+### ❤️ Contributors
+
+- Alegal200 ([@alegal200](https://github.com/alegal200))
+- Yacine-bens ([@yacine-bens](http://github.com/yacine-bens))
+- Ayden ([@AydenGen](https://github.com/AydenGen))
+- Wuzequanyouzi ([@wuzequanyouzi](http://github.com/wuzequanyouzi))
+- Can Rau ([@CanRau](http://github.com/CanRau))
+- 日高 凌 ([@ryohidaka](http://github.com/ryohidaka))
+- Bas Van Zanten ([@Bas950](http://github.com/Bas950))
+- ThinkStu ([@Bistutu](http://github.com/Bistutu))
+
+## v0.17.12
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.11...v0.17.12)
+
+### 🚀 Enhancements
+
+- Add hooks for extending vite config ([#599](https://github.com/wxt-dev/wxt/pull/599))
+
+### 🩹 Fixes
+
+- **content-script-ui:** Properly assign and unassign mounted value ([#598](https://github.com/wxt-dev/wxt/pull/598))
+
+### 📖 Documentation
+
+- Add discord server link ([#593](https://github.com/wxt-dev/wxt/pull/593))
+
+### 🏡 Chore
+
+- Remove unnecssary 'Omit' types ([db57c8e](https://github.com/wxt-dev/wxt/commit/db57c8e))
+- **deps-dev:** Bump @aklinker1/check from 1.1.1 to 1.2.0 ([#588](https://github.com/wxt-dev/wxt/pull/588))
+- **deps-dev:** Bump vue from 3.3.10 to 3.4.21 ([#589](https://github.com/wxt-dev/wxt/pull/589))
+- **deps-dev:** Bump sass from 1.69.5 to 1.72.0 ([#591](https://github.com/wxt-dev/wxt/pull/591))
+
+## v0.17.11
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.10...v0.17.11)
+
+### 🩹 Fixes
+
+- Resolve absolute paths from the public directory properly ([#583](https://github.com/wxt-dev/wxt/pull/583))
+
+### 📖 Documentation
+
+- Fix `zip.includeSources` example ([16fc584](https://github.com/wxt-dev/wxt/commit/16fc584))
+- Add "GitHub Custom Notifier" to homepage ([#580](https://github.com/wxt-dev/wxt/pull/580))
+
+### 🏡 Chore
+
+- Simplify virtual module setup ([#581](https://github.com/wxt-dev/wxt/pull/581))
+- Add some array utils ([#582](https://github.com/wxt-dev/wxt/pull/582))
+- Extract helper function ([d3b14af](https://github.com/wxt-dev/wxt/commit/d3b14af))
+
+### ❤️ Contributors
+
+- Qiwei Yang ([@qiweiii](http://github.com/qiweiii))
+
+## v0.17.10
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.9...v0.17.10)
+
+### 🚀 Enhancements
+
+- Add `dev.server.port` config ([#577](https://github.com/wxt-dev/wxt/pull/577))
+
+### 🏡 Chore
+
+- Use `@aklinker1/check` to simplify checks setup ([#550](https://github.com/wxt-dev/wxt/pull/550))
+- Refactor order of config resolution ([#578](https://github.com/wxt-dev/wxt/pull/578))
+
+### ❤️ Contributors
+
+- Zizheng Tai ([@zizhengtai](http://github.com/zizhengtai))
+
+## v0.17.9
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.8...v0.17.9)
+
+### 🚀 Enhancements
+
+- Add `{{mode}}` Template Variable ([#566](https://github.com/wxt-dev/wxt/pull/566))
+
+### 🩹 Fixes
+
+- Don't override `wxt.config.ts` options when CLI flags are not passed ([#567](https://github.com/wxt-dev/wxt/pull/567))
+
+### 🏡 Chore
+
+- Merge user config using `defu` ([#568](https://github.com/wxt-dev/wxt/pull/568))
+
+### ❤️ Contributors
+
+- Guillaume ([@GuiEpi](http://github.com/GuiEpi))
+
+## v0.17.8
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.7...v0.17.8)
+
+### 🚀 Enhancements
+
+- **analysis:** Open `stats.html` file automatically ([#564](https://github.com/wxt-dev/wxt/pull/564))
+
+### 🩹 Fixes
+
+- **init:** Better error logging when templates fail to load ([b47c150](https://github.com/wxt-dev/wxt/commit/b47c150))
+- Remove deprecated extension from Vue template ([#534](https://github.com/wxt-dev/wxt/pull/534))
+- Append option description error ([#546](https://github.com/wxt-dev/wxt/pull/546))
+- **init:** Don't overwrite existing files when initializing a new project ([#556](https://github.com/wxt-dev/wxt/pull/556))
+- **dev:** Don't crash dev mode when rebuild fails ([#565](https://github.com/wxt-dev/wxt/pull/565))
+
+### 📖 Documentation
+
+- Replace `pnpx` with `pnpm dlx` ([#527](https://github.com/wxt-dev/wxt/pull/527))
+- Update homepage demo video ([35269da](https://github.com/wxt-dev/wxt/commit/35269da))
+- Update README demo video ([#539](https://github.com/wxt-dev/wxt/pull/539))
+- Add Plex skipper to "Using WXT" section ([#541](https://github.com/wxt-dev/wxt/pull/541))
+- Add documentation for Bun.sh ([#543](https://github.com/wxt-dev/wxt/pull/543))
+- Mention adding unlisted scripts and pages to `web_accessible_resources` ([121b521](https://github.com/wxt-dev/wxt/commit/121b521))
+- Add examples for GitHub Actions ([#540](https://github.com/wxt-dev/wxt/pull/540))
+- Fixed "Reload the Extension" section ([#559](https://github.com/wxt-dev/wxt/pull/559))
+
+### 🏡 Chore
+
+- Increase unit test timeout ([d9cba55](https://github.com/wxt-dev/wxt/commit/d9cba55))
+- Increase hook timeout for Windows/NPM tests ([56b7149](https://github.com/wxt-dev/wxt/commit/56b7149))
+- Switch to seed plugin for testing ([#547](https://github.com/wxt-dev/wxt/pull/547))
+- **vue-template:** Upgrade to `vue-tsc` v2 ([#549](https://github.com/wxt-dev/wxt/pull/549))
+- Add a test covering `wxt init` in a non-empty directory ([#563](https://github.com/wxt-dev/wxt/pull/563))
+
+### ❤️ Contributors
+
+- Btea ([@btea](http://github.com/btea))
+- Vlad Fedosov ([@StyleT](http://github.com/StyleT))
+- Lpmvb ([@Lpmvb](http://github.com/Lpmvb))
+- Guillaume ([@GuiEpi](http://github.com/GuiEpi))
+- Sunshio ([@MPB-Tech](http://github.com/MPB-Tech))
+- Luca Dalli ([@lucadalli](http://github.com/lucadalli))
+
+## v0.17.7
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.6...v0.17.7)
+
+### 🩹 Fixes
+
+- **zip:** List `.wxt/local_modules` overrides relative to the `package.json` they're added to ([#526](https://github.com/wxt-dev/wxt/pull/526))
+
+### 🏡 Chore
+
+- Bump templates to v0.17 ([#524](https://github.com/wxt-dev/wxt/pull/524))
+- Increase test timeout for windows NPM tests ([#525](https://github.com/wxt-dev/wxt/pull/525))
+
+## v0.17.6
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.5...v0.17.6)
+
+### 🚀 Enhancements
+
+- Add warnings when important directories are missing ([#516](https://github.com/wxt-dev/wxt/pull/516))
+- Automatically remove top-level MV2-only or MV3-only keys ([#518](https://github.com/wxt-dev/wxt/pull/518))
+- Automatically generate `browser_action` based on `action` for MV2 ([#519](https://github.com/wxt-dev/wxt/pull/519))
+
+### 🩹 Fixes
+
+- **zip:** List all private packages correctly in a PNPM workspace ([#520](https://github.com/wxt-dev/wxt/pull/520))
+
+### 📖 Documentation
+
+- Mentions moving folders into `srcDir` ([9cd4e83](https://github.com/wxt-dev/wxt/commit/9cd4e83))
+
+### 🏡 Chore
+
+- **deps-dev:** Bump @types/react from 18.2.34 to 18.2.61 ([#510](https://github.com/wxt-dev/wxt/pull/510))
+
+## v0.17.5
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.4...v0.17.5)
+
+### 🚀 Enhancements
+
+- Expose package management utils under `wxt.pm` ([#502](https://github.com/wxt-dev/wxt/pull/502))
+- Download and override private packages for Firefox code review ([#507](https://github.com/wxt-dev/wxt/pull/507))
+
+### 📖 Documentation
+
+- Fix typos ([#503](https://github.com/wxt-dev/wxt/pull/503))
+- Add docs about configuring the manifest as a function ([195d2cc](https://github.com/wxt-dev/wxt/commit/195d2cc))
+- Fix CLI generation ([b754435](https://github.com/wxt-dev/wxt/commit/b754435))
+
+### 🏡 Chore
+
+- Use JSZip for `wxt zip`, enabling future features ([#501](https://github.com/wxt-dev/wxt/pull/501))
+
+### ❤️ Contributors
+
+- Btea ([@btea](http://github.com/btea))
+
+## v0.17.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.3...v0.17.4)
+
+### 🚀 Enhancements
+
+- Add basic content script to templates ([#495](https://github.com/wxt-dev/wxt/pull/495))
+- Add `ResolvedConfig.wxtModuleDir`, resolving the directory once ([#497](https://github.com/wxt-dev/wxt/pull/497))
+
+### 🩹 Fixes
+
+- Resolve the path to `node_modules/wxt` correctly ([#498](https://github.com/wxt-dev/wxt/pull/498))
+
+### 📖 Documentation
+
+- Added DocVersionRedirector to "Using WXT" section ([#492](https://github.com/wxt-dev/wxt/pull/492))
+- Fix typos ([f80fb42](https://github.com/wxt-dev/wxt/commit/f80fb42))
+- Add CRXJS to comparison page ([cb4f9aa](https://github.com/wxt-dev/wxt/commit/cb4f9aa))
+- Update comparison page ([35778f7](https://github.com/wxt-dev/wxt/commit/35778f7))
+- Update context usage ([012bd7e](https://github.com/wxt-dev/wxt/commit/012bd7e))
+- Add testing example for `ContentScriptContext` ([e1c6020](https://github.com/wxt-dev/wxt/commit/e1c6020))
+
+### 🏡 Chore
+
+- Fix tests after template change ([f9b0aa4](https://github.com/wxt-dev/wxt/commit/f9b0aa4))
+
+### ❤️ Contributors
+
+- Btea ([@btea](http://github.com/btea))
+- Leo Shklovskii
+
+## v0.17.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.2...v0.17.3)
+
+### 🚀 Enhancements
+
+- **storage:** Guarantee `storage.getItems` returns values in the same order as requested ([b5f4d8c](https://github.com/wxt-dev/wxt/commit/b5f4d8c))
+
+### 🩹 Fixes
+
+- Content scripts crash when using `storage.defineItem` ([77e6d1f](https://github.com/wxt-dev/wxt/commit/77e6d1f))
+- **storage:** Revert #478 and run migrations when item is defined and properly wait for migrations before allowing read/writes ([#487](https://github.com/wxt-dev/wxt/pull/487), [#478](https://github.com/wxt-dev/wxt/issues/478))
+
+## v0.17.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.1...v0.17.2)
+
+### 🩹 Fixes
+
+- Don't use sub-dependency binaries directly ([#482](https://github.com/wxt-dev/wxt/pull/482))
+
+## v0.17.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.0...v0.17.1)
+
+### 🩹 Fixes
+
+- Content scripts not loading in dev mode ([3fbbe2c](https://github.com/wxt-dev/wxt/commit/3fbbe2c))
+
+### 📖 Documentation
+
+- Lots of small typo fixes ([#480](https://github.com/wxt-dev/wxt/pull/480))
+
+### ❤️ Contributors
+
+- Leo Shklovskii ([@leos](https://github.com/leos))
+
+## v0.17.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.11...v0.17.0)
+
+### 🚀 Enhancements
+
+- **storage:** ⚠️ Improved support for default values on storage items ([#477](https://github.com/wxt-dev/wxt/pull/477))
+
+### 🩹 Fixes
+
+- **storage:** ⚠️ Only run migrations when the extension is updated ([#478](https://github.com/wxt-dev/wxt/pull/478))
+- Improve dev mode for content scripts registered at runtime ([#474](https://github.com/wxt-dev/wxt/pull/474))
+
+### 📖 Documentation
+
+- **storage:** Update docs ([91fc41c](https://github.com/wxt-dev/wxt/commit/91fc41c))
+
+#### ⚠️ Breaking Changes
+
+`v0.17.0` introduces several breaking changes to `wxt/storage`.
+
+First, if you were using `defineItem` with versioning and no default value, you will need to add `defaultValue: null` to the options and update the first type parameter:
+
+```ts
+// < 0.17
+const item = storage.defineItem("local:count", {
+ version: ...,
+ migrations: ...,
+})
+
+// >= 0.17
+const item = storage.defineItem("local:count", {
+ defaultValue: null,
+ version: ...,
+ migrations: ...,
+})
+```
+
+The `defaultValue` property is now required if passing in the second options argument.
+
+If you exclude the second options argument, it will default to being nullable, as before.
+
+```ts
+const item: WxtStorageItem =
+ storage.defineItem('local:count');
+const value: number | null = await item.getValue();
+```
+
+> If you don't use typescript, there aren't any breaking changes, this is just a type change.
+
+For storage items that are not nullable, the `watch` callback types has improved and will use the default value instead of `null` when the value is missing:
+
+```ts
+// >=0.17
+const item = storage.defineItem('local:count', { defaultValue: 0 });
+item.watch((newValue: number | null, oldValue: number | null) => {
+ // ...
+});
+
+// >=0.17
+const item = storage.defineItem('local:count', { defaultValue: 0 });
+item.watch((newValue: number, oldValue: number) => {
+ // ...
+});
+```
+
+You can also access the default value directly off the item:
+
+```ts
+console.log(item.defaultValue); // 0
+```
+
+The second breaking change is that migrations for versioned items only run when the extension is updated. Before, they were ran whenever the storage item was created, in any entrypoint (background, popup, content script, etc). Now, in v0.17, storage items will only run migrations when the `browser.runtime.onInstalled` event is fired with `reason = "update"` in the background. See the updated docs to make sure they run correctly: https://wxt.dev/guide/storage.html#running-migrations. TLDR: you need to import all storage items into the background entrypoint for the `onInstalled` hook to fire properly and thus run the migrations.
+
+To keep the old behavior, call the new `migrate` function to run migrations as soon as an item is defined:
+
+```ts
+const item = storage.defineItem(...);
+item.migrate();
+```
+
+## v0.16.11
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.10...v0.16.11)
+
+### 🩹 Fixes
+
+- Output main JS file for HTML entrypoints to chunks directory ([#473](https://github.com/wxt-dev/wxt/pull/473))
+
+### 🏡 Chore
+
+- **e2e:** Remove log ([4fda203](https://github.com/wxt-dev/wxt/commit/4fda203))
+
+### 🤖 CI
+
+- Fix codecov warning in release workflow ([7c6973f](https://github.com/wxt-dev/wxt/commit/7c6973f))
+- Upgrade `pnpm/action-setup` to v3 ([905bfc7](https://github.com/wxt-dev/wxt/commit/905bfc7))
+
+## v0.16.10
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.9...v0.16.10)
+
+### 🚀 Enhancements
+
+- Customize when content scripts are registered, in the manifest or at runtime ([#471](https://github.com/wxt-dev/wxt/pull/471))
+
+### 🩹 Fixes
+
+- Don't assume react when importing JSX entrypoints during build ([#470](https://github.com/wxt-dev/wxt/pull/470))
+- Respect `configFile` option ([#472](https://github.com/wxt-dev/wxt/pull/472))
+
+## v0.16.9
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.8...v0.16.9)
+
+### 🚀 Enhancements
+
+- Support setting side panel options in HTML file ([#468](https://github.com/wxt-dev/wxt/pull/468))
+
+### 🩹 Fixes
+
+- Fix order of ShadowRootUI hooks calling ([#459](https://github.com/wxt-dev/wxt/pull/459))
+
+### 📖 Documentation
+
+- Add wrapper div to react's `createShadowRootUi` example ([bc24ea4](https://github.com/wxt-dev/wxt/commit/bc24ea4))
+
+### 🏡 Chore
+
+- Simplify entrypoint types ([#464](https://github.com/wxt-dev/wxt/pull/464))
+
+### ❤️ Contributors
+
+- Okou ([@ookkoouu](https://github.com/ookkoouu))
+
+## v0.16.8
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.7...v0.16.8)
+
+### 🩹 Fixes
+
+- Watch files outside project root during development ([#454](https://github.com/wxt-dev/wxt/pull/454))
+
+### 📖 Documentation
+
+- Add loading and error states for "Who's using WXT" section ([447a48f](https://github.com/wxt-dev/wxt/commit/447a48f))
+
+## v0.16.7
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.6...v0.16.7)
+
+### 🚀 Enhancements
+
+- Generate ESLint globals file for auto-imports ([#450](https://github.com/wxt-dev/wxt/pull/450))
+
+### 🔥 Performance
+
+- Upgrade Vite to 5.1 ([#452](https://github.com/wxt-dev/wxt/pull/452))
+
+### 📖 Documentation
+
+- Add section about dev mode differences ([a0d1643](https://github.com/wxt-dev/wxt/commit/a0d1643))
+- Remove anchor from content script ui examples ([87a62a1](https://github.com/wxt-dev/wxt/commit/87a62a1))
+
+### 🏡 Chore
+
+- **e2e:** Use `wxt prepare` instead of `wxt build` when possible to speed up E2E tests ([#451](https://github.com/wxt-dev/wxt/pull/451))
+
+## v0.16.6
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.5...v0.16.6)
+
+### 🚀 Enhancements
+
+- Add option to customize the analysis artifacts output ([#431](https://github.com/wxt-dev/wxt/pull/431))
+
+### 🩹 Fixes
+
+- Use `insertBefore` on mounting content script UI ([ba85fdf](https://github.com/wxt-dev/wxt/commit/ba85fdf))
+
+### 💅 Refactors
+
+- Use `Element.prepend` on mounting UI ([295f860](https://github.com/wxt-dev/wxt/commit/295f860))
+
+### 📖 Documentation
+
+- Fix `createShadowRootUi` unmount calls ([946072f](https://github.com/wxt-dev/wxt/commit/946072f))
+
+### 🏡 Chore
+
+- Enable skipped test since it works now ([6b8dfdf](https://github.com/wxt-dev/wxt/commit/6b8dfdf))
+
+### ❤️ Contributors
+
+- Lionelhorn ([@Lionelhorn](https://github.com/Lionelhorn))
+- Okou ([@ookkoouu](https://github.com/ookkoouu))
+
+## v0.16.5
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.4...v0.16.5)
+
+### 🩹 Fixes
+
+- Support node 20 when running `wxt submit` ([e835502](https://github.com/wxt-dev/wxt/commit/e835502))
+
+### 📖 Documentation
+
+- Remove "coming soon" from automated publishing feature ([2b374b9](https://github.com/wxt-dev/wxt/commit/2b374b9))
+
+## v0.16.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.3...v0.16.4)
+
+### 🚀 Enhancements
+
+- Automatically convert MV3 `web_accessible_resources` to MV2 ([#423](https://github.com/wxt-dev/wxt/pull/423))
+- Add option to customize the analysis output filename ([#426](https://github.com/wxt-dev/wxt/pull/426))
+
+### 🩹 Fixes
+
+- Don't use immer for `transformManifest` ([#424](https://github.com/wxt-dev/wxt/pull/424))
+- Exclude analysis files from the build summary ([#425](https://github.com/wxt-dev/wxt/pull/425))
+
+### 🏡 Chore
+
+- Fix fake path in test data generator ([d0f1c70](https://github.com/wxt-dev/wxt/commit/d0f1c70))
+
+## v0.16.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.2...v0.16.3)
+
+### 🚀 Enhancements
+
+- Hooks ([#419](https://github.com/wxt-dev/wxt/pull/419))
+
+### 🩹 Fixes
+
+- **init:** Use `ungh` to prevent rate limits when loading templates ([37ad2c7](https://github.com/wxt-dev/wxt/commit/37ad2c7))
+
+### 📖 Documentation
+
+- Fix typo of intuitive ([#415](https://github.com/wxt-dev/wxt/pull/415))
+- Fix typo of opinionated ([#416](https://github.com/wxt-dev/wxt/pull/416))
+
+### 🏡 Chore
+
+- Add dependabot for github actions ([#404](https://github.com/wxt-dev/wxt/pull/404))
+- **deps-dev:** Bump happy-dom from 12.10.3 to 13.3.8 ([#411](https://github.com/wxt-dev/wxt/pull/411))
+- **deps-dev:** Bump typescript from 5.3.2 to 5.3.3 ([#409](https://github.com/wxt-dev/wxt/pull/409))
+- Register global `wxt` instance ([#418](https://github.com/wxt-dev/wxt/pull/418))
+
+### ❤️ Contributors
+
+- Chen Hua ([@hcljsq](https://github.com/hcljsq))
+- Florian Metz ([@Timeraa](http://github.com/Timeraa))
+
+## v0.16.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.1...v0.16.2)
+
+### 🩹 Fixes
+
+- Don't crash background service worker when using `import.meta.url` ([#402](https://github.com/wxt-dev/wxt/pull/402))
+
+## v0.16.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.0...v0.16.1)
+
+### 🩹 Fixes
+
+- Don't require config to run `wxt submit init` ([9318346](https://github.com/wxt-dev/wxt/commit/9318346))
+
+### 📖 Documentation
+
+- Add premid extension to homepage ([#399](https://github.com/wxt-dev/wxt/pull/399))
+
+### 🏡 Chore
+
+- **templates:** Upgrade to wxt `^0.16.0` ([f0b2a12](https://github.com/wxt-dev/wxt/commit/f0b2a12))
+
+### ❤️ Contributors
+
+- Florian Metz ([@Timeraa](http://github.com/Timeraa))
+
+## v0.16.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.4...v0.16.0)
+
+### 🚀 Enhancements
+
+- ⚠️ ESM background support ([#398](https://github.com/wxt-dev/wxt/pull/398))
+
+### 📖 Documentation
+
+- Document how to opt into ESM ([1e12ce2](https://github.com/wxt-dev/wxt/commit/1e12ce2))
+
+### 🏡 Chore
+
+- **deps-dev:** Bump lint-staged from 15.2.0 to 15.2.1 ([#395](https://github.com/wxt-dev/wxt/pull/395))
+- **deps-dev:** Bump p-map from 7.0.0 to 7.0.1 ([#396](https://github.com/wxt-dev/wxt/pull/396))
+- **deps-dev:** Bump @vitest/coverage-v8 from 1.0.1 to 1.2.2 ([#397](https://github.com/wxt-dev/wxt/pull/397))
+
+#### ⚠️ Breaking Changes
+
+In [#398](https://github.com/wxt-dev/wxt/pull/398), HTML pages' JS entrypoints in the output directory have been moved. Unless you're doing some kind of post-build work referencing files, you don't have to make any changes.
+
+- Before:
+ ```
+ .output/
+ /
+ chunks/
+ some-shared-chunk-.js
+ popup-.js
+ popup.html
+ ```
+- After:
+ ```
+ .output/
+ /
+ chunks/
+ some-shared-chunk-.js
+ popup.html
+ popup.js
+ ```
+
+This effects all HTML files, not just the Popup. The hash has been removed, and it's been moved to the root of the build target folder, not inside the `chunks/` directory. Moving files like this has not historically increased review times or triggered in-depth reviews when submitting updates to the stores.
+
+## v0.15.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.3...v0.15.4)
+
+### 🩹 Fixes
+
+- **submit:** Load `.env.submit` automatically when running `wxt submit` and `wxt submit init` ([#391](https://github.com/wxt-dev/wxt/pull/391))
+
+## v0.15.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.2...v0.15.3)
+
+### 🩹 Fixes
+
+- **dev:** Reload `/index.html` entrypoints properly on save ([#390](https://github.com/wxt-dev/wxt/pull/390))
+
+## v0.15.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.1...v0.15.2)
+
+### 🚀 Enhancements
+
+- Add `submit` command ([#370](https://github.com/wxt-dev/wxt/pull/370))
+
+### 🩹 Fixes
+
+- **dev:** Resolve `script` and `link` aliases ([#387](https://github.com/wxt-dev/wxt/pull/387))
+
+### ❤️ Contributors
+
+- Nenad Novaković ([@dvlden](https://github.com/dvlden))
+
+## v0.15.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.0...v0.15.1)
+
+### 🚀 Enhancements
+
+- Allow passing custom preferences to chrome, enabling dev mode on `chrome://extensions` and allowing content script sourcemaps automatically ([#384](https://github.com/wxt-dev/wxt/pull/384))
+
+### 🩹 Fixes
+
+- **security:** Upgrade to vite@5.0.12 to resolve CVE-2024-23331 ([39b76d3](https://github.com/wxt-dev/wxt/commit/39b76d3))
+
+### 📖 Documentation
+
+- Fixed doc errors on the guide/extension-api page ([#383](https://github.com/wxt-dev/wxt/pull/383))
+
+### 🏡 Chore
+
+- Fix vite version conflicts in demo extension ([98d2792](https://github.com/wxt-dev/wxt/commit/98d2792))
+
+### ❤️ Contributors
+
+- 0x7a7a ([@0x7a7a](https://github.com/0x7a7a))
+
+## v0.15.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.7...v0.15.0)
+
+### 🚀 Enhancements
+
+- **zip:** ⚠️ Add `includeSources` and rename `ignoredSources` to `excludeSources` ([#378](https://github.com/wxt-dev/wxt/pull/378))
+
+### 🩹 Fixes
+
+- Generate missing sourcemap in `wxt:unimport` plugin ([#381](https://github.com/wxt-dev/wxt/pull/381))
+- ⚠️ Move browser constants to `import.meta.env` ([#380](https://github.com/wxt-dev/wxt/pull/380))
+- Enable inline sourcemaps by default during development ([#382](https://github.com/wxt-dev/wxt/pull/382))
+
+### 📖 Documentation
+
+- Fix typo ([f9718a1](https://github.com/wxt-dev/wxt/commit/f9718a1))
+
+### 🏡 Chore
+
+- Update contributor docs ([eb758bd](https://github.com/wxt-dev/wxt/commit/eb758bd))
+
+#### ⚠️ Breaking Changes
+
+Renamed `zip.ignoredSources` to `zip.excludeSources` in [#378](https://github.com/wxt-dev/wxt/pull/378)
+
+Renamed undocumented constants for detecting the build config at runtime in [#380](https://github.com/wxt-dev/wxt/pull/380). Now documented here: https://wxt.dev/guide/multiple-browsers.html#runtime
+
+- `__BROWSER__` → `import.meta.env.BROWSER`
+- `__COMMAND__` → `import.meta.env.COMMAND`
+- `__MANIFEST_VERSION__` → `import.meta.env.MANIFEST_VERSION`
+- `__IS_CHROME__` → `import.meta.env.CHROME`
+- `__IS_FIREFOX__` → `import.meta.env.FIREFOX`
+- `__IS_SAFARI__` → `import.meta.env.SAFARI`
+- `__IS_EDGE__` → `import.meta.env.EDGE`
+- `__IS_OPERA__` → `import.meta.env.OPERA`
+
+### ❤️ Contributors
+
+- Nenad Novaković ([@dvlden](https://github.com/dvlden))
+
+## v0.14.7
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.6...v0.14.7)
+
+### 🩹 Fixes
+
+- Improve error messages when importing and building entrypoints ([3b63a51](https://github.com/wxt-dev/wxt/commit/3b63a51))
+- **storage:** Throw better error message when importing outside a extension environment ([35865ad](https://github.com/wxt-dev/wxt/commit/35865ad))
+- Upgrade `web-ext-run` ([62ecb6f](https://github.com/wxt-dev/wxt/commit/62ecb6f))
+
+### 📖 Documentation
+
+- Add `matches` to content script examples ([dab8efa](https://github.com/wxt-dev/wxt/commit/dab8efa))
+- Fix incorrect sample code ([#372](https://github.com/wxt-dev/wxt/pull/372))
+- Document defined constants for the build target ([68874e6](https://github.com/wxt-dev/wxt/commit/68874e6))
+- Add missing `await` to `createShadowRootUi` examples ([fc45c37](https://github.com/wxt-dev/wxt/commit/fc45c37))
+
+### ❤️ Contributors
+
+- 東奈比 ([@dongnaebi](http://github.com/dongnaebi))
+
+## v0.14.6
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.5...v0.14.6)
+
+### 🚀 Enhancements
+
+- Restart dev mode when saving config ([#365](https://github.com/wxt-dev/wxt/pull/365))
+- Add basic validation for entrypoint options ([#368](https://github.com/wxt-dev/wxt/pull/368))
+
+### 🩹 Fixes
+
+- Add subdependency bin directory so `wxt build --analyze` works with PNPM ([#363](https://github.com/wxt-dev/wxt/pull/363))
+- Sort build output files naturally ([#364](https://github.com/wxt-dev/wxt/pull/364))
+
+### 🤖 CI
+
+- Check for type errors in demo before building ([4b005b4](https://github.com/wxt-dev/wxt/commit/4b005b4))
+
+## v0.14.5
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.4...v0.14.5)
+
+### 🚀 Enhancements
+
+- Add `dev.reloadCommand` config ([#362](https://github.com/wxt-dev/wxt/pull/362))
+
+### 🩹 Fixes
+
+- Disable reload dev command when 4 commands are already registered ([#361](https://github.com/wxt-dev/wxt/pull/361))
+
+## v0.14.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.3...v0.14.4)
+
+### 🩹 Fixes
+
+- Allow requiring built-in node modules from ESM CLI ([#356](https://github.com/wxt-dev/wxt/pull/356))
+
+### 🏡 Chore
+
+- Add unit tests for passing flags via the CLI ([#354](https://github.com/wxt-dev/wxt/pull/354))
+
+## v0.14.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.2...v0.14.3)
+
+### 🩹 Fixes
+
+- Make `getArrayFromFlags` result can be undefined ([#352](https://github.com/wxt-dev/wxt/pull/352))
+
+### ❤️ Contributors
+
+- Yuns ([@yunsii](http://github.com/yunsii))
+
+## v0.14.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.1...v0.14.2)
+
+### 🚀 Enhancements
+
+- Add `filterEntrypoints` option to speed up development ([#344](https://github.com/wxt-dev/wxt/pull/344))
+
+### 🔥 Performance
+
+- Only call `findEntrypoint` once per build ([#342](https://github.com/wxt-dev/wxt/pull/342))
+
+### 🩹 Fixes
+
+- Improve error message and document use of imported variables outside an entrypoint's `main` function ([#346](https://github.com/wxt-dev/wxt/pull/346))
+- Allow `browser.runtime.getURL` to include hashes and query params for HTML paths ([#350](https://github.com/wxt-dev/wxt/pull/350))
+
+### 📖 Documentation
+
+- Fix typos and outdated ui function usage ([#347](https://github.com/wxt-dev/wxt/pull/347))
+
+### 🏡 Chore
+
+- Update templates to `^0.14.0` ([70a4961](https://github.com/wxt-dev/wxt/commit/70a4961))
+- Fix typo in function name ([a329e24](https://github.com/wxt-dev/wxt/commit/a329e24))
+
+### ❤️ Contributors
+
+- Yuns ([@yunsii](http://github.com/yunsii))
+- Armin
+
+## v0.14.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.0...v0.14.1)
+
+### 🩹 Fixes
+
+- Use `Alt+R`/`Opt+R` to reload extension during development ([b6ab7a9](https://github.com/wxt-dev/wxt/commit/b6ab7a9))
+
+## v0.14.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.5...v0.14.0)
+
+### 🚀 Enhancements
+
+- ⚠️ Refactor content script UI functions and add helper for "integrated" UIs ([#333](https://github.com/wxt-dev/wxt/pull/333))
+
+#### ⚠️ Breaking Changes
+
+`createContentScriptUi` and `createContentScriptIframe`, and some of their options, have been renamed:
+
+- `createContentScriptUi({ ... })` → `createShadowRootUi({ ... })`
+- `createContentScriptIframe({ ... })` → `createIframeUi({ ... })`
+- `type: "inline" | "overlay" | "modal"` has been changed to `position: "inline" | "overlay" | "modal"`
+- `onRemove` is now called **_before_** the UI is removed from the DOM, previously it was called after the UI was removed
+- `mount` option has been renamed to `onMount`, to better match the related option, `onRemove`.
+
+## v0.13.5
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.4...v0.13.5)
+
+### 🩹 Fixes
+
+- Strip path from `web_accessible_resources[0].matches` ([#332](https://github.com/wxt-dev/wxt/pull/332))
+
+### 📖 Documentation
+
+- Add section about customizing other browser options during development ([8683bd4](https://github.com/wxt-dev/wxt/commit/8683bd4))
+
+### 🏡 Chore
+
+- Update bug report template ([9a2cc18](https://github.com/wxt-dev/wxt/commit/9a2cc18))
+
+## v0.13.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.3...v0.13.4)
+
+### 🩹 Fixes
+
+- Disable minification during development ([b7cdf15](https://github.com/wxt-dev/wxt/commit/b7cdf15))
+
+### 🏡 Chore
+
+- Use `const` instead of `let` ([2770974](https://github.com/wxt-dev/wxt/commit/2770974))
+
+## v0.13.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.2...v0.13.3)
+
+### 🚀 Enhancements
+
+- **DX:** Add `ctrl+E`/`cmd+E` shortcut to reload extension during development ([#322](https://github.com/wxt-dev/wxt/pull/322))
+
+### 🏡 Chore
+
+- **deps-dev:** Bump tsx from 4.6.2 to 4.7.0 ([#320](https://github.com/wxt-dev/wxt/pull/320))
+- **deps-dev:** Bump prettier from 3.1.0 to 3.1.1 ([#318](https://github.com/wxt-dev/wxt/pull/318))
+- **deps-dev:** Bump vitepress from 1.0.0-rc.31 to 1.0.0-rc.34 ([#316](https://github.com/wxt-dev/wxt/pull/316))
+- Refactor manifest generation E2E tests to unit tests ([#323](https://github.com/wxt-dev/wxt/pull/323))
+
+## v0.13.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.1...v0.13.2)
+
+### 🚀 Enhancements
+
+- Add `isolateEvents` option to `createContentScripUi` ([#313](https://github.com/wxt-dev/wxt/pull/313))
+
+### 📖 Documentation
+
+- Remove duplicate `entrypoints/` path ([76e63e2](https://github.com/wxt-dev/wxt/commit/76e63e2))
+- Update unlisted pages/scripts description ([c99a281](https://github.com/wxt-dev/wxt/commit/c99a281))
+- Update content script entrypoint docs ([1360eb7](https://github.com/wxt-dev/wxt/commit/1360eb7))
+- Add example for setting up custom panels/panes in devtools ([#308](https://github.com/wxt-dev/wxt/pull/308))
+- Use example tags to automate relevant example lists ([#311](https://github.com/wxt-dev/wxt/pull/311))
+
+### 🏡 Chore
+
+- Update templates to `^0.13.0` ([#309](https://github.com/wxt-dev/wxt/pull/309))
+- Upgrade template dependencies ([#310](https://github.com/wxt-dev/wxt/pull/310))
+- Re-enable coverage ([#312](https://github.com/wxt-dev/wxt/pull/312))
+
+### ❤️ Contributors
+
+- 冯不游
+
+## v0.13.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.0...v0.13.1)
+
+### 🩹 Fixes
+
+- **storage:** Support multiple `:` characters in storage keys ([#303](https://github.com/wxt-dev/wxt/pull/303))
+- Ship `vite/client` types internally for proper resolution using PNPM ([#304](https://github.com/wxt-dev/wxt/pull/304))
+
+### 📖 Documentation
+
+- Reorder guide ([6421ab3](https://github.com/wxt-dev/wxt/commit/6421ab3))
+- General fixes and improvements ([2ad099b](https://github.com/wxt-dev/wxt/commit/2ad099b))
+
+### 🏡 Chore
+
+- Update `scripts/build.ts` show current build step in progress, not completed count ([#306](https://github.com/wxt-dev/wxt/pull/306))
+
+## v0.13.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.5...v0.13.0)
+
+### 🚀 Enhancements
+
+- ⚠️ New `wxt/storage` APIs ([#300](https://github.com/wxt-dev/wxt/pull/300))
+
+#### ⚠️ Breaking Changes
+
+- `wxt/storage` no longer relies on [`unstorage`](https://www.npmjs.com/package/unstorage). Some `unstorage` APIs, like `prefixStorage`, have been removed, while others, like `snapshot`, are methods on the new `storage` object. Most of the standard usage remains the same. See https://wxt.dev/guide/storage and https://wxt.dev/api/wxt/storage/ for more details ([#300](https://github.com/wxt-dev/wxt/pull/300))
+
+## v0.12.5
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.4...v0.12.5)
+
+### 🩹 Fixes
+
+- Correct import in dev-only, noop background ([#298](https://github.com/wxt-dev/wxt/pull/298))
+
+## v0.12.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.3...v0.12.4)
+
+### 🩹 Fixes
+
+- Disable Vite CJS warnings ([#296](https://github.com/wxt-dev/wxt/pull/296))
+
+## v0.12.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.2...v0.12.3)
+
+### 🩹 Fixes
+
+- Correctly mock `webextension-polyfill` for Vitest ([#294](https://github.com/wxt-dev/wxt/pull/294))
+
+## v0.12.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.1...v0.12.2)
+
+### 🚀 Enhancements
+
+- Support PNPM without hoisting dependencies ([#291](https://github.com/wxt-dev/wxt/pull/291))
+
+## v0.12.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.0...v0.12.1)
+
+### 🩹 Fixes
+
+- Upgrade `@webext-core/match-patterns` to `1.0.3` ([#289](https://github.com/wxt-dev/wxt/pull/289))
+- Fix `package.json` lint errors ([#290](https://github.com/wxt-dev/wxt/pull/290))
+
+### 🏡 Chore
+
+- Upgrade templates to `wxt@^0.12.0` ([#285](https://github.com/wxt-dev/wxt/pull/285))
+
+## v0.12.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.11.2...v0.12.0)
+
+### 🚀 Enhancements
+
+- ⚠️ Add support for "main world" content scripts ([#284](https://github.com/wxt-dev/wxt/pull/284))
+
+### 🩹 Fixes
+
+- Only use type imports for Vite ([#278](https://github.com/wxt-dev/wxt/pull/278))
+- Throw error when no entrypoints are found ([#283](https://github.com/wxt-dev/wxt/pull/283))
+
+### 📖 Documentation
+
+- Improve content script UI guide ([#272](https://github.com/wxt-dev/wxt/pull/272))
+- Fix dead links ([291d25b](https://github.com/wxt-dev/wxt/commit/291d25b))
+
+### 🏡 Chore
+
+- Convert WXT CLI to an ESM binary ([#279](https://github.com/wxt-dev/wxt/pull/279))
+
+#### ⚠️ Breaking Changes
+
+`defineContentScript` and `defineBackground` are now exported from `wxt/sandbox` instead of `wxt/client`. ([#284](https://github.com/wxt-dev/wxt/pull/284))
+
+- If you use auto-imports, no changes are required.
+- If you have disabled auto-imports, you'll need to manually update your import statements:
+ ```diff
+ - import { defineBackground, defineContentScript } from 'wxt/client';
+ + import { defineBackground, defineContentScript } from 'wxt/sandbox';
+ ```
+
+## v0.11.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.11.1...v0.11.2)
+
+### 🩹 Fixes
+
+- Discover `.js`, `.jsx`, and `.tsx` unlisted scripts correctly ([#274](https://github.com/wxt-dev/wxt/pull/274))
+- Improve duplicate entrypoint name detection and catch the error before loading their config ([#276](https://github.com/wxt-dev/wxt/pull/276))
+
+### 📖 Documentation
+
+- Improve content script UI docs ([#268](https://github.com/wxt-dev/wxt/pull/268))
+
+### 🏡 Chore
+
+- Update sSolid template to vite 5 ([#265](https://github.com/wxt-dev/wxt/pull/265))
+- Add missing navigation item ([bcb93af](https://github.com/wxt-dev/wxt/commit/bcb93af))
+
+## v0.11.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.11.0...v0.11.1)
+
+### 🚀 Enhancements
+
+- Add util for detecting URL changes in content scripts ([#264](https://github.com/wxt-dev/wxt/pull/264))
+
+### 🏡 Chore
+
+- Upgrade templates to `wxt@^0.11.0` ([#263](https://github.com/wxt-dev/wxt/pull/263))
+
+## v0.11.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.4...v0.11.0)
+
+### 🚀 Enhancements
+
+- ⚠️ Vite 5 support ([#261](https://github.com/wxt-dev/wxt/pull/261))
+
+### 📖 Documentation
+
+- Adds tl;dv to homepage ([#260](https://github.com/wxt-dev/wxt/pull/260))
+
+### 🏡 Chore
+
+- Speed up CI using `pnpm` instead of `npm` ([#259](https://github.com/wxt-dev/wxt/pull/259))
+- Abstract vite from WXT's core logic ([#242](https://github.com/wxt-dev/wxt/pull/242))
+
+#### ⚠️ Breaking Changes
+
+- You will need to update any other Vite plugins to a version that supports Vite 5 ([#261](https://github.com/wxt-dev/wxt/pull/261))
+
+### ❤️ Contributors
+
+- Ítalo Brasil ([@italodeverdade](http://github.com/italodeverdade))
+
+## v0.10.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.3...v0.10.4)
+
+### 🚀 Enhancements
+
+- Add config to customize `outDir` ([#258](https://github.com/wxt-dev/wxt/pull/258))
+
+### 📖 Documentation
+
+- Add Doozy to homepage ([#249](https://github.com/wxt-dev/wxt/pull/249))
+- Update sidepanel availability ([#250](https://github.com/wxt-dev/wxt/pull/250))
+
+### 🏡 Chore
+
+- **deps-dev:** Bump prettier from 3.0.3 to 3.1.0 ([#254](https://github.com/wxt-dev/wxt/pull/254))
+- **deps-dev:** Bump @types/lodash.merge from 4.6.8 to 4.6.9 ([#255](https://github.com/wxt-dev/wxt/pull/255))
+- **deps-dev:** Bump tsx from 3.14.0 to 4.6.1 ([#252](https://github.com/wxt-dev/wxt/pull/252))
+
+### ❤️ Contributors
+
+- 冯不游
+
+## v0.10.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.2...v0.10.3)
+
+### 🩹 Fixes
+
+- **auto-imports:** Don't add imports to `node_module` dependencies ([#247](https://github.com/wxt-dev/wxt/pull/247))
+
+### 📖 Documentation
+
+- Fix typo ([317b1b6](https://github.com/wxt-dev/wxt/commit/317b1b6))
+
+### 🏡 Chore
+
+- Trigger docs upgrade via webhook ([742b996](https://github.com/wxt-dev/wxt/commit/742b996))
+- Use `normalize-path` instead of `vite.normalizePath` ([#244](https://github.com/wxt-dev/wxt/pull/244))
+- Use `defu` for merging some config objects ([#243](https://github.com/wxt-dev/wxt/pull/243))
+
+### 🤖 CI
+
+- Publish docs on push to main ([1611c1d](https://github.com/wxt-dev/wxt/commit/1611c1d))
+- Only print response headers from docs webhook ([97cbda3](https://github.com/wxt-dev/wxt/commit/97cbda3))
+
+## v0.10.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.1...v0.10.2)
+
+### 🩹 Fixes
+
+- Apply `mode` option to build steps correctly ([82ed821](https://github.com/wxt-dev/wxt/commit/82ed821))
+
+### 🏡 Chore
+
+- Upgrade templates to v0.10 ([#239](https://github.com/wxt-dev/wxt/pull/239))
+
+## v0.10.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.0...v0.10.1)
+
+### 🩹 Fixes
+
+- Remove WXT global to remove unused modules from production builds ([3da3e07](https://github.com/wxt-dev/wxt/commit/3da3e07))
+
+## v0.10.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.9.2...v0.10.0)
+
+### 🚀 Enhancements
+
+- List `bun` as an experimental option in `wxt init` ([#233](https://github.com/wxt-dev/wxt/pull/233))
+- ⚠️ Allow plural directory and only png's for manifest icons ([#237](https://github.com/wxt-dev/wxt/pull/237))
+- Add `wxt/storage` API ([#234](https://github.com/wxt-dev/wxt/pull/234))
+
+### 🩹 Fixes
+
+- Don't use `bun` to load entrypoint config ([#232](https://github.com/wxt-dev/wxt/pull/232))
+
+### 📖 Documentation
+
+- Update main README links ([207b750](https://github.com/wxt-dev/wxt/commit/207b750))
+
+#### ⚠️ Breaking Changes
+
+- ⚠️ No longer discover icons with extensions other than `.png`. If you previously used `.jpg`, `.jpeg`, `.bmp`, or `.svg`, you'll need to convert your icons to `.png` files or manually add them to the manifest inside your `wxt.config.ts` file ([#237](https://github.com/wxt-dev/wxt/pull/237))
+
+### ❤️ Contributors
+
+- Nenad Novaković ([@dvlden](https://github.com/dvlden))
+
+## v0.9.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.9.1...v0.9.2)
+
+### 🚀 Enhancements
+
+- Experimental option to exclude `webextension-polyfill` ([#231](https://github.com/wxt-dev/wxt/pull/231))
+
+### 🤖 CI
+
+- Fix sync-release workflow ([d1b5230](https://github.com/wxt-dev/wxt/commit/d1b5230))
+
+## v0.9.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.9.0...v0.9.1)
+
+### 🚀 Enhancements
+
+- Add `alias` config for customizing path aliases ([#216](https://github.com/wxt-dev/wxt/pull/216))
+
+### 🩹 Fixes
+
+- Move `webextension-polyfill` from peer to regular dependencies ([609ae2a](https://github.com/wxt-dev/wxt/commit/609ae2a))
+- Generate valid manifest for Firefox MV3 ([#229](https://github.com/wxt-dev/wxt/pull/229))
+
+### 📖 Documentation
+
+- Add examples ([c81dfff](https://github.com/wxt-dev/wxt/commit/c81dfff))
+- Improve the "Used By" section on homepage ([#220](https://github.com/wxt-dev/wxt/pull/220))
+- Add UltraWideo to homepage ([#193](https://github.com/wxt-dev/wxt/pull/193))
+- Add StayFree to homepage ([#221](https://github.com/wxt-dev/wxt/pull/221))
+- Update feature comparison ([67ffa44](https://github.com/wxt-dev/wxt/commit/67ffa44))
+
+### 🏡 Chore
+
+- Remove whitespace from generated `.wxt` files ([#211](https://github.com/wxt-dev/wxt/pull/211))
+- Upgrade templates to `wxt@^0.9.0` ([#214](https://github.com/wxt-dev/wxt/pull/214))
+- Update Vite dependency range to `^4.0.0 || ^5.0.0-0` ([f1e8084](https://github.com/wxt-dev/wxt/commit/f1e8084be89e512dde441b9197a99183c497f67d))
+
+### 🤖 CI
+
+- Automatically sync GitHub releases with `CHANGELOG.md` on push ([#218](https://github.com/wxt-dev/wxt/pull/218))
+
+### ❤️ Contributors
+
+- Aaron Klinker ([@aaronklinker-st](http://github.com/aaronklinker-st))
+
+## v0.9.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.7...v0.9.0)
+
+### 🩹 Fixes
+
+- ⚠️ Remove `lib` from `.wxt/tsconfig.json` ([#209](https://github.com/wxt-dev/wxt/pull/209))
+
+### 📖 Documentation
+
+- Fix heading ([345406f](https://github.com/wxt-dev/wxt/commit/345406f))
+- Add demo video ([#208](https://github.com/wxt-dev/wxt/pull/208))
+
+### 🏡 Chore
+
+- Fix Svelte and React template READMEs ([#207](https://github.com/wxt-dev/wxt/pull/207))
+
+#### ⚠️ Breaking Changes
+
+- ⚠️ Removed [`"WebWorker"` types](https://www.typescriptlang.org/tsconfig/lib.html) from `.wxt/tsconfig.json` ([#209](https://github.com/wxt-dev/wxt/pull/209)). These types are useful for MV3 projects using a service worker. To add them back to your project, add the following to your project's TSConfig:
+ ```diff
+ {
+ "extends": "./.wxt/tsconfig.json",
+ + "compilerOptions": {
+ + "lib": ["ESNext", "DOM", "WebWorker"]
+ + }
+ }
+ ```
+
+### ❤️ Contributors
+
+- yyyanghj ([@yyyanghj](https://github.com/yyyanghj))
+
+## v0.8.7
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.6...v0.8.7)
+
+### 🚀 Enhancements
+
+- `createContentScriptIframe` utility ([#206](https://github.com/wxt-dev/wxt/pull/206))
+
+### 🏡 Chore
+
+- **deps-dev:** Bump happy-dom from 12.4.0 to 12.10.3 ([#194](https://github.com/wxt-dev/wxt/pull/194))
+- **deps-dev:** Bump tsx from 3.12.8 to 3.14.0 ([#198](https://github.com/wxt-dev/wxt/pull/198))
+- Upgrade types ([f3874da](https://github.com/wxt-dev/wxt/commit/f3874da))
+- **deps-dev:** Upgrade `lint-staged` to `^15.0.2` ([5f74a54](https://github.com/wxt-dev/wxt/commit/5f74a54))
+- **deps-dev:** Upgrade `execa` to `^8.0.1` ([#200](https://github.com/wxt-dev/wxt/pull/200))
+- **deps-dev:** Upgrade `typedoc` to `^0.25.3` ([#201](https://github.com/wxt-dev/wxt/pull/201))
+- **deps-dev:** Upgrade `vue` to `3.3.7` ([0b8d101](https://github.com/wxt-dev/wxt/commit/0b8d101))
+- **deps-dev:** Upgrade `vitepress` to `1.0.0-rc.24` ([5de18e5](https://github.com/wxt-dev/wxt/commit/5de18e5))
+- **deps-dev:** Update `@type/*` packages for demo ([cd4d00e](https://github.com/wxt-dev/wxt/commit/cd4d00e))
+- **deps-dev:** Update `sass` to `1.69.5` ([183bb02](https://github.com/wxt-dev/wxt/commit/183bb02))
+- Improve prettier git hook ([0f09cbe](https://github.com/wxt-dev/wxt/commit/0f09cbe))
+- Run E2E tests in parallel ([#204](https://github.com/wxt-dev/wxt/pull/204))
+
+### 🤖 CI
+
+- Separate validation into multiple jobs ([#203](https://github.com/wxt-dev/wxt/pull/203))
+
+## v0.8.6
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.5...v0.8.6)
+
+### 🩹 Fixes
+
+- Inline WXT modules inside `WxtVitest` plugin ([b75c553](https://github.com/wxt-dev/wxt/commit/b75c553))
+
+## v0.8.5
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.4...v0.8.5)
+
+### 🚀 Enhancements
+
+- Refactor project structure to export `initialize`, `prepare`, and `zip` functions ([#182](https://github.com/wxt-dev/wxt/pull/182))
+
+### 🩹 Fixes
+
+- Enable Vue SFC auto-imports in `vue` template ([f8a0fb3](https://github.com/wxt-dev/wxt/commit/f8a0fb3))
+
+### 📖 Documentation
+
+- Improve `runner.binaries` documentation ([d9e9b43](https://github.com/wxt-dev/wxt/commit/d9e9b43))
+- Update auto-imports.md ([#186](https://github.com/wxt-dev/wxt/pull/186))
+- Add `test.server.deps.inline` to Vitest guide ([19756c6](https://github.com/wxt-dev/wxt/commit/19756c6))
+
+### 🏡 Chore
+
+- Update template docs ([2e24b9e](https://github.com/wxt-dev/wxt/commit/2e24b9e))
+- Reduce package size by 70%, 1.92 MB to 590 kB ([#190](https://github.com/wxt-dev/wxt/pull/190))
+
+### ❤️ Contributors
+
+- Nenad Novaković ([@dvlden](https://github.com/dvlden))
+
+## v0.8.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.3...v0.8.4)
+
+### 🩹 Fixes
+
+- Allow actions without a popup ([#181](https://github.com/wxt-dev/wxt/pull/181))
+
+## v0.8.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.2...v0.8.3)
+
+### 🚀 Enhancements
+
+- Add testing utils under `wxt/testing` ([#178](https://github.com/wxt-dev/wxt/pull/178))
+
+## v0.8.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.1...v0.8.2)
+
+### 🩹 Fixes
+
+- **firefox:** Stop extending `AbortController` to fix crash in content scripts ([#176](https://github.com/wxt-dev/wxt/pull/176))
+
+### 🏡 Chore
+
+- Improve output consistency ([#175](https://github.com/wxt-dev/wxt/pull/175))
+
+## v0.8.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.0...v0.8.1)
+
+### 🩹 Fixes
+
+- Output `action.browser_style` correctly ([6a93f20](https://github.com/wxt-dev/wxt/commit/6a93f20))
+
+### 📖 Documentation
+
+- Generate full API docs with typedoc ([#174](https://github.com/wxt-dev/wxt/pull/174))
+
+## v0.8.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.5...v0.8.0)
+
+### 🚀 Enhancements
+
+- ⚠️ Use `defineUnlistedScript` to define unlisted scripts ([#167](https://github.com/wxt-dev/wxt/pull/167))
+
+### 📖 Documentation
+
+- Fix wrong links ([#166](https://github.com/wxt-dev/wxt/pull/166))
+
+### 🌊 Types
+
+- ⚠️ Rename `BackgroundScriptDefintition` to `BackgroundDefinition` ([446f265](https://github.com/wxt-dev/wxt/commit/446f265))
+
+#### ⚠️ Breaking Changes
+
+- ⚠️ Unlisted scripts must now `export default defineUnlistedScript(...)` ([#167](https://github.com/wxt-dev/wxt/pull/167))
+- ⚠️ Rename `BackgroundScriptDefintition` to `BackgroundDefinition` ([446f265](https://github.com/wxt-dev/wxt/commit/446f265))
+
+### ❤️ Contributors
+
+- 渣渣120 [@WOSHIZHAZHA120](https://github.com/WOSHIZHAZHA120)
+
+## v0.7.5
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.4...v0.7.5)
+
+### 🩹 Fixes
+
+- More consistent `version_name` generation between browsers ([#163](https://github.com/wxt-dev/wxt/pull/163))
+- Ignore non-manifest fields when merging content script entries ([#164](https://github.com/wxt-dev/wxt/pull/164))
+- Add `browser_style` to popup options ([#165](https://github.com/wxt-dev/wxt/pull/165))
+
+## v0.7.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.3...v0.7.4)
+
+### 🩹 Fixes
+
+- Support `react-refresh` when pre-rendering HTML pages in dev mode ([#158](https://github.com/wxt-dev/wxt/pull/158))
+
+### 📖 Documentation
+
+- Add migration guides ([b58fb02](https://github.com/wxt-dev/wxt/commit/b58fb02))
+
+### 🏡 Chore
+
+- Upgrade templates to v0.7 ([#156](https://github.com/wxt-dev/wxt/pull/156))
+
+## v0.7.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.2...v0.7.3)
+
+### 🚀 Enhancements
+
+- Support JS entrypoints ([#155](https://github.com/wxt-dev/wxt/pull/155))
+
+## v0.7.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.1...v0.7.2)
+
+### 🚀 Enhancements
+
+- Allow customizing entrypoint options per browser ([#154](https://github.com/wxt-dev/wxt/pull/154))
+
+### 🩹 Fixes
+
+- Default safari to MV2 ([5807931](https://github.com/wxt-dev/wxt/commit/5807931))
+- Add missing `persistent` type to `defineBackgroundScript` ([d9fdcb5](https://github.com/wxt-dev/wxt/commit/d9fdcb5))
+
+### 📖 Documentation
+
+- Restructure website to improve UX ([#149](https://github.com/wxt-dev/wxt/pull/149))
+- Add docs for development and testing ([f58d69d](https://github.com/wxt-dev/wxt/commit/f58d69d))
+
+### 🏡 Chore
+
+- **deps-dev:** Bump @types/fs-extra from 11.0.1 to 11.0.2 ([#144](https://github.com/wxt-dev/wxt/pull/144))
+- **deps-dev:** Bump @faker-js/faker from 8.0.2 to 8.1.0 ([#146](https://github.com/wxt-dev/wxt/pull/146))
+- **deps-dev:** Bump vitest-mock-extended from 1.2.1 to 1.3.0 ([#147](https://github.com/wxt-dev/wxt/pull/147))
+- **deps-dev:** Bump vitest from 0.34.3 to 0.34.6 ([#145](https://github.com/wxt-dev/wxt/pull/145))
+- **deps-dev:** Bump typescript from 5.1 to 5.2 ([#148](https://github.com/wxt-dev/wxt/pull/148))
+
+## v0.7.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.0...v0.7.1)
+
+### 🚀 Enhancements
+
+- `createContentScriptUi` helper ([#143](https://github.com/wxt-dev/wxt/pull/143))
+
+### 📖 Documentation
+
+- Add docs for `createContentScriptUi` ([65fcfc0](https://github.com/wxt-dev/wxt/commit/65fcfc0))
+
+### 🏡 Chore
+
+- **release:** V0.7.1-alpha1 ([2d4983e](https://github.com/wxt-dev/wxt/commit/2d4983e))
+
+## v0.7.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.6...v0.7.0)
+
+### 🚀 Enhancements
+
+- Content script `cssInjectionMode` ([#141](https://github.com/wxt-dev/wxt/pull/141))
+
+### 🩹 Fixes
+
+- Validate transformed manifest correctly ([4b2012c](https://github.com/wxt-dev/wxt/commit/4b2012c))
+- ⚠️ Output content script CSS to `content-scripts/.css` ([#140](https://github.com/wxt-dev/wxt/pull/140))
+- Reorder typescript paths to give priority to `@` and `~` over `@@` and `~~` ([#142](https://github.com/wxt-dev/wxt/pull/142))
+
+### 🏡 Chore
+
+- Store user config metadata in memory ([0591050](https://github.com/wxt-dev/wxt/commit/0591050))
+
+#### ⚠️ Breaking Changes
+
+- ⚠️ Content script CSS used to be output to `assets/.css`, but is now `content-scripts/.css` to match the docs. ([#140](https://github.com/wxt-dev/wxt/pull/140))
+
+## v0.6.6
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.5...v0.6.6)
+
+### 🚀 Enhancements
+
+- Disable opening browser automatically during dev mode ([#136](https://github.com/wxt-dev/wxt/pull/136))
+
+## v0.6.5
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.4...v0.6.5)
+
+### 🩹 Fixes
+
+- Don't crash when `` matches is used in dev mode ([b48cee9](https://github.com/wxt-dev/wxt/commit/b48cee9))
+- Support loading `tsx` entrypoints ([#134](https://github.com/wxt-dev/wxt/pull/134))
+
+### 📖 Documentation
+
+- Add tags for SEO and socials ([96be879](https://github.com/wxt-dev/wxt/commit/96be879))
+- Add more content to the homepage ([5570793](https://github.com/wxt-dev/wxt/commit/5570793))
+- Fix DX section sizing ([41e1549](https://github.com/wxt-dev/wxt/commit/41e1549))
+- Add link to update extensions using WXT ([24e69fe](https://github.com/wxt-dev/wxt/commit/24e69fe))
+
+### 🏡 Chore
+
+- Code coverage improvements ([#131](https://github.com/wxt-dev/wxt/pull/131))
+
+## v0.6.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.3...v0.6.4)
+
+### 🩹 Fixes
+
+- **content-scripts:** Don't throw an error when including `include` or `exclude` options on a content script ([455e7f3](https://github.com/wxt-dev/wxt/commit/455e7f3))
+- Use `execaCommand` instead of `node:child_process` ([#130](https://github.com/wxt-dev/wxt/pull/130))
+
+### 🏡 Chore
+
+- **templates:** Add `.wxt` directory to gitignore ([#129](https://github.com/wxt-dev/wxt/pull/129))
+- Increase E2E test timeout ([5482b2f](https://github.com/wxt-dev/wxt/commit/5482b2f))
+
+## v0.6.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.2...v0.6.3)
+
+### 🚀 Enhancements
+
+- **client:** Add `block` and `addEventListener` utils to `ContentScriptContext` ([#128](https://github.com/wxt-dev/wxt/pull/128))
+
+## v0.6.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.1...v0.6.2)
+
+### 🚀 Enhancements
+
+- `--analyze` build flag ([#125](https://github.com/wxt-dev/wxt/pull/125))
+- Show spinner when building entrypoints ([#126](https://github.com/wxt-dev/wxt/pull/126))
+
+### 📖 Documentation
+
+- Fix import typo ([4c43072](https://github.com/wxt-dev/wxt/commit/4c43072))
+- Update vite docs to use function ([e0929a6](https://github.com/wxt-dev/wxt/commit/e0929a6))
+
+## v0.6.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.0...v0.6.1)
+
+### 🚀 Enhancements
+
+- Add `transformManifest` option ([#124](https://github.com/wxt-dev/wxt/pull/124))
+
+### 🩹 Fixes
+
+- Don't open browser during development when using WSL ([#123](https://github.com/wxt-dev/wxt/pull/123))
+
+### 📖 Documentation
+
+- Load extension details from CWS ([8e0a189](https://github.com/wxt-dev/wxt/commit/8e0a189))
+
+## v0.6.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.6...v0.6.0)
+
+### 🚀 Enhancements
+
+- Export `ContentScriptContext` from `wxt/client` ([1f448d1](https://github.com/wxt-dev/wxt/commit/1f448d1))
+- ⚠️ Require a function for `vite` configuration ([#121](https://github.com/wxt-dev/wxt/pull/121))
+
+### 🩹 Fixes
+
+- Use the same mode for each build step ([1f6a931](https://github.com/wxt-dev/wxt/commit/1f6a931))
+- Disable dev logs in production ([3f260ee](https://github.com/wxt-dev/wxt/commit/3f260ee))
+
+#### ⚠️ Breaking Changes
+
+- ⚠️ The `vite` config option must now be a function. If you were using an object before, change it from `vite: { ... }` to `vite: () => ({ ... })`. ([#121](https://github.com/wxt-dev/wxt/pull/121))
+
+## v0.5.6
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.5...v0.5.6)
+
+### 🚀 Enhancements
+
+- Add `ContentScriptContext` util for stopping invalidated content scripts ([#120](https://github.com/wxt-dev/wxt/pull/120))
+
+## v0.5.5
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.4...v0.5.5)
+
+### 🩹 Fixes
+
+- Automatically replace vite's `process.env.NODE_ENV` output in lib mode with the mode ([92039b8](https://github.com/wxt-dev/wxt/commit/92039b8))
+
+## v0.5.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.3...v0.5.4)
+
+### 🩹 Fixes
+
+- Recognize `background/index.ts` as an entrypoint ([419fab8](https://github.com/wxt-dev/wxt/commit/419fab8))
+- Don't warn about deep entrypoint subdirectories not being recognized ([87e8df9](https://github.com/wxt-dev/wxt/commit/87e8df9))
+
+## v0.5.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.2...v0.5.3)
+
+### 🩹 Fixes
+
+- Allow function for vite config ([4ec904e](https://github.com/wxt-dev/wxt/commit/4ec904e))
+
+### 🏡 Chore
+
+- Refactor how config is resolved ([#118](https://github.com/wxt-dev/wxt/pull/118))
+
+## v0.5.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.1...v0.5.2)
+
+### 🩹 Fixes
+
+- Import client utils when getting entrypoint config ([#117](https://github.com/wxt-dev/wxt/pull/117))
+
+## v0.5.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.0...v0.5.1)
+
+### 🚀 Enhancements
+
+- Allow disabling auto-imports ([#114](https://github.com/wxt-dev/wxt/pull/114))
+- Include/exclude entrypoints based on target browser ([#115](https://github.com/wxt-dev/wxt/pull/115))
+
+### 🩹 Fixes
+
+- Allow any string for target browser ([b4de93d](https://github.com/wxt-dev/wxt/commit/b4de93d))
+
+## v0.5.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.4.1...v0.5.0)
+
+### 🩹 Fixes
+
+- **types:** Don't write to files if nothing changes ([#107](https://github.com/wxt-dev/wxt/pull/107))
+- ⚠️ Change default `publicDir` to `/public` ([5f15f9c](https://github.com/wxt-dev/wxt/commit/5f15f9c))
+
+### 📖 Documentation
+
+- Add link to examples repo ([46a5036](https://github.com/wxt-dev/wxt/commit/46a5036))
+- Fix typos ([beafa6a](https://github.com/wxt-dev/wxt/commit/beafa6a))
+- Make README pretty ([b33b663](https://github.com/wxt-dev/wxt/commit/b33b663))
+- Add migration docs ([e2350fe](https://github.com/wxt-dev/wxt/commit/e2350fe))
+- Add vite customization docs ([fe966b6](https://github.com/wxt-dev/wxt/commit/fe966b6))
+
+### 🏡 Chore
+
+- Move repo to wxt-dev org ([ac7cbfc](https://github.com/wxt-dev/wxt/commit/ac7cbfc))
+- **deps-dev:** Bump prettier from 3.0.1 to 3.0.3 ([#111](https://github.com/wxt-dev/wxt/pull/111))
+- **deps-dev:** Bump tsx from 3.12.7 to 3.12.8 ([#109](https://github.com/wxt-dev/wxt/pull/109))
+- **deps-dev:** Bump @types/node from 20.5.0 to 20.5.9 ([#110](https://github.com/wxt-dev/wxt/pull/110))
+- Add entrypoints debug log ([dbd84c8](https://github.com/wxt-dev/wxt/commit/dbd84c8))
+
+#### ⚠️ Breaking Changes
+
+- ⚠️ Change default `publicDir` to `/public` ([5f15f9c](https://github.com/wxt-dev/wxt/commit/5f15f9c))
+
+## v0.4.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.4.0...v0.4.1)
+
+### 🚀 Enhancements
+
+- **cli:** Add `wxt clean` command to delete generated files ([#106](https://github.com/wxt-dev/wxt/pull/106))
+
+### 🩹 Fixes
+
+- **init:** Don't show `cd .` when initializing the current directory ([e086374](https://github.com/wxt-dev/wxt/commit/e086374))
+
+## v0.4.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.3.2...v0.4.0)
+
+### 🚀 Enhancements
+
+- Add `--debug` flag for printing debug logs for all CLI commands ([#75](https://github.com/wxt-dev/wxt/pull/75))
+- Replace `web-ext` with `web-ext-run` ([#101](https://github.com/wxt-dev/wxt/pull/101))
+- Generate types for `browser.i18n.getMessage` ([#103](https://github.com/wxt-dev/wxt/pull/103))
+
+### 🩹 Fixes
+
+- Allow adding custom content scripts ([b428a62](https://github.com/wxt-dev/wxt/commit/b428a62))
+- Don't overwrite `wxt.config.ts` content scripts, append entrypoints to it ([5f5f1d9](https://github.com/wxt-dev/wxt/commit/5f5f1d9))
+- ⚠️ Use relative path aliases inside `.wxt/tsconfig.json` ([#102](https://github.com/wxt-dev/wxt/pull/102))
+
+### 📖 Documentation
+
+- Add contribution guide ([#76](https://github.com/wxt-dev/wxt/pull/76))
+
+### 🏡 Chore
+
+- Setup dependabot for upgrading dependencies ([d66293c](https://github.com/wxt-dev/wxt/commit/d66293c))
+- Update social preview ([e164bd5](https://github.com/wxt-dev/wxt/commit/e164bd5))
+- Setup bug and feature issue templates ([2bde917](https://github.com/wxt-dev/wxt/commit/2bde917))
+- Upgrade to prettier 3 ([#77](https://github.com/wxt-dev/wxt/pull/77))
+- **deps-dev:** Bump vitest from 0.32.4 to 0.34.1 ([#81](https://github.com/wxt-dev/wxt/pull/81))
+- **deps-dev:** Bump ora from 6.3.1 to 7.0.1 ([#79](https://github.com/wxt-dev/wxt/pull/79))
+- **deps-dev:** Bump @types/node from 20.4.5 to 20.5.0 ([#78](https://github.com/wxt-dev/wxt/pull/78))
+- **deps-dev:** Bump tsup from 7.1.0 to 7.2.0 ([#80](https://github.com/wxt-dev/wxt/pull/80))
+- **deps-dev:** Bump @vitest/coverage-v8 from 0.32.4 to 0.34.1 ([#84](https://github.com/wxt-dev/wxt/pull/84))
+- **deps-dev:** Bump vitepress from 1.0.0-beta.5 to 1.0.0-rc.4 ([#85](https://github.com/wxt-dev/wxt/pull/85))
+- **deps-dev:** Bump vitest-mock-extended from 1.1.4 to 1.2.0 ([#87](https://github.com/wxt-dev/wxt/pull/87))
+- **deps-dev:** Bump lint-staged from 13.3.0 to 14.0.0 ([#89](https://github.com/wxt-dev/wxt/pull/89))
+- Fix remote code E2E test ([83e62a1](https://github.com/wxt-dev/wxt/commit/83e62a1))
+- Fix failing demo build ([b58a15e](https://github.com/wxt-dev/wxt/commit/b58a15e))
+- **deps-dev:** Bump vitest-mock-extended from 1.2.0 to 1.2.1 ([#97](https://github.com/wxt-dev/wxt/pull/97))
+- **deps-dev:** Bump lint-staged from 14.0.0 to 14.0.1 ([#100](https://github.com/wxt-dev/wxt/pull/100))
+- **deps-dev:** Bump vitest from 0.34.1 to 0.34.3 ([#99](https://github.com/wxt-dev/wxt/pull/99))
+- Increase E2E test timeout because GitHub Actions Window runner is slow ([2a0842b](https://github.com/wxt-dev/wxt/commit/2a0842b))
+- **deps-dev:** Bump vitepress from 1.0.0-rc.4 to 1.0.0-rc.10 ([#96](https://github.com/wxt-dev/wxt/pull/96))
+- Fix test watcher restarting indefinitely ([2c7922c](https://github.com/wxt-dev/wxt/commit/2c7922c))
+- Remove explicit icon config from templates ([93bfee0](https://github.com/wxt-dev/wxt/commit/93bfee0))
+- Use import aliases in Vue template ([#104](https://github.com/wxt-dev/wxt/pull/104))
+
+#### ⚠️ Breaking Changes
+
+- ⚠️ Use relative path aliases inside `.wxt/tsconfig.json` ([#102](https://github.com/wxt-dev/wxt/pull/102))
+
+## v0.3.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.3.1...v0.3.2)
+
+### 🚀 Enhancements
+
+- Discover icons from the public directory ([#72](https://github.com/wxt-dev/wxt/pull/72))
+- Don't allow auto-importing from subdirectories ([d54d611](https://github.com/wxt-dev/wxt/commit/d54d611))
+
+### 📖 Documentation
+
+- Document the `url:` import prefix for remote code ([323045a](https://github.com/wxt-dev/wxt/commit/323045a))
+- Fix typos ([97f0938](https://github.com/wxt-dev/wxt/commit/97f0938))
+- Fix capitalization ([39467d1](https://github.com/wxt-dev/wxt/commit/39467d1))
+- Generate markdown for config reference ([#74](https://github.com/wxt-dev/wxt/pull/74))
+
+### 🏡 Chore
+
+- Upgrade dependencies ([798f02f](https://github.com/wxt-dev/wxt/commit/798f02f))
+- Upgrade vite (`v4.3` → `v4.4`) ([547c185](https://github.com/wxt-dev/wxt/commit/547c185))
+- Update templates to work with CSS entrypoints ([7f15305](https://github.com/wxt-dev/wxt/commit/7f15305))
+- Improve file list output in CI ([#73](https://github.com/wxt-dev/wxt/pull/73))
+
+### 🤖 CI
+
+- Validate templates against `main` ([#66](https://github.com/wxt-dev/wxt/pull/66))
+- List vite version when validating project templates ([ef140dc](https://github.com/wxt-dev/wxt/commit/ef140dc))
+- Validate templates using tarball to avoid version conflicts within the `wxt/node_modules` directory ([edfa075](https://github.com/wxt-dev/wxt/commit/edfa075))
+
+## v0.3.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.3.0...v0.3.1)
+
+### 🚀 Enhancements
+
+- CSS entrypoints ([#61](https://github.com/wxt-dev/wxt/pull/61))
+- `init` command for bootstrapping new projects ([#65](https://github.com/wxt-dev/wxt/pull/65))
+
+### 📖 Documentation
+
+- Add zip command to installation scripts ([94a1097](https://github.com/wxt-dev/wxt/commit/94a1097))
+- Add output paths to entrypoint docs ([3a336eb](https://github.com/wxt-dev/wxt/commit/3a336eb))
+- Update installation docs ([aea866c](https://github.com/wxt-dev/wxt/commit/aea866c))
+- Add publishing docs ([4184b05](https://github.com/wxt-dev/wxt/commit/4184b05))
+- Add a section for extensions using WXT ([709b61a](https://github.com/wxt-dev/wxt/commit/709b61a))
+- Add a comparison page to compare and contrast against Plasmo ([38d4f9c](https://github.com/wxt-dev/wxt/commit/38d4f9c))
+
+### 🏡 Chore
+
+- Update template projects to v0.3 ([#56](https://github.com/wxt-dev/wxt/pull/56))
+- Branding and logo ([#60](https://github.com/wxt-dev/wxt/pull/60))
+- Simplify binary setup ([#62](https://github.com/wxt-dev/wxt/pull/62))
+- Add Solid template ([#63](https://github.com/wxt-dev/wxt/pull/63))
+- Increase E2E test timeout to fix flakey test ([dfe424f](https://github.com/wxt-dev/wxt/commit/dfe424f))
+
+### 🤖 CI
+
+- Speed up demo validation ([3a9fd39](https://github.com/wxt-dev/wxt/commit/3a9fd39))
+- Fix flakey failure when validating templates ([25677ba](https://github.com/wxt-dev/wxt/commit/25677ba))
+
+### ❤️ Contributors
+
+- BeanWei ([@BeanWei](https://github.com/BeanWei))
+
+## v0.3.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.5...v0.3.0)
+
+### 🚀 Enhancements
+
+- ⚠️ Add type safety to `browser.runtime.getURL` ([58a84ec](https://github.com/wxt-dev/wxt/commit/58a84ec))
+- ⚠️ Change default `publicDir` to `/public` ([19c0948](https://github.com/wxt-dev/wxt/commit/19c0948))
+- Windows support ([#50](https://github.com/wxt-dev/wxt/pull/50))
+
+### 🩹 Fixes
+
+- Add `WebWorker` lib to generated tsconfig ([2c70246](https://github.com/wxt-dev/wxt/commit/2c70246))
+
+### 📖 Documentation
+
+- Update entrypoint directory links ([0aebb67](https://github.com/wxt-dev/wxt/commit/0aebb67))
+
+### 🌊 Types
+
+- Allow any string for the `__BROWSER__` global ([6092235](https://github.com/wxt-dev/wxt/commit/6092235))
+
+### 🤖 CI
+
+- Improve checks against `demo/` extension ([9cc464f](https://github.com/wxt-dev/wxt/commit/9cc464f))
+
+#### ⚠️ Breaking Changes
+
+- ⚠️ Add type safety to `browser.runtime.getURL` ([58a84ec](https://github.com/wxt-dev/wxt/commit/58a84ec))
+- ⚠️ Change default `publicDir` to `/public` ([19c0948](https://github.com/wxt-dev/wxt/commit/19c0948))
+
+## v0.2.5
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.4...v0.2.5)
+
+### 🚀 Enhancements
+
+- Auto-import from subdirectories ([547fee0](https://github.com/wxt-dev/wxt/commit/547fee0))
+- Include background script in dev mode if user doesn't define one ([ca20a21](https://github.com/wxt-dev/wxt/commit/ca20a21))
+
+### 🩹 Fixes
+
+- Don't crash when generating types in dev mode ([d8c1903](https://github.com/wxt-dev/wxt/commit/d8c1903))
+- Properly load entrypoints that reference `import.meta` ([54b18cc](https://github.com/wxt-dev/wxt/commit/54b18cc))
+
+### 🏡 Chore
+
+- Update templates to wxt@0.2 ([9d00eb2](https://github.com/wxt-dev/wxt/commit/9d00eb2))
+
+### 🤖 CI
+
+- Validate project templates ([9ac756f](https://github.com/wxt-dev/wxt/commit/9ac756f))
+
+## v0.2.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.3...v0.2.4)
+
+### 🚀 Enhancements
+
+- Add `wxt zip` command ([#47](https://github.com/wxt-dev/wxt/pull/47))
+
+## v0.2.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.2...v0.2.3)
+
+### 🩹 Fixes
+
+- Correctly lookup open port ([#45](https://github.com/wxt-dev/wxt/pull/45))
+- Read boolean maniest options from meta tags correctly ([495c5c8](https://github.com/wxt-dev/wxt/commit/495c5c8))
+- Some fields cannot be overridden from `config.manifest` ([#46](https://github.com/wxt-dev/wxt/pull/46))
+
+## v0.2.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.1...v0.2.2)
+
+### 🩹 Fixes
+
+- Register content scripts correctly in dev mode ([2fb5a54](https://github.com/wxt-dev/wxt/commit/2fb5a54))
+
+## v0.2.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.0...v0.2.1)
+
+### 🚀 Enhancements
+
+- Support all content script options ([6f5bf89](https://github.com/wxt-dev/wxt/commit/6f5bf89))
+
+### 🩹 Fixes
+
+- Remove HMR log ([90fa6bf](https://github.com/wxt-dev/wxt/commit/90fa6bf))
+
+## v0.2.0
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.6...v0.2.0)
+
+### 🚀 Enhancements
+
+- ⚠️ Rename `defineBackgroundScript` to `defineBackground` ([5b48ae9](https://github.com/wxt-dev/wxt/commit/5b48ae9))
+- Recongize unnamed content scripts (`content.ts` and `content/index.ts`) ([3db5cec](https://github.com/wxt-dev/wxt/commit/3db5cec))
+
+### 📖 Documentation
+
+- Update templates ([f28a29e](https://github.com/wxt-dev/wxt/commit/f28a29e))
+- Add docs for each type of entrypoint ([77cbfc1](https://github.com/wxt-dev/wxt/commit/77cbfc1))
+- Add inline JSDoc for public types ([375a2a6](https://github.com/wxt-dev/wxt/commit/375a2a6))
+
+### 🏡 Chore
+
+- Run `wxt prepare` on `postinstall` ([c1ea9ba](https://github.com/wxt-dev/wxt/commit/c1ea9ba))
+- Don't format lockfile ([5c7e041](https://github.com/wxt-dev/wxt/commit/5c7e041))
+
+#### ⚠️ Breaking Changes
+
+- ⚠️ Rename `defineBackgroundScript` to `defineBackground` ([5b48ae9](https://github.com/wxt-dev/wxt/commit/5b48ae9))
+
+## v0.1.6
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.5...v0.1.6)
+
+### 🩹 Fixes
+
+- Resolve tsconfig paths in vite ([ea92a27](https://github.com/wxt-dev/wxt/commit/ea92a27))
+- Add logs when a hot reload happens ([977246f](https://github.com/wxt-dev/wxt/commit/977246f))
+
+### 🏡 Chore
+
+- React and Vue starter templates ([#33](https://github.com/wxt-dev/wxt/pull/33))
+- Svelte template ([#34](https://github.com/wxt-dev/wxt/pull/34))
+
+## v0.1.5
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.4...v0.1.5)
+
+### 🩹 Fixes
+
+- Include `vite/client` types ([371be99](https://github.com/wxt-dev/wxt/commit/371be99))
+
+## v0.1.4
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.3...v0.1.4)
+
+### 🩹 Fixes
+
+- Fix regression where manifest was not listed first in build summary ([fa2b656](https://github.com/wxt-dev/wxt/commit/fa2b656))
+- Fix config hook implementations for vite plugins ([49965e7](https://github.com/wxt-dev/wxt/commit/49965e7))
+
+### 📖 Documentation
+
+- Update CLI screenshot ([0a26673](https://github.com/wxt-dev/wxt/commit/0a26673))
+
+### 🏡 Chore
+
+- Update prettier ignore ([68611ae](https://github.com/wxt-dev/wxt/commit/68611ae))
+
+## v0.1.3
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.2...v0.1.3)
+
+### 🚀 Enhancements
+
+- Add tsconfig path aliases ([#32](https://github.com/wxt-dev/wxt/pull/32))
+
+### 🩹 Fixes
+
+- Merge `manifest` option from both inline and user config ([05ca998](https://github.com/wxt-dev/wxt/commit/05ca998))
+- Cleanup build summary with sourcemaps ([ac0b28e](https://github.com/wxt-dev/wxt/commit/ac0b28e))
+
+### 📖 Documentation
+
+- Create documentation site ([#31](https://github.com/wxt-dev/wxt/pull/31))
+
+### 🏡 Chore
+
+- Upgrade to pnpm 8 ([0ce7c9d](https://github.com/wxt-dev/wxt/commit/0ce7c9d))
+
+## v0.1.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.1...v0.1.2)
+
+### 🚀 Enhancements
+
+- Accept a function for `config.manifest` ([ee49837](https://github.com/wxt-dev/wxt/commit/ee49837))
+
+### 🩹 Fixes
+
+- Add missing types for `webextension-polyfill` and the `manifest` option ([636aa48](https://github.com/wxt-dev/wxt/commit/636aa48))
+- Only add imports to JS files ([b29c3c6](https://github.com/wxt-dev/wxt/commit/b29c3c6))
+- Generate valid type for `EntrypointPath` when there are no entrypoints ([6e7184d](https://github.com/wxt-dev/wxt/commit/6e7184d))
+
+### 🌊 Types
+
+- Change `config.vite` to `UserConfig` ([ef6001e](https://github.com/wxt-dev/wxt/commit/ef6001e))
+
+## v0.1.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.0...v0.1.1)
+
+### 🩹 Fixes
+
+- Allow dashes in entrypoint names ([2e51e73](https://github.com/wxt-dev/wxt/commit/2e51e73))
+- Unable to read entrypoint options ([#28](https://github.com/wxt-dev/wxt/pull/28))
+
+## v0.1.0
+
+Initial release of WXT. Full support for production builds and initial toolkit for development:
+
+- HMR support when HTML page dependencies change
+- Reload extension when background changes
+- Reload HTML pages when saving them directly
+- Re-register and reload tabs when content scripts change
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.0.2...v0.1.0)
+
+### 🚀 Enhancements
+
+- Content scripts reloading ([#25](https://github.com/wxt-dev/wxt/pull/25))
+
+### 📖 Documentation
+
+- Update feature list ([0255028](https://github.com/wxt-dev/wxt/commit/0255028))
+
+### 🤖 CI
+
+- Create github release ([b7c078f](https://github.com/wxt-dev/wxt/commit/b7c078f))
+
+## v0.0.2
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.0.1...v0.0.2)
+
+### 🚀 Enhancements
+
+- Reload extension when source code is changed ([#17](https://github.com/wxt-dev/wxt/pull/17))
+- Setup background script web socket/reload ([#22](https://github.com/wxt-dev/wxt/pull/22))
+- Reload HTML files individually ([#23](https://github.com/wxt-dev/wxt/pull/23))
+
+### 🩹 Fixes
+
+- Output chunks to a chunks directory ([2dd7a99](https://github.com/wxt-dev/wxt/commit/2dd7a99))
+- Remove hash from content script css outputs ([#20](https://github.com/wxt-dev/wxt/pull/20))
+- Overwrite files with the same name when renaming entrypoints in dev mode ([37986bf](https://github.com/wxt-dev/wxt/commit/37986bf))
+- Separate template builds to prevent sharing chunks ([7f3a1e8](https://github.com/wxt-dev/wxt/commit/7f3a1e8))
+- Show Vite warnings and errors ([c51f0e0](https://github.com/wxt-dev/wxt/commit/c51f0e0))
+
+### 📖 Documentation
+
+- Add milestone progress badge to README ([684197d](https://github.com/wxt-dev/wxt/commit/684197d))
+- Fix milestone link in README ([e14f81d](https://github.com/wxt-dev/wxt/commit/e14f81d))
+
+### 🏡 Chore
+
+- Refactor build output type ([#19](https://github.com/wxt-dev/wxt/pull/19))
+- Refactor build outputs to support transpiled templates ([a78aada](https://github.com/wxt-dev/wxt/commit/a78aada))
+- Rename `templates` to `virtual-modules` ([#24](https://github.com/wxt-dev/wxt/pull/24))
+- Update cli screenshot ([54eb118](https://github.com/wxt-dev/wxt/commit/54eb118))
+
+## v0.0.1
+
+[compare changes](https://github.com/wxt-dev/wxt/compare/v0.0.0...v0.0.1)
+
+### 🚀 Enhancements
+
+- Add logger to config ([232ff7a](https://github.com/wxt-dev/wxt/commit/232ff7a))
+- Export and bootstrap the `/client` package ([5b07c95](https://github.com/wxt-dev/wxt/commit/5b07c95))
+- Resolve entrypoints based on filesystem ([a63f061](https://github.com/wxt-dev/wxt/commit/a63f061))
+- Separate output directories for each browser/manifest version ([f09ffbb](https://github.com/wxt-dev/wxt/commit/f09ffbb))
+- Build entrypoints and output `manifest.json` ([1e7c738](https://github.com/wxt-dev/wxt/commit/1e7c738))
+- Automatically add CSS files to content scripts ([047ce04](https://github.com/wxt-dev/wxt/commit/047ce04))
+- Download and bundle remote URL imports ([523c7df](https://github.com/wxt-dev/wxt/commit/523c7df))
+- Generate type declarations and config for project types and auto-imports ([21debad](https://github.com/wxt-dev/wxt/commit/21debad))
+- Good looking console output ([e2cc995](https://github.com/wxt-dev/wxt/commit/e2cc995))
+- Dev server working and a valid extension is built ([505e419](https://github.com/wxt-dev/wxt/commit/505e419))
+- Virtualized content script entrypoint ([ca29537](https://github.com/wxt-dev/wxt/commit/ca29537))
+- Provide custom, typed globals defined by Vite ([8c59a1c](https://github.com/wxt-dev/wxt/commit/8c59a1c))
+- Copy public directory to outputs ([1a25f2b](https://github.com/wxt-dev/wxt/commit/1a25f2b))
+- Support browser and chrome styles for mv2 popups ([7945c94](https://github.com/wxt-dev/wxt/commit/7945c94))
+- Support browser and chrome styles for mv2 popups ([7abb577](https://github.com/wxt-dev/wxt/commit/7abb577))
+- Support more CLI flags for `build` and `dev` ([#9](https://github.com/wxt-dev/wxt/pull/9))
+- Add more supported browser types ([f114c5b](https://github.com/wxt-dev/wxt/commit/f114c5b))
+- Open browser when starting dev server ([#11](https://github.com/wxt-dev/wxt/pull/11))
+
+### 🩹 Fixes
+
+- Support `srcDir` config ([739d19f](https://github.com/wxt-dev/wxt/commit/739d19f))
+- Root path customization now works ([4faa3b3](https://github.com/wxt-dev/wxt/commit/4faa3b3))
+- Print durations as ms/s based on total time ([3e37de9](https://github.com/wxt-dev/wxt/commit/3e37de9))
+- Don't print error twice when background crashes ([407627c](https://github.com/wxt-dev/wxt/commit/407627c))
+- Load package.json from root not cwd ([3ca16ee](https://github.com/wxt-dev/wxt/commit/3ca16ee))
+- Only allow a single entrypoint with a given name ([8eb4e86](https://github.com/wxt-dev/wxt/commit/8eb4e86))
+- Respect the mv2 popup type ([0f37ceb](https://github.com/wxt-dev/wxt/commit/0f37ceb))
+- Respect background type and persistent manifest options ([573ef80](https://github.com/wxt-dev/wxt/commit/573ef80))
+- Make content script array orders consistent ([f380378](https://github.com/wxt-dev/wxt/commit/f380378))
+- Firefox manifest warnings in dev mode ([50bb845](https://github.com/wxt-dev/wxt/commit/50bb845))
+
+### 📖 Documentation
+
+- Update README ([785ea54](https://github.com/wxt-dev/wxt/commit/785ea54))
+- Update README ([99ccadb](https://github.com/wxt-dev/wxt/commit/99ccadb))
+- Update description ([07a262e](https://github.com/wxt-dev/wxt/commit/07a262e))
+- Update README ([58a0ef4](https://github.com/wxt-dev/wxt/commit/58a0ef4))
+- Update README ([23ed6f7](https://github.com/wxt-dev/wxt/commit/23ed6f7))
+- Add initial release milestone link to README ([b400e54](https://github.com/wxt-dev/wxt/commit/b400e54))
+- Fix typo in README ([5590c9d](https://github.com/wxt-dev/wxt/commit/5590c9d))
+
+### 🏡 Chore
+
+- Refactor cli files into their own directory ([e6c0d84](https://github.com/wxt-dev/wxt/commit/e6c0d84))
+- Simplify `BuildOutput` type ([1f6c4a0](https://github.com/wxt-dev/wxt/commit/1f6c4a0))
+- Move `.exvite` directory into `srcDir` instead of `root` ([53fb805](https://github.com/wxt-dev/wxt/commit/53fb805))
+- Refactor CLI commands ([b8952b6](https://github.com/wxt-dev/wxt/commit/b8952b6))
+- Improve build summary sorting ([ec57e8c](https://github.com/wxt-dev/wxt/commit/ec57e8c))
+- Remove comments ([e3e9c0d](https://github.com/wxt-dev/wxt/commit/e3e9c0d))
+- Refactor internal config creation ([7c634f4](https://github.com/wxt-dev/wxt/commit/7c634f4))
+- Check virtual entrypoints feature in README ([70208f4](https://github.com/wxt-dev/wxt/commit/70208f4))
+- Add E2E tests and convert to vitest workspace ([5813302](https://github.com/wxt-dev/wxt/commit/5813302))
+- Rename package to wxt ([51a1072](https://github.com/wxt-dev/wxt/commit/51a1072))
+- Fix header log's timestamp ([8ca5657](https://github.com/wxt-dev/wxt/commit/8ca5657))
+- Fix demo global usage ([1ecfedd](https://github.com/wxt-dev/wxt/commit/1ecfedd))
+- Refactor folder structure ([9ab3953](https://github.com/wxt-dev/wxt/commit/9ab3953))
+- Fix release workflow ([2e94f2a](https://github.com/wxt-dev/wxt/commit/2e94f2a))
+
+### 🤖 CI
+
+- Create validation workflow ([#12](https://github.com/wxt-dev/wxt/pull/12))
+- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
diff --git a/packages/wxt/README.md b/packages/wxt/README.md
new file mode 100644
index 0000000..d5e2c82
--- /dev/null
+++ b/packages/wxt/README.md
@@ -0,0 +1,70 @@
+
+
+ WXT
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Next-gen framework for developing web extensions.
+
+ ⚡
+
+ It's like Nuxt, but for Chrome Extensions
+
+
+
+ Get Started
+ •
+ Installation
+ •
+ Configuration
+ •
+ Examples
+ •
+ Discord
+
+
+
+
+## Demo
+
+https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94
+
+## Quick Start
+
+Bootstrap a new project:
+
+```sh
+pnpm dlx wxt@latest init
+```
+
+Or see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.
+
+## Features
+
+- 🌐 Supports all browsers
+- ✅ Supports both MV2 and MV3
+- ⚡ Dev mode with HMR & fast reload
+- 📂 File based entrypoints
+- 🚔 TypeScript
+- 🦾 Auto-imports
+- 🤖 Automated publishing
+- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
+- 🖍️ Quickly bootstrap a new project
+- 📏 Bundle analysis
+- ⬇️ Download and bundle remote URL imports
+
+## Contributors
+
+
+
+
diff --git a/packages/wxt/bin/wxt-publish-extension.cjs b/packages/wxt/bin/wxt-publish-extension.cjs
new file mode 100755
index 0000000..265200c
--- /dev/null
+++ b/packages/wxt/bin/wxt-publish-extension.cjs
@@ -0,0 +1,7 @@
+#!/usr/bin/env node
+/**
+ * A alias around `publish-extension` that is always installed on the path without having to install
+ * `publish-browser-extension` as a direct dependency (like for PNPM, which doesn't link
+ * sub-dependency binaries to "node_modules/.bin")
+ */
+require('publish-browser-extension/cli');
diff --git a/packages/wxt/bin/wxt.mjs b/packages/wxt/bin/wxt.mjs
new file mode 100755
index 0000000..7c3d154
--- /dev/null
+++ b/packages/wxt/bin/wxt.mjs
@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+import '../dist/cli.js';
diff --git a/packages/wxt/e2e/tests/analysis.test.ts b/packages/wxt/e2e/tests/analysis.test.ts
new file mode 100644
index 0000000..9770fe2
--- /dev/null
+++ b/packages/wxt/e2e/tests/analysis.test.ts
@@ -0,0 +1,120 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { TestProject } from '../utils';
+import { resetBundleIncrement } from '~/core/builders/vite/plugins';
+import open from 'open';
+
+vi.mock('open');
+const openMock = vi.mocked(open);
+
+vi.mock('ci-info', () => ({
+ isCI: false,
+}));
+
+describe('Analysis', () => {
+ beforeEach(() => {
+ resetBundleIncrement();
+ });
+
+ it('should output a stats.html with no part files by default', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html');
+ project.addFile('entrypoints/options.html');
+ project.addFile(
+ 'entrypoints/background.ts',
+ 'export default defineBackground(() => {});',
+ );
+
+ await project.build({
+ analysis: {
+ enabled: true,
+ },
+ });
+
+ expect(await project.fileExists('stats.html')).toBe(true);
+ expect(await project.fileExists('.output/chrome-mv3/stats-0.json')).toBe(
+ false,
+ );
+ });
+
+ it('should save part files when requested', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html');
+ project.addFile('entrypoints/options.html');
+ project.addFile(
+ 'entrypoints/background.ts',
+ 'export default defineBackground(() => {});',
+ );
+
+ await project.build({
+ analysis: {
+ enabled: true,
+ keepArtifacts: true,
+ },
+ });
+
+ expect(await project.fileExists('stats.html')).toBe(true);
+ expect(await project.fileExists('stats-0.json')).toBe(true);
+ expect(await project.fileExists('stats-1.json')).toBe(true);
+ });
+
+ it('should support customizing the stats output directory', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html');
+ project.addFile('entrypoints/options.html');
+ project.addFile(
+ 'entrypoints/background.ts',
+ 'export default defineBackground(() => {});',
+ );
+
+ await project.build({
+ analysis: {
+ enabled: true,
+ outputFile: 'stats/bundle.html',
+ },
+ });
+
+ expect(await project.fileExists('stats/bundle.html')).toBe(true);
+ });
+
+ it('should place artifacts next to the custom output file', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html');
+ project.addFile('entrypoints/options.html');
+ project.addFile(
+ 'entrypoints/background.ts',
+ 'export default defineBackground(() => {});',
+ );
+
+ await project.build({
+ analysis: {
+ enabled: true,
+ outputFile: 'stats/bundle.html',
+ keepArtifacts: true,
+ },
+ });
+
+ expect(await project.fileExists('stats/bundle.html')).toBe(true);
+ expect(await project.fileExists('stats/bundle-0.json')).toBe(true);
+ expect(await project.fileExists('stats/bundle-1.json')).toBe(true);
+ });
+
+ it('should open the stats in the browser when requested', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html');
+ project.addFile('entrypoints/options.html');
+ project.addFile(
+ 'entrypoints/background.ts',
+ 'export default defineBackground(() => {});',
+ );
+
+ await project.build({
+ analysis: {
+ enabled: true,
+ open: true,
+ },
+ });
+
+ expect(openMock).toBeCalledTimes(1);
+ expect(openMock).toBeCalledWith(project.resolvePath('stats.html'));
+ });
+});
diff --git a/packages/wxt/e2e/tests/auto-imports.test.ts b/packages/wxt/e2e/tests/auto-imports.test.ts
new file mode 100644
index 0000000..2c6619d
--- /dev/null
+++ b/packages/wxt/e2e/tests/auto-imports.test.ts
@@ -0,0 +1,174 @@
+import { describe, it, expect } from 'vitest';
+import { TestProject } from '../utils';
+
+describe('Auto Imports', () => {
+ describe('imports: { ... }', () => {
+ it('should generate a declaration file, imports.d.ts, for auto-imports', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html', ``);
+
+ await project.prepare();
+
+ expect(await project.serializeFile('.wxt/types/imports.d.ts'))
+ .toMatchInlineSnapshot(`
+ ".wxt/types/imports.d.ts
+ ----------------------------------------
+ // Generated by wxt
+ export {}
+ declare global {
+ const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
+ const InvalidMatchPattern: typeof import('wxt/sandbox')['InvalidMatchPattern']
+ const MatchPattern: typeof import('wxt/sandbox')['MatchPattern']
+ const browser: typeof import('wxt/browser')['browser']
+ const createIframeUi: typeof import('wxt/client')['createIframeUi']
+ const createIntegratedUi: typeof import('wxt/client')['createIntegratedUi']
+ const createShadowRootUi: typeof import('wxt/client')['createShadowRootUi']
+ const defineBackground: typeof import('wxt/sandbox')['defineBackground']
+ const defineConfig: typeof import('wxt')['defineConfig']
+ const defineContentScript: typeof import('wxt/sandbox')['defineContentScript']
+ const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript']
+ const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
+ const storage: typeof import('wxt/storage')['storage']
+ }
+ "
+ `);
+ });
+
+ it('should include auto-imports in the project', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html', ``);
+
+ await project.prepare();
+
+ expect(await project.serializeFile('.wxt/wxt.d.ts'))
+ .toMatchInlineSnapshot(`
+ ".wxt/wxt.d.ts
+ ----------------------------------------
+ // Generated by wxt
+ ///
+ ///
+ ///
+ ///
+ ///
+ "
+ `);
+ });
+ });
+
+ describe('imports: false', () => {
+ it('should not generate a imports.d.ts file', async () => {
+ const project = new TestProject();
+ project.setConfigFileConfig({
+ imports: false,
+ });
+ project.addFile('entrypoints/popup.html', ``);
+
+ await project.prepare();
+
+ expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
+ });
+
+ it('should not include imports.d.ts in the type references', async () => {
+ const project = new TestProject();
+ project.setConfigFileConfig({
+ imports: false,
+ });
+ project.addFile('entrypoints/popup.html', ``);
+
+ await project.prepare();
+
+ expect(
+ await project.serializeFile('.wxt/wxt.d.ts'),
+ ).toMatchInlineSnapshot(
+ `
+ ".wxt/wxt.d.ts
+ ----------------------------------------
+ // Generated by wxt
+ ///
+ ///
+ ///
+ ///
+ "
+ `,
+ );
+ });
+ });
+
+ describe('eslintrc', () => {
+ it('should output the globals list for ESLint to consume', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html', ``);
+
+ await project.prepare({
+ imports: {
+ eslintrc: {
+ enabled: true,
+ },
+ },
+ });
+
+ expect(await project.serializeFile('.wxt/eslintrc-auto-import.json'))
+ .toMatchInlineSnapshot(`
+ ".wxt/eslintrc-auto-import.json
+ ----------------------------------------
+ {
+ "globals": {
+ "ContentScriptContext": true,
+ "InvalidMatchPattern": true,
+ "MatchPattern": true,
+ "browser": true,
+ "createIframeUi": true,
+ "createIntegratedUi": true,
+ "createShadowRootUi": true,
+ "defineBackground": true,
+ "defineConfig": true,
+ "defineContentScript": true,
+ "defineUnlistedScript": true,
+ "fakeBrowser": true,
+ "storage": true
+ }
+ }
+ "
+ `);
+ });
+
+ it('should allow customizing the output', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html', ``);
+
+ await project.prepare({
+ imports: {
+ eslintrc: {
+ enabled: true,
+ filePath: project.resolvePath('example.json'),
+ globalsPropValue: 'readonly',
+ },
+ },
+ });
+
+ expect(await project.serializeFile('example.json'))
+ .toMatchInlineSnapshot(`
+ "example.json
+ ----------------------------------------
+ {
+ "globals": {
+ "ContentScriptContext": "readonly",
+ "InvalidMatchPattern": "readonly",
+ "MatchPattern": "readonly",
+ "browser": "readonly",
+ "createIframeUi": "readonly",
+ "createIntegratedUi": "readonly",
+ "createShadowRootUi": "readonly",
+ "defineBackground": "readonly",
+ "defineConfig": "readonly",
+ "defineContentScript": "readonly",
+ "defineUnlistedScript": "readonly",
+ "fakeBrowser": "readonly",
+ "storage": "readonly"
+ }
+ }
+ "
+ `);
+ });
+ });
+});
diff --git a/packages/wxt/e2e/tests/hooks.test.ts b/packages/wxt/e2e/tests/hooks.test.ts
new file mode 100644
index 0000000..f346fb4
--- /dev/null
+++ b/packages/wxt/e2e/tests/hooks.test.ts
@@ -0,0 +1,112 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { TestProject } from '../utils';
+import { WxtHooks } from '~/types';
+
+const hooks: WxtHooks = {
+ ready: vi.fn(),
+ 'build:before': vi.fn(),
+ 'build:done': vi.fn(),
+ 'build:manifestGenerated': vi.fn(),
+ 'entrypoints:resolved': vi.fn(),
+ 'entrypoints:grouped': vi.fn(),
+ 'vite:build:extendConfig': vi.fn(),
+ 'vite:devServer:extendConfig': vi.fn(),
+};
+
+function expectHooksToBeCalled(
+ called: Record,
+) {
+ Object.keys(hooks).forEach((key) => {
+ const hookName = key as keyof WxtHooks;
+ const value = called[hookName];
+ const times = typeof value === 'number' ? value : value ? 1 : 0;
+ expect(
+ hooks[hookName],
+ `Expected "${hookName}" to be called ${times} time(s)`,
+ ).toBeCalledTimes(times);
+ });
+}
+
+describe('Hooks', () => {
+ beforeEach(() => {
+ Object.values(hooks).forEach((fn) => fn.mockReset());
+ });
+
+ it('prepare should call hooks', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html', '');
+
+ await project.prepare({ hooks });
+
+ expectHooksToBeCalled({
+ ready: true,
+ 'build:before': false,
+ 'build:done': false,
+ 'build:manifestGenerated': false,
+ 'entrypoints:grouped': false,
+ 'entrypoints:resolved': true,
+ 'vite:build:extendConfig': false,
+ 'vite:devServer:extendConfig': false,
+ });
+ });
+
+ it('build should call hooks', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html', '');
+
+ await project.build({ hooks });
+
+ expectHooksToBeCalled({
+ ready: true,
+ 'build:before': true,
+ 'build:done': true,
+ 'build:manifestGenerated': true,
+ 'entrypoints:grouped': true,
+ 'entrypoints:resolved': true,
+ 'vite:build:extendConfig': 1,
+ 'vite:devServer:extendConfig': false,
+ });
+ });
+
+ it('zip should call hooks', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html', '');
+
+ await project.zip({ hooks });
+
+ expectHooksToBeCalled({
+ ready: true,
+ 'build:before': true,
+ 'build:done': true,
+ 'build:manifestGenerated': true,
+ 'entrypoints:grouped': true,
+ 'entrypoints:resolved': true,
+ 'vite:build:extendConfig': 1,
+ 'vite:devServer:extendConfig': false,
+ });
+ });
+
+ it('server.start should call hooks', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html', '');
+
+ const server = await project.startServer({
+ hooks,
+ runner: {
+ disabled: true,
+ },
+ });
+ await server.stop();
+
+ expectHooksToBeCalled({
+ ready: true,
+ 'build:before': true,
+ 'build:done': true,
+ 'build:manifestGenerated': true,
+ 'entrypoints:grouped': true,
+ 'entrypoints:resolved': true,
+ 'vite:build:extendConfig': 2,
+ 'vite:devServer:extendConfig': 1,
+ });
+ });
+});
diff --git a/packages/wxt/e2e/tests/init.test.ts b/packages/wxt/e2e/tests/init.test.ts
new file mode 100644
index 0000000..e9786b8
--- /dev/null
+++ b/packages/wxt/e2e/tests/init.test.ts
@@ -0,0 +1,62 @@
+import { describe, it, expect } from 'vitest';
+import { TestProject } from '../utils';
+import { execaCommand } from 'execa';
+import glob from 'fast-glob';
+import { mkdir, writeJson } from 'fs-extra';
+import { WXT_PACKAGE_DIR } from '../utils';
+
+describe('Init command', () => {
+ it('should download and create a template', async () => {
+ const project = new TestProject();
+
+ await execaCommand(`pnpm -s wxt init ${project.root} -t vue --pm npm`, {
+ env: { ...process.env, CI: 'true' },
+ stdio: 'ignore',
+ cwd: WXT_PACKAGE_DIR,
+ });
+ const files = await glob('**/*', {
+ cwd: project.root,
+ onlyFiles: true,
+ dot: true,
+ });
+
+ expect(files.sort()).toMatchInlineSnapshot(`
+ [
+ ".gitignore",
+ ".vscode/extensions.json",
+ "README.md",
+ "assets/vue.svg",
+ "components/HelloWorld.vue",
+ "entrypoints/background.ts",
+ "entrypoints/content.ts",
+ "entrypoints/popup/App.vue",
+ "entrypoints/popup/index.html",
+ "entrypoints/popup/main.ts",
+ "entrypoints/popup/style.css",
+ "package.json",
+ "public/icon/128.png",
+ "public/icon/16.png",
+ "public/icon/32.png",
+ "public/icon/48.png",
+ "public/icon/96.png",
+ "public/wxt.svg",
+ "tsconfig.json",
+ "wxt.config.ts",
+ ]
+ `);
+ });
+
+ it('should throw an error if the directory is not empty', async () => {
+ const project = new TestProject();
+ await mkdir(project.root, { recursive: true });
+ await writeJson(project.resolvePath('package.json'), {});
+
+ await expect(() =>
+ execaCommand(`pnpm -s wxt init ${project.root} -t vue --pm npm`, {
+ env: { ...process.env, CI: 'true' },
+ stdio: 'ignore',
+ cwd: WXT_PACKAGE_DIR,
+ }),
+ ).rejects.toThrowError('Command failed with exit code 1:');
+ });
+});
diff --git a/packages/wxt/e2e/tests/manifest-content.test.ts b/packages/wxt/e2e/tests/manifest-content.test.ts
new file mode 100644
index 0000000..fff27e5
--- /dev/null
+++ b/packages/wxt/e2e/tests/manifest-content.test.ts
@@ -0,0 +1,36 @@
+import { describe, it, expect } from 'vitest';
+import { TestProject } from '../utils';
+
+describe.each([true, false])(
+ 'Manifest Content (Vite runtime? %s)',
+ (viteRuntime) => {
+ it.each([
+ { browser: undefined, outDir: 'chrome-mv3', expected: undefined },
+ { browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
+ { browser: 'firefox', outDir: 'firefox-mv2', expected: true },
+ { browser: 'safari', outDir: 'safari-mv2', expected: false },
+ ])(
+ 'should respect the per-browser entrypoint option with %j',
+ async ({ browser, expected, outDir }) => {
+ const project = new TestProject();
+
+ project.addFile(
+ 'entrypoints/background.ts',
+ `export default defineBackground({
+ persistent: {
+ firefox: true,
+ safari: false,
+ },
+ main: () => {},
+ })`,
+ );
+ await project.build({ browser, experimental: { viteRuntime } });
+
+ const safariManifest = await project.getOutputManifest(
+ `.output/${outDir}/manifest.json`,
+ );
+ expect(safariManifest.background.persistent).toBe(expected);
+ },
+ );
+ },
+);
diff --git a/packages/wxt/e2e/tests/output-structure.test.ts b/packages/wxt/e2e/tests/output-structure.test.ts
new file mode 100644
index 0000000..b37dcf2
--- /dev/null
+++ b/packages/wxt/e2e/tests/output-structure.test.ts
@@ -0,0 +1,402 @@
+import { describe, it, expect } from 'vitest';
+import { TestProject } from '../utils';
+
+describe('Output Directory Structure', () => {
+ it('should not output hidden files and directories that start with "."', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/.DS_Store');
+ project.addFile('entrypoints/.hidden1/index.html');
+ project.addFile('entrypoints/.hidden2.html');
+ project.addFile('entrypoints/unlisted.html');
+
+ await project.build();
+
+ expect(await project.serializeOutput()).toMatchInlineSnapshot(`
+ ".output/chrome-mv3/manifest.json
+ ----------------------------------------
+ {"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0"}
+ ================================================================================
+ .output/chrome-mv3/unlisted.html
+ ----------------------------------------
+ "
+ `);
+ });
+
+ it('should output separate CSS files for each content script', async () => {
+ const project = new TestProject();
+ project.addFile(
+ 'entrypoints/one.content/index.ts',
+ `import './style.css';
+ export default defineContentScript({
+ matches: ["*://*/*"],
+ main: () => {},
+ })`,
+ );
+ project.addFile(
+ 'entrypoints/one.content/style.css',
+ `body { color: blue }`,
+ );
+ project.addFile(
+ 'entrypoints/two.content/index.ts',
+ `import './style.css';
+ export default defineContentScript({
+ matches: ["*://*/*"],
+ main: () => {},
+ })`,
+ );
+ project.addFile('entrypoints/two.content/style.css', `body { color: red }`);
+
+ await project.build();
+
+ expect(
+ await project.serializeOutput([
+ '.output/chrome-mv3/content-scripts/one.js',
+ '.output/chrome-mv3/content-scripts/two.js',
+ ]),
+ ).toMatchInlineSnapshot(`
+ ".output/chrome-mv3/content-scripts/one.css
+ ----------------------------------------
+ body{color:#00f}
+
+ ================================================================================
+ .output/chrome-mv3/content-scripts/one.js
+ ----------------------------------------
+
+ ================================================================================
+ .output/chrome-mv3/content-scripts/two.css
+ ----------------------------------------
+ body{color:red}
+
+ ================================================================================
+ .output/chrome-mv3/content-scripts/two.js
+ ----------------------------------------
+
+ ================================================================================
+ .output/chrome-mv3/manifest.json
+ ----------------------------------------
+ {"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["*://*/*"],"css":["content-scripts/one.css","content-scripts/two.css"],"js":["content-scripts/one.js","content-scripts/two.js"]}]}"
+ `);
+ });
+
+ it('should allow inputs with invalid JS variable names, like dashes', async () => {
+ const project = new TestProject();
+ project.addFile(
+ 'entrypoints/overlay-one.content.ts',
+ `export default defineContentScript({
+ matches: ["*://*/*"],
+ main: () => {},
+ })`,
+ );
+
+ await project.build();
+
+ expect(
+ await project.serializeOutput([
+ '.output/chrome-mv3/content-scripts/overlay-one.js',
+ ]),
+ ).toMatchInlineSnapshot(`
+ ".output/chrome-mv3/content-scripts/overlay-one.js
+ ----------------------------------------
+
+ ================================================================================
+ .output/chrome-mv3/manifest.json
+ ----------------------------------------
+ {"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["*://*/*"],"js":["content-scripts/overlay-one.js"]}]}"
+ `);
+ });
+
+ it('should not include an entrypoint if the target browser is not in the list of included targets', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/options.html', '');
+ project.addFile(
+ 'entrypoints/background.ts',
+ `
+ export default defineBackground({
+ include: ["chrome"],
+ main() {},
+ })
+ `,
+ );
+
+ await project.build({ browser: 'firefox' });
+
+ expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
+ false,
+ );
+ });
+
+ it('should not include an entrypoint if the target browser is in the list of excluded targets', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/options.html', '');
+ project.addFile(
+ 'entrypoints/background.ts',
+ `
+ export default defineBackground({
+ exclude: ["chrome"],
+ main() {},
+ })
+ `,
+ );
+
+ await project.build({ browser: 'chrome' });
+
+ expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
+ false,
+ );
+ });
+
+ it('should generate a stats file when analyzing the bundle', async () => {
+ const project = new TestProject();
+ project.setConfigFileConfig({
+ analysis: {
+ enabled: true,
+ template: 'sunburst',
+ },
+ });
+ project.addFile(
+ 'entrypoints/background.ts',
+ `export default defineBackground(() => {});`,
+ );
+ project.addFile('entrypoints/popup.html', '');
+ project.addFile(
+ 'entrypoints/overlay.content.html',
+ `export default defineContentScript({
+ matches: [],
+ main() {},
+ });`,
+ );
+
+ await project.build();
+
+ expect(await project.fileExists('stats.html')).toBe(true);
+ });
+
+ it('should support JavaScript entrypoints', async () => {
+ const project = new TestProject();
+ project.addFile(
+ 'entrypoints/background.js',
+ `export default defineBackground(() => {});`,
+ );
+ project.addFile(
+ 'entrypoints/unlisted.js',
+ `export default defineUnlistedScript(() => {})`,
+ );
+ project.addFile(
+ 'entrypoints/content.js',
+ `export default defineContentScript({
+ matches: ["*://*.google.com/*"],
+ main() {},
+ })`,
+ );
+ project.addFile(
+ 'entrypoints/named.content.jsx',
+ `export default defineContentScript({
+ matches: ["*://*.duckduckgo.com/*"],
+ main() {},
+ })`,
+ );
+
+ await project.build();
+
+ expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
+ .toMatchInlineSnapshot(`
+ ".output/chrome-mv3/manifest.json
+ ----------------------------------------
+ {"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"},"content_scripts":[{"matches":["*://*.google.com/*"],"js":["content-scripts/content.js"]},{"matches":["*://*.duckduckgo.com/*"],"js":["content-scripts/named.js"]}]}"
+ `);
+ expect(await project.fileExists('.output/chrome-mv3/background.js'));
+ expect(
+ await project.fileExists('.output/chrome-mv3/content-scripts/content.js'),
+ );
+ expect(
+ await project.fileExists('.output/chrome-mv3/content-scripts/named.js'),
+ );
+ expect(await project.fileExists('.output/chrome-mv3/unlisted.js'));
+ });
+
+ it("should output to a custom directory when overriding 'outDir'", async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/unlisted.html');
+ project.setConfigFileConfig({
+ outDir: 'dist',
+ });
+
+ await project.build();
+
+ expect(await project.fileExists('dist/chrome-mv3/manifest.json')).toBe(
+ true,
+ );
+ });
+
+ it('should generate ESM background script when type=module', async () => {
+ const project = new TestProject();
+ project.addFile(
+ 'utils/log.ts',
+ `export function logHello(name: string) {
+ console.log(\`Hello \${name}!\`);
+ }`,
+ );
+ project.addFile(
+ 'entrypoints/background.ts',
+ `export default defineBackground({
+ type: "module",
+ main() {
+ logHello("background");
+ },
+ })`,
+ );
+ project.addFile(
+ 'entrypoints/popup/index.html',
+ `
+
+
+
+ `,
+ );
+ project.addFile('entrypoints/popup/main.ts', `logHello('popup')`);
+
+ await project.build({
+ experimental: {
+ // Simplify the build output for comparison
+ includeBrowserPolyfill: false,
+ },
+ vite: () => ({
+ build: {
+ // Make output for snapshot readible
+ minify: false,
+ },
+ }),
+ });
+
+ expect(await project.serializeFile('.output/chrome-mv3/background.js'))
+ .toMatchInlineSnapshot(`
+ ".output/chrome-mv3/background.js
+ ----------------------------------------
+ import { l as logHello } from "./chunks/log-BsZv2eRn.js";
+ function defineBackground(arg) {
+ if (typeof arg === "function")
+ return { main: arg };
+ return arg;
+ }
+ const definition = defineBackground({
+ type: "module",
+ main() {
+ logHello("background");
+ }
+ });
+ chrome;
+ function print(method, ...args) {
+ return;
+ }
+ var logger = {
+ debug: (...args) => print(console.debug, ...args),
+ log: (...args) => print(console.log, ...args),
+ warn: (...args) => print(console.warn, ...args),
+ error: (...args) => print(console.error, ...args)
+ };
+ var result;
+ try {
+ result = definition.main();
+ if (result instanceof Promise) {
+ console.warn(
+ "The background's main() function return a promise, but it must be synchronous"
+ );
+ }
+ } catch (err) {
+ logger.error("The background crashed on startup!");
+ throw err;
+ }
+ "
+ `);
+ });
+
+ it('should generate IIFE background script when type=undefined', async () => {
+ const project = new TestProject();
+ project.addFile(
+ 'utils/log.ts',
+ `export function logHello(name: string) {
+ console.log(\`Hello \${name}!\`);
+ }`,
+ );
+ project.addFile(
+ 'entrypoints/background.ts',
+ `export default defineBackground({
+ main() {
+ logHello("background");
+ },
+ })`,
+ );
+ project.addFile(
+ 'entrypoints/popup/index.html',
+ `
+
+
+
+ `,
+ );
+ project.addFile('entrypoints/popup/main.ts', `logHello('popup')`);
+
+ await project.build({
+ experimental: {
+ // Simplify the build output for comparison
+ includeBrowserPolyfill: false,
+ },
+ vite: () => ({
+ build: {
+ // Make output for snapshot readible
+ minify: false,
+ },
+ }),
+ });
+
+ expect(await project.serializeFile('.output/chrome-mv3/background.js'))
+ .toMatchInlineSnapshot(`
+ ".output/chrome-mv3/background.js
+ ----------------------------------------
+ var _background = function() {
+ "use strict";
+ function defineBackground(arg) {
+ if (typeof arg === "function")
+ return { main: arg };
+ return arg;
+ }
+ function logHello(name) {
+ console.log(\`Hello \${name}!\`);
+ }
+ _background;
+ const definition = defineBackground({
+ main() {
+ logHello("background");
+ }
+ });
+ _background;
+ chrome;
+ function print(method, ...args) {
+ return;
+ }
+ var logger = {
+ debug: (...args) => print(console.debug, ...args),
+ log: (...args) => print(console.log, ...args),
+ warn: (...args) => print(console.warn, ...args),
+ error: (...args) => print(console.error, ...args)
+ };
+ var result;
+ try {
+ result = definition.main();
+ if (result instanceof Promise) {
+ console.warn(
+ "The background's main() function return a promise, but it must be synchronous"
+ );
+ }
+ } catch (err) {
+ logger.error("The background crashed on startup!");
+ throw err;
+ }
+ var background_entrypoint_default = result;
+ return background_entrypoint_default;
+ }();
+ _background;
+ "
+ `);
+ });
+});
diff --git a/packages/wxt/e2e/tests/react.test.ts b/packages/wxt/e2e/tests/react.test.ts
new file mode 100644
index 0000000..4e01672
--- /dev/null
+++ b/packages/wxt/e2e/tests/react.test.ts
@@ -0,0 +1,43 @@
+import { describe, expect, it } from 'vitest';
+import { TestProject } from '../utils';
+
+describe('React', () => {
+ it('should prepare and build an project with a tsx entrypoint', async () => {
+ const project = new TestProject({
+ dependencies: {
+ react: '^18.2.0',
+ 'react-dom': '^18.2.0',
+ },
+ devDependencies: {
+ '@types/react': '^18.2.14',
+ '@types/react-dom': '^18.2.6',
+ },
+ });
+ project.addFile(
+ 'entrypoints/demo.content.tsx',
+ `import ReactDOM from 'react-dom/client';
+
+ export default defineContentScript({
+ matches: "",
+ main() {
+ const container = document.createElement("div");
+ document.body.append(container)
+ const root = ReactDOM.createRoot(container);
+ root.render(Hello, world!
);
+ }
+ })`,
+ );
+
+ await project.build();
+
+ expect(
+ await project.fileExists('.output/chrome-mv3/content-scripts/demo.js'),
+ ).toBe(true);
+ expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
+ .toMatchInlineSnapshot(`
+ ".output/chrome-mv3/manifest.json
+ ----------------------------------------
+ {"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":"","js":["content-scripts/demo.js"]}]}"
+ `);
+ });
+});
diff --git a/packages/wxt/e2e/tests/remote-code.test.ts b/packages/wxt/e2e/tests/remote-code.test.ts
new file mode 100644
index 0000000..481a9e9
--- /dev/null
+++ b/packages/wxt/e2e/tests/remote-code.test.ts
@@ -0,0 +1,26 @@
+import { describe, it, expect } from 'vitest';
+import { TestProject } from '../utils';
+
+describe('Remote Code', () => {
+ it('should download "url:*" modules and include them in the final bundle', async () => {
+ const url = 'https://code.jquery.com/jquery-3.7.1.slim.min.js';
+ const project = new TestProject();
+ project.addFile(
+ 'entrypoints/popup.ts',
+ `import "url:${url}"
+ export default defineUnlistedScript(() => {})`,
+ );
+
+ await project.build();
+
+ const output = await project.serializeFile('.output/chrome-mv3/popup.js');
+ expect(output).toContain(
+ // Some text that will hopefully be in future versions of this script
+ 'jQuery v3.7.1',
+ );
+ expect(output).not.toContain(url);
+ expect(
+ await project.fileExists(`.wxt/cache/${encodeURIComponent(url)}`),
+ ).toBe(true);
+ });
+});
diff --git a/packages/wxt/e2e/tests/typescript-project.test.ts b/packages/wxt/e2e/tests/typescript-project.test.ts
new file mode 100644
index 0000000..511dd95
--- /dev/null
+++ b/packages/wxt/e2e/tests/typescript-project.test.ts
@@ -0,0 +1,378 @@
+import { describe, it, expect } from 'vitest';
+import { TestProject } from '../utils';
+
+describe('TypeScript Project', () => {
+ it('should generate defined constants correctly', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/unlisted.html');
+
+ await project.prepare();
+
+ const output = await project.serializeFile('.wxt/types/globals.d.ts');
+ expect(output).toMatchInlineSnapshot(`
+ ".wxt/types/globals.d.ts
+ ----------------------------------------
+ // Generated by wxt
+ export {}
+ interface ImportMetaEnv {
+ readonly MANIFEST_VERSION: 2 | 3;
+ readonly BROWSER: string;
+ readonly CHROME: boolean;
+ readonly FIREFOX: boolean;
+ readonly SAFARI: boolean;
+ readonly EDGE: boolean;
+ readonly OPERA: boolean;
+ readonly COMMAND: "build" | "serve";
+ readonly ENTRYPOINT: string;
+ }
+ interface ImportMeta {
+ readonly env: ImportMetaEnv
+ }
+ "
+ `);
+ });
+
+ it('should augment the types for browser.runtime.getURL', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/popup.html');
+ project.addFile('entrypoints/options.html');
+ project.addFile('entrypoints/sandbox.html');
+
+ await project.prepare();
+
+ const output = await project.serializeFile('.wxt/types/paths.d.ts');
+ expect(output).toMatchInlineSnapshot(`
+ ".wxt/types/paths.d.ts
+ ----------------------------------------
+ // Generated by wxt
+ import "wxt/browser";
+
+ declare module "wxt/browser" {
+ export type PublicPath =
+ | "/options.html"
+ | "/popup.html"
+ | "/sandbox.html"
+ type HtmlPublicPath = Extract
+ export interface WxtRuntime extends Runtime.Static {
+ getURL(path: PublicPath): string;
+ getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
+ }
+ }
+ "
+ `);
+ });
+
+ it('should augment the types for browser.i18n.getMessage', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/unlisted.html');
+ project.addFile(
+ 'public/_locales/en/messages.json',
+ JSON.stringify({
+ prompt_for_name: {
+ message: "What's your name?",
+ description: "Ask for the user's name",
+ },
+ hello: {
+ message: 'Hello, $USER$',
+ description: 'Greet the user',
+ placeholders: {
+ user: {
+ content: '$1',
+ example: 'Cira',
+ },
+ },
+ },
+ bye: {
+ message: 'Goodbye, $USER$. Come back to $OUR_SITE$ soon!',
+ description: 'Say goodbye to the user',
+ placeholders: {
+ our_site: {
+ content: 'Example.com',
+ },
+ user: {
+ content: '$1',
+ example: 'Cira',
+ },
+ },
+ },
+ }),
+ );
+ project.setConfigFileConfig({
+ manifest: {
+ default_locale: 'en',
+ },
+ });
+
+ await project.prepare();
+
+ const output = await project.serializeFile('.wxt/types/i18n.d.ts');
+ expect(output).toMatchInlineSnapshot(`
+ ".wxt/types/i18n.d.ts
+ ----------------------------------------
+ // Generated by wxt
+ import "wxt/browser";
+
+ declare module "wxt/browser" {
+ /**
+ * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
+ */
+ interface GetMessageOptions {
+ /**
+ * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
+ */
+ escapeLt?: boolean
+ }
+
+ export interface WxtI18n extends I18n.Static {
+ /**
+ * The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.
+ Note: You can't use this message in a manifest file.
+ *
+ * ""
+ */
+ getMessage(
+ messageName: "@@extension_id",
+ substitutions?: string | string[],
+ options?: GetMessageOptions,
+ ): string;
+ /**
+ * No message description.
+ *
+ * ""
+ */
+ getMessage(
+ messageName: "@@ui_locale",
+ substitutions?: string | string[],
+ options?: GetMessageOptions,
+ ): string;
+ /**
+ * The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.
+ *
+ * ""
+ */
+ getMessage(
+ messageName: "@@bidi_dir",
+ substitutions?: string | string[],
+ options?: GetMessageOptions,
+ ): string;
+ /**
+ * If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it's "ltr".
+ *
+ * ""
+ */
+ getMessage(
+ messageName: "@@bidi_reversed_dir",
+ substitutions?: string | string[],
+ options?: GetMessageOptions,
+ ): string;
+ /**
+ * If the @@bidi_dir is "ltr", then this is "left"; otherwise, it's "right".
+ *
+ * ""
+ */
+ getMessage(
+ messageName: "@@bidi_start_edge",
+ substitutions?: string | string[],
+ options?: GetMessageOptions,
+ ): string;
+ /**
+ * If the @@bidi_dir is "ltr", then this is "right"; otherwise, it's "left".
+ *
+ * ""
+ */
+ getMessage(
+ messageName: "@@bidi_end_edge",
+ substitutions?: string | string[],
+ options?: GetMessageOptions,
+ ): string;
+ /**
+ * Ask for the user's name
+ *
+ * "What's your name?"
+ */
+ getMessage(
+ messageName: "prompt_for_name",
+ substitutions?: string | string[],
+ options?: GetMessageOptions,
+ ): string;
+ /**
+ * Greet the user
+ *
+ * "Hello, $USER$"
+ */
+ getMessage(
+ messageName: "hello",
+ substitutions?: string | string[],
+ options?: GetMessageOptions,
+ ): string;
+ /**
+ * Say goodbye to the user
+ *
+ * "Goodbye, $USER$. Come back to $OUR_SITE$ soon!"
+ */
+ getMessage(
+ messageName: "bye",
+ substitutions?: string | string[],
+ options?: GetMessageOptions,
+ ): string;
+ }
+ }
+ "
+ `);
+ });
+
+ it('should reference all the required types in a single declaration file', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/unlisted.html');
+
+ await project.prepare();
+
+ const output = await project.serializeFile('.wxt/wxt.d.ts');
+ expect(output).toMatchInlineSnapshot(`
+ ".wxt/wxt.d.ts
+ ----------------------------------------
+ // Generated by wxt
+ ///
+ ///
+ ///
+ ///
+ ///
+ "
+ `);
+ });
+
+ it('should generate a TSConfig file for the project', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/unlisted.html');
+
+ await project.prepare();
+
+ const output = await project.serializeFile('.wxt/tsconfig.json');
+ expect(output).toMatchInlineSnapshot(`
+ ".wxt/tsconfig.json
+ ----------------------------------------
+ {
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "noEmit": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "paths": {
+ "@": [".."],
+ "@/*": ["../*"],
+ "~": [".."],
+ "~/*": ["../*"],
+ "@@": [".."],
+ "@@/*": ["../*"],
+ "~~": [".."],
+ "~~/*": ["../*"]
+ }
+ },
+ "include": [
+ "../**/*",
+ "./wxt.d.ts"
+ ],
+ "exclude": ["../.output"]
+ }"
+ `);
+ });
+
+ it('should generate correct path aliases for a custom srcDir', async () => {
+ const project = new TestProject();
+ project.addFile('src/entrypoints/unlisted.html');
+ project.setConfigFileConfig({
+ srcDir: 'src',
+ });
+
+ await project.prepare();
+
+ const output = await project.serializeFile('.wxt/tsconfig.json');
+ expect(output).toMatchInlineSnapshot(`
+ ".wxt/tsconfig.json
+ ----------------------------------------
+ {
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "noEmit": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "paths": {
+ "@": ["../src"],
+ "@/*": ["../src/*"],
+ "~": ["../src"],
+ "~/*": ["../src/*"],
+ "@@": [".."],
+ "@@/*": ["../*"],
+ "~~": [".."],
+ "~~/*": ["../*"]
+ }
+ },
+ "include": [
+ "../**/*",
+ "./wxt.d.ts"
+ ],
+ "exclude": ["../.output"]
+ }"
+ `);
+ });
+
+ it('should add additional path aliases listed in the alias config, preventing defaults from being overridden', async () => {
+ const project = new TestProject();
+ project.addFile('src/entrypoints/unlisted.html');
+ project.setConfigFileConfig({
+ srcDir: 'src',
+ alias: {
+ example: 'example',
+ '@': 'ignored-path',
+ },
+ });
+
+ await project.prepare();
+
+ const output = await project.serializeFile('.wxt/tsconfig.json');
+ expect(output).toMatchInlineSnapshot(`
+ ".wxt/tsconfig.json
+ ----------------------------------------
+ {
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "noEmit": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "paths": {
+ "example": ["../example"],
+ "example/*": ["../example/*"],
+ "@": ["../src"],
+ "@/*": ["../src/*"],
+ "~": ["../src"],
+ "~/*": ["../src/*"],
+ "@@": [".."],
+ "@@/*": ["../*"],
+ "~~": [".."],
+ "~~/*": ["../*"]
+ }
+ },
+ "include": [
+ "../**/*",
+ "./wxt.d.ts"
+ ],
+ "exclude": ["../.output"]
+ }"
+ `);
+ });
+});
diff --git a/packages/wxt/e2e/tests/user-config.test.ts b/packages/wxt/e2e/tests/user-config.test.ts
new file mode 100644
index 0000000..1a40d1a
--- /dev/null
+++ b/packages/wxt/e2e/tests/user-config.test.ts
@@ -0,0 +1,135 @@
+import { describe, it, expect } from 'vitest';
+import { TestProject } from '../utils';
+import { InlineConfig } from '~/types';
+
+describe('User Config', () => {
+ // Root directory is tested with all tests.
+
+ it("should respect the 'src' directory", async () => {
+ const project = new TestProject();
+ project.setConfigFileConfig({
+ srcDir: 'src',
+ });
+ project.addFile(
+ 'src/entrypoints/background.ts',
+ `export default defineBackground(
+ () => console.log('Hello background'),
+ );`,
+ );
+
+ await project.build();
+
+ const output = await project.serializeOutput([
+ '.output/chrome-mv3/background.js',
+ ]);
+ expect(output).toMatchInlineSnapshot(`
+ ".output/chrome-mv3/background.js
+ ----------------------------------------
+
+ ================================================================================
+ .output/chrome-mv3/manifest.json
+ ----------------------------------------
+ {"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"}}"
+ `);
+ });
+
+ it("should respect the 'entrypoints' directory", async () => {
+ const project = new TestProject();
+ project.setConfigFileConfig({
+ entrypointsDir: 'entries',
+ });
+ project.addFile(
+ 'entries/background.ts',
+ `export default defineBackground(() => console.log('Hello background'));`,
+ );
+
+ await project.build();
+
+ const output = await project.serializeOutput([
+ '.output/chrome-mv3/background.js',
+ ]);
+ expect(output).toMatchInlineSnapshot(`
+ ".output/chrome-mv3/background.js
+ ----------------------------------------
+
+ ================================================================================
+ .output/chrome-mv3/manifest.json
+ ----------------------------------------
+ {"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"}}"
+ `);
+ });
+
+ it('should merge inline and user config based manifests', async () => {
+ const project = new TestProject();
+ project.addFile('entrypoints/unlisted.html');
+ project.addFile(
+ 'wxt.config.ts',
+ `import { defineConfig } from 'wxt';
+ export default defineConfig({
+ manifest: ({ mode, browser }) => ({
+ // @ts-expect-error
+ example_customization: [mode, browser],
+ })
+ })`,
+ );
+
+ await project.build({
+ // @ts-expect-error: Specifically setting an invalid field for the test - it should show up in the snapshot
+ manifest: ({ manifestVersion, command }) => ({
+ example_customization: [String(manifestVersion), command],
+ }),
+ });
+
+ expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
+ .toMatchInlineSnapshot(`
+ ".output/chrome-mv3/manifest.json
+ ----------------------------------------
+ {"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","example_customization":["3","build","production","chrome"]}"
+ `);
+ });
+
+ it('should exclude the polyfill when the experimental setting is set to false', async () => {
+ const buildBackground = async (config?: InlineConfig) => {
+ const background = `export default defineBackground(() => console.log(browser.runtime.id));`;
+ const projectWithPolyfill = new TestProject();
+ projectWithPolyfill.addFile('entrypoints/background.ts', background);
+ await projectWithPolyfill.build(config);
+ return await projectWithPolyfill.serializeFile(
+ '.output/chrome-mv3/background.js',
+ );
+ };
+
+ const withPolyfill = await buildBackground();
+ const withoutPolyfill = await buildBackground({
+ experimental: {
+ includeBrowserPolyfill: false,
+ },
+ });
+ expect(withoutPolyfill).not.toBe(withPolyfill);
+ });
+
+ it('should respect changing config files', async () => {
+ const project = new TestProject();
+ project.addFile(
+ 'src/entrypoints/background.ts',
+ `export default defineBackground(
+ () => console.log('Hello background'),
+ );`,
+ );
+ project.addFile(
+ 'test.config.ts',
+ `import { defineConfig } from 'wxt';
+
+ export default defineConfig({
+ outDir: ".custom-output",
+ srcDir: "src",
+ });`,
+ );
+
+ await project.build({ configFile: 'test.config.ts' });
+
+ expect(
+ await project.fileExists('.custom-output/chrome-mv3/background.js'),
+ ).toBe(true);
+ });
+});
diff --git a/packages/wxt/e2e/tests/zip.test.ts b/packages/wxt/e2e/tests/zip.test.ts
new file mode 100644
index 0000000..a01f573
--- /dev/null
+++ b/packages/wxt/e2e/tests/zip.test.ts
@@ -0,0 +1,143 @@
+import { describe, expect, it } from 'vitest';
+import { TestProject } from '../utils';
+import extract from 'extract-zip';
+import { execaCommand } from 'execa';
+import { readFile, writeFile } from 'fs-extra';
+
+process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
+
+describe('Zipping', () => {
+ it('should download packages and produce a valid build when zipping sources', async () => {
+ const project = new TestProject({
+ name: 'test',
+ version: '1.0.0',
+ dependencies: {
+ flatten: '1.0.3',
+ },
+ });
+ project.addFile(
+ 'entrypoints/background.ts',
+ 'export default defineBackground(() => {});',
+ );
+ const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
+ const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
+
+ await project.zip({
+ browser: 'firefox',
+ zip: { downloadPackages: ['flatten'] },
+ });
+ expect(await project.fileExists('.output/')).toBe(true);
+
+ await extract(sourcesZip, { dir: unzipDir });
+ // Update package json wxt path
+ const packageJsonPath = project.resolvePath(unzipDir, 'package.json');
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
+ packageJson.dependencies.wxt = '../../../../..';
+ await writeFile(
+ packageJsonPath,
+ JSON.stringify(packageJson, null, 2),
+ 'utf-8',
+ );
+
+ // Build zipped extension
+ await expect(
+ execaCommand('pnpm i --ignore-workspace --frozen-lockfile false', {
+ cwd: unzipDir,
+ }),
+ ).resolves.toMatchObject({ exitCode: 0 });
+ await expect(
+ execaCommand('pnpm wxt build -b firefox', { cwd: unzipDir }),
+ ).resolves.toMatchObject({ exitCode: 0 });
+
+ await expect(project.fileExists(unzipDir, '.output')).resolves.toBe(true);
+ expect(
+ await project.serializeFile(
+ project.resolvePath(unzipDir, 'package.json'),
+ ),
+ ).toMatchInlineSnapshot(`
+ ".output/test-1.0.0-sources/package.json
+ ----------------------------------------
+ {
+ "name": "test",
+ "description": "Example description",
+ "version": "1.0.0",
+ "dependencies": {
+ "wxt": "../../../../..",
+ "flatten": "1.0.3"
+ },
+ "resolutions": {
+ "flatten@1.0.3": "file://./.wxt/local_modules/flatten-1.0.3.tgz"
+ }
+ }"
+ `);
+ });
+
+ it('should correctly apply template variables for zip file names based on provided config', async () => {
+ const project = new TestProject({
+ name: 'test',
+ version: '1.0.0',
+ });
+ project.addFile(
+ 'entrypoints/background.ts',
+ 'export default defineBackground(() => {});',
+ );
+ const artifactZip = '.output/test-1.0.0-firefox-development.zip';
+ const sourcesZip = '.output/test-1.0.0-development-sources.zip';
+
+ await project.zip({
+ browser: 'firefox',
+ mode: 'development',
+ zip: {
+ artifactTemplate: '{{name}}-{{version}}-{{browser}}-{{mode}}.zip',
+ sourcesTemplate: '{{name}}-{{version}}-{{mode}}-sources.zip',
+ },
+ });
+
+ expect(await project.fileExists(artifactZip)).toBe(true);
+ expect(await project.fileExists(sourcesZip)).toBe(true);
+ });
+
+ it('should not zip hidden files into sources by default', async () => {
+ const project = new TestProject({
+ name: 'test',
+ version: '1.0.0',
+ });
+ project.addFile(
+ 'entrypoints/background.ts',
+ 'export default defineBackground(() => {});',
+ );
+ project.addFile('.env');
+ const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
+ const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
+
+ await project.zip({
+ browser: 'firefox',
+ });
+ await extract(sourcesZip, { dir: unzipDir });
+ console.log(unzipDir); // TODO: Remove log
+ expect(await project.fileExists(unzipDir, '.env')).toBe(false);
+ });
+
+ it('should allow zipping hidden files into sources when explicitly listed', async () => {
+ const project = new TestProject({
+ name: 'test',
+ version: '1.0.0',
+ });
+ project.addFile(
+ 'entrypoints/background.ts',
+ 'export default defineBackground(() => {});',
+ );
+ project.addFile('.env');
+ const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
+ const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
+
+ await project.zip({
+ browser: 'firefox',
+ zip: {
+ includeSources: ['.env'],
+ },
+ });
+ await extract(sourcesZip, { dir: unzipDir });
+ expect(await project.fileExists(unzipDir, '.env')).toBe(true);
+ });
+});
diff --git a/packages/wxt/e2e/utils.ts b/packages/wxt/e2e/utils.ts
new file mode 100644
index 0000000..d2d91e8
--- /dev/null
+++ b/packages/wxt/e2e/utils.ts
@@ -0,0 +1,195 @@
+import { dirname, relative, resolve } from 'path';
+import fs, { mkdir } from 'fs-extra';
+import glob from 'fast-glob';
+import { execaCommand } from 'execa';
+import {
+ InlineConfig,
+ UserConfig,
+ build,
+ createServer,
+ prepare,
+ zip,
+} from '../src';
+import { normalizePath } from '../src/core/utils/paths';
+import merge from 'lodash.merge';
+
+// Run "pnpm wxt" to use the "wxt" dev script, not the "wxt" binary from the
+// wxt package. This uses the TS files instead of the compiled JS package
+// files.
+export const WXT_PACKAGE_DIR = resolve(__dirname, '..');
+
+export const E2E_DIR = resolve(WXT_PACKAGE_DIR, 'e2e');
+
+export class TestProject {
+ files: Array<[string, string]> = [];
+ config: UserConfig | undefined;
+ readonly root: string;
+
+ constructor(packageJson: any = {}) {
+ // We can't put each test's project inside e2e/dist directly, otherwise the wxt.config.ts
+ // file is cached and cannot be different between each test. Instead, we add a random ID to the
+ // end to make each test's path unique.
+ const id = Math.random().toString(32).substring(3);
+ this.root = resolve(E2E_DIR, 'dist', id);
+ this.files.push([
+ 'package.json',
+ JSON.stringify(
+ merge(
+ {
+ name: 'E2E Extension',
+ description: 'Example description',
+ version: '0.0.0',
+ dependencies: {
+ wxt: '../../..',
+ },
+ },
+ packageJson,
+ ),
+ null,
+ 2,
+ ),
+ ]);
+ }
+
+ /**
+ * Add a `wxt.config.ts` to the project with specific contents.
+ */
+ setConfigFileConfig(config: UserConfig = {}) {
+ this.config = config;
+ this.files.push([
+ 'wxt.config.ts',
+ `import { defineConfig } from 'wxt'\n\nexport default defineConfig(${JSON.stringify(
+ config,
+ null,
+ 2,
+ )})`,
+ ]);
+ }
+
+ /**
+ * Adds the file to the project. Stored in memory until `.build` is called.
+ *
+ * @param filename Filename relative to the project's root.
+ * @param content File content.
+ */
+ addFile(filename: string, content?: string) {
+ this.files.push([filename, content ?? '']);
+ if (filename === 'wxt.config.ts') this.config = {};
+ }
+
+ async prepare(config: InlineConfig = {}) {
+ await this.writeProjectToDisk();
+ await prepare({ ...config, root: this.root });
+ }
+
+ async build(config: InlineConfig = {}) {
+ await this.writeProjectToDisk();
+ await build({ ...config, root: this.root });
+ }
+
+ async zip(config: InlineConfig = {}) {
+ await this.writeProjectToDisk();
+ await zip({ ...config, root: this.root });
+ }
+
+ async startServer(config: InlineConfig = {}) {
+ await this.writeProjectToDisk();
+ const server = await createServer({ ...config, root: this.root });
+ await server.start();
+ return server;
+ }
+
+ /**
+ * Call `path.resolve` relative to the project's root directory.
+ */
+ resolvePath(...path: string[]): string {
+ return resolve(this.root, ...path);
+ }
+
+ private async writeProjectToDisk() {
+ if (this.config == null) this.setConfigFileConfig();
+
+ for (const file of this.files) {
+ const [name, content] = file;
+ const filePath = this.resolvePath(name);
+ const fileDir = dirname(filePath);
+ await fs.ensureDir(fileDir);
+ await fs.writeFile(filePath, content ?? '', 'utf-8');
+ }
+
+ await execaCommand('pnpm --ignore-workspace i --ignore-scripts', {
+ cwd: this.root,
+ });
+ await mkdir(resolve(this.root, 'public'), { recursive: true }).catch(
+ () => {},
+ );
+ }
+
+ /**
+ * Read all the files from the test project's `.output` directory and combine them into a string
+ * that can be used in a snapshot.
+ *
+ * Optionally, provide a list of filenames whose content is not printed (because it's inconsistent
+ * or not relevant to a test).
+ */
+ serializeOutput(ignoreContentsOfFilenames?: string[]): Promise {
+ return this.serializeDir('.output', ignoreContentsOfFilenames);
+ }
+
+ /**
+ * Read all the files from the test project's `.wxt` directory and combine them into a string
+ * that can be used in a snapshot.
+ */
+ serializeWxtDir(): Promise {
+ return this.serializeDir(resolve(this.root, '.wxt/types'));
+ }
+
+ /**
+ * Deeply print the filename and contents of all files in a directory.
+ *
+ * Optionally, provide a list of filenames whose content is not printed (because it's inconsistent
+ * or not relevant to a test).
+ */
+ private async serializeDir(
+ dir: string,
+ ignoreContentsOfFilenames?: string[],
+ ): Promise {
+ const outputFiles = await glob('**/*', {
+ cwd: this.resolvePath(dir),
+ ignore: ['**/node_modules', '**/.output'],
+ });
+ outputFiles.sort();
+ const fileContents = [];
+ for (const file of outputFiles) {
+ const path = this.resolvePath(dir, file);
+ const isContentIgnored = !!ignoreContentsOfFilenames?.find(
+ (ignoredFile) => normalizePath(path).endsWith(ignoredFile),
+ );
+ fileContents.push(await this.serializeFile(path, isContentIgnored));
+ }
+ return fileContents.join(`\n${''.padEnd(80, '=')}\n`);
+ }
+
+ /**
+ * @param path An absolute path to a file or a path relative to the root.
+ * @param ignoreContents An optional boolean that, when true, causes this function to not print
+ * the file contents.
+ */
+ async serializeFile(path: string, ignoreContents?: boolean): Promise {
+ const absolutePath = this.resolvePath(path);
+ return [
+ normalizePath(relative(this.root, absolutePath)),
+ ignoreContents ? '' : await fs.readFile(absolutePath),
+ ].join(`\n${''.padEnd(40, '-')}\n`);
+ }
+
+ fileExists(...path: string[]): Promise {
+ return fs.exists(this.resolvePath(...path));
+ }
+
+ async getOutputManifest(
+ path: string = '.output/chrome-mv3/manifest.json',
+ ): Promise {
+ return await fs.readJson(this.resolvePath(path));
+ }
+}
diff --git a/packages/wxt/package.json b/packages/wxt/package.json
new file mode 100644
index 0000000..e8aab21
--- /dev/null
+++ b/packages/wxt/package.json
@@ -0,0 +1,168 @@
+{
+ "name": "@refly/wxt",
+ "type": "module",
+ "version": "0.18.4",
+ "description": "Next gen framework for developing web extensions",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/wxt-dev/wxt"
+ },
+ "homepage": "https://wxt.dev",
+ "keywords": [
+ "vite",
+ "chrome",
+ "web",
+ "extension",
+ "browser",
+ "bundler",
+ "framework"
+ ],
+ "author": {
+ "name": "Aaron Klinker",
+ "email": "aaronklinker1+wxt@gmail.com"
+ },
+ "license": "MIT",
+ "files": [
+ "bin",
+ "dist"
+ ],
+ "bin": {
+ "wxt": "./bin/wxt.mjs",
+ "wxt-publish-extension": "./bin/wxt-publish-extension.cjs"
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/index.cjs"
+ }
+ },
+ "./client": {
+ "types": "./dist/client.d.ts",
+ "import": "./dist/client.js"
+ },
+ "./sandbox": {
+ "types": "./dist/sandbox.d.ts",
+ "import": "./dist/sandbox.js"
+ },
+ "./browser": {
+ "types": "./dist/browser.d.ts",
+ "import": "./dist/browser.js"
+ },
+ "./testing": {
+ "import": {
+ "types": "./dist/testing.d.ts",
+ "default": "./dist/testing.js"
+ },
+ "require": {
+ "types": "./dist/testing.d.cts",
+ "default": "./dist/testing.cjs"
+ }
+ },
+ "./storage": {
+ "import": {
+ "types": "./dist/storage.d.ts",
+ "default": "./dist/storage.js"
+ },
+ "require": {
+ "types": "./dist/storage.d.cts",
+ "default": "./dist/storage.cjs"
+ }
+ },
+ "./vite-builder-env": {
+ "types": "./dist/vite-builder-env.d.ts"
+ }
+ },
+ "scripts": {
+ "wxt": "tsx src/cli/index.ts",
+ "build": "tsx scripts/build.ts",
+ "check": "run-s -c check:*",
+ "check:default": "check",
+ "check:tsc-virtual": "tsc --noEmit -p src/virtual",
+ "test": "vitest",
+ "test:e2e": "vitest -r e2e",
+ "sync-releases": "pnpx changelogen@latest gh release"
+ },
+ "dependencies": {
+ "@aklinker1/rollup-plugin-visualizer": "5.12.0",
+ "@types/webextension-polyfill": "^0.10.5",
+ "@webext-core/fake-browser": "^1.3.1",
+ "@webext-core/isolated-element": "^1.1.2",
+ "@webext-core/match-patterns": "^1.0.3",
+ "async-mutex": "^0.4.0",
+ "c12": "^1.5.1",
+ "cac": "^6.7.14",
+ "chokidar": "^3.5.3",
+ "ci-info": "^4.0.0",
+ "defu": "^6.1.3",
+ "dequal": "^2.0.3",
+ "esbuild": "^0.19.5",
+ "fast-glob": "^3.3.1",
+ "filesize": "^10.0.8",
+ "fs-extra": "^11.1.1",
+ "get-port": "^7.0.0",
+ "giget": "^1.1.3",
+ "hookable": "^5.5.3",
+ "is-wsl": "^3.0.0",
+ "jiti": "^1.21.0",
+ "json5": "^2.2.3",
+ "jszip": "^3.10.1",
+ "linkedom": "^0.16.1",
+ "magicast": "^0.3.4",
+ "minimatch": "^9.0.3",
+ "natural-compare": "^1.4.0",
+ "normalize-path": "^3.0.0",
+ "nypm": "^0.3.6",
+ "open": "^10.1.0",
+ "ora": "^7.0.1",
+ "picocolors": "^1.0.0",
+ "prompts": "^2.4.2",
+ "publish-browser-extension": "^2.1.3",
+ "unimport": "^3.4.0",
+ "vite": "^5.2.8",
+ "web-ext-run": "^0.2.0",
+ "webextension-polyfill": "^0.10.0"
+ },
+ "devDependencies": {
+ "@aklinker1/check": "^1.1.1",
+ "@faker-js/faker": "^8.3.1",
+ "@types/fs-extra": "^11.0.4",
+ "@types/lodash.merge": "^4.6.9",
+ "@types/natural-compare": "^1.4.3",
+ "@types/normalize-path": "^3.0.2",
+ "@types/prompts": "^2.4.9",
+ "execa": "^8.0.1",
+ "extract-zip": "^2.0.1",
+ "happy-dom": "^13.3.8",
+ "lodash.merge": "^4.6.2",
+ "p-map": "^7.0.0",
+ "publint": "^0.2.6",
+ "tsup": "^8.0.1",
+ "tsx": "^4.6.2",
+ "vitest-plugin-random-seed": "^1.0.2",
+ "@vitest/coverage-v8": "^1.0.1",
+ "consola": "^3.2.3",
+ "lint-staged": "^15.2.0",
+ "npm-run-all": "^4.1.5",
+ "prettier": "^3.1.0",
+ "simple-git-hooks": "^2.9.0",
+ "typedoc": "^0.25.4",
+ "typedoc-plugin-markdown": "4.0.0-next.23",
+ "typedoc-vitepress-theme": "1.0.0-next.3",
+ "vitest": "^1.5.3",
+ "vitest-mock-extended": "^1.3.1",
+ "vue": "^3.3.10"
+ },
+ "changelog": {
+ "excludeAuthors": [
+ "aaronklinker1@gmail.com"
+ ]
+ }
+}
diff --git a/packages/wxt/scripts/build.ts b/packages/wxt/scripts/build.ts
new file mode 100644
index 0000000..d95b5dc
--- /dev/null
+++ b/packages/wxt/scripts/build.ts
@@ -0,0 +1,121 @@
+import tsup from 'tsup';
+import glob from 'fast-glob';
+import { printFileList } from '~/core/utils/log';
+import { formatDuration } from '~/core/utils/time';
+import ora from 'ora';
+import fs from 'fs-extra';
+import { consola } from 'consola';
+import pMap from 'p-map';
+import os from 'node:os';
+import path from 'node:path';
+import {
+ virtualModuleNames,
+ virtualEntrypointModuleNames,
+} from '~/core/utils/virtual-modules';
+
+const spinnerText = 'Building WXT';
+const spinner = ora(spinnerText).start();
+
+const startTime = Date.now();
+const outDir = 'dist';
+await fs.rm(path.join(outDir, '*'), { recursive: true, force: true });
+
+const preset = {
+ dts: true,
+ silent: true,
+ sourcemap: false,
+ external: virtualEntrypointModuleNames.map((name) => `virtual:user-${name}`),
+} satisfies tsup.Options;
+
+function spinnerPMap(configs: tsup.Options[]) {
+ let progress = 1;
+ const updateSpinner = () => {
+ spinner.text = `${spinnerText} [${progress}/${configs.length}]`;
+ };
+ updateSpinner();
+
+ return pMap(
+ config,
+ async (config) => {
+ const res = await tsup.build(config);
+ progress++;
+ updateSpinner();
+ return res;
+ },
+ {
+ stopOnError: true,
+ concurrency: process.env.CI === 'true' ? os.cpus().length : Infinity,
+ },
+ );
+}
+
+const config: tsup.Options[] = [
+ // CJS/ESM
+ {
+ ...preset,
+ entry: {
+ index: 'src/index.ts',
+ testing: 'src/testing/index.ts',
+ storage: 'src/storage.ts',
+ },
+ format: ['cjs', 'esm'],
+ clean: true,
+ },
+ // ESM-only
+ {
+ ...preset,
+ entry: {
+ browser: 'src/browser.ts',
+ sandbox: 'src/sandbox/index.ts',
+ client: 'src/client/index.ts',
+ },
+ format: ['esm'],
+ },
+ {
+ ...preset,
+ entry: virtualModuleNames.reduce>(
+ (acc, moduleName) => {
+ acc[`virtual/${moduleName}`] = `src/virtual/${moduleName}.ts`;
+ return acc;
+ },
+ {},
+ ),
+ format: ['esm'],
+ splitting: false,
+ dts: false,
+ external: [...preset.external, 'wxt'],
+ },
+ // CJS-only
+ {
+ ...preset,
+ entry: {
+ cli: 'src/cli/index.ts',
+ },
+ format: ['esm'],
+ banner: {
+ // Fixes dynamic require of nodejs modules. See https://github.com/wxt-dev/wxt/issues/355
+ // https://github.com/evanw/esbuild/issues/1921#issuecomment-1152991694
+ js: "import { createRequire } from 'module';const require = createRequire(import.meta.url);",
+ },
+ },
+];
+
+await spinnerPMap(config).catch((err) => {
+ spinner.fail();
+ console.error(err);
+ process.exit(1);
+});
+
+// Copy "public" files that need shipped inside WXT
+await fs.copyFile('src/vite-builder-env.d.ts', 'dist/vite-builder-env.d.ts');
+
+spinner.clear().stop();
+
+const duration = Date.now() - startTime;
+const outFiles = await glob(`${outDir}/**`, { absolute: true });
+await printFileList(
+ consola.success,
+ `Built WXT in ${formatDuration(duration)}`,
+ outDir,
+ outFiles,
+);
diff --git a/packages/wxt/src/__tests__/storage.test.ts b/packages/wxt/src/__tests__/storage.test.ts
new file mode 100644
index 0000000..08e225f
--- /dev/null
+++ b/packages/wxt/src/__tests__/storage.test.ts
@@ -0,0 +1,854 @@
+import { fakeBrowser } from '@webext-core/fake-browser';
+import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
+import { browser } from '~/browser';
+import { WxtStorageItem, storage } from '~/storage';
+
+/**
+ * This works because fakeBrowser is synchronous, and is will finish any number of chained
+ * calls within a single tick of the event loop, ie: a timeout of 0.
+ */
+async function waitForMigrations() {
+ return new Promise((res) => setTimeout(res));
+}
+
+describe('Storage Utils', () => {
+ beforeEach(() => {
+ fakeBrowser.reset();
+ storage.unwatch();
+ });
+
+ describe.each(['local', 'sync', 'managed', 'session'] as const)(
+ 'storage - %s',
+ (storageArea) => {
+ describe('getItem', () => {
+ it('should return the value from the correct storage area', async () => {
+ const expected = 123;
+ await fakeBrowser.storage[storageArea].set({ count: expected });
+
+ const actual = await storage.getItem(`${storageArea}:count`);
+
+ expect(actual).toBe(expected);
+ });
+
+ it('should return the value if multiple : are use in the key', async () => {
+ const expected = 'value';
+ await fakeBrowser.storage[storageArea].set({ 'some:key': expected });
+
+ const actual = await storage.getItem(`${storageArea}:some:key`);
+
+ expect(actual).toBe(expected);
+ });
+
+ it("should return null if the value doesn't exist", async () => {
+ const actual = await storage.getItem(`${storageArea}:count`);
+
+ expect(actual).toBeNull();
+ });
+
+ it('should return the default value if passed in options', async () => {
+ const expected = 0;
+ const actual = await storage.getItem(`${storageArea}:count`, {
+ defaultValue: expected,
+ });
+
+ expect(actual).toBe(expected);
+ });
+ });
+
+ describe('getItems', () => {
+ it('should return an array of values', async () => {
+ const expected = [
+ { key: `${storageArea}:count`, value: 234 },
+ { key: `${storageArea}:installDate`, value: null },
+ { key: `${storageArea}:otherValue`, value: 345 },
+ ];
+ const params = [
+ expected[0].key,
+ expected[1].key,
+ {
+ key: expected[2].key,
+ options: { defaultValue: expected[2].value },
+ },
+ ];
+ await fakeBrowser.storage[storageArea].set({
+ count: expected[0].value,
+ });
+
+ const actual = await storage.getItems(params);
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('getMeta', () => {
+ it('should return item metadata from key+$', async () => {
+ const expected = { v: 1 };
+ await fakeBrowser.storage[storageArea].set({ count$: expected });
+
+ const actual = await storage.getMeta(`${storageArea}:count`);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should return an empty object if missing', async () => {
+ const actual = await storage.getMeta(`${storageArea}:count`);
+
+ expect(actual).toEqual({});
+ });
+ });
+
+ describe('setItem', () => {
+ it('should set the value in the correct storage area', async () => {
+ const key = `${storageArea}:count`;
+ const value = 321;
+
+ await storage.setItem(key, value);
+ });
+
+ it.each([undefined, null])(
+ 'should remove the item from storage when setting the value to %s',
+ async (value) => {
+ await fakeBrowser.storage[storageArea].set({ count: 345 });
+ await storage.setItem(`${storageArea}:count`, value as null);
+
+ // For some reason storage sets the value to "null" instead of deleting it. So using
+ // fakeBrowser during the expect fails. Using storage works. I've confirmed that this
+ // doesn't happen in a real extension environment.
+ expect(await storage.getItem(`${storageArea}:count`)).toBeNull();
+ },
+ );
+ });
+
+ describe('setItems', () => {
+ it('should set multiple items in storage', async () => {
+ const expected = [
+ { key: `${storageArea}:count`, value: 234 },
+ { key: `${storageArea}:installDate`, value: null },
+ ];
+ await fakeBrowser.storage[storageArea].set({
+ count: 123,
+ installDate: 321,
+ });
+
+ await storage.setItems(expected);
+ const actual = await storage.getItems(
+ expected.map((item) => item.key),
+ );
+
+ expect(actual).toHaveLength(2);
+ expected.forEach((item) => {
+ expect(actual).toContainEqual(item);
+ });
+ });
+ });
+
+ describe('setMeta', () => {
+ it('should set metadata at key+$', async () => {
+ const existing = { v: 1 };
+ await browser.storage[storageArea].set({ count$: existing });
+ const newValues = {
+ date: Date.now(),
+ };
+ const expected = { ...existing, ...newValues };
+
+ await storage.setMeta(`${storageArea}:count`, newValues);
+ const actual = await storage.getMeta(`${storageArea}:count`);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it.each([undefined, null])(
+ 'should remove any properties set to %s',
+ async (version) => {
+ const existing = { v: 1 };
+ await browser.storage[storageArea].set({ count$: existing });
+ const expected = {};
+
+ await storage.setMeta(`${storageArea}:count`, { v: version });
+ const actual = await storage.getMeta(`${storageArea}:count`);
+
+ expect(actual).toEqual(expected);
+ },
+ );
+ });
+
+ describe('removeItem', () => {
+ it('should remove the key from storage', async () => {
+ await fakeBrowser.storage[storageArea].set({ count: 456 });
+
+ await storage.removeItem(`${storageArea}:count`);
+ const actual = await storage.getItem(`${storageArea}:count`);
+
+ expect(actual).toBeNull();
+ });
+
+ it('should not remove the metadata by default', async () => {
+ const expected = { v: 1 };
+ await fakeBrowser.storage[storageArea].set({
+ count$: expected,
+ count: 3,
+ });
+
+ await storage.removeItem(`${storageArea}:count`);
+ const actual = await storage.getMeta(`${storageArea}:count`);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should remove the metadata when requested', async () => {
+ await fakeBrowser.storage[storageArea].set({
+ count$: { v: 1 },
+ count: 3,
+ });
+
+ await storage.removeItem(`${storageArea}:count`, {
+ removeMeta: true,
+ });
+ const actual = await storage.getMeta(`${storageArea}:count`);
+
+ expect(actual).toEqual({});
+ });
+ });
+
+ describe('removeItems', () => {
+ it('should remove multiple items', async () => {
+ const key1 = `${storageArea}:one`;
+ const key2 = `${storageArea}:two`;
+ const key3 = `${storageArea}:three`;
+ await fakeBrowser.storage[storageArea].set({
+ ['one']: '1',
+ ['two']: null,
+ ['two$']: { v: 1 },
+ ['three']: '1',
+ ['three$']: { v: 1 },
+ });
+
+ await storage.removeItems([
+ key1,
+ key2,
+ { key: key3, options: { removeMeta: true } },
+ ]);
+
+ expect(await storage.getItem(key1)).toBeNull();
+ expect(await storage.getItem(key2)).toBeNull();
+ expect(await storage.getMeta(key2)).toEqual({ v: 1 });
+ expect(await storage.getItem(key3)).toBeNull();
+ expect(await storage.getMeta(key3)).toEqual({});
+ });
+ });
+
+ describe('removeMeta', () => {
+ it('should remove all metadata', async () => {
+ await fakeBrowser.storage[storageArea].set({ count$: { v: 4 } });
+
+ await storage.removeMeta(`${storageArea}:count`);
+ const actual = await storage.getMeta(`${storageArea}:count`);
+
+ expect(actual).toEqual({});
+ });
+
+ it('should only remove specific properties', async () => {
+ await fakeBrowser.storage[storageArea].set({
+ count$: { v: 4, d: Date.now() },
+ });
+
+ await storage.removeMeta(`${storageArea}:count`, ['d']);
+ const actual = await storage.getMeta(`${storageArea}:count`);
+
+ expect(actual).toEqual({ v: 4 });
+ });
+ });
+
+ describe('snapshot', () => {
+ it('should return a snapshot of the entire storage without area prefixes', async () => {
+ const expected = {
+ count: 1,
+ count$: { v: 2 },
+ example: 'test',
+ };
+
+ await fakeBrowser.storage[storageArea].set(expected);
+ const actual = await storage.snapshot(storageArea);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should exclude specific properties and their metadata', async () => {
+ const input = {
+ count: 1,
+ count$: { v: 2 },
+ example: 'test',
+ };
+ const excludeKeys = ['count'];
+ const expected = {
+ example: 'test',
+ };
+
+ await fakeBrowser.storage[storageArea].set(input);
+ const actual = await storage.snapshot(storageArea, { excludeKeys });
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('restoreSnapshot', () => {
+ it('should restore a snapshot object by setting all values in storage', async () => {
+ const data = {
+ one: 'one',
+ two: 'two',
+ };
+ const existing = {
+ two: 'two-two',
+ three: 'three',
+ };
+ await fakeBrowser.storage[storageArea].set(existing);
+
+ await storage.restoreSnapshot(storageArea, data);
+ const actual = await storage.snapshot(storageArea);
+
+ expect(actual).toEqual({ ...existing, ...data });
+ });
+
+ it('should overwrite, not merge, any metadata keys in the snapshot', async () => {
+ const existing = {
+ count: 1,
+ count$: {
+ v: 2,
+ },
+ };
+ const data = {
+ count$: {
+ restoredAt: Date.now(),
+ },
+ };
+ const expected = {
+ ...existing,
+ count$: data.count$,
+ };
+ await fakeBrowser.storage[storageArea].set(existing);
+
+ await storage.restoreSnapshot(storageArea, data);
+ const actual = await storage.snapshot(storageArea);
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('watch', () => {
+ it('should not trigger if the changed key is different from the requested key', async () => {
+ const cb = vi.fn();
+
+ storage.watch(`${storageArea}:key`, cb);
+ await storage.setItem(`${storageArea}:not-the-key`, '123');
+
+ expect(cb).not.toBeCalled();
+ });
+
+ it("should not trigger if the value doesn't change", async () => {
+ const cb = vi.fn();
+ const value = '123';
+
+ await storage.setItem(`${storageArea}:key`, value);
+ storage.watch(`${storageArea}:key`, cb);
+ await storage.setItem(`${storageArea}:key`, value);
+
+ expect(cb).not.toBeCalled();
+ });
+
+ it('should call the callback when the value changes', async () => {
+ const cb = vi.fn();
+ const newValue = '123';
+ const oldValue = null;
+
+ storage.watch(`${storageArea}:key`, cb);
+ await storage.setItem(`${storageArea}:key`, newValue);
+
+ expect(cb).toBeCalledTimes(1);
+ expect(cb).toBeCalledWith(newValue, oldValue);
+ });
+
+ it('should remove the listener when calling the returned function', async () => {
+ const cb = vi.fn();
+
+ const unwatch = storage.watch(`${storageArea}:key`, cb);
+ unwatch();
+ await storage.setItem(`${storageArea}:key`, '123');
+
+ expect(cb).not.toBeCalled();
+ });
+ });
+
+ describe('unwatch', () => {
+ it('should remove all watch listeners', async () => {
+ const cb = vi.fn();
+
+ storage.watch(`${storageArea}:key`, cb);
+ storage.unwatch();
+ await storage.setItem(`${storageArea}:key`, '123');
+
+ expect(cb).not.toBeCalled();
+ });
+ });
+ },
+ );
+
+ describe('defineItem', () => {
+ describe('versioning', () => {
+ it('should migrate values to the latest when a version upgrade is detected', async () => {
+ await fakeBrowser.storage.local.set({
+ count: 2,
+ count$: { v: 1 },
+ });
+ const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
+ const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
+
+ const item = storage.defineItem(`local:count`, {
+ defaultValue: 0,
+ version: 3,
+ migrations: {
+ 2: migrateToV2,
+ 3: migrateToV3,
+ },
+ });
+ await waitForMigrations();
+
+ const actualValue = await item.getValue();
+ const actualMeta = await item.getMeta();
+
+ expect(actualValue).toEqual(12);
+ expect(actualMeta).toEqual({ v: 3 });
+
+ expect(migrateToV2).toBeCalledTimes(1);
+ expect(migrateToV2).toBeCalledWith(2);
+
+ expect(migrateToV3).toBeCalledTimes(1);
+ expect(migrateToV3).toBeCalledWith(4);
+ });
+
+ it("should not run migrations if the value doesn't exist yet", async () => {
+ const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
+ const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
+
+ const item = storage.defineItem(`local:count`, {
+ defaultValue: 0,
+ version: 3,
+ migrations: {
+ 2: migrateToV2,
+ 3: migrateToV3,
+ },
+ });
+ await waitForMigrations();
+
+ const actualValue = await item.getValue();
+ const actualMeta = await item.getMeta();
+
+ expect(actualValue).toEqual(0);
+ expect(actualMeta).toEqual({});
+
+ expect(migrateToV2).not.toBeCalled();
+ expect(migrateToV3).not.toBeCalled();
+ });
+
+ it('should run the v2 migration when converting an unversioned item to a versioned one', async () => {
+ await fakeBrowser.storage.local.set({
+ count: 2,
+ });
+ const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
+
+ const item = storage.defineItem(`local:count`, {
+ defaultValue: 0,
+ version: 2,
+ migrations: {
+ 2: migrateToV2,
+ },
+ });
+ await waitForMigrations();
+
+ const actualValue = await item.getValue();
+ const actualMeta = await item.getMeta();
+
+ expect(actualValue).toEqual(4);
+ expect(actualMeta).toEqual({ v: 2 });
+
+ expect(migrateToV2).toBeCalledTimes(1);
+ expect(migrateToV2).toBeCalledWith(2);
+ });
+
+ it('should not run old migrations if the version is unchanged', async () => {
+ await fakeBrowser.storage.local.set({
+ count: 2,
+ count$: { v: 3 },
+ });
+ const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
+ const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
+
+ storage.defineItem(`local:count`, {
+ defaultValue: 0,
+ version: 3,
+ migrations: {
+ 2: migrateToV2,
+ 3: migrateToV3,
+ },
+ });
+ await waitForMigrations();
+
+ expect(migrateToV2).not.toBeCalled();
+ expect(migrateToV3).not.toBeCalled();
+ });
+
+ it('should skip missing migration functions', async () => {
+ await fakeBrowser.storage.local.set({
+ count: 2,
+ count$: { v: 0 },
+ });
+ const migrateToV1 = vi.fn((oldCount) => oldCount * 1);
+ const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
+
+ const item = storage.defineItem(`local:count`, {
+ defaultValue: 0,
+ version: 3,
+ migrations: {
+ 1: migrateToV1,
+ 3: migrateToV3,
+ },
+ });
+ await waitForMigrations();
+
+ const actualValue = await item.getValue();
+ const actualMeta = await item.getMeta();
+
+ expect(actualValue).toEqual(6);
+ expect(actualMeta).toEqual({ v: 3 });
+
+ expect(migrateToV1).toBeCalledTimes(1);
+ expect(migrateToV1).toBeCalledWith(2);
+
+ expect(migrateToV3).toBeCalledTimes(1);
+ expect(migrateToV3).toBeCalledWith(2);
+ });
+
+ it('should throw an error if the new version is less than the previous version', async () => {
+ const prevVersion = 2;
+ const nextVersion = 1;
+ await fakeBrowser.storage.local.set({
+ count: 0,
+ count$: { v: prevVersion },
+ });
+
+ const item = storage.defineItem(`local:count`, {
+ defaultValue: 0,
+ version: nextVersion,
+ });
+ await waitForMigrations();
+
+ await expect(item.migrate()).rejects.toThrow(
+ 'Version downgrade detected (v2 -> v1) for "local:count"',
+ );
+ });
+ });
+
+ describe('getValue', () => {
+ it('should return the value from storage', async () => {
+ const expected = 2;
+ const item = storage.defineItem(`local:count`);
+ await fakeBrowser.storage.local.set({ count: expected });
+
+ const actual = await item.getValue();
+
+ expect(actual).toBe(expected);
+ });
+
+ it('should return null if missing', async () => {
+ const item = storage.defineItem(`local:count`);
+
+ const actual = await item.getValue();
+
+ expect(actual).toBeNull();
+ });
+
+ it('should return the provided default value if missing', async () => {
+ const expected = 0;
+ const item = storage.defineItem(`local:count`, {
+ defaultValue: expected,
+ });
+
+ const actual = await item.getValue();
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('getMeta', () => {
+ it('should return the value from storage at key+$', async () => {
+ const expected = { v: 2 };
+ const item = storage.defineItem(`local:count`);
+ await fakeBrowser.storage.local.set({ count$: expected });
+
+ const actual = await item.getMeta();
+
+ expect(actual).toBe(expected);
+ });
+
+ it('should return an empty object if missing', async () => {
+ const expected = {};
+ const item = storage.defineItem(`local:count`);
+
+ const actual = await item.getMeta();
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('setValue', () => {
+ it('should set the value in storage', async () => {
+ const expected = 1;
+ const item = storage.defineItem(`local:count`);
+
+ await item.setValue(expected);
+ const actual = await item.getValue();
+
+ expect(actual).toBe(expected);
+ });
+
+ it.each([undefined, null])(
+ 'should remove the value in storage when %s is passed in',
+ async (value) => {
+ const item = storage.defineItem(`local:count`);
+
+ // @ts-expect-error: undefined is not assignable to null, but we're testing that case on purpose
+ await item.setValue(value);
+ const actual = await item.getValue();
+
+ expect(actual).toBeNull();
+ },
+ );
+ });
+
+ describe('setMeta', () => {
+ it('should set metadata at key+$', async () => {
+ const expected = { date: Date.now() };
+ const item = storage.defineItem(
+ `local:count`,
+ );
+
+ await item.setMeta(expected);
+ const actual = await item.getMeta();
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should add to metadata if already present', async () => {
+ const existing = { v: 2 };
+ const newFields = { date: Date.now() };
+ const expected = { ...existing, ...newFields };
+ const item = storage.defineItem(
+ `local:count`,
+ );
+ await fakeBrowser.storage.local.set({
+ count$: existing,
+ });
+
+ await item.setMeta(newFields);
+ const actual = await item.getMeta();
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('removeValue', () => {
+ it('should remove the key from storage', async () => {
+ const item = storage.defineItem(`local:count`);
+ await fakeBrowser.storage.local.set({ count: 456 });
+
+ await item.removeValue();
+ const actual = await item.getValue();
+
+ expect(actual).toBeNull();
+ });
+
+ it('should not remove the metadata by default', async () => {
+ const item = storage.defineItem(`local:count`);
+ const expected = { v: 1 };
+ await fakeBrowser.storage.local.set({
+ count$: expected,
+ count: 3,
+ });
+
+ await item.removeValue();
+ const actual = await item.getMeta();
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should remove the metadata when requested', async () => {
+ const item = storage.defineItem(`local:count`);
+ await fakeBrowser.storage.local.set({
+ count$: { v: 1 },
+ count: 3,
+ });
+
+ await item.removeValue({ removeMeta: true });
+ const actual = await item.getMeta();
+
+ expect(actual).toEqual({});
+ });
+ });
+
+ describe('removeMeta', () => {
+ it('should remove all metadata', async () => {
+ const item = storage.defineItem(`local:count`);
+ await fakeBrowser.storage.local.set({ count$: { v: 4 } });
+
+ await item.removeMeta();
+ const actual = await item.getMeta();
+
+ expect(actual).toEqual({});
+ });
+
+ it('should only remove specific properties', async () => {
+ const item = storage.defineItem(
+ `local:count`,
+ );
+ await fakeBrowser.storage.local.set({
+ count$: { v: 4, d: Date.now() },
+ });
+
+ await item.removeMeta(['d']);
+ const actual = await item.getMeta();
+
+ expect(actual).toEqual({ v: 4 });
+ });
+ });
+
+ describe('watch', () => {
+ it("should not trigger if the changed key is different from the item's key", async () => {
+ const item = storage.defineItem(`local:key`);
+ const cb = vi.fn();
+
+ item.watch(cb);
+ await storage.setItem(`local:not-the-key`, '123');
+
+ expect(cb).not.toBeCalled();
+ });
+
+ it("should not trigger if the value doesn't change", async () => {
+ const item = storage.defineItem(`local:key`);
+ const cb = vi.fn();
+ const value = '123';
+
+ await item.setValue(value);
+ item.watch(cb);
+ await item.setValue(value);
+
+ expect(cb).not.toBeCalled();
+ });
+
+ it('should call the callback when the value changes', async () => {
+ const item = storage.defineItem(`local:key`);
+ const cb = vi.fn();
+ const newValue = '123';
+ const oldValue = null;
+
+ item.watch(cb);
+ await item.setValue(newValue);
+
+ expect(cb).toBeCalledTimes(1);
+ expect(cb).toBeCalledWith(newValue, oldValue);
+ });
+
+ it('should use the default value for the newValue when the item is removed', async () => {
+ const defaultValue = 'default';
+ const item = storage.defineItem(`local:key`, {
+ defaultValue,
+ });
+ const cb = vi.fn();
+ const oldValue = '123';
+ await item.setValue(oldValue);
+
+ item.watch(cb);
+ await item.removeValue();
+
+ expect(cb).toBeCalledTimes(1);
+ expect(cb).toBeCalledWith(defaultValue, oldValue);
+ });
+
+ it("should use the default value for the oldItem when the item didn't exist in storage yet", async () => {
+ const defaultValue = 'default';
+ const item = storage.defineItem(`local:key`, {
+ defaultValue,
+ });
+ const cb = vi.fn();
+ const newValue = '123';
+ await item.removeValue();
+
+ item.watch(cb);
+ await item.setValue(newValue);
+
+ expect(cb).toBeCalledTimes(1);
+ expect(cb).toBeCalledWith(newValue, defaultValue);
+ });
+
+ it('should remove the listener when calling the returned function', async () => {
+ const item = storage.defineItem(`local:key`);
+ const cb = vi.fn();
+
+ const unwatch = item.watch(cb);
+ unwatch();
+ await item.setValue('123');
+
+ expect(cb).not.toBeCalled();
+ });
+ });
+
+ describe('unwatch', () => {
+ it('should remove all watch listeners', async () => {
+ const item = storage.defineItem(`local:key`);
+ const cb = vi.fn();
+
+ item.watch(cb);
+ storage.unwatch();
+ await item.setValue('123');
+
+ expect(cb).not.toBeCalled();
+ });
+ });
+
+ describe('defaultValue', () => {
+ it('should return the default value when provided', () => {
+ const defaultValue = 123;
+ const item = storage.defineItem(`local:test`, {
+ defaultValue,
+ });
+
+ expect(item.defaultValue).toBe(defaultValue);
+ });
+
+ it('should return null when not provided', () => {
+ const item = storage.defineItem(`local:test`);
+
+ expect(item.defaultValue).toBeNull();
+ });
+ });
+
+ describe('types', () => {
+ it('should define a nullable value when options are not passed', () => {
+ const item = storage.defineItem(`local:test`);
+ expectTypeOf(item).toEqualTypeOf>();
+ });
+
+ it('should define a non-null value when options are passed with a nullish default value', () => {
+ const item = storage.defineItem(`local:test`, {
+ defaultValue: 123,
+ });
+ expectTypeOf(item).toEqualTypeOf>();
+ });
+
+ it('should define a nullable value when options are passed with null default value', () => {
+ const item = storage.defineItem(`local:test`, {
+ defaultValue: null,
+ });
+ expectTypeOf(item).toEqualTypeOf>();
+ });
+ });
+ });
+});
diff --git a/packages/wxt/src/browser.ts b/packages/wxt/src/browser.ts
new file mode 100644
index 0000000..93f0ddb
--- /dev/null
+++ b/packages/wxt/src/browser.ts
@@ -0,0 +1,81 @@
+/**
+ * @module wxt/browser
+ */
+import originalBrowser, { Browser, Runtime, I18n } from 'webextension-polyfill';
+
+export interface AugmentedBrowser extends Browser {
+ runtime: WxtRuntime;
+ i18n: WxtI18n;
+}
+
+export interface WxtRuntime extends Runtime.Static {
+ // Overriden per-project
+}
+
+export interface WxtI18n extends I18n.Static {
+ // Overriden per-project
+}
+
+export const browser: AugmentedBrowser = originalBrowser;
+
+// re-export all the types from webextension-polyfill
+// Because webextension-polyfill uses a weird namespace with "import export", there isn't a good way
+// to get these types without re-listing them.
+/** @ignore */
+export type {
+ ActivityLog,
+ Alarms,
+ Bookmarks,
+ Action,
+ BrowserAction,
+ BrowserSettings,
+ BrowsingData,
+ CaptivePortal,
+ Clipboard,
+ Commands,
+ ContentScripts,
+ ContextualIdentities,
+ Cookies,
+ DeclarativeNetRequest,
+ Devtools,
+ Dns,
+ Downloads,
+ Events,
+ Experiments,
+ Extension,
+ ExtensionTypes,
+ Find,
+ GeckoProfiler,
+ History,
+ I18n,
+ Identity,
+ Idle,
+ Management,
+ Manifest, // TODO: Export custom manifest types that are valid for both Chrome and Firefox.
+ ContextMenus,
+ Menus,
+ NetworkStatus,
+ NormandyAddonStudy,
+ Notifications,
+ Omnibox,
+ PageAction,
+ Permissions,
+ Pkcs11,
+ Privacy,
+ Proxy,
+ Runtime,
+ Scripting,
+ Search,
+ Sessions,
+ SidebarAction,
+ Storage,
+ Tabs,
+ Theme,
+ TopSites,
+ Types,
+ Urlbar,
+ UserScripts,
+ WebNavigation,
+ WebRequest,
+ Windows,
+} from 'webextension-polyfill';
diff --git a/packages/wxt/src/cli/__tests__/index.test.ts b/packages/wxt/src/cli/__tests__/index.test.ts
new file mode 100644
index 0000000..14a1a3c
--- /dev/null
+++ b/packages/wxt/src/cli/__tests__/index.test.ts
@@ -0,0 +1,394 @@
+import { describe, it, vi, beforeEach, expect } from 'vitest';
+import { build } from '~/core/build';
+import { createServer } from '~/core/create-server';
+import { zip } from '~/core/zip';
+import { prepare } from '~/core/prepare';
+import { clean } from '~/core/clean';
+import { initialize } from '~/core/initialize';
+import { mock } from 'vitest-mock-extended';
+import consola from 'consola';
+
+vi.mock('~/core/build');
+const buildMock = vi.mocked(build);
+
+vi.mock('~/core/create-server');
+const createServerMock = vi.mocked(createServer);
+
+vi.mock('~/core/zip');
+const zipMock = vi.mocked(zip);
+
+vi.mock('~/core/prepare');
+const prepareMock = vi.mocked(prepare);
+
+vi.mock('~/core/clean');
+const cleanMock = vi.mocked(clean);
+
+vi.mock('~/core/initialize');
+const initializeMock = vi.mocked(initialize);
+
+consola.wrapConsole();
+
+const ogArgv = process.argv;
+
+function mockArgv(...args: string[]) {
+ process.argv = ['/bin/node', 'bin/wxt.mjs', ...args];
+}
+
+async function importCli() {
+ await import('~/cli');
+}
+
+describe('CLI', () => {
+ beforeEach(() => {
+ vi.resetModules();
+ process.argv = ogArgv;
+ createServerMock.mockResolvedValue(mock());
+ });
+
+ describe('dev', () => {
+ it('should not pass any config when no flags are passed', async () => {
+ mockArgv();
+ await importCli();
+
+ expect(createServerMock).toBeCalledWith({});
+ });
+
+ it('should respect passing a custom root', async () => {
+ mockArgv('path/to/root');
+ await importCli();
+
+ expect(createServerMock).toBeCalledWith({
+ root: 'path/to/root',
+ });
+ });
+
+ it('should respect a custom config file', async () => {
+ mockArgv('-c', './path/to/config.ts');
+ await importCli();
+
+ expect(createServerMock).toBeCalledWith({
+ configFile: './path/to/config.ts',
+ });
+ });
+
+ it('should respect passing a custom mode', async () => {
+ mockArgv('-m', 'development');
+ await importCli();
+
+ expect(createServerMock).toBeCalledWith({
+ mode: 'development',
+ });
+ });
+
+ it('should respect passing a custom browser', async () => {
+ mockArgv('-b', 'firefox');
+ await importCli();
+
+ expect(createServerMock).toBeCalledWith({
+ browser: 'firefox',
+ });
+ });
+
+ it('should pass correct filtered entrypoints', async () => {
+ mockArgv('-e', 'popup', '-e', 'options');
+ await importCli();
+
+ expect(createServerMock).toBeCalledWith({
+ filterEntrypoints: ['popup', 'options'],
+ });
+ });
+
+ it('should respect passing --mv2', async () => {
+ mockArgv('--mv2');
+ await importCli();
+
+ expect(createServerMock).toBeCalledWith({
+ manifestVersion: 2,
+ });
+ });
+
+ it('should respect passing --mv3', async () => {
+ mockArgv('--mv3');
+ await importCli();
+
+ expect(createServerMock).toBeCalledWith({
+ manifestVersion: 3,
+ });
+ });
+
+ it('should respect passing --port', async () => {
+ const expectedPort = 3100;
+ mockArgv('--port', String(expectedPort));
+ await importCli();
+
+ expect(createServerMock).toBeCalledWith({
+ dev: {
+ server: {
+ port: expectedPort,
+ },
+ },
+ });
+ });
+
+ it('should respect passing --debug', async () => {
+ mockArgv('--debug');
+ await importCli();
+
+ expect(createServerMock).toBeCalledWith({
+ debug: true,
+ });
+ });
+ });
+
+ describe('build', () => {
+ it('should not pass any config when no flags are passed', async () => {
+ mockArgv('build');
+ await importCli();
+
+ expect(buildMock).toBeCalledWith({});
+ });
+
+ it('should respect passing a custom root', async () => {
+ mockArgv('build', 'path/to/root');
+ await importCli();
+
+ expect(buildMock).toBeCalledWith({
+ root: 'path/to/root',
+ });
+ });
+
+ it('should respect a custom config file', async () => {
+ mockArgv('build', '-c', './path/to/config.ts');
+ await importCli();
+
+ expect(buildMock).toBeCalledWith({
+ configFile: './path/to/config.ts',
+ });
+ });
+
+ it('should respect passing a custom mode', async () => {
+ mockArgv('build', '-m', 'development');
+ await importCli();
+
+ expect(buildMock).toBeCalledWith({
+ mode: 'development',
+ });
+ });
+
+ it('should respect passing a custom browser', async () => {
+ mockArgv('build', '-b', 'firefox');
+ await importCli();
+
+ expect(buildMock).toBeCalledWith({
+ browser: 'firefox',
+ });
+ });
+
+ it('should pass correct filtered entrypoints', async () => {
+ mockArgv('build', '-e', 'popup', '-e', 'options');
+ await importCli();
+
+ expect(buildMock).toBeCalledWith({
+ filterEntrypoints: ['popup', 'options'],
+ });
+ });
+
+ it('should respect passing --mv2', async () => {
+ mockArgv('build', '--mv2');
+ await importCli();
+
+ expect(buildMock).toBeCalledWith({
+ manifestVersion: 2,
+ });
+ });
+
+ it('should respect passing --mv3', async () => {
+ mockArgv('build', '--mv3');
+ await importCli();
+
+ expect(buildMock).toBeCalledWith({
+ manifestVersion: 3,
+ });
+ });
+
+ it('should include analysis in the build', async () => {
+ mockArgv('build', '--analyze');
+ await importCli();
+
+ expect(buildMock).toBeCalledWith({
+ analysis: {
+ enabled: true,
+ },
+ });
+ });
+
+ it('should respect passing --debug', async () => {
+ mockArgv('build', '--debug');
+ await importCli();
+
+ expect(buildMock).toBeCalledWith({
+ debug: true,
+ });
+ });
+ });
+
+ describe('zip', () => {
+ it('should not pass any config when no flags are passed', async () => {
+ mockArgv('zip');
+ await importCli();
+
+ expect(zipMock).toBeCalledWith({});
+ });
+
+ it('should respect passing a custom root', async () => {
+ mockArgv('zip', 'path/to/root');
+ await importCli();
+
+ expect(zipMock).toBeCalledWith({
+ root: 'path/to/root',
+ });
+ });
+
+ it('should respect a custom config file', async () => {
+ mockArgv('zip', '-c', './path/to/config.ts');
+ await importCli();
+
+ expect(zipMock).toBeCalledWith({
+ configFile: './path/to/config.ts',
+ });
+ });
+
+ it('should respect passing a custom mode', async () => {
+ mockArgv('zip', '-m', 'development');
+ await importCli();
+
+ expect(zipMock).toBeCalledWith({
+ mode: 'development',
+ });
+ });
+
+ it('should respect passing a custom browser', async () => {
+ mockArgv('zip', '-b', 'firefox');
+ await importCli();
+
+ expect(zipMock).toBeCalledWith({
+ browser: 'firefox',
+ });
+ });
+
+ it('should respect passing --mv2', async () => {
+ mockArgv('zip', '--mv2');
+ await importCli();
+
+ expect(zipMock).toBeCalledWith({
+ manifestVersion: 2,
+ });
+ });
+
+ it('should respect passing --mv3', async () => {
+ mockArgv('zip', '--mv3');
+ await importCli();
+
+ expect(zipMock).toBeCalledWith({
+ manifestVersion: 3,
+ });
+ });
+
+ it('should respect passing --debug', async () => {
+ mockArgv('zip', '--debug');
+ await importCli();
+
+ expect(zipMock).toBeCalledWith({
+ debug: true,
+ });
+ });
+ });
+
+ describe('prepare', () => {
+ it('should not pass any config when no flags are passed', async () => {
+ mockArgv('prepare');
+ await importCli();
+
+ expect(prepareMock).toBeCalledWith({});
+ });
+
+ it('should respect passing a custom root', async () => {
+ mockArgv('prepare', 'path/to/root');
+ await importCli();
+
+ expect(prepareMock).toBeCalledWith({
+ root: 'path/to/root',
+ });
+ });
+
+ it('should respect a custom config file', async () => {
+ mockArgv('prepare', '-c', './path/to/config.ts');
+ await importCli();
+
+ expect(prepareMock).toBeCalledWith({
+ configFile: './path/to/config.ts',
+ });
+ });
+
+ it('should respect passing --debug', async () => {
+ mockArgv('prepare', '--debug');
+ await importCli();
+
+ expect(prepareMock).toBeCalledWith({
+ debug: true,
+ });
+ });
+ });
+
+ describe('clean', () => {
+ it('should not pass any config when no flags are passed', async () => {
+ mockArgv('clean');
+ await importCli();
+
+ expect(cleanMock).toBeCalledWith(undefined);
+ });
+
+ it('should respect passing a custom root', async () => {
+ mockArgv('clean', 'path/to/root');
+ await importCli();
+
+ expect(cleanMock).toBeCalledWith('path/to/root');
+ });
+ });
+
+ describe('init', () => {
+ it('should not pass any options when no flags are passed', async () => {
+ mockArgv('init');
+ await importCli();
+
+ expect(initializeMock).toBeCalledWith({});
+ });
+
+ it('should respect the provided folder', async () => {
+ mockArgv('init', 'path/to/folder');
+ await importCli();
+
+ expect(initializeMock).toBeCalledWith({
+ directory: 'path/to/folder',
+ });
+ });
+
+ it('should respect passing --template', async () => {
+ mockArgv('init', '-t', 'vue');
+ await importCli();
+
+ expect(initializeMock).toBeCalledWith({
+ template: 'vue',
+ });
+ });
+
+ it('should respect passing --pm', async () => {
+ mockArgv('init', '--pm', 'pnpm');
+ await importCli();
+
+ expect(initializeMock).toBeCalledWith({
+ packageManager: 'pnpm',
+ });
+ });
+ });
+});
diff --git a/packages/wxt/src/cli/cli-utils.ts b/packages/wxt/src/cli/cli-utils.ts
new file mode 100644
index 0000000..f332e00
--- /dev/null
+++ b/packages/wxt/src/cli/cli-utils.ts
@@ -0,0 +1,103 @@
+import { CAC, Command } from 'cac';
+import consola, { LogLevels } from 'consola';
+import { filterTruthy, toArray } from '~/core/utils/arrays';
+import { printHeader } from '~/core/utils/log';
+import { formatDuration } from '~/core/utils/time';
+import { ValidationError } from '~/core/utils/validation';
+import { registerWxt } from '~/core/wxt';
+
+/**
+ * Wrap an action handler to add a timer, error handling, and maybe enable debug mode.
+ */
+export function wrapAction(
+ cb: (
+ ...args: any[]
+ ) => void | { isOngoing?: boolean } | Promise,
+ options?: {
+ disableFinishedLog?: boolean;
+ },
+) {
+ return async (...args: any[]) => {
+ // Enable consola's debug mode globally at the start of all commands when the `--debug` flag is
+ // passed
+ const isDebug = !!args.find((arg) => arg?.debug);
+ if (isDebug) {
+ consola.level = LogLevels.debug;
+ }
+
+ const startTime = Date.now();
+ try {
+ printHeader();
+
+ const status = await cb(...args);
+
+ if (!status?.isOngoing && !options?.disableFinishedLog)
+ consola.success(
+ `Finished in ${formatDuration(Date.now() - startTime)}`,
+ );
+ } catch (err) {
+ consola.fail(
+ `Command failed after ${formatDuration(Date.now() - startTime)}`,
+ );
+ if (err instanceof ValidationError) {
+ // Don't log these errors, they've already been logged
+ } else {
+ consola.error(err);
+ }
+ process.exit(1);
+ }
+ };
+}
+
+/**
+ * Array flags, when not passed, are either `undefined` or `[undefined]`. This function filters out
+ * the
+ */
+export function getArrayFromFlags(
+ flags: any,
+ name: string,
+): T[] | undefined {
+ const array = toArray(flags[name]);
+ const result = filterTruthy(array);
+ return result.length ? result : undefined;
+}
+
+const aliasCommandNames = new Set();
+/**
+ * @param base Command to add this one to
+ * @param name The command name to add
+ * @param alias The CLI tool being aliased
+ * @param bin The CLI tool binary name. Usually the same as the alias
+ * @param docsUrl URL to the docs for the aliased CLI tool
+ */
+export function createAliasedCommand(
+ base: CAC,
+ name: string,
+ alias: string,
+ bin: string,
+ docsUrl: string,
+) {
+ const aliasedCommand = base
+ .command(name, `Alias for ${alias} (${docsUrl})`)
+ .allowUnknownOptions()
+ .action(async () => {
+ try {
+ await registerWxt('build');
+
+ const args = process.argv.slice(
+ process.argv.indexOf(aliasedCommand.name) + 1,
+ );
+ const { execa } = await import('execa');
+ await execa(bin, args, {
+ stdio: 'inherit',
+ });
+ } catch {
+ // Let the other aliased CLI log errors, just exit
+ process.exit(1);
+ }
+ });
+ aliasCommandNames.add(aliasedCommand.name);
+}
+export function isAliasedCommand(command: Command | undefined): boolean {
+ return !!command && aliasCommandNames.has(command.name);
+}
diff --git a/packages/wxt/src/cli/commands.ts b/packages/wxt/src/cli/commands.ts
new file mode 100644
index 0000000..c17a39d
--- /dev/null
+++ b/packages/wxt/src/cli/commands.ts
@@ -0,0 +1,162 @@
+import cac from 'cac';
+import { build, clean, createServer, initialize, prepare, zip } from '~/core';
+import {
+ createAliasedCommand,
+ getArrayFromFlags,
+ wrapAction,
+} from './cli-utils';
+
+const cli = cac('wxt');
+
+cli.option('--debug', 'enable debug mode');
+
+// DEV
+cli
+ .command('[root]', 'start dev server')
+ .option('-c, --config ', 'use specified config file')
+ .option('-m, --mode ', 'set env mode')
+ .option('-b, --browser ', 'specify a browser')
+ .option('-p, --port ', 'specify a port for the dev server')
+ .option(
+ '-e, --filter-entrypoint ',
+ 'only build specific entrypoints',
+ {
+ type: [],
+ },
+ )
+ .option('--mv3', 'target manifest v3')
+ .option('--mv2', 'target manifest v2')
+ .action(
+ wrapAction(async (root, flags) => {
+ const server = await createServer({
+ root,
+ mode: flags.mode,
+ browser: flags.browser,
+ manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
+ configFile: flags.config,
+ debug: flags.debug,
+ filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
+ dev:
+ flags.port == null
+ ? undefined
+ : {
+ server: {
+ port: parseInt(flags.port),
+ },
+ },
+ });
+ await server.start();
+ return { isOngoing: true };
+ }),
+ );
+
+// BUILD
+cli
+ .command('build [root]', 'build for production')
+ .option('-c, --config ', 'use specified config file')
+ .option('-m, --mode ', 'set env mode')
+ .option('-b, --browser ', 'specify a browser')
+ .option(
+ '-e, --filter-entrypoint ',
+ 'only build specific entrypoints',
+ {
+ type: [],
+ },
+ )
+ .option('--mv3', 'target manifest v3')
+ .option('--mv2', 'target manifest v2')
+ .option('--analyze', 'visualize extension bundle')
+ .option('--analyze-open', 'automatically open stats.html in browser')
+ .action(
+ wrapAction(async (root, flags) => {
+ await build({
+ root,
+ mode: flags.mode,
+ browser: flags.browser,
+ manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
+ configFile: flags.config,
+ debug: flags.debug,
+ analysis: flags.analyze
+ ? {
+ enabled: true,
+ open: flags.analyzeOpen,
+ }
+ : undefined,
+ filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
+ });
+ }),
+ );
+
+// ZIP
+cli
+ .command('zip [root]', 'build for production and zip output')
+ .option('-c, --config ', 'use specified config file')
+ .option('-m, --mode ', 'set env mode')
+ .option('-b, --browser ', 'specify a browser')
+ .option('--mv3', 'target manifest v3')
+ .option('--mv2', 'target manifest v2')
+ .action(
+ wrapAction(async (root, flags) => {
+ await zip({
+ root,
+ mode: flags.mode,
+ browser: flags.browser,
+ manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
+ configFile: flags.config,
+ debug: flags.debug,
+ });
+ }),
+ );
+
+// PREPARE
+cli
+ .command('prepare [root]', 'prepare typescript project')
+ .option('-c, --config ', 'use specified config file')
+ .action(
+ wrapAction(async (root, flags) => {
+ await prepare({
+ root,
+ configFile: flags.config,
+ debug: flags.debug,
+ });
+ }),
+ );
+
+// CLEAN
+cli
+ .command('clean [root]', 'clean generated files and caches')
+ .alias('cleanup')
+ .action(
+ wrapAction(async (root, flags) => {
+ await clean(root);
+ }),
+ );
+
+// INIT
+cli
+ .command('init [directory]', 'initialize a new project')
+ .option('-t, --template ', 'template to use')
+ .option('--pm ', 'which package manager to use')
+ .action(
+ wrapAction(
+ async (directory, flags) => {
+ await initialize({
+ directory,
+ template: flags.template,
+ packageManager: flags.pm,
+ });
+ },
+ { disableFinishedLog: true },
+ ),
+ );
+
+// SUBMIT
+createAliasedCommand(
+ cli,
+ 'submit',
+ 'publish-extension',
+ 'wxt-publish-extension',
+ 'https://www.npmjs.com/publish-browser-extension',
+);
+
+export default cli;
diff --git a/packages/wxt/src/cli/index.ts b/packages/wxt/src/cli/index.ts
new file mode 100644
index 0000000..7bb3040
--- /dev/null
+++ b/packages/wxt/src/cli/index.ts
@@ -0,0 +1,19 @@
+import cli from './commands';
+import { version } from '~/version';
+import { isAliasedCommand } from './cli-utils';
+
+// TODO: Remove. See https://github.com/wxt-dev/wxt/issues/277
+process.env.VITE_CJS_IGNORE_WARNING = 'true';
+
+// Grab the command that we're trying to run
+cli.parse(process.argv, { run: false });
+
+// If it's not an alias, add the help and version options, then parse again
+if (!isAliasedCommand(cli.matchedCommand)) {
+ cli.help();
+ cli.version(version);
+ cli.parse(process.argv, { run: false });
+}
+
+// Run the alias or command
+await cli.runMatchedCommand();
diff --git a/packages/wxt/src/client/content-scripts/content-script-context.ts b/packages/wxt/src/client/content-scripts/content-script-context.ts
new file mode 100644
index 0000000..2e6401d
--- /dev/null
+++ b/packages/wxt/src/client/content-scripts/content-script-context.ts
@@ -0,0 +1,244 @@
+import { ContentScriptDefinition } from '~/types';
+import { browser } from '~/browser';
+import { logger } from '~/sandbox/utils/logger';
+import { WxtLocationChangeEvent, getUniqueEventName } from './custom-events';
+import { createLocationWatcher } from './location-watcher';
+
+/**
+ * Implements [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
+ * Used to detect and stop content script code when the script is invalidated.
+ *
+ * It also provides several utilities like `ctx.setTimeout` and `ctx.setInterval` that should be used in
+ * content scripts instead of `window.setTimeout` or `window.setInterval`.
+ *
+ * To create context for testing, you can use the class's constructor:
+ *
+ * ```ts
+ * import { ContentScriptContext } from 'wxt/client';
+ *
+ * test("storage listener should be removed when context is invalidated", () => {
+ * const ctx = new ContentScriptContext('test');
+ * const item = storage.defineItem("local:count", { defaultValue: 0 });
+ * const watcher = vi.fn();
+ *
+ * const unwatch = item.watch(watcher);
+ * ctx.onInvalidated(unwatch); // Listen for invalidate here
+ *
+ * await item.setValue(1);
+ * expect(watcher).toBeCalledTimes(1);
+ * expect(watcher).toBeCalledWith(1, 0);
+ *
+ * ctx.notifyInvalidated(); // Use this function to invalidate the context
+ * await item.setValue(2);
+ * expect(watcher).toBeCalledTimes(1);
+ * });
+ * ```
+ */
+export class ContentScriptContext implements AbortController {
+ private static SCRIPT_STARTED_MESSAGE_TYPE = 'wxt:content-script-started';
+
+ #isTopFrame = window.self === window.top;
+ #abortController: AbortController;
+ #locationWatcher = createLocationWatcher(this);
+
+ constructor(
+ private readonly contentScriptName: string,
+ public readonly options?: Omit,
+ ) {
+ this.#abortController = new AbortController();
+ if (this.#isTopFrame) {
+ this.#stopOldScripts();
+ }
+ this.setTimeout(() => {
+ // Run on next tick so the listener it adds isn't triggered by stopOldScript
+ this.#listenForNewerScripts();
+ });
+ }
+
+ get signal() {
+ return this.#abortController.signal;
+ }
+
+ abort(reason?: any): void {
+ return this.#abortController.abort(reason);
+ }
+
+ get isInvalid(): boolean {
+ if (browser.runtime.id == null) {
+ this.notifyInvalidated(); // Sets `signal.aborted` to true
+ }
+ return this.signal.aborted;
+ }
+
+ get isValid(): boolean {
+ return !this.isInvalid;
+ }
+
+ /**
+ * Add a listener that is called when the content script's context is invalidated.
+ *
+ * @returns A function to remove the listener.
+ *
+ * @example
+ * browser.runtime.onMessage.addListener(cb);
+ * const removeInvalidatedListener = ctx.onInvalidated(() => {
+ * browser.runtime.onMessage.removeListener(cb);
+ * })
+ * // ...
+ * removeInvalidatedListener();
+ */
+ onInvalidated(cb: () => void): () => void {
+ this.signal.addEventListener('abort', cb);
+ return () => this.signal.removeEventListener('abort', cb);
+ }
+
+ /**
+ * Return a promise that never resolves. Useful if you have an async function that shouldn't run
+ * after the context is expired.
+ *
+ * @example
+ * const getValueFromStorage = async () => {
+ * if (ctx.isInvalid) return ctx.block();
+ *
+ * // ...
+ * }
+ */
+ block(): Promise {
+ return new Promise(() => {
+ // noop
+ });
+ }
+
+ /**
+ * Wrapper around `window.setInterval` that automatically clears the interval when invalidated.
+ */
+ setInterval(handler: () => void, timeout?: number): number {
+ const id = setInterval(() => {
+ if (this.isValid) handler();
+ }, timeout) as unknown as number;
+ this.onInvalidated(() => clearInterval(id));
+ return id;
+ }
+
+ /**
+ * Wrapper around `window.setTimeout` that automatically clears the interval when invalidated.
+ */
+ setTimeout(handler: () => void, timeout?: number): number {
+ const id = setTimeout(() => {
+ if (this.isValid) handler();
+ }, timeout) as unknown as number;
+ this.onInvalidated(() => clearTimeout(id));
+ return id;
+ }
+
+ /**
+ * Wrapper around `window.requestAnimationFrame` that automatically cancels the request when
+ * invalidated.
+ */
+ requestAnimationFrame(callback: FrameRequestCallback): number {
+ const id = requestAnimationFrame((...args) => {
+ if (this.isValid) callback(...args);
+ });
+
+ this.onInvalidated(() => cancelAnimationFrame(id));
+ return id;
+ }
+
+ /**
+ * Wrapper around `window.requestIdleCallback` that automatically cancels the request when
+ * invalidated.
+ */
+ requestIdleCallback(
+ callback: IdleRequestCallback,
+ options?: IdleRequestOptions,
+ ): number {
+ const id = requestIdleCallback((...args) => {
+ if (!this.signal.aborted) callback(...args);
+ }, options);
+
+ this.onInvalidated(() => cancelIdleCallback(id));
+ return id;
+ }
+
+ /**
+ * Call `target.addEventListener` and remove the event listener when the context is invalidated.
+ *
+ * Includes additional events useful for content scripts:
+ *
+ * - `"wxt:locationchange"` - Triggered when HTML5 history mode is used to change URL. Content
+ * scripts are not reloaded when navigating this way, so this can be used to reset the content
+ * script state on URL change, or run custom code.
+ *
+ * @example
+ * ctx.addEventListener(document, "visibilitychange", () => {
+ * // ...
+ * });
+ * ctx.addEventListener(document, "wxt:locationchange", () => {
+ * // ...
+ * });
+ */
+ addEventListener<
+ TTarget extends EventTarget,
+ TType extends keyof WxtContentScriptEventMap,
+ >(
+ target: TTarget,
+ type: TType,
+ handler: (event: WxtContentScriptEventMap[TType]) => void,
+ options?: AddEventListenerOptions,
+ ) {
+ if (type === 'wxt:locationchange') {
+ // Start the location watcher when adding the event for the first time
+ if (this.isValid) this.#locationWatcher.run();
+ }
+
+ target.addEventListener?.(
+ type.startsWith('wxt:') ? getUniqueEventName(type) : type,
+ // @ts-expect-error: Event don't match, but that's OK, EventTarget doesn't allow custom types in the callback
+ handler,
+ {
+ ...options,
+ signal: this.signal,
+ },
+ );
+ }
+
+ /**
+ * @internal
+ * Abort the abort controller and execute all `onInvalidated` listeners.
+ */
+ notifyInvalidated() {
+ this.abort('Content script context invalidated');
+ logger.debug(
+ `Content script "${this.contentScriptName}" context invalidated`,
+ );
+ }
+
+ #stopOldScripts() {
+ // Use postMessage so it get's sent to all the frames of the page.
+ window.postMessage(
+ {
+ type: ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE,
+ contentScriptName: this.contentScriptName,
+ },
+ '*',
+ );
+ }
+
+ #listenForNewerScripts() {
+ const cb = (event: MessageEvent) => {
+ if (
+ event.data?.type === ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE &&
+ event.data?.contentScriptName === this.contentScriptName
+ ) {
+ this.notifyInvalidated();
+ }
+ };
+
+ addEventListener('message', cb);
+ this.onInvalidated(() => removeEventListener('message', cb));
+ }
+}
+
+interface WxtContentScriptEventMap extends WindowEventMap {
+ 'wxt:locationchange': WxtLocationChangeEvent;
+}
diff --git a/packages/wxt/src/client/content-scripts/custom-events.ts b/packages/wxt/src/client/content-scripts/custom-events.ts
new file mode 100644
index 0000000..df3a973
--- /dev/null
+++ b/packages/wxt/src/client/content-scripts/custom-events.ts
@@ -0,0 +1,26 @@
+import { browser } from '~/browser';
+
+export class WxtLocationChangeEvent extends Event {
+ static EVENT_NAME = getUniqueEventName('wxt:locationchange');
+
+ constructor(
+ readonly newUrl: URL,
+ readonly oldUrl: URL,
+ ) {
+ super(WxtLocationChangeEvent.EVENT_NAME, {});
+ }
+}
+
+/**
+ * Returns an event name unique to the extension and content script that's running.
+ */
+export function getUniqueEventName(eventName: string): string {
+ // During the build process, import.meta.env is not defined when importing
+ // entrypoints to get their metadata.
+ const entrypointName =
+ typeof import.meta.env === 'undefined'
+ ? 'build'
+ : import.meta.env.ENTRYPOINT;
+
+ return `${browser.runtime.id}:${entrypointName}:${eventName}`;
+}
diff --git a/packages/wxt/src/client/content-scripts/index.ts b/packages/wxt/src/client/content-scripts/index.ts
new file mode 100644
index 0000000..538b383
--- /dev/null
+++ b/packages/wxt/src/client/content-scripts/index.ts
@@ -0,0 +1,2 @@
+export * from './content-script-context';
+export * from './ui';
diff --git a/packages/wxt/src/client/content-scripts/location-watcher.ts b/packages/wxt/src/client/content-scripts/location-watcher.ts
new file mode 100644
index 0000000..d0ae276
--- /dev/null
+++ b/packages/wxt/src/client/content-scripts/location-watcher.ts
@@ -0,0 +1,30 @@
+import { ContentScriptContext } from '.';
+import { WxtLocationChangeEvent } from './custom-events';
+
+/**
+ * Create a util that watches for URL changes, dispatching the custom event when detected. Stops
+ * watching when content script is invalidated.
+ */
+export function createLocationWatcher(ctx: ContentScriptContext) {
+ let interval: number | undefined;
+ let oldUrl: URL;
+
+ return {
+ /**
+ * Ensure the location watcher is actively looking for URL changes. If it's already watching,
+ * this is a noop.
+ */
+ run() {
+ if (interval != null) return;
+
+ oldUrl = new URL(location.href);
+ interval = ctx.setInterval(() => {
+ let newUrl = new URL(location.href);
+ if (newUrl.href !== oldUrl.href) {
+ window.dispatchEvent(new WxtLocationChangeEvent(newUrl, oldUrl));
+ oldUrl = newUrl;
+ }
+ }, 1e3);
+ },
+ };
+}
diff --git a/packages/wxt/src/client/content-scripts/ui/__tests__/index.test.ts b/packages/wxt/src/client/content-scripts/ui/__tests__/index.test.ts
new file mode 100644
index 0000000..b006ed3
--- /dev/null
+++ b/packages/wxt/src/client/content-scripts/ui/__tests__/index.test.ts
@@ -0,0 +1,465 @@
+/** @vitest-environment happy-dom */
+import { describe, it, beforeEach, vi, expect } from 'vitest';
+import { createIntegratedUi, createIframeUi, createShadowRootUi } from '..';
+import { ContentScriptContext } from '../../content-script-context';
+
+function appendTestApp(container: HTMLElement) {
+ container.innerHTML = 'Hello world';
+}
+
+const fetch = vi.fn();
+
+describe('Content Script UIs', () => {
+ let ctx: ContentScriptContext;
+
+ beforeEach(() => {
+ document.body.innerHTML = `
+
+ `;
+ window.fetch = fetch;
+ fetch.mockResolvedValue({ text: () => Promise.resolve('') });
+ ctx = new ContentScriptContext('test');
+ });
+
+ describe('type', () => {
+ describe('integrated', () => {
+ it('should add a wrapper and custom UI to the page', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ onMount: appendTestApp,
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector('div[data-wxt-integrated]'),
+ ).not.toBeNull();
+ expect(document.querySelector('app')).not.toBeNull();
+ });
+
+ it('should allow customizing the wrapper tag', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ tag: 'pre',
+ onMount: appendTestApp,
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector('pre[data-wxt-integrated]'),
+ ).not.toBeNull();
+ expect(document.querySelector('app')).not.toBeNull();
+ });
+ });
+
+ describe('iframe', () => {
+ it('should add a wrapper and iframe to the page', () => {
+ const ui = createIframeUi(ctx, {
+ page: '/page.html',
+ position: 'inline',
+ });
+ ui.mount();
+
+ expect(document.querySelector('div[data-wxt-iframe]')).toBeDefined();
+ expect(document.querySelector('iframe')).toBeDefined();
+ });
+ });
+
+ describe('shadow-root', () => {
+ it('should load a shadow root to the page', async () => {
+ const ui = await createShadowRootUi(ctx, {
+ position: 'inline',
+ name: 'test-component',
+ onMount(uiContainer) {
+ appendTestApp(uiContainer);
+ },
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector('test-component[data-wxt-shadow-root]'),
+ ).not.toBeNull();
+ expect(ui.shadow.querySelector('app')).not.toBeNull();
+ });
+
+ it.each([
+ ['open', 'open'],
+ [undefined, 'open'],
+ ['closed', 'closed'],
+ ] as const)(
+ 'should respect the shadow root mode (%s -> %s)',
+ async (input, expected) => {
+ const ui = await createShadowRootUi(ctx, {
+ position: 'inline',
+ name: 'test-component',
+ mode: input,
+ onMount: appendTestApp,
+ });
+
+ expect(ui.shadow.mode).toBe(expected);
+ },
+ );
+ });
+ });
+
+ describe('position', () => {
+ describe('inline', () => {
+ it('should wrap the UI in a simple div', () => {
+ const ui = createIframeUi(ctx, {
+ position: 'inline',
+ page: '/page.html',
+ });
+
+ expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(
+ `""`,
+ );
+ });
+ });
+
+ describe('overlay', () => {
+ it('should wrap the UI in a positioned div when alignment=undefined', () => {
+ const ui = createIframeUi(ctx, {
+ position: 'overlay',
+ page: '/page.html',
+ });
+ ui.mount();
+
+ expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(
+ `""`,
+ );
+ });
+
+ it('should wrap the UI in a positioned div when alignment=top-left', () => {
+ const ui = createIframeUi(ctx, {
+ position: 'overlay',
+ page: '/page.html',
+ alignment: 'top-left',
+ });
+ ui.mount();
+
+ expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(
+ `""`,
+ );
+ });
+
+ it('should wrap the UI in a positioned div when alignment=top-right', () => {
+ const ui = createIframeUi(ctx, {
+ position: 'overlay',
+ page: '/page.html',
+ alignment: 'top-right',
+ });
+ ui.mount();
+
+ expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(
+ `""`,
+ );
+ });
+
+ it('should wrap the UI in a positioned div when alignment=bottom-right', () => {
+ const ui = createIframeUi(ctx, {
+ position: 'overlay',
+ page: '/page.html',
+ alignment: 'bottom-right',
+ });
+ ui.mount();
+
+ expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(
+ `""`,
+ );
+ });
+
+ it('should wrap the UI in a positioned div when alignment=bottom-left', () => {
+ const ui = createIframeUi(ctx, {
+ position: 'overlay',
+ page: '/page.html',
+ alignment: 'bottom-left',
+ });
+ ui.mount();
+
+ expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(
+ `""`,
+ );
+ });
+
+ it('should respect the provided zIndex', () => {
+ const zIndex = 123;
+ const ui = createIframeUi(ctx, {
+ position: 'overlay',
+ page: '/page.html',
+ zIndex,
+ });
+ ui.mount();
+
+ expect(ui.wrapper.style.zIndex).toBe(String(zIndex));
+ });
+ });
+
+ describe('modal', () => {
+ it('should wrap the UI in a div with a fixed position', () => {
+ const ui = createIframeUi(ctx, {
+ position: 'modal',
+ page: '/page.html',
+ });
+ ui.mount();
+
+ expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(
+ `""`,
+ );
+ });
+
+ it('should respect the provided zIndex', () => {
+ const zIndex = 123;
+ const ui = createIframeUi(ctx, {
+ position: 'modal',
+ page: '/page.html',
+ zIndex,
+ });
+ ui.mount();
+
+ expect(ui.wrapper.style.zIndex).toBe(String(zIndex));
+ });
+ });
+ });
+
+ describe('anchor', () => {
+ describe('undefined', () => {
+ it('should append the element to the body', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ onMount: appendTestApp,
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector('body > div[data-wxt-integrated]'),
+ ).not.toBeNull();
+ });
+ });
+
+ describe('string', () => {
+ it('should append the element using the specified query selector', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ onMount: appendTestApp,
+ anchor: '#parent',
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector('#parent > div[data-wxt-integrated]'),
+ ).not.toBeNull();
+ });
+ });
+
+ describe('Element', () => {
+ it('should append the element using the specified element', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ onMount: appendTestApp,
+ anchor: document.getElementById('parent'),
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector('#parent > div[data-wxt-integrated]'),
+ ).not.toBeNull();
+ });
+ });
+
+ describe('function', () => {
+ it('should append the element using the specified function', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ onMount: appendTestApp,
+ anchor: () => document.getElementById('parent'),
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector('#parent > div[data-wxt-integrated]'),
+ ).not.toBeNull();
+ });
+ });
+
+ it('should throw an error when the anchor does not exist', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ onMount: appendTestApp,
+ anchor: () => document.getElementById('i-do-not-exist'),
+ });
+
+ expect(ui.mount).toThrow();
+ });
+ });
+
+ describe('append', () => {
+ describe.each([undefined, 'last'] as const)('%s', (append) => {
+ it('should append the element as the last child of the anchor', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ anchor: '#parent',
+ append,
+ onMount: appendTestApp,
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector(
+ '#parent > div[data-wxt-integrated]:last-child',
+ ),
+ ).not.toBeNull();
+ });
+ });
+
+ describe('first', () => {
+ it('should append the element as the last child of the anchor', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ anchor: '#parent',
+ append: 'first',
+ onMount: appendTestApp,
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector(
+ '#parent > div[data-wxt-integrated]:first-child',
+ ),
+ ).not.toBeNull();
+ });
+ });
+
+ describe('replace', () => {
+ it('should replace the the anchor', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ anchor: '#parent',
+ append: 'replace',
+ onMount: appendTestApp,
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector('body > div[data-wxt-integrated]'),
+ ).not.toBeNull();
+ expect(document.querySelector('#parent')).toBeNull();
+ });
+ });
+
+ describe('before', () => {
+ it('should append the UI before the anchor', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ anchor: '#one',
+ append: 'before',
+ onMount: appendTestApp,
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector(
+ '#parent > div[data-wxt-integrated]:first-child',
+ ),
+ ).not.toBeNull();
+ });
+ });
+
+ describe('after', () => {
+ it('should append the UI after the anchor', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ anchor: '#three',
+ append: 'after',
+ onMount: appendTestApp,
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector(
+ '#parent > div[data-wxt-integrated]:last-child',
+ ),
+ ).not.toBeNull();
+ });
+ });
+
+ describe('function', () => {
+ it('should append the UI using a function', () => {
+ const ui = createIntegratedUi(ctx, {
+ position: 'inline',
+ anchor: '#parent',
+ append: (anchor, ui) => {
+ anchor.replaceWith(ui);
+ },
+ onMount: appendTestApp,
+ });
+ ui.mount();
+
+ expect(
+ document.querySelector('body > div[data-wxt-integrated]'),
+ ).not.toBeNull();
+ expect(document.querySelector('#parent')).toBeNull();
+ });
+ });
+ });
+
+ describe('mounted value', () => {
+ describe('integrated', () => {
+ it('should set the mounted value based on the onMounted return value', () => {
+ const expected = Symbol();
+
+ const ui = createIntegratedUi(new ContentScriptContext('test'), {
+ position: 'inline',
+ onMount: () => expected,
+ });
+ expect(ui.mounted).toBeUndefined();
+
+ ui.mount();
+ expect(ui.mounted).toBe(expected);
+
+ ui.remove();
+ expect(ui.mounted).toBeUndefined();
+ });
+ });
+
+ describe('iframe', () => {
+ it('should set the mounted value based on the onMounted return value', async () => {
+ const expected = Symbol();
+
+ const ui = createIframeUi(new ContentScriptContext('test'), {
+ page: '',
+ position: 'inline',
+ onMount: () => expected,
+ });
+ expect(ui.mounted).toBeUndefined();
+
+ ui.mount();
+ expect(ui.mounted).toBe(expected);
+
+ ui.remove();
+ expect(ui.mounted).toBeUndefined();
+ });
+ });
+
+ describe('shadow-root', () => {
+ it('should set the mounted value based on the onMounted return value', async () => {
+ const expected = Symbol();
+
+ const ui = await createShadowRootUi(new ContentScriptContext('test'), {
+ name: 'test-component',
+ position: 'inline',
+ onMount: () => expected,
+ });
+ expect(ui.mounted).toBeUndefined();
+
+ ui.mount();
+ expect(ui.mounted).toBe(expected);
+
+ ui.remove();
+ expect(ui.mounted).toBeUndefined();
+ });
+ });
+ });
+});
diff --git a/packages/wxt/src/client/content-scripts/ui/index.ts b/packages/wxt/src/client/content-scripts/ui/index.ts
new file mode 100644
index 0000000..62d6a9a
--- /dev/null
+++ b/packages/wxt/src/client/content-scripts/ui/index.ts
@@ -0,0 +1,257 @@
+import { browser } from '~/browser';
+import { ContentScriptContext } from '..';
+import {
+ ContentScriptAnchoredOptions,
+ ContentScriptPositioningOptions,
+ IframeContentScriptUi,
+ IframeContentScriptUiOptions,
+ IntegratedContentScriptUi,
+ IntegratedContentScriptUiOptions,
+ ShadowRootContentScriptUi,
+ ShadowRootContentScriptUiOptions,
+} from './types';
+import { logger } from '~/sandbox/utils/logger';
+import { createIsolatedElement } from '@webext-core/isolated-element';
+export * from './types';
+
+/**
+ * Create a content script UI without any isolation.
+ *
+ * @see https://wxt.dev/guide/content-script-ui.html#integrated
+ */
+export function createIntegratedUi(
+ ctx: ContentScriptContext,
+ options: IntegratedContentScriptUiOptions,
+): IntegratedContentScriptUi {
+ const wrapper = document.createElement(options.tag || 'div');
+ wrapper.setAttribute('data-wxt-integrated', '');
+
+ let mounted: TMounted | undefined = undefined;
+ const mount = () => {
+ applyPosition(wrapper, undefined, options);
+ mountUi(wrapper, options);
+ mounted = options.onMount?.(wrapper);
+ };
+ const remove = () => {
+ options.onRemove?.(mounted);
+ wrapper.remove();
+ mounted = undefined;
+ };
+
+ ctx.onInvalidated(remove);
+
+ return {
+ get mounted() {
+ return mounted;
+ },
+ wrapper,
+ mount,
+ remove,
+ };
+}
+
+/**
+ * Create a content script UI using an iframe.
+ *
+ * @see https://wxt.dev/guide/content-script-ui.html#iframe
+ */
+export function createIframeUi(
+ ctx: ContentScriptContext,
+ options: IframeContentScriptUiOptions,
+): IframeContentScriptUi {
+ const wrapper = document.createElement('div');
+ wrapper.setAttribute('data-wxt-iframe', '');
+ const iframe = document.createElement('iframe');
+ iframe.src = browser.runtime.getURL(options.page);
+ wrapper.appendChild(iframe);
+
+ let mounted: TMounted | undefined = undefined;
+ const mount = () => {
+ applyPosition(wrapper, iframe, options);
+ mountUi(wrapper, options);
+ mounted = options.onMount?.(wrapper, iframe);
+ };
+ const remove = () => {
+ options.onRemove?.(mounted);
+ wrapper.remove();
+ mounted = undefined;
+ };
+
+ ctx.onInvalidated(remove);
+
+ return {
+ get mounted() {
+ return mounted;
+ },
+ iframe,
+ wrapper,
+ mount,
+ remove,
+ };
+}
+
+/**
+ * Create a content script UI inside a [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot).
+ *
+ * > This function is async because it has to load the CSS via a network call.
+ *
+ * @see https://wxt.dev/guide/content-script-ui.html#shadowroot
+ */
+export async function createShadowRootUi(
+ ctx: ContentScriptContext,
+ options: ShadowRootContentScriptUiOptions,
+): Promise> {
+ const css = [options.css ?? ''];
+ if (ctx.options?.cssInjectionMode === 'ui') {
+ const entryCss = await loadCss();
+ // Replace :root selectors with :host since we're in a shadow root
+ css.push(entryCss.replaceAll(':root', ':host'));
+ }
+
+ const {
+ isolatedElement: uiContainer,
+ parentElement: shadowHost,
+ shadow,
+ } = await createIsolatedElement({
+ name: options.name,
+ css: {
+ textContent: css.join('\n').trim(),
+ },
+ mode: options.mode ?? 'open',
+ isolateEvents: options.isolateEvents,
+ });
+ shadowHost.setAttribute('data-wxt-shadow-root', '');
+
+ let mounted: TMounted | undefined;
+
+ const mount = () => {
+ // Add shadow root element to DOM
+ mountUi(shadowHost, options);
+ applyPosition(shadowHost, shadow.querySelector('html'), options);
+ // Mount UI inside shadow root
+ mounted = options.onMount(uiContainer, shadow, shadowHost);
+ };
+
+ const remove = () => {
+ // Cleanup mounted state
+ options.onRemove?.(mounted);
+ // Detatch shadow root from DOM
+ shadowHost.remove();
+ // Remove children from uiContainer
+ while (uiContainer.lastChild)
+ uiContainer.removeChild(uiContainer.lastChild);
+ // Clear mounted value
+ mounted = undefined;
+ };
+
+ ctx.onInvalidated(remove);
+
+ return {
+ shadow,
+ shadowHost,
+ uiContainer,
+ mount,
+ remove,
+ get mounted() {
+ return mounted;
+ },
+ };
+}
+
+function applyPosition(
+ root: HTMLElement,
+ positionedElement: HTMLElement | undefined | null,
+ options: ContentScriptPositioningOptions,
+): void {
+ // No positioning for inline UIs
+ if (options.position === 'inline') return;
+
+ if (options.zIndex != null) root.style.zIndex = String(options.zIndex);
+
+ root.style.overflow = 'visible';
+ root.style.position = 'relative';
+ root.style.width = '0';
+ root.style.height = '0';
+ root.style.display = 'block';
+
+ if (positionedElement) {
+ if (options.position === 'overlay') {
+ positionedElement.style.position = 'absolute';
+ if (options.alignment?.startsWith('bottom-'))
+ positionedElement.style.bottom = '0';
+ else positionedElement.style.top = '0';
+
+ if (options.alignment?.endsWith('-right'))
+ positionedElement.style.right = '0';
+ else positionedElement.style.left = '0';
+ } else {
+ positionedElement.style.position = 'fixed';
+ positionedElement.style.top = '0';
+ positionedElement.style.bottom = '0';
+ positionedElement.style.left = '0';
+ positionedElement.style.right = '0';
+ }
+ }
+}
+
+function getAnchor(options: ContentScriptAnchoredOptions): Element | undefined {
+ if (options.anchor == null) return document.body;
+
+ let resolved =
+ typeof options.anchor === 'function' ? options.anchor() : options.anchor;
+ if (typeof resolved === 'string')
+ return document.querySelector(resolved) ?? undefined;
+ return resolved ?? undefined;
+}
+
+function mountUi(
+ root: HTMLElement,
+ options: ContentScriptAnchoredOptions,
+): void {
+ const anchor = getAnchor(options);
+ if (anchor == null)
+ throw Error(
+ 'Failed to mount content script UI: could not find anchor element',
+ );
+
+ switch (options.append) {
+ case undefined:
+ case 'last':
+ anchor.append(root);
+ break;
+ case 'first':
+ anchor.prepend(root);
+ break;
+ case 'replace':
+ anchor.replaceWith(root);
+ break;
+ case 'after':
+ anchor.parentElement?.insertBefore(root, anchor.nextElementSibling);
+ break;
+ case 'before':
+ anchor.parentElement?.insertBefore(root, anchor);
+ break;
+ default:
+ options.append(anchor, root);
+ break;
+ }
+}
+
+/**
+ * Load the CSS for the current entrypoint.
+ */
+async function loadCss(): Promise {
+ const url = browser.runtime.getURL(
+ `/content-scripts/${import.meta.env.ENTRYPOINT}.css`,
+ );
+ try {
+ const res = await fetch(url);
+ return await res.text();
+ } catch (err) {
+ logger.warn(
+ `Failed to load styles @ ${url}. Did you forget to import the stylesheet in your entrypoint?`,
+ err,
+ );
+ return '';
+ }
+}
diff --git a/packages/wxt/src/client/content-scripts/ui/types.ts b/packages/wxt/src/client/content-scripts/ui/types.ts
new file mode 100644
index 0000000..23b71fd
--- /dev/null
+++ b/packages/wxt/src/client/content-scripts/ui/types.ts
@@ -0,0 +1,217 @@
+export interface IntegratedContentScriptUi
+ extends ContentScriptUi {
+ /**
+ * A wrapper div that assists in positioning.
+ */
+ wrapper: HTMLElement;
+}
+
+export interface IframeContentScriptUi
+ extends ContentScriptUi {
+ /**
+ * The iframe added to the DOM.
+ */
+ iframe: HTMLIFrameElement;
+ /**
+ * A wrapper div that assists in positioning.
+ */
+ wrapper: HTMLDivElement;
+}
+
+export interface ShadowRootContentScriptUi
+ extends ContentScriptUi {
+ /**
+ * The `HTMLElement` hosting the shadow root used to isolate the UI's styles. This is the element
+ * that get's added to the DOM. This element's style is not isolated from the webpage.
+ */
+ shadowHost: HTMLElement;
+ /**
+ * The container element inside the `ShadowRoot` whose styles are isolated. The UI is mounted
+ * inside this `HTMLElement`.
+ */
+ uiContainer: HTMLElement;
+ /**
+ * The shadow root performing the isolation.
+ */
+ shadow: ShadowRoot;
+}
+
+export interface ContentScriptUi {
+ /**
+ * 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.
+ */
+ mounted: TMounted | undefined;
+}
+
+export type ContentScriptUiOptions = ContentScriptPositioningOptions &
+ ContentScriptAnchoredOptions & {
+ /**
+ * Callback called before the UI is removed from the webpage. Use to cleanup your UI, like
+ * unmounting your Vue or React apps.
+ */
+ onRemove?: (mounted: TMounted | undefined) => void;
+ };
+
+export type IntegratedContentScriptUiOptions =
+ ContentScriptUiOptions & {
+ /**
+ * Tag used to create the wrapper element.
+ *
+ * @default "div"
+ */
+ tag?: string;
+ /**
+ * Callback executed when mounting the UI. This function should create and append the UI to the
+ * `wrapper` element. It is called every time `ui.mount()` is called.
+ *
+ * Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback.
+ */
+ onMount: (wrapper: HTMLElement) => TMounted;
+ };
+
+export type IframeContentScriptUiOptions =
+ ContentScriptUiOptions & {
+ /**
+ * The path to the HTML page that will be shown in the iframe. This string is passed into
+ * `browser.runtime.getURL`.
+ */
+ page: PublicPath;
+ /**
+ * Callback executed when mounting the UI. Use this function to customize the iframe or wrapper
+ * element's appearance. It is called every time `ui.mount()` is called.
+ *
+ * Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback.
+ */
+ onMount?: (wrapper: HTMLElement, iframe: HTMLIFrameElement) => TMounted;
+ };
+
+export type ShadowRootContentScriptUiOptions =
+ ContentScriptUiOptions & {
+ /**
+ * The name of the custom component used to host the ShadowRoot. Must be kebab-case.
+ */
+ name: string;
+ /**
+ * Custom CSS text to apply to the UI. If your content script imports/generates CSS and you've
+ * set `cssInjectionMode: "ui"`, the imported CSS will be included automatically. You do not need
+ * to pass those styles in here. This is for any additional styles not in the imported CSS.
+ */
+ css?: string;
+ /**
+ * ShadowRoot's mode.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/mode
+ * @default "open"
+ */
+ mode?: 'open' | 'closed';
+ /**
+ * When enabled, `event.stopPropagation` will be called on events trying to bubble out of the
+ * shadow root.
+ *
+ * - Set to `true` to stop the propagation of a default set of events,
+ * `["keyup", "keydown", "keypress"]`
+ * - Set to an array of event names to stop the propagation of a custom list of events
+ */
+ isolateEvents?: boolean | string[];
+ /**
+ * Callback executed when mounting the UI. This function should create and append the UI to the
+ * `uiContainer` element. It is called every time `ui.mount()` is called.
+ *
+ * Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback.
+ */
+ onMount: (
+ uiContainer: HTMLElement,
+ shadow: ShadowRoot,
+ shadowHost: HTMLElement,
+ ) => TMounted;
+ };
+
+export type ContentScriptOverlayAlignment =
+ | 'top-left'
+ | 'top-right'
+ | 'bottom-left'
+ | 'bottom-right';
+
+/**
+ * 
+ */
+export type ContentScriptAppendMode =
+ | 'last'
+ | 'first'
+ | 'replace'
+ | 'before'
+ | 'after'
+ | ((anchor: Element, ui: Element) => void);
+
+export interface ContentScriptInlinePositioningOptions {
+ position: 'inline';
+}
+
+export interface ContentScriptOverlayPositioningOptions {
+ position: 'overlay';
+ /**
+ * The `z-index` used on the `wrapper` element. Set to a positive number to show your UI over website
+ * content.
+ */
+ zIndex?: number;
+ /**
+ * When using `type: "overlay"`, the mounted element is 0px by 0px in size. Alignment specifies
+ * which corner is aligned with that 0x0 pixel space.
+ *
+ * 
+ *
+ * @default "top-left"
+ */
+ alignment?: ContentScriptOverlayAlignment;
+}
+
+export interface ContentScriptModalPositioningOptions {
+ position: 'modal';
+ /**
+ * The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
+ * content.
+ */
+ zIndex?: number;
+}
+
+/**
+ * Choose between `"inline"`, `"overlay"`, or `"modal" `types.
+ *
+ * 
+ */
+export type ContentScriptPositioningOptions =
+ | ContentScriptInlinePositioningOptions
+ | ContentScriptOverlayPositioningOptions
+ | ContentScriptModalPositioningOptions;
+
+export interface ContentScriptAnchoredOptions {
+ /**
+ * A CSS selector, element, or function that returns one of the two. Along with `append`, the
+ * `anchor` dictates where in the page the UI will be added.
+ */
+ anchor?:
+ | string
+ | Element
+ | null
+ | undefined
+ | (() => string | Element | null | undefined);
+ /**
+ * In combination with `anchor`, decide how to add the UI to the DOM.
+ *
+ * - `"last"` (default) - Add the UI as the last child of the `anchor` element
+ * - `"first"` - Add the UI as the first child of the `anchor` element
+ * - `"replace"` - Replace the `anchor` element with the UI.
+ * - `"before"` - Add the UI as the sibling before the `anchor` element
+ * - `"after"` - Add the UI as the sibling after the `anchor` element
+ * - `(anchor, ui) => void` - Customizable function that let's you add the UI to the DOM
+ */
+ append?: ContentScriptAppendMode | ((anchor: Element, ui: Element) => void);
+}
diff --git a/packages/wxt/src/client/index.ts b/packages/wxt/src/client/index.ts
new file mode 100644
index 0000000..d54ca0e
--- /dev/null
+++ b/packages/wxt/src/client/index.ts
@@ -0,0 +1,6 @@
+/**
+ * Any runtime APIs that use the web extension APIs.
+ *
+ * @module wxt/client
+ */
+export * from './content-scripts';
diff --git a/packages/wxt/src/core/build.ts b/packages/wxt/src/core/build.ts
new file mode 100644
index 0000000..bcb3847
--- /dev/null
+++ b/packages/wxt/src/core/build.ts
@@ -0,0 +1,22 @@
+import { BuildOutput, InlineConfig } from '~/types';
+import { internalBuild } from './utils/building';
+import { registerWxt } from './wxt';
+
+/**
+ * Bundles the extension for production. Returns a promise of the build result. Discovers the `wxt.config.ts` file in
+ * the root directory, and merges that config with what is passed in.
+ *
+ * @example
+ * // Use config from `wxt.config.ts`
+ * const res = await build()
+ *
+ * // or override config `from wxt.config.ts`
+ * const res = await build({
+ * // Override config...
+ * })
+ */
+export async function build(config?: InlineConfig): Promise {
+ await registerWxt('build', config);
+
+ return await internalBuild();
+}
diff --git a/packages/wxt/src/core/builders/vite/__tests__/fixtures/module.ts b/packages/wxt/src/core/builders/vite/__tests__/fixtures/module.ts
new file mode 100644
index 0000000..1ca73a9
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/__tests__/fixtures/module.ts
@@ -0,0 +1,12 @@
+import { a } from './test';
+
+function defineSomething(config: T): T {
+ return config;
+}
+
+export default defineSomething({
+ option: 'some value',
+ main: () => {
+ console.log('main', a);
+ },
+});
diff --git a/packages/wxt/src/core/builders/vite/__tests__/fixtures/test.ts b/packages/wxt/src/core/builders/vite/__tests__/fixtures/test.ts
new file mode 100644
index 0000000..a68289e
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/__tests__/fixtures/test.ts
@@ -0,0 +1,2 @@
+console.log('Side-effect in test.ts');
+export const a = 'a';
diff --git a/packages/wxt/src/core/builders/vite/__tests__/index.test.ts b/packages/wxt/src/core/builders/vite/__tests__/index.test.ts
new file mode 100644
index 0000000..3d476f8
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/__tests__/index.test.ts
@@ -0,0 +1,22 @@
+import { describe, it, expect } from 'vitest';
+import { createViteBuilder } from '../index';
+import { fakeResolvedConfig } from '~/core/utils/testing/fake-objects';
+import { createHooks } from 'hookable';
+
+describe('Vite Builder', () => {
+ describe('importEntrypoint', () => {
+ it('should import entrypoints, removing runtime values (like the main function)', async () => {
+ const {
+ default: { main: _, ...expected },
+ } = await import('./fixtures/module');
+ const builder = await createViteBuilder(
+ fakeResolvedConfig({ root: __dirname }),
+ createHooks(),
+ );
+ const actual = await builder.importEntrypoint<{ default: any }>(
+ './fixtures/module.ts',
+ );
+ expect(actual).toEqual(expected);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts
new file mode 100644
index 0000000..33b5f30
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/index.ts
@@ -0,0 +1,313 @@
+import type * as vite from 'vite';
+import {
+ BuildStepOutput,
+ Entrypoint,
+ ResolvedConfig,
+ WxtBuilder,
+ WxtBuilderServer,
+ WxtDevServer,
+ WxtHooks,
+} from '~/types';
+import * as wxtPlugins from './plugins';
+import {
+ getEntrypointBundlePath,
+ isHtmlEntrypoint,
+} from '~/core/utils/entrypoints';
+import {
+ VirtualEntrypointType,
+ VirtualModuleId,
+} from '~/core/utils/virtual-modules';
+import { Hookable } from 'hookable';
+import { toArray } from '~/core/utils/arrays';
+import { safeVarName } from '~/core/utils/strings';
+
+export async function createViteBuilder(
+ wxtConfig: ResolvedConfig,
+ hooks: Hookable,
+ server?: WxtDevServer,
+): Promise {
+ const vite = await import('vite');
+
+ /**
+ * Returns the base vite config shared by all builds based on the inline and user config.
+ */
+ const getBaseConfig = async () => {
+ const config: vite.InlineConfig = await wxtConfig.vite(wxtConfig.env);
+
+ config.root = wxtConfig.root;
+ config.configFile = false;
+ config.logLevel = 'warn';
+ config.mode = wxtConfig.mode;
+
+ config.build ??= {};
+ config.publicDir = wxtConfig.publicDir;
+ config.build.copyPublicDir = false;
+ config.build.outDir = wxtConfig.outDir;
+ config.build.emptyOutDir = false;
+ // Disable minification for the dev command
+ if (config.build.minify == null && wxtConfig.command === 'serve') {
+ config.build.minify = false;
+ }
+ // Enable inline sourcemaps for the dev command (so content scripts have sourcemaps)
+ if (config.build.sourcemap == null && wxtConfig.command === 'serve') {
+ config.build.sourcemap = 'inline';
+ }
+
+ config.plugins ??= [];
+ config.plugins.push(
+ wxtPlugins.download(wxtConfig),
+ wxtPlugins.devHtmlPrerender(wxtConfig, server),
+ wxtPlugins.unimport(wxtConfig),
+ wxtPlugins.resolveVirtualModules(wxtConfig),
+ wxtPlugins.devServerGlobals(wxtConfig, server),
+ wxtPlugins.tsconfigPaths(wxtConfig),
+ wxtPlugins.noopBackground(),
+ wxtPlugins.globals(wxtConfig),
+ wxtPlugins.excludeBrowserPolyfill(wxtConfig),
+ wxtPlugins.defineImportMeta(),
+ );
+ if (wxtConfig.analysis.enabled) {
+ config.plugins.push(wxtPlugins.bundleAnalysis(wxtConfig));
+ }
+
+ return config;
+ };
+
+ /**
+ * Return the basic config for building an entrypoint in [lib mode](https://vitejs.dev/guide/build.html#library-mode).
+ */
+ const getLibModeConfig = (entrypoint: Entrypoint): vite.InlineConfig => {
+ const entry = getRollupEntry(entrypoint);
+ const plugins: NonNullable = [
+ wxtPlugins.entrypointGroupGlobals(entrypoint),
+ ];
+ if (
+ entrypoint.type === 'content-script-style' ||
+ entrypoint.type === 'unlisted-style'
+ ) {
+ plugins.push(wxtPlugins.cssEntrypoints(entrypoint, wxtConfig));
+ }
+
+ const iifeReturnValueName = safeVarName(entrypoint.name);
+ const libMode: vite.UserConfig = {
+ mode: wxtConfig.mode,
+ plugins,
+ esbuild: {
+ // Add a footer with the returned value so it can return values to `scripting.executeScript`
+ // Footer is added apart of esbuild to make sure it's not minified. It
+ // get's removed if added to `build.rollupOptions.output.footer`
+ // See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript#return_value
+ footer: iifeReturnValueName + ';',
+ },
+ build: {
+ lib: {
+ entry,
+ formats: ['iife'],
+ name: iifeReturnValueName,
+ fileName: entrypoint.name,
+ },
+ rollupOptions: {
+ output: {
+ // There's only a single output for this build, so we use the desired bundle path for the
+ // entry output (like "content-scripts/overlay.js")
+ entryFileNames: getEntrypointBundlePath(
+ entrypoint,
+ wxtConfig.outDir,
+ '.js',
+ ),
+ // Output content script CSS to `content-scripts/`, but all other scripts are written to
+ // `assets/`.
+ assetFileNames: ({ name }) => {
+ if (
+ entrypoint.type === 'content-script' &&
+ name?.endsWith('css')
+ ) {
+ return `content-scripts/${entrypoint.name}.[ext]`;
+ } else {
+ return `assets/${entrypoint.name}.[ext]`;
+ }
+ },
+ },
+ },
+ },
+ define: {
+ // See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
+ 'process.env.NODE_ENV': JSON.stringify(wxtConfig.mode),
+ },
+ };
+ return libMode;
+ };
+
+ /**
+ * Return the basic config for building multiple entrypoints in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app).
+ */
+ const getMultiPageConfig = (entrypoints: Entrypoint[]): vite.InlineConfig => {
+ const htmlEntrypoints = new Set(
+ entrypoints.filter(isHtmlEntrypoint).map((e) => e.name),
+ );
+ return {
+ mode: wxtConfig.mode,
+ plugins: [
+ wxtPlugins.multipageMove(entrypoints, wxtConfig),
+ wxtPlugins.entrypointGroupGlobals(entrypoints),
+ ],
+ build: {
+ rollupOptions: {
+ input: entrypoints.reduce>((input, entry) => {
+ input[entry.name] = getRollupEntry(entry);
+ return input;
+ }, {}),
+ output: {
+ // Include a hash to prevent conflicts
+ chunkFileNames: 'chunks/[name]-[hash].js',
+ entryFileNames: ({ name }) => {
+ // HTML main JS files go in the chunks folder
+ if (htmlEntrypoints.has(name)) return 'chunks/[name]-[hash].js';
+ // Scripts are output in the root folder
+ return '[name].js';
+ },
+ // We can't control the "name", so we need a hash to prevent conflicts
+ assetFileNames: 'assets/[name]-[hash].[ext]',
+ },
+ },
+ },
+ };
+ };
+
+ /**
+ * Return the basic config for building a sinlge CSS entrypoint in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app).
+ */
+ const getCssConfig = (entrypoint: Entrypoint): vite.InlineConfig => {
+ return {
+ mode: wxtConfig.mode,
+ plugins: [wxtPlugins.entrypointGroupGlobals(entrypoint)],
+ build: {
+ rollupOptions: {
+ input: {
+ [entrypoint.name]: entrypoint.inputPath,
+ },
+ output: {
+ assetFileNames: () => {
+ if (entrypoint.type === 'content-script-style') {
+ return `content-scripts/${entrypoint.name}.[ext]`;
+ } else {
+ return `assets/${entrypoint.name}.[ext]`;
+ }
+ },
+ },
+ },
+ },
+ };
+ };
+
+ return {
+ name: 'Vite',
+ version: vite.version,
+ async importEntrypoint(url) {
+ const baseConfig = await getBaseConfig();
+ const envConfig: vite.InlineConfig = {
+ plugins: [
+ wxtPlugins.webextensionPolyfillMock(wxtConfig),
+ wxtPlugins.removeEntrypointMainFunction(wxtConfig, url),
+ ],
+ };
+ const config = vite.mergeConfig(baseConfig, envConfig);
+ const server = await vite.createServer(config);
+ await server.listen();
+ const runtime = await vite.createViteRuntime(server, { hmr: false });
+ const module = await runtime.executeUrl(url);
+ await server.close();
+ return module.default;
+ },
+ async build(group) {
+ let entryConfig;
+ if (Array.isArray(group)) entryConfig = getMultiPageConfig(group);
+ else if (group.inputPath.endsWith('.css'))
+ entryConfig = getCssConfig(group);
+ else entryConfig = getLibModeConfig(group);
+
+ const buildConfig = vite.mergeConfig(await getBaseConfig(), entryConfig);
+ await hooks.callHook(
+ 'vite:build:extendConfig',
+ toArray(group),
+ buildConfig,
+ );
+ const result = await vite.build(buildConfig);
+ return {
+ entrypoints: group,
+ chunks: getBuildOutputChunks(result),
+ };
+ },
+ async createServer(info) {
+ const serverConfig: vite.InlineConfig = {
+ server: {
+ port: info.port,
+ strictPort: true,
+ host: info.hostname,
+ origin: info.origin,
+ },
+ };
+ const baseConfig = await getBaseConfig();
+ const finalConfig = vite.mergeConfig(baseConfig, serverConfig);
+ await hooks.callHook('vite:devServer:extendConfig', finalConfig);
+ const viteServer = await vite.createServer(finalConfig);
+
+ const server: WxtBuilderServer = {
+ async listen() {
+ await viteServer.listen(info.port);
+ },
+ async close() {
+ await viteServer.close();
+ },
+ transformHtml(...args) {
+ return viteServer.transformIndexHtml(...args);
+ },
+ ws: {
+ send(message, payload) {
+ return viteServer.ws.send(message, payload);
+ },
+ on(message, cb) {
+ viteServer.ws.on(message, cb);
+ },
+ },
+ watcher: viteServer.watcher,
+ };
+
+ return server;
+ },
+ };
+}
+
+function getBuildOutputChunks(
+ result: Awaited>,
+): BuildStepOutput['chunks'] {
+ if ('on' in result) throw Error('wxt does not support vite watch mode.');
+ if (Array.isArray(result)) return result.flatMap(({ output }) => output);
+ return result.output;
+}
+
+/**
+ * Returns the input module ID (virtual or real file) for an entrypoint. The returned string should
+ * be passed as an input to rollup.
+ */
+function getRollupEntry(entrypoint: Entrypoint): string {
+ let virtualEntrypointType: VirtualEntrypointType | undefined;
+ switch (entrypoint.type) {
+ case 'background':
+ case 'unlisted-script':
+ virtualEntrypointType = entrypoint.type;
+ break;
+ case 'content-script':
+ virtualEntrypointType =
+ entrypoint.options.world === 'MAIN'
+ ? 'content-script-main-world'
+ : 'content-script-isolated-world';
+ break;
+ }
+
+ if (virtualEntrypointType) {
+ const moduleId: VirtualModuleId = `virtual:wxt-${virtualEntrypointType}-entrypoint`;
+ return `${moduleId}?${entrypoint.inputPath}`;
+ }
+ return entrypoint.inputPath;
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/__tests__/devHtmlPrerender.test.ts b/packages/wxt/src/core/builders/vite/plugins/__tests__/devHtmlPrerender.test.ts
new file mode 100644
index 0000000..0b8eb66
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/__tests__/devHtmlPrerender.test.ts
@@ -0,0 +1,64 @@
+import { describe, expect, it } from 'vitest';
+import { Window } from 'happy-dom';
+import { pointToDevServer } from '../devHtmlPrerender';
+import {
+ fakeDevServer,
+ fakeResolvedConfig,
+} from '~/core/utils/testing/fake-objects';
+import { normalizePath } from '~/core/utils/paths';
+import { resolve } from 'node:path';
+
+describe('Dev HTML Prerender Plugin', () => {
+ describe('pointToDevServer', () => {
+ it.each([
+ // File paths should be resolved
+ ['style.css', 'http://localhost:5173/entrypoints/popup/style.css'],
+ ['./style.css', 'http://localhost:5173/entrypoints/popup/style.css'],
+ ['../style.css', 'http://localhost:5173/entrypoints/style.css'],
+ ['~/assets/style.css', 'http://localhost:5173/assets/style.css'],
+ ['~~/assets/style.css', 'http://localhost:5173/assets/style.css'],
+ ['~local/style.css', 'http://localhost:5173/style.css'],
+ ['~absolute/style.css', 'http://localhost:5173/assets/style.css'],
+ ['~file', 'http://localhost:5173/example.css'],
+ // Absolute paths are loaded with the `/@fs/` base path
+ [
+ '~outside/test.css',
+ `http://localhost:5173/@fs${
+ process.platform === 'win32'
+ ? '/' + normalizePath(resolve('/some/non-root/test.css')) // "/D:/some/non-root/test.css"
+ : '/some/non-root/test.css'
+ }`,
+ ],
+ // URLs should not be changed
+ ['https://example.com/style.css', 'https://example.com/style.css'],
+ ])('should transform "%s" into "%s"', (input, expected) => {
+ const { document } = new Window({
+ url: 'http://localhost',
+ });
+ const root = '/some/root';
+ const config = fakeResolvedConfig({
+ root,
+ alias: {
+ '~local': '.',
+ '~absolute': `${root}/assets`,
+ '~file': `${root}/example.css`,
+ '~outside': `${root}/../non-root`,
+ '~~': root,
+ '~': root,
+ },
+ });
+ const server = fakeDevServer({
+ hostname: 'localhost',
+ port: 5173,
+ origin: 'http://localhost:5173',
+ });
+ const id = root + '/entrypoints/popup/index.html';
+
+ document.head.innerHTML = ``;
+ pointToDevServer(config, server, id, document as any, 'link', 'href');
+
+ const actual = document.querySelector('link')!;
+ expect(actual.getAttribute('href')).toBe(expected);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts b/packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts
new file mode 100644
index 0000000..61a3131
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts
@@ -0,0 +1,23 @@
+import type * as vite from 'vite';
+import { visualizer } from '@aklinker1/rollup-plugin-visualizer';
+import { ResolvedConfig } from '~/types';
+import path from 'node:path';
+
+let increment = 0;
+
+export function bundleAnalysis(config: ResolvedConfig): vite.Plugin {
+ return visualizer({
+ template: 'raw-data',
+ filename: path.resolve(
+ config.analysis.outputDir,
+ `${config.analysis.outputName}-${increment++}.json`,
+ ),
+ });
+}
+
+/**
+ * @deprecated FOR TESTING ONLY.
+ */
+export function resetBundleIncrement() {
+ increment = 0;
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/cssEntrypoints.ts b/packages/wxt/src/core/builders/vite/plugins/cssEntrypoints.ts
new file mode 100644
index 0000000..bf8cca9
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/cssEntrypoints.ts
@@ -0,0 +1,39 @@
+import type * as vite from 'vite';
+import { Entrypoint, ResolvedConfig } from '~/types';
+import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
+
+/**
+ * Rename CSS entrypoint outputs to ensure a JS file is not generated, and that the CSS file is
+ * placed in the correct place.
+ *
+ * It:
+ * 1. Renames CSS files to their final paths
+ * 2. Removes the JS file that get's output by lib mode
+ *
+ * THIS PLUGIN SHOULD ONLY BE APPLIED TO CSS LIB MODE BUILDS. It should not be added to every build.
+ */
+export function cssEntrypoints(
+ entrypoint: Entrypoint,
+ config: ResolvedConfig,
+): vite.Plugin {
+ return {
+ name: 'wxt:css-entrypoint',
+ config() {
+ return {
+ build: {
+ rollupOptions: {
+ output: {
+ assetFileNames: () =>
+ getEntrypointBundlePath(entrypoint, config.outDir, '.css'),
+ },
+ },
+ },
+ };
+ },
+ generateBundle(_, bundle) {
+ Object.keys(bundle).forEach((file) => {
+ if (file.endsWith('.js')) delete bundle[file];
+ });
+ },
+ };
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/defineImportMeta.ts b/packages/wxt/src/core/builders/vite/plugins/defineImportMeta.ts
new file mode 100644
index 0000000..9d4b978
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/defineImportMeta.ts
@@ -0,0 +1,19 @@
+/**
+ * Overrides definitions for `import.meta.*`
+ *
+ * - `import.meta.url`: Without this, background service workers crash trying to access
+ * `document.location`, see https://github.com/wxt-dev/wxt/issues/392
+ */
+export function defineImportMeta() {
+ return {
+ name: 'wxt:define',
+ config() {
+ return {
+ define: {
+ // This works for all extension contexts, including background service worker
+ 'import.meta.url': 'self.location.href',
+ },
+ };
+ },
+ };
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/devHtmlPrerender.ts b/packages/wxt/src/core/builders/vite/plugins/devHtmlPrerender.ts
new file mode 100644
index 0000000..dbdba4b
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/devHtmlPrerender.ts
@@ -0,0 +1,193 @@
+import type * as vite from 'vite';
+import { ResolvedConfig, WxtDevServer } from '~/types';
+import { getEntrypointName } from '~/core/utils/entrypoints';
+import { parseHTML } from 'linkedom';
+import { dirname, relative, resolve } from 'node:path';
+import { normalizePath } from '~/core/utils/paths';
+
+// Cache the preamble script for all devHtmlPrerender plugins, not just one
+let reactRefreshPreamble = '';
+
+/**
+ * Pre-renders the HTML entrypoints when building the extension to connect to the dev server.
+ */
+export function devHtmlPrerender(
+ config: ResolvedConfig,
+ server: WxtDevServer | undefined,
+): vite.PluginOption {
+ const htmlReloadId = '@wxt/reload-html';
+ const resolvedHtmlReloadId = resolve(
+ config.wxtModuleDir,
+ 'dist/virtual/reload-html.js',
+ );
+ const virtualReactRefreshId = '@wxt/virtual-react-refresh';
+ const resolvedVirtualReactRefreshId = '\0' + virtualReactRefreshId;
+
+ return [
+ {
+ apply: 'build',
+ name: 'wxt:dev-html-prerender',
+ config() {
+ return {
+ resolve: {
+ alias: {
+ [htmlReloadId]: resolvedHtmlReloadId,
+ },
+ },
+ };
+ },
+ // Convert scripts like src="./main.tsx" -> src="http://localhost:3000/entrypoints/popup/main.tsx"
+ // before the paths are replaced with their bundled path
+ transform(code, id) {
+ if (
+ config.command !== 'serve' ||
+ server == null ||
+ !id.endsWith('.html')
+ )
+ return;
+
+ const { document } = parseHTML(code);
+
+ const _pointToDevServer = (querySelector: string, attr: string) =>
+ pointToDevServer(config, server, id, document, querySelector, attr);
+ _pointToDevServer('script[type=module]', 'src');
+ _pointToDevServer('link[rel=stylesheet]', 'href');
+
+ // Add a script to add page reloading
+ const reloader = document.createElement('script');
+ reloader.src = htmlReloadId;
+ reloader.type = 'module';
+ document.head.appendChild(reloader);
+
+ const newHtml = document.toString();
+ config.logger.debug('transform ' + id);
+ config.logger.debug('Old HTML:\n' + code);
+ config.logger.debug('New HTML:\n' + newHtml);
+ return newHtml;
+ },
+
+ // Pass the HTML through the dev server to add dev-mode specific code
+ async transformIndexHtml(html, ctx) {
+ if (config.command !== 'serve' || server == null) return;
+
+ const originalUrl = `${server.origin}${ctx.path}`;
+ const name = getEntrypointName(config.entrypointsDir, ctx.filename);
+ const url = `${server.origin}/${name}.html`;
+ const serverHtml = await server.transformHtml(url, html, originalUrl);
+ const { document } = parseHTML(serverHtml);
+
+ // React pages include a preamble as an unsafe-inline type="module" script to enable fast refresh, as shown here:
+ // https://github.com/wxt-dev/wxt/issues/157#issuecomment-1756497616
+ // Since unsafe-inline scripts are blocked by MV3 CSPs, we need to virtualize it.
+ const reactRefreshScript = Array.from(
+ document.querySelectorAll('script[type=module]'),
+ ).find((script) => script.innerHTML.includes('@react-refresh'));
+ if (reactRefreshScript) {
+ // Save preamble to serve from server
+ reactRefreshPreamble = reactRefreshScript.innerHTML;
+
+ // Replace unsafe inline script
+ const virtualScript = document.createElement('script');
+ virtualScript.type = 'module';
+ virtualScript.src = `${server.origin}/${virtualReactRefreshId}`;
+ reactRefreshScript.replaceWith(virtualScript);
+ }
+
+ // Change /@vite/client -> http://localhost:3000/@vite/client
+ const viteClientScript = document.querySelector(
+ "script[src='/@vite/client']",
+ );
+ if (viteClientScript) {
+ viteClientScript.src = `${server.origin}${viteClientScript.src}`;
+ }
+
+ const newHtml = document.toString();
+ config.logger.debug('transformIndexHtml ' + ctx.filename);
+ config.logger.debug('Old HTML:\n' + html);
+ config.logger.debug('New HTML:\n' + newHtml);
+ return newHtml;
+ },
+ },
+ {
+ name: 'wxt:virtualize-react-refresh',
+ apply: 'serve',
+ resolveId(id) {
+ if (id === `/${virtualReactRefreshId}`) {
+ return resolvedVirtualReactRefreshId;
+ }
+ // Ignore chunk contents when pre-rendering
+ if (id.startsWith('/chunks/')) {
+ return '\0noop';
+ }
+ },
+ load(id) {
+ if (id === resolvedVirtualReactRefreshId) {
+ return reactRefreshPreamble;
+ }
+ if (id === '\0noop') {
+ return '';
+ }
+ },
+ },
+ ];
+}
+
+export function pointToDevServer(
+ config: ResolvedConfig,
+ server: WxtDevServer,
+ id: string,
+ document: Document,
+ querySelector: string,
+ attr: string,
+) {
+ document.querySelectorAll(querySelector).forEach((element) => {
+ const src = element.getAttribute(attr);
+ if (!src || isUrl(src)) return;
+
+ let resolvedAbsolutePath: string | undefined;
+
+ // Check if src uses a project alias
+ const matchingAlias = Object.entries(config.alias).find(([key]) =>
+ src.startsWith(key),
+ );
+ if (matchingAlias) {
+ // Matches a import alias
+ const [alias, replacement] = matchingAlias;
+ resolvedAbsolutePath = resolve(
+ config.root,
+ src.replace(alias, replacement),
+ );
+ } else {
+ // Some file path relative to the HTML file
+ resolvedAbsolutePath = resolve(dirname(id), src);
+ }
+
+ // Apply the final file path
+ if (resolvedAbsolutePath) {
+ const relativePath = normalizePath(
+ relative(config.root, resolvedAbsolutePath),
+ );
+
+ if (relativePath.startsWith('.')) {
+ // Outside the config.root directory, serve the absolute path
+ let path = normalizePath(resolvedAbsolutePath);
+ // Add "/" to start of windows paths ("D:/some/path" -> "/D:/some/path")
+ if (!path.startsWith('/')) path = '/' + path;
+ element.setAttribute(attr, `${server.origin}/@fs${path}`);
+ } else {
+ // Inside the project, use relative path
+ const url = new URL(relativePath, server.origin);
+ element.setAttribute(attr, url.href);
+ }
+ }
+ });
+}
+
+function isUrl(str: string): boolean {
+ try {
+ new URL(str);
+ return true;
+ } catch {
+ return false;
+ }
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/devServerGlobals.ts b/packages/wxt/src/core/builders/vite/plugins/devServerGlobals.ts
new file mode 100644
index 0000000..4bf1ceb
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/devServerGlobals.ts
@@ -0,0 +1,25 @@
+import { Plugin } from 'vite';
+import { ResolvedConfig, WxtDevServer } from '~/types';
+
+/**
+ * Defines global constants about the dev server. Helps scripts connect to the server's web socket.
+ */
+export function devServerGlobals(
+ config: ResolvedConfig,
+ server: WxtDevServer | undefined,
+): Plugin {
+ return {
+ name: 'wxt:dev-server-globals',
+ config() {
+ if (server == null || config.command == 'build') return;
+
+ return {
+ define: {
+ __DEV_SERVER_PROTOCOL__: JSON.stringify('ws:'),
+ __DEV_SERVER_HOSTNAME__: JSON.stringify(server.hostname),
+ __DEV_SERVER_PORT__: JSON.stringify(server.port),
+ },
+ };
+ },
+ };
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/download.ts b/packages/wxt/src/core/builders/vite/plugins/download.ts
new file mode 100644
index 0000000..481d126
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/download.ts
@@ -0,0 +1,26 @@
+import { Plugin } from 'vite';
+import { ResolvedConfig } from '~/types';
+import { fetchCached } from '~/core/utils/network';
+
+/**
+ * Downloads any URL imports, like Google Analytics, into virtual modules so they are bundled with
+ * the extension instead of depending on remote code at runtime.
+ *
+ * @example
+ * import "url:https://google-tagmanager.com/gtag?id=XYZ";
+ */
+export function download(config: ResolvedConfig): Plugin {
+ return {
+ name: 'wxt:download',
+ resolveId(id) {
+ if (id.startsWith('url:')) return '\0' + id;
+ },
+ async load(id) {
+ if (!id.startsWith('\0url:')) return;
+
+ // Load file from network or cache
+ const url = id.replace('\0url:', '');
+ return await fetchCached(url, config);
+ },
+ };
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/entrypointGroupGlobals.ts b/packages/wxt/src/core/builders/vite/plugins/entrypointGroupGlobals.ts
new file mode 100644
index 0000000..d3c3c03
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/entrypointGroupGlobals.ts
@@ -0,0 +1,24 @@
+import type * as vite from 'vite';
+import { EntrypointGroup } from '~/types';
+import { getEntrypointGlobals } from '~/core/utils/globals';
+
+/**
+ * Define a set of global variables specific to an entrypoint.
+ */
+export function entrypointGroupGlobals(
+ entrypointGroup: EntrypointGroup,
+): vite.PluginOption {
+ return {
+ name: 'wxt:entrypoint-group-globals',
+ config() {
+ const define: vite.InlineConfig['define'] = {};
+ let name = Array.isArray(entrypointGroup) ? 'html' : entrypointGroup.name;
+ for (const global of getEntrypointGlobals(name)) {
+ define[`import.meta.env.${global.name}`] = JSON.stringify(global.value);
+ }
+ return {
+ define,
+ };
+ },
+ };
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/excludeBrowserPolyfill.ts b/packages/wxt/src/core/builders/vite/plugins/excludeBrowserPolyfill.ts
new file mode 100644
index 0000000..bd677ac
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/excludeBrowserPolyfill.ts
@@ -0,0 +1,33 @@
+import { ResolvedConfig } from '~/types';
+import type * as vite from 'vite';
+
+/**
+ * Apply the experimental config for disabling the polyfill. It works by aliasing the
+ * `webextension-polyfill` module to a virtual module and exporting the `chrome` global from the
+ * virtual module.
+ */
+export function excludeBrowserPolyfill(config: ResolvedConfig): vite.Plugin {
+ const virtualId = 'virtual:wxt-webextension-polyfill-disabled';
+
+ return {
+ name: 'wxt:exclude-browser-polyfill',
+ config() {
+ // Only apply the config if we're disabling the polyfill
+ if (config.experimental.includeBrowserPolyfill) return;
+
+ return {
+ resolve: {
+ alias: {
+ 'webextension-polyfill': virtualId,
+ },
+ },
+ };
+ },
+ load(id) {
+ if (id === virtualId) {
+ // Use chrome instead of the polyfill when disabled.
+ return 'export default chrome';
+ }
+ },
+ };
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/globals.ts b/packages/wxt/src/core/builders/vite/plugins/globals.ts
new file mode 100644
index 0000000..94d2fc9
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/globals.ts
@@ -0,0 +1,18 @@
+import type * as vite from 'vite';
+import { ResolvedConfig } from '~/types';
+import { getGlobals } from '~/core/utils/globals';
+
+export function globals(config: ResolvedConfig): vite.PluginOption {
+ return {
+ name: 'wxt:globals',
+ config() {
+ const define: vite.InlineConfig['define'] = {};
+ for (const global of getGlobals(config)) {
+ define[`import.meta.env.${global.name}`] = JSON.stringify(global.value);
+ }
+ return {
+ define,
+ };
+ },
+ };
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/index.ts b/packages/wxt/src/core/builders/vite/plugins/index.ts
new file mode 100644
index 0000000..5d39010
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/index.ts
@@ -0,0 +1,16 @@
+export * from './devHtmlPrerender';
+export * from './devServerGlobals';
+export * from './download';
+export * from './multipageMove';
+export * from './unimport';
+export * from './resolveVirtualModules';
+export * from './tsconfigPaths';
+export * from './noopBackground';
+export * from './cssEntrypoints';
+export * from './bundleAnalysis';
+export * from './globals';
+export * from './webextensionPolyfillMock';
+export * from './excludeBrowserPolyfill';
+export * from './entrypointGroupGlobals';
+export * from './defineImportMeta';
+export * from './removeEntrypointMainFunction';
diff --git a/packages/wxt/src/core/builders/vite/plugins/multipageMove.ts b/packages/wxt/src/core/builders/vite/plugins/multipageMove.ts
new file mode 100644
index 0000000..93963d9
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/multipageMove.ts
@@ -0,0 +1,96 @@
+import type * as vite from 'vite';
+import { Entrypoint, ResolvedConfig } from '~/types';
+import { dirname, extname, resolve, join } from 'node:path';
+import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
+import fs, { ensureDir } from 'fs-extra';
+import { normalizePath } from '~/core/utils/paths';
+
+/**
+ * Ensures the HTML files output by a multipage build are in the correct location. This does two
+ * things:
+ *
+ * 1. Moves the HMTL files to their final location at `/.html`.
+ * 2. Updates the bundle so it summarizes the files correctly in the returned build output.
+ *
+ * Assets (JS and CSS) are output to the `/assets` directory, and don't need to be modified.
+ * HTML files access them via absolute URLs, so we don't need to update any import paths in the HTML
+ * files either.
+ *
+ * THIS PLUGIN SHOULD ONLY BE APPLIED TO MULTIPAGE BUILDS. It should not be added to every build.
+ */
+export function multipageMove(
+ entrypoints: Entrypoint[],
+ config: ResolvedConfig,
+): vite.Plugin {
+ return {
+ name: 'wxt:multipage-move',
+ async writeBundle(_, bundle) {
+ for (const oldBundlePath in bundle) {
+ // oldBundlePath = 'entrypoints/popup.html' or 'entrypoints/options/index.html'
+
+ // Find a matching entrypoint - oldBundlePath is the same as end end of the input path.
+ const entrypoint = entrypoints.find(
+ (entry) => !!normalizePath(entry.inputPath).endsWith(oldBundlePath),
+ );
+ if (entrypoint == null) {
+ config.logger.debug(
+ `No entrypoint found for ${oldBundlePath}, leaving in chunks directory`,
+ );
+ continue;
+ }
+
+ // Get the new bundle path
+ const newBundlePath = getEntrypointBundlePath(
+ entrypoint,
+ config.outDir,
+ extname(oldBundlePath),
+ );
+ if (newBundlePath === oldBundlePath) {
+ config.logger.debug(
+ 'HTML file is already in the correct location',
+ oldBundlePath,
+ );
+ continue;
+ }
+
+ // Move file and update bundle
+ // Do this inside a mutex lock so it only runs one at a time for concurrent multipage builds
+ const oldAbsPath = resolve(config.outDir, oldBundlePath);
+ const newAbsPath = resolve(config.outDir, newBundlePath);
+ await ensureDir(dirname(newAbsPath));
+ await fs.move(oldAbsPath, newAbsPath, { overwrite: true });
+
+ const renamedChunk = {
+ ...bundle[oldBundlePath],
+ fileName: newBundlePath,
+ };
+ delete bundle[oldBundlePath];
+ bundle[newBundlePath] = renamedChunk;
+ }
+
+ // Remove directories that were created
+ // TODO: Optimize and only delete old path directories
+ removeEmptyDirs(config.outDir);
+ },
+ };
+}
+
+/**
+ * Recursively remove all directories that are empty/
+ */
+export async function removeEmptyDirs(dir: string): Promise {
+ const files = await fs.readdir(dir);
+ for (const file of files) {
+ const filePath = join(dir, file);
+ const stats = await fs.stat(filePath);
+ if (stats.isDirectory()) {
+ await removeEmptyDirs(filePath);
+ }
+ }
+
+ try {
+ await fs.rmdir(dir);
+ } catch {
+ // noop on failure - this means the directory was not empty.
+ }
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/noopBackground.ts b/packages/wxt/src/core/builders/vite/plugins/noopBackground.ts
new file mode 100644
index 0000000..ebc76e5
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/noopBackground.ts
@@ -0,0 +1,22 @@
+import { Plugin } from 'vite';
+import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
+
+/**
+ * In dev mode, if there's not a background script listed, we need to add one so that the web socket
+ * connection is setup and the extension reloads HTML pages and content scripts correctly.
+ */
+export function noopBackground(): Plugin {
+ const virtualModuleId = VIRTUAL_NOOP_BACKGROUND_MODULE_ID;
+ const resolvedVirtualModuleId = '\0' + virtualModuleId;
+ return {
+ name: 'wxt:noop-background',
+ resolveId(id) {
+ if (id === virtualModuleId) return resolvedVirtualModuleId;
+ },
+ load(id) {
+ if (id === resolvedVirtualModuleId) {
+ return `import { defineBackground } from 'wxt/sandbox';\nexport default defineBackground(() => void 0)`;
+ }
+ },
+ };
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/removeEntrypointMainFunction.ts b/packages/wxt/src/core/builders/vite/plugins/removeEntrypointMainFunction.ts
new file mode 100644
index 0000000..38ca7c1
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/removeEntrypointMainFunction.ts
@@ -0,0 +1,21 @@
+import { ResolvedConfig } from '~/types';
+import * as vite from 'vite';
+import { normalizePath } from '~/core/utils/paths';
+import { removeMainFunctionCode } from '~/core/utils/transform';
+import { resolve } from 'node:path';
+
+/**
+ * Transforms entrypoints, removing the main function from the entrypoint if it exists.
+ */
+export function removeEntrypointMainFunction(
+ config: ResolvedConfig,
+ path: string,
+): vite.Plugin {
+ const absPath = normalizePath(resolve(config.root, path));
+ return {
+ name: 'wxt:remove-entrypoint-main-function',
+ transform(code, id) {
+ if (id === absPath) return removeMainFunctionCode(code);
+ },
+ };
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts b/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts
new file mode 100644
index 0000000..600ba7d
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts
@@ -0,0 +1,41 @@
+import { Plugin } from 'vite';
+import { ResolvedConfig } from '~/types';
+import { normalizePath } from '~/core/utils/paths';
+import {
+ VirtualModuleId,
+ virtualModuleNames,
+} from '~/core/utils/virtual-modules';
+import fs from 'fs-extra';
+import { resolve } from 'path';
+
+/**
+ * Resolve all the virtual modules to the `node_modules/wxt/dist/virtual` directory.
+ */
+export function resolveVirtualModules(config: ResolvedConfig): Plugin[] {
+ return virtualModuleNames.map((name) => {
+ const virtualId: `${VirtualModuleId}?` = `virtual:wxt-${name}?`;
+ const resolvedVirtualId = '\0' + virtualId;
+ return {
+ name: `wxt:resolve-virtual-${name}`,
+ resolveId(id) {
+ // Id doesn't start with prefix, it looks like this:
+ // /path/to/project/virtual:wxt-background?/path/to/project/entrypoints/background.ts
+ const index = id.indexOf(virtualId);
+ if (index === -1) return;
+
+ const inputPath = normalizePath(id.substring(index + virtualId.length));
+ return resolvedVirtualId + inputPath;
+ },
+ async load(id) {
+ if (!id.startsWith(resolvedVirtualId)) return;
+
+ const inputPath = id.replace(resolvedVirtualId, '');
+ const template = await fs.readFile(
+ resolve(config.wxtModuleDir, `dist/virtual/${name}.js`),
+ 'utf-8',
+ );
+ return template.replace(`virtual:user-${name}`, inputPath);
+ },
+ };
+ });
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/tsconfigPaths.ts b/packages/wxt/src/core/builders/vite/plugins/tsconfigPaths.ts
new file mode 100644
index 0000000..68cad07
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/tsconfigPaths.ts
@@ -0,0 +1,15 @@
+import { ResolvedConfig } from '~/types';
+import type * as vite from 'vite';
+
+export function tsconfigPaths(config: ResolvedConfig): vite.Plugin {
+ return {
+ name: 'wxt:aliases',
+ async config() {
+ return {
+ resolve: {
+ alias: config.alias,
+ },
+ };
+ },
+ };
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/unimport.ts b/packages/wxt/src/core/builders/vite/plugins/unimport.ts
new file mode 100644
index 0000000..fa9c7ea
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/unimport.ts
@@ -0,0 +1,43 @@
+import { createUnimport } from 'unimport';
+import { ResolvedConfig } from '~/types';
+import type * as vite from 'vite';
+import { extname } from 'path';
+
+const ENABLED_EXTENSIONS = new Set([
+ '.js',
+ '.jsx',
+ '.ts',
+ '.tsx',
+ '.vue',
+ '.svelte',
+]);
+
+/**
+ * Inject any global imports defined by unimport
+ */
+export function unimport(config: ResolvedConfig): vite.PluginOption {
+ const options = config.imports;
+ if (options === false) return [];
+
+ const unimport = createUnimport(options);
+
+ return {
+ name: 'wxt:unimport',
+ async config() {
+ await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
+ },
+ async transform(code, id) {
+ // Don't transform dependencies
+ if (id.includes('node_modules')) return;
+
+ // Don't transform non-js files
+ if (!ENABLED_EXTENSIONS.has(extname(id))) return;
+
+ const injected = await unimport.injectImports(code, id);
+ return {
+ code: injected.code,
+ map: injected.s.generateMap({ hires: 'boundary', source: id }),
+ };
+ },
+ };
+}
diff --git a/packages/wxt/src/core/builders/vite/plugins/webextensionPolyfillMock.ts b/packages/wxt/src/core/builders/vite/plugins/webextensionPolyfillMock.ts
new file mode 100644
index 0000000..0230d2f
--- /dev/null
+++ b/packages/wxt/src/core/builders/vite/plugins/webextensionPolyfillMock.ts
@@ -0,0 +1,41 @@
+import path from 'node:path';
+import type * as vite from 'vite';
+import { ResolvedConfig } from '~/types';
+
+/**
+ * Mock `webextension-polyfill` by inlining all dependencies that import it and adding a custom
+ * alias so that Vite resolves to a mocked version of the module.
+ *
+ * There are two ways to mark a module as inline:
+ * 1. Use partial file paths ("wxt/dist/browser.js") in the `test.server.deps.inline` option.
+ * 2. Use module names ("wxt" or "@webext-core/messaging") in the `ssr.noExternalize` option.
+ *
+ * This plugin uses the second approach since it's a little more intuative to understand.
+ *
+ * TODO: Detect non-wxt dependencies (like `@webext-core/*`) that import `webextension-polyfill` via
+ * `npm list` and inline them automatically.
+ */
+export function webextensionPolyfillMock(
+ config: ResolvedConfig,
+): vite.PluginOption {
+ return {
+ name: 'wxt:testing-inline-deps',
+ config() {
+ return {
+ resolve: {
+ alias: {
+ // Alias to use a mocked version of the polyfill
+ 'webextension-polyfill': path.resolve(
+ config.wxtModuleDir,
+ 'dist/virtual/mock-browser',
+ ),
+ },
+ },
+ ssr: {
+ // Inline all WXT modules
+ noExternal: ['wxt'],
+ },
+ };
+ },
+ };
+}
diff --git a/packages/wxt/src/core/clean.ts b/packages/wxt/src/core/clean.ts
new file mode 100644
index 0000000..261283e
--- /dev/null
+++ b/packages/wxt/src/core/clean.ts
@@ -0,0 +1,44 @@
+import path from 'node:path';
+import glob from 'fast-glob';
+import fs from 'fs-extra';
+import { consola } from 'consola';
+import pc from 'picocolors';
+
+/**
+ * Remove generated/temp files from the directory.
+ *
+ * @param root The directory to look for generated/temp files in. Defaults to `process.cwd()`. Can be relative to `process.cwd()` or absolute.
+ *
+ * @example
+ * await clean();
+ */
+export async function clean(root = process.cwd()) {
+ consola.info('Cleaning Project');
+
+ const tempDirs = [
+ 'node_modules/.vite',
+ 'node_modules/.cache',
+ '**/.wxt',
+ '.output/*',
+ ];
+ consola.debug('Looking for:', tempDirs.map(pc.cyan).join(', '));
+ const directories = await glob(tempDirs, {
+ cwd: path.resolve(root),
+ absolute: true,
+ onlyDirectories: true,
+ deep: 2,
+ });
+ if (directories.length === 0) {
+ consola.debug('No generated files found.');
+ return;
+ }
+
+ consola.debug(
+ 'Found:',
+ directories.map((dir) => pc.cyan(path.relative(root, dir))).join(', '),
+ );
+ for (const directory of directories) {
+ await fs.rm(directory, { force: true, recursive: true });
+ consola.debug('Deleted ' + pc.cyan(path.relative(root, directory)));
+ }
+}
diff --git a/packages/wxt/src/core/create-server.ts b/packages/wxt/src/core/create-server.ts
new file mode 100644
index 0000000..e418802
--- /dev/null
+++ b/packages/wxt/src/core/create-server.ts
@@ -0,0 +1,265 @@
+import { BuildStepOutput, EntrypointGroup, InlineConfig, ServerInfo, WxtDevServer } from '~/types';
+import { getEntrypointBundlePath, isHtmlEntrypoint } from '~/core/utils/entrypoints';
+import { getContentScriptCssFiles, getContentScriptsCssMap } from '~/core/utils/manifest';
+import { internalBuild, detectDevChanges, rebuild, findEntrypoints } from '~/core/utils/building';
+import { createExtensionRunner } from '~/core/runners';
+import { consola } from 'consola';
+import { Mutex } from 'async-mutex';
+import pc from 'picocolors';
+import { relative } from 'node:path';
+import { registerWxt, wxt } from './wxt';
+import { unnormalizePath } from './utils/paths';
+import { getContentScriptJs, mapWxtOptionsToRegisteredContentScript } from './utils/content-scripts';
+
+/**
+ * Creates a dev server and pre-builds all the files that need to exist before loading the extension.
+ *
+ * @example
+ * const server = await wxt.createServer({
+ * // Enter config...
+ * });
+ * await server.start();
+ */
+export async function createServer(inlineConfig?: InlineConfig): Promise {
+ await registerWxt('serve', inlineConfig, async (config) => {
+ const { port, hostname } = config.dev.server!;
+ const serverInfo: ServerInfo = {
+ port,
+ hostname,
+ origin: `http://${hostname}:${port}`,
+ };
+
+ // Server instance must be created first so its reference can be added to the internal config used
+ // to pre-render entrypoints
+ const server: WxtDevServer = {
+ ...serverInfo,
+ get watcher() {
+ return builderServer.watcher;
+ },
+ get ws() {
+ return builderServer.ws;
+ },
+ currentOutput: undefined,
+ async start() {
+ await builderServer.listen();
+ wxt.logger.success(`Started dev server @ ${serverInfo.origin}`);
+ await buildAndOpenBrowser();
+ },
+ async stop() {
+ await runner.closeBrowser();
+ await builderServer.close();
+ },
+ async restart() {
+ await closeAndRecreateRunner();
+ await buildAndOpenBrowser();
+ },
+ transformHtml(url, html, originalUrl) {
+ return builderServer.transformHtml(url, html, originalUrl);
+ },
+ reloadContentScript(payload) {
+ server.ws.send('wxt:reload-content-script', payload);
+ },
+ reloadPage(path) {
+ server.ws.send('wxt:reload-page', path);
+ },
+ reloadExtension() {
+ server.ws.send('wxt:reload-extension');
+ },
+ async restartBrowser() {
+ await closeAndRecreateRunner();
+ await runner.openBrowser();
+ },
+ };
+ return server;
+ });
+
+ const server = wxt.server!;
+ let [runner, builderServer] = await Promise.all([createExtensionRunner(), wxt.builder.createServer(server)]);
+
+ const buildAndOpenBrowser = async () => {
+ // Build after starting the dev server so it can be used to transform HTML files
+ server.currentOutput = await internalBuild();
+
+ // Add file watchers for files not loaded by the dev server. See
+ // https://github.com/wxt-dev/wxt/issues/428#issuecomment-1944731870
+ try {
+ server.watcher.add(getExternalOutputDependencies(server));
+ } catch (err) {
+ wxt.config.logger.warn('Failed to register additional file paths:', err);
+ }
+
+ // Open browser after everything is ready to go.
+ await runner.openBrowser();
+ };
+
+ /**
+ * Stops the previous runner, grabs the latest config, and recreates the runner.
+ */
+ const closeAndRecreateRunner = async () => {
+ await runner.closeBrowser();
+ await wxt.reloadConfig();
+ runner = await createExtensionRunner();
+ };
+
+ // Register content scripts for the first time after the background starts up since they're not
+ // listed in the manifest
+ server.ws.on('wxt:background-initialized', () => {
+ if (server.currentOutput == null) return;
+ reloadContentScripts(server.currentOutput.steps, server);
+ });
+
+ // Listen for file changes and reload different parts of the extension accordingly
+ const reloadOnChange = createFileReloader(server);
+ server.watcher.on('all', reloadOnChange);
+
+ return server;
+}
+
+/**
+ * Returns a function responsible for reloading different parts of the extension when a file
+ * changes.
+ */
+function createFileReloader(server: WxtDevServer) {
+ const fileChangedMutex = new Mutex();
+ const changeQueue: Array<[string, string]> = [];
+
+ return async (event: string, path: string) => {
+ await wxt.reloadConfig();
+
+ // Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
+ if (path.startsWith(wxt.config.outBaseDir)) return;
+ changeQueue.push([event, path]);
+
+ await fileChangedMutex.runExclusive(async () => {
+ if (server.currentOutput == null) return;
+
+ const fileChanges = changeQueue.splice(0, changeQueue.length).map(([_, file]) => file);
+ if (fileChanges.length === 0) return;
+
+ const changes = detectDevChanges(fileChanges, server.currentOutput);
+ if (changes.type === 'no-change') return;
+
+ if (changes.type === 'full-restart') {
+ wxt.logger.info('Config changed, restarting server...');
+ server.restart();
+ return;
+ }
+
+ if (changes.type === 'browser-restart') {
+ wxt.logger.info('Runner config changed, restarting browser...');
+ server.restartBrowser();
+ return;
+ }
+
+ // Log the entrypoints that were effected
+ wxt.logger.info(
+ `Changed: ${Array.from(new Set(fileChanges))
+ .map((file) => pc.dim(relative(wxt.config.root, file)))
+ .join(', ')}`,
+ );
+
+ // Rebuild entrypoints on change
+ const allEntrypoints = await findEntrypoints();
+ try {
+ const { output: newOutput } = await rebuild(
+ allEntrypoints,
+ // TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
+ changes.rebuildGroups,
+ changes.cachedOutput,
+ );
+ server.currentOutput = newOutput;
+
+ // Perform reloads
+ switch (changes.type) {
+ case 'extension-reload':
+ server.reloadExtension();
+ consola.success(`Reloaded extension`);
+ break;
+ case 'html-reload':
+ const { reloadedNames } = reloadHtmlPages(changes.rebuildGroups, server);
+ consola.success(`Reloaded: ${getFilenameList(reloadedNames)}`);
+ break;
+ case 'content-script-reload':
+ reloadContentScripts(changes.changedSteps, server);
+ const rebuiltNames = changes.rebuildGroups.flat().map((entry) => entry.name);
+ consola.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
+ break;
+ }
+ } catch (err) {
+ // Catch build errors instead of crashing. Don't log error either, builder should have already logged it
+ }
+ });
+ };
+}
+
+/**
+ * From the server, tell the client to reload content scripts from the provided build step outputs.
+ */
+function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) {
+ if (wxt.config.manifestVersion === 3) {
+ steps.forEach((step) => {
+ if (server.currentOutput == null) return;
+
+ const entry = step.entrypoints;
+ if (Array.isArray(entry) || entry.type !== 'content-script') return;
+
+ const js = getContentScriptJs(wxt.config, entry);
+ const cssMap = getContentScriptsCssMap(server.currentOutput, [entry]);
+ const css = getContentScriptCssFiles([entry], cssMap);
+
+ server.reloadContentScript({
+ registration: entry.options.registration,
+ contentScript: mapWxtOptionsToRegisteredContentScript(entry.options, js, css),
+ });
+ });
+ } else {
+ server.reloadExtension();
+ }
+}
+
+function reloadHtmlPages(groups: EntrypointGroup[], server: WxtDevServer): { reloadedNames: string[] } {
+ // groups might contain other files like background/content scripts, and we only care about the HTMl pages
+ const htmlEntries = groups.flat().filter(isHtmlEntrypoint);
+
+ htmlEntries.forEach((entry) => {
+ const path = getEntrypointBundlePath(entry, wxt.config.outDir, '.html');
+ server.reloadPage(path);
+ });
+
+ return {
+ reloadedNames: htmlEntries.map((entry) => entry.name),
+ };
+}
+
+function getFilenameList(names: string[]): string {
+ return names
+ .map((name) => {
+ return pc.cyan(name);
+ })
+ .join(pc.dim(', '));
+}
+
+/**
+ * Based on the current build output, return a list of files that are:
+ * 1. Not in node_modules
+ * 2. Not inside project root
+ */
+function getExternalOutputDependencies(server: WxtDevServer) {
+ return (
+ server.currentOutput?.steps
+ .flatMap((step, i) => {
+ if (Array.isArray(step.entrypoints) && i === 0) {
+ // Dev server is already watching all HTML/esm files
+ return [];
+ }
+
+ return step.chunks.flatMap((chunk) => {
+ if (chunk.type === 'asset') return [];
+ return chunk.moduleIds;
+ });
+ })
+ .filter((file) => !file.includes('node_modules') && !file.startsWith('\x00'))
+ .map(unnormalizePath)
+ .filter((file) => !file.startsWith(wxt.config.root)) ?? []
+ );
+}
diff --git a/packages/wxt/src/core/define-config.ts b/packages/wxt/src/core/define-config.ts
new file mode 100644
index 0000000..d260575
--- /dev/null
+++ b/packages/wxt/src/core/define-config.ts
@@ -0,0 +1,5 @@
+import { UserConfig } from '~/types';
+
+export function defineConfig(config: UserConfig): UserConfig {
+ return config;
+}
diff --git a/packages/wxt/src/core/define-runner-config.ts b/packages/wxt/src/core/define-runner-config.ts
new file mode 100644
index 0000000..baf3ac5
--- /dev/null
+++ b/packages/wxt/src/core/define-runner-config.ts
@@ -0,0 +1,7 @@
+import { ExtensionRunnerConfig } from '~/types';
+
+export function defineRunnerConfig(
+ config: ExtensionRunnerConfig,
+): ExtensionRunnerConfig {
+ return config;
+}
diff --git a/packages/wxt/src/core/index.ts b/packages/wxt/src/core/index.ts
new file mode 100644
index 0000000..9c2c01c
--- /dev/null
+++ b/packages/wxt/src/core/index.ts
@@ -0,0 +1,8 @@
+export * from './build';
+export * from './clean';
+export * from './define-config';
+export * from './define-runner-config';
+export * from './create-server';
+export * from './initialize';
+export * from './prepare';
+export * from './zip';
diff --git a/packages/wxt/src/core/initialize.ts b/packages/wxt/src/core/initialize.ts
new file mode 100644
index 0000000..0e183ba
--- /dev/null
+++ b/packages/wxt/src/core/initialize.ts
@@ -0,0 +1,182 @@
+import prompts from 'prompts';
+import { consola } from 'consola';
+import { downloadTemplate } from 'giget';
+import fs from 'fs-extra';
+import path from 'node:path';
+import pc from 'picocolors';
+import { Formatter } from 'picocolors/types';
+
+export async function initialize(options: {
+ directory: string;
+ template: string;
+ packageManager: string;
+}) {
+ consola.info('Initalizing new project');
+
+ const templates = await listTemplates();
+ const defaultTemplate = templates.find(
+ (template) => template.name === options.template?.toLowerCase().trim(),
+ );
+
+ const input = await prompts(
+ [
+ {
+ name: 'directory',
+ type: () => (options.directory == null ? 'text' : undefined),
+ message: 'Project Directory',
+ initial: options.directory,
+ },
+ {
+ name: 'template',
+ type: () => (defaultTemplate == null ? 'select' : undefined),
+ message: 'Choose a template',
+ choices: templates.map((template) => ({
+ title:
+ TEMPLATE_COLORS[template.name]?.(template.name) ?? template.name,
+ value: template,
+ })),
+ },
+ {
+ name: 'packageManager',
+ type: () => (options.packageManager == null ? 'select' : undefined),
+ message: 'Package Manager',
+ choices: [
+ { title: pc.red('npm'), value: 'npm' },
+ { title: pc.yellow('pnpm'), value: 'pnpm' },
+ { title: pc.cyan('yarn'), value: 'yarn' },
+ {
+ title: `${pc.magenta('bun')}${pc.gray(' (experimental)')}`,
+ value: 'bun',
+ },
+ ],
+ },
+ ],
+ {
+ onCancel: () => process.exit(1),
+ },
+ );
+ input.directory ??= options.directory;
+ input.template ??= defaultTemplate;
+ input.packageManager ??= options.packageManager;
+
+ const isExists = await fs.pathExists(input.directory);
+ if (isExists) {
+ const isEmpty = (await fs.readdir(input.directory)).length === 0;
+ if (!isEmpty) {
+ consola.error(
+ `The directory ${path.resolve(input.directory)} is not empty. Aborted.`,
+ );
+ process.exit(1);
+ }
+ }
+ await cloneProject(input);
+
+ const cdPath = path.relative(process.cwd(), path.resolve(input.directory));
+ console.log();
+ consola.log(
+ `✨ WXT project created with the ${
+ TEMPLATE_COLORS[input.template.name]?.(input.template.name) ??
+ input.template.name
+ } template.`,
+ );
+ console.log();
+ consola.log('Next steps:');
+ let step = 0;
+ if (cdPath !== '') consola.log(` ${++step}.`, pc.cyan(`cd ${cdPath}`));
+ consola.log(` ${++step}.`, pc.cyan(`${input.packageManager} install`));
+ console.log();
+}
+
+interface Template {
+ /**
+ * Template's name.
+ */
+ name: string;
+ /**
+ * Path to template directory in github repo.
+ */
+ path: string;
+}
+
+async function listTemplates(): Promise {
+ try {
+ const res = await fetch('https://ungh.cc/repos/wxt-dev/wxt/files/main');
+ if (res.status >= 300)
+ throw Error(`Request failed with status ${res.status} ${res.statusText}`);
+
+ const data = (await res.json()) as {
+ meta: {
+ sha: string;
+ };
+ files: Array<{
+ path: string;
+ mode: string;
+ sha: string;
+ size: number;
+ }>;
+ };
+ return data.files
+ .map((item) => item.path.match(/templates\/(.+)\/package\.json/)?.[1])
+ .filter((name) => name != null)
+ .map((name) => ({ name: name!, path: `templates/${name}` }))
+ .sort((l, r) => {
+ const lWeight = TEMPLATE_SORT_WEIGHT[l.name] ?? Number.MAX_SAFE_INTEGER;
+ const rWeight = TEMPLATE_SORT_WEIGHT[r.name] ?? Number.MAX_SAFE_INTEGER;
+ const diff = lWeight - rWeight;
+ if (diff !== 0) return diff;
+ return l.name.localeCompare(r.name);
+ });
+ } catch (err) {
+ consola.error(err);
+ throw Error(`Failed to load templates`);
+ }
+}
+
+async function cloneProject({
+ directory,
+ template,
+ packageManager,
+}: {
+ directory: string;
+ template: Template;
+ packageManager: string;
+}) {
+ const { default: ora } = await import('ora');
+ const spinner = ora('Downloading template').start();
+ try {
+ // 1. Clone repo
+ await downloadTemplate(`gh:wxt-dev/wxt/${template.path}`, {
+ dir: directory,
+ force: true,
+ });
+
+ // 2. Move _gitignore -> .gitignore
+ await fs
+ .move(
+ path.join(directory, '_gitignore'),
+ path.join(directory, '.gitignore'),
+ )
+ .catch((err) =>
+ consola.warn('Failed to move _gitignore to .gitignore:', err),
+ );
+
+ spinner.succeed();
+ } catch (err) {
+ spinner.fail();
+ throw Error(`Failed to setup new project: ${JSON.stringify(err, null, 2)}`);
+ }
+}
+
+const TEMPLATE_COLORS: Record = {
+ vanilla: pc.blue,
+ vue: pc.green,
+ react: pc.cyan,
+ svelte: pc.red,
+ solid: pc.blue,
+};
+
+const TEMPLATE_SORT_WEIGHT: Record = {
+ vanilla: 0,
+ vue: 1,
+ react: 2,
+};
diff --git a/packages/wxt/src/core/package-managers/__tests__/bun.test.ts b/packages/wxt/src/core/package-managers/__tests__/bun.test.ts
new file mode 100644
index 0000000..4baa8cc
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/__tests__/bun.test.ts
@@ -0,0 +1,29 @@
+import { describe, expect, it } from 'vitest';
+import path from 'node:path';
+import { bun } from '../bun';
+
+const cwd = path.resolve(__dirname, 'fixtures/bun-project');
+
+describe.skipIf(() => process.platform === 'win32')(
+ 'Bun Package Management Utils',
+ () => {
+ describe('listDependencies', () => {
+ it('should list direct dependencies', async () => {
+ const actual = await bun.listDependencies({ cwd });
+ expect(actual).toEqual([
+ { name: 'flatten', version: '1.0.3' },
+ { name: 'mime-types', version: '2.1.35' },
+ ]);
+ });
+
+ it('should list all dependencies', async () => {
+ const actual = await bun.listDependencies({ cwd, all: true });
+ expect(actual).toEqual([
+ { name: 'flatten', version: '1.0.3' },
+ { name: 'mime-db', version: '1.52.0' },
+ { name: 'mime-types', version: '2.1.35' },
+ ]);
+ });
+ });
+ },
+);
diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/bun-project/bun.lockb b/packages/wxt/src/core/package-managers/__tests__/fixtures/bun-project/bun.lockb
new file mode 100755
index 0000000..8155349
Binary files /dev/null and b/packages/wxt/src/core/package-managers/__tests__/fixtures/bun-project/bun.lockb differ
diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/bun-project/package.json b/packages/wxt/src/core/package-managers/__tests__/fixtures/bun-project/package.json
new file mode 100644
index 0000000..27ce0fc
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/__tests__/fixtures/bun-project/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "bun-ls",
+ "dependencies": {
+ "mime-types": "2.1.35"
+ },
+ "devDependencies": {
+ "flatten": "1.0.3"
+ }
+}
diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/npm-project/package-lock.json b/packages/wxt/src/core/package-managers/__tests__/fixtures/npm-project/package-lock.json
new file mode 100644
index 0000000..3850389
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/__tests__/fixtures/npm-project/package-lock.json
@@ -0,0 +1,42 @@
+{
+ "name": "npm-ls",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "npm-ls",
+ "dependencies": {
+ "mime-types": "2.1.35"
+ },
+ "devDependencies": {
+ "flatten": "1.0.3"
+ }
+ },
+ "node_modules/flatten": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz",
+ "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==",
+ "deprecated": "flatten is deprecated in favor of utility frameworks such as lodash.",
+ "dev": true
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ }
+ }
+}
diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/npm-project/package.json b/packages/wxt/src/core/package-managers/__tests__/fixtures/npm-project/package.json
new file mode 100644
index 0000000..8d06b7d
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/__tests__/fixtures/npm-project/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "npm-ls",
+ "dependencies": {
+ "mime-types": "2.1.35"
+ },
+ "devDependencies": {
+ "flatten": "1.0.3"
+ }
+}
diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/pnpm-project/package.json b/packages/wxt/src/core/package-managers/__tests__/fixtures/pnpm-project/package.json
new file mode 100644
index 0000000..f671946
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/__tests__/fixtures/pnpm-project/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "pnpm-ls",
+ "dependencies": {
+ "mime-types": "2.1.35"
+ },
+ "devDependencies": {
+ "flatten": "1.0.3"
+ }
+}
diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/pnpm-project/pnpm-lock.yaml b/packages/wxt/src/core/package-managers/__tests__/fixtures/pnpm-project/pnpm-lock.yaml
new file mode 100644
index 0000000..0a0f7e1
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/__tests__/fixtures/pnpm-project/pnpm-lock.yaml
@@ -0,0 +1,34 @@
+lockfileVersion: '6.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+dependencies:
+ mime-types:
+ specifier: 2.1.35
+ version: 2.1.35
+
+devDependencies:
+ flatten:
+ specifier: 1.0.3
+ version: 1.0.3
+
+packages:
+
+ /flatten@1.0.3:
+ resolution: {integrity: sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==}
+ deprecated: flatten is deprecated in favor of utility frameworks such as lodash.
+ dev: true
+
+ /mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ mime-db: 1.52.0
+ dev: false
diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/yarn-project/package.json b/packages/wxt/src/core/package-managers/__tests__/fixtures/yarn-project/package.json
new file mode 100644
index 0000000..b75f341
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/__tests__/fixtures/yarn-project/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "yarn-ls",
+ "packageManager": "yarn@1.22.19",
+ "dependencies": {
+ "mime-types": "2.1.35"
+ },
+ "devDependencies": {
+ "flatten": "1.0.3"
+ }
+}
diff --git a/packages/wxt/src/core/package-managers/__tests__/npm.test.ts b/packages/wxt/src/core/package-managers/__tests__/npm.test.ts
new file mode 100644
index 0000000..668de8e
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/__tests__/npm.test.ts
@@ -0,0 +1,46 @@
+import { beforeAll, describe, expect, it } from 'vitest';
+import path from 'node:path';
+import { npm } from '../npm';
+import { execaCommand } from 'execa';
+import { exists } from 'fs-extra';
+
+const cwd = path.resolve(__dirname, 'fixtures/npm-project');
+
+describe('NPM Package Management Utils', () => {
+ beforeAll(async () => {
+ // NPM needs the modules installed for 'npm ls' to work
+ await execaCommand('npm i', { cwd });
+ }, 60e3);
+
+ describe('listDependencies', () => {
+ it('should list direct dependencies', async () => {
+ const actual = await npm.listDependencies({ cwd });
+ expect(actual).toEqual([
+ { name: 'flatten', version: '1.0.3' },
+ { name: 'mime-types', version: '2.1.35' },
+ ]);
+ });
+
+ it('should list all dependencies', async () => {
+ const actual = await npm.listDependencies({ cwd, all: true });
+ expect(actual).toEqual([
+ { name: 'flatten', version: '1.0.3' },
+ { name: 'mime-types', version: '2.1.35' },
+ { name: 'mime-db', version: '1.52.0' },
+ ]);
+ });
+ });
+
+ describe('downloadDependency', () => {
+ it('should download the dependency as a tarball', async () => {
+ const downloadDir = path.resolve(cwd, 'dist');
+ const id = 'mime-db@1.52.0';
+ const expected = path.resolve(downloadDir, 'mime-db-1.52.0.tgz');
+
+ const actual = await npm.downloadDependency(id, downloadDir);
+
+ expect(actual).toEqual(expected);
+ expect(await exists(actual)).toBe(true);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/package-managers/__tests__/pnpm.test.ts b/packages/wxt/src/core/package-managers/__tests__/pnpm.test.ts
new file mode 100644
index 0000000..84edcb4
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/__tests__/pnpm.test.ts
@@ -0,0 +1,34 @@
+import { beforeAll, describe, expect, it } from 'vitest';
+import path from 'node:path';
+import { pnpm } from '../pnpm';
+import { execaCommand } from 'execa';
+
+const cwd = path.resolve(__dirname, 'fixtures/pnpm-project');
+
+process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
+
+describe('PNPM Package Management Utils', () => {
+ beforeAll(async () => {
+ // PNPM needs the modules installed, or 'pnpm ls' will return a blank list.
+ await execaCommand('pnpm i --ignore-workspace', { cwd });
+ });
+
+ describe('listDependencies', () => {
+ it('should list direct dependencies', async () => {
+ const actual = await pnpm.listDependencies({ cwd });
+ expect(actual).toEqual([
+ { name: 'flatten', version: '1.0.3' },
+ { name: 'mime-types', version: '2.1.35' },
+ ]);
+ });
+
+ it('should list all dependencies', async () => {
+ const actual = await pnpm.listDependencies({ cwd, all: true });
+ expect(actual).toEqual([
+ { name: 'flatten', version: '1.0.3' },
+ { name: 'mime-types', version: '2.1.35' },
+ { name: 'mime-db', version: '1.52.0' },
+ ]);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/package-managers/__tests__/yarn.test.ts b/packages/wxt/src/core/package-managers/__tests__/yarn.test.ts
new file mode 100644
index 0000000..7195e04
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/__tests__/yarn.test.ts
@@ -0,0 +1,27 @@
+import { describe, expect, it } from 'vitest';
+import path from 'node:path';
+import { yarn } from '../yarn';
+
+const cwd = path.resolve(__dirname, 'fixtures/yarn-project');
+
+describe('Yarn Package Management Utils', () => {
+ describe('listDependencies', () => {
+ it('should list direct dependencies', async () => {
+ const actual = await yarn.listDependencies({ cwd });
+ expect(actual).toEqual([
+ { name: 'mime-db', version: '1.52.0' },
+ { name: 'flatten', version: '1.0.3' },
+ { name: 'mime-types', version: '2.1.35' },
+ ]);
+ });
+
+ it('should list all dependencies', async () => {
+ const actual = await yarn.listDependencies({ cwd, all: true });
+ expect(actual).toEqual([
+ { name: 'mime-db', version: '1.52.0' },
+ { name: 'flatten', version: '1.0.3' },
+ { name: 'mime-types', version: '2.1.35' },
+ ]);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/package-managers/bun.ts b/packages/wxt/src/core/package-managers/bun.ts
new file mode 100644
index 0000000..25b888b
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/bun.ts
@@ -0,0 +1,27 @@
+import { dedupeDependencies, npm } from './npm';
+import { WxtPackageManagerImpl } from './types';
+
+export const bun: WxtPackageManagerImpl = {
+ overridesKey: 'overrides', // But also supports "resolutions"
+ downloadDependency(...args) {
+ return npm.downloadDependency(...args);
+ },
+ async listDependencies(options) {
+ const args = ['pm', 'ls'];
+ if (options?.all) {
+ args.push('--all');
+ }
+ const { execa } = await import('execa');
+ const res = await execa('bun', args, { cwd: options?.cwd });
+ return dedupeDependencies(
+ res.stdout
+ .split('\n')
+ .slice(1) // Skip the first line, is not a dependency
+ .map((line) => line.trim())
+ .map((line) => /.* (@?\S+)@(\S+)$/.exec(line))
+ // @ts-expect-error: Filtering to known non-null matches
+ .filter((match) => !!match)
+ .map(([_, name, version]) => ({ name, version })),
+ );
+ },
+};
diff --git a/packages/wxt/src/core/package-managers/index.ts b/packages/wxt/src/core/package-managers/index.ts
new file mode 100644
index 0000000..ae2a25d
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/index.ts
@@ -0,0 +1,76 @@
+import {
+ detectPackageManager,
+ addDependency,
+ addDevDependency,
+ ensureDependencyInstalled,
+ installDependencies,
+ removeDependency,
+ PackageManager,
+ PackageManagerName,
+} from 'nypm';
+import { WxtPackageManager } from '~/types';
+import { bun } from './bun';
+import { WxtPackageManagerImpl } from './types';
+import { yarn } from './yarn';
+import { pnpm } from './pnpm';
+import { npm } from './npm';
+
+export async function createWxtPackageManager(
+ root: string,
+): Promise {
+ const pm = await detectPackageManager(root, {
+ includeParentDirs: true,
+ });
+
+ // Use requirePm to prevent throwing errors before the package manager utils are used.
+ const requirePm = (cb: (pm: PackageManager) => T) => {
+ if (pm == null) throw Error('Could not detect package manager');
+ return cb(pm);
+ };
+
+ return {
+ get name() {
+ return requirePm((pm) => pm.name);
+ },
+ get command() {
+ return requirePm((pm) => pm.command);
+ },
+ get version() {
+ return requirePm((pm) => pm.version);
+ },
+ get majorVersion() {
+ return requirePm((pm) => pm.majorVersion);
+ },
+ get lockFile() {
+ return requirePm((pm) => pm.lockFile);
+ },
+ get files() {
+ return requirePm((pm) => pm.files);
+ },
+ addDependency,
+ addDevDependency,
+ ensureDependencyInstalled,
+ installDependencies,
+ removeDependency,
+ get overridesKey() {
+ return requirePm((pm) => packageManagers[pm.name].overridesKey);
+ },
+ downloadDependency(...args) {
+ return requirePm((pm) =>
+ packageManagers[pm.name].downloadDependency(...args),
+ );
+ },
+ listDependencies(...args) {
+ return requirePm((pm) =>
+ packageManagers[pm.name].listDependencies(...args),
+ );
+ },
+ };
+}
+
+const packageManagers: Record = {
+ npm,
+ pnpm,
+ bun,
+ yarn,
+};
diff --git a/packages/wxt/src/core/package-managers/npm.ts b/packages/wxt/src/core/package-managers/npm.ts
new file mode 100644
index 0000000..05c49dd
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/npm.ts
@@ -0,0 +1,85 @@
+import { Dependency } from '~/types';
+import { WxtPackageManagerImpl } from './types';
+import path from 'node:path';
+import { ensureDir } from 'fs-extra';
+
+export const npm: WxtPackageManagerImpl = {
+ overridesKey: 'overrides',
+ async downloadDependency(id, downloadDir) {
+ await ensureDir(downloadDir);
+ const { execa } = await import('execa');
+ const res = await execa('npm', ['pack', id, '--json'], {
+ cwd: downloadDir,
+ });
+ const packed: PackedDependency[] = JSON.parse(res.stdout);
+ return path.resolve(downloadDir, packed[0].filename);
+ },
+ async listDependencies(options) {
+ const args = ['ls', '--json'];
+ if (options?.all) {
+ args.push('--depth', 'Infinity');
+ }
+ const { execa } = await import('execa');
+ const res = await execa('npm', args, { cwd: options?.cwd });
+ const project: NpmListProject = JSON.parse(res.stdout);
+
+ return flattenNpmListOutput([project]);
+ },
+};
+
+export function flattenNpmListOutput(projects: NpmListProject[]): Dependency[] {
+ const queue: Record[] = projects.flatMap(
+ (project) => {
+ const acc: Record[] = [];
+ if (project.dependencies) acc.push(project.dependencies);
+ if (project.devDependencies) acc.push(project.devDependencies);
+ return acc;
+ },
+ );
+ const dependencies: Dependency[] = [];
+ while (queue.length > 0) {
+ Object.entries(queue.pop()!).forEach(([name, meta]) => {
+ dependencies.push({
+ name,
+ version: meta.version,
+ });
+ if (meta.dependencies) queue.push(meta.dependencies);
+ if (meta.devDependencies) queue.push(meta.devDependencies);
+ });
+ }
+ return dedupeDependencies(dependencies);
+}
+
+export function dedupeDependencies(dependencies: Dependency[]): Dependency[] {
+ const hashes = new Set();
+ return dependencies.filter((dep) => {
+ const hash = `${dep.name}@${dep.version}`;
+ if (hashes.has(hash)) {
+ return false;
+ } else {
+ hashes.add(hash);
+ return true;
+ }
+ });
+}
+
+export interface NpmListProject {
+ name: string;
+ dependencies?: Record;
+ devDependencies?: Record;
+}
+
+export interface NpmListDependency {
+ version: string;
+ resolved?: string;
+ overridden?: boolean;
+ dependencies?: Record;
+ devDependencies?: Record;
+}
+
+interface PackedDependency {
+ id: string;
+ name: string;
+ version: string;
+ filename: string;
+}
diff --git a/packages/wxt/src/core/package-managers/pnpm.ts b/packages/wxt/src/core/package-managers/pnpm.ts
new file mode 100644
index 0000000..891fe18
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/pnpm.ts
@@ -0,0 +1,28 @@
+import { NpmListProject, flattenNpmListOutput, npm } from './npm';
+import { WxtPackageManagerImpl } from './types';
+
+export const pnpm: WxtPackageManagerImpl = {
+ overridesKey: 'resolutions', // "pnpm.overrides" has a higher priority, but I don't want to deal with nesting
+ downloadDependency(...args) {
+ return npm.downloadDependency(...args);
+ },
+ async listDependencies(options) {
+ const args = ['ls', '-r', '--json'];
+ if (options?.all) {
+ args.push('--depth', 'Infinity');
+ }
+ // Helper for testing - since WXT uses pnpm workspaces, folders inside it don't behave like
+ // standalone projects unless you pass the --ignore-workspace flag.
+ if (
+ typeof process !== 'undefined' &&
+ process.env.WXT_PNPM_IGNORE_WORKSPACE === 'true'
+ ) {
+ args.push('--ignore-workspace');
+ }
+ const { execa } = await import('execa');
+ const res = await execa('pnpm', args, { cwd: options?.cwd });
+ const projects: NpmListProject[] = JSON.parse(res.stdout);
+
+ return flattenNpmListOutput(projects);
+ },
+};
diff --git a/packages/wxt/src/core/package-managers/types.ts b/packages/wxt/src/core/package-managers/types.ts
new file mode 100644
index 0000000..46ddbf9
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/types.ts
@@ -0,0 +1,6 @@
+import { WxtPackageManager } from '~/types';
+
+export type WxtPackageManagerImpl = Pick<
+ WxtPackageManager,
+ 'downloadDependency' | 'listDependencies' | 'overridesKey'
+>;
diff --git a/packages/wxt/src/core/package-managers/yarn.ts b/packages/wxt/src/core/package-managers/yarn.ts
new file mode 100644
index 0000000..ad97e40
--- /dev/null
+++ b/packages/wxt/src/core/package-managers/yarn.ts
@@ -0,0 +1,54 @@
+import { Dependency } from '~/types';
+import { WxtPackageManagerImpl } from './types';
+import { dedupeDependencies, npm } from './npm';
+
+export const yarn: WxtPackageManagerImpl = {
+ overridesKey: 'resolutions',
+ downloadDependency(...args) {
+ return npm.downloadDependency(...args);
+ },
+ async listDependencies(options) {
+ const args = ['list', '--json'];
+ if (options?.all) {
+ args.push('--depth', 'Infinity');
+ }
+ const { execa } = await import('execa');
+ const res = await execa('yarn', args, { cwd: options?.cwd });
+ const tree = res.stdout
+ .split('\n')
+ .map((line) => JSON.parse(line))
+ .find((line) => line.type === 'tree')?.data as JsonLineTree | undefined;
+ if (tree == null) throw Error("'yarn list --json' did not output a tree");
+
+ const queue = [...tree.trees];
+ const dependencies: Dependency[] = [];
+
+ while (queue.length > 0) {
+ const { name: treeName, children } = queue.pop()!;
+ const match = /(@?\S+)@(\S+)$/.exec(treeName);
+ if (match) {
+ const [_, name, version] = match;
+ dependencies.push({ name, version });
+ }
+ if (children != null) {
+ queue.push(...children);
+ }
+ }
+
+ return dedupeDependencies(dependencies);
+ },
+};
+
+type JsonLine =
+ | { type: unknown; data: unknown }
+ | { type: 'tree'; data: JsonLineTree };
+
+interface JsonLineTree {
+ type: 'list';
+ trees: Tree[];
+}
+
+interface Tree {
+ name: string;
+ children?: Tree[];
+}
diff --git a/packages/wxt/src/core/prepare.ts b/packages/wxt/src/core/prepare.ts
new file mode 100644
index 0000000..bd66286
--- /dev/null
+++ b/packages/wxt/src/core/prepare.ts
@@ -0,0 +1,11 @@
+import { InlineConfig } from '~/types';
+import { findEntrypoints, generateTypesDir } from '~/core/utils/building';
+import { registerWxt, wxt } from './wxt';
+
+export async function prepare(config: InlineConfig) {
+ await registerWxt('build', config);
+ wxt.logger.info('Generating types...');
+
+ const entrypoints = await findEntrypoints();
+ await generateTypesDir(entrypoints);
+}
diff --git a/packages/wxt/src/core/runners/__tests__/index.test.ts b/packages/wxt/src/core/runners/__tests__/index.test.ts
new file mode 100644
index 0000000..8218ad9
--- /dev/null
+++ b/packages/wxt/src/core/runners/__tests__/index.test.ts
@@ -0,0 +1,87 @@
+import { describe, expect, it, vi } from 'vitest';
+import { createExtensionRunner } from '..';
+import { setFakeWxt } from '~/core/utils/testing/fake-objects';
+import { mock } from 'vitest-mock-extended';
+import { createSafariRunner } from '../safari';
+import { createWslRunner } from '../wsl';
+import { createManualRunner } from '../manual';
+import { isWsl } from '../../utils/wsl';
+import { createWebExtRunner } from '../web-ext';
+import { ExtensionRunner } from '~/types';
+
+vi.mock('../../utils/wsl');
+const isWslMock = vi.mocked(isWsl);
+
+vi.mock('../safari');
+const createSafariRunnerMock = vi.mocked(createSafariRunner);
+
+vi.mock('../wsl');
+const createWslRunnerMock = vi.mocked(createWslRunner);
+
+vi.mock('../manual');
+const createManualRunnerMock = vi.mocked(createManualRunner);
+
+vi.mock('../web-ext');
+const createWebExtRunnerMock = vi.mocked(createWebExtRunner);
+
+describe('createExtensionRunner', () => {
+ it('should return a Safari runner when browser is "safari"', async () => {
+ setFakeWxt({
+ config: {
+ browser: 'safari',
+ },
+ });
+ const safariRunner = mock();
+ createSafariRunnerMock.mockReturnValue(safariRunner);
+
+ await expect(createExtensionRunner()).resolves.toBe(safariRunner);
+ });
+
+ it('should return a WSL runner when `is-wsl` is true', async () => {
+ isWslMock.mockResolvedValueOnce(true);
+ setFakeWxt({
+ config: {
+ browser: 'chrome',
+ },
+ });
+ const wslRunner = mock();
+ createWslRunnerMock.mockReturnValue(wslRunner);
+
+ await expect(createExtensionRunner()).resolves.toBe(wslRunner);
+ });
+
+ it('should return a manual runner when `runner.disabled` is true', async () => {
+ isWslMock.mockResolvedValueOnce(false);
+ setFakeWxt({
+ config: {
+ browser: 'chrome',
+ runnerConfig: {
+ config: {
+ disabled: true,
+ },
+ },
+ },
+ });
+ const manualRunner = mock();
+ createManualRunnerMock.mockReturnValue(manualRunner);
+
+ await expect(createExtensionRunner()).resolves.toBe(manualRunner);
+ });
+
+ it('should return a web-ext runner otherwise', async () => {
+ setFakeWxt({
+ config: {
+ browser: 'chrome',
+ runnerConfig: {
+ config: {
+ disabled: undefined,
+ },
+ },
+ },
+ });
+ const manualRunner = mock();
+ createWebExtRunnerMock.mockReturnValue(manualRunner);
+
+ await expect(createExtensionRunner()).resolves.toBe(manualRunner);
+ });
+});
diff --git a/packages/wxt/src/core/runners/index.ts b/packages/wxt/src/core/runners/index.ts
new file mode 100644
index 0000000..6eafa20
--- /dev/null
+++ b/packages/wxt/src/core/runners/index.ts
@@ -0,0 +1,16 @@
+import { ExtensionRunner } from '~/types';
+import { createWslRunner } from './wsl';
+import { createWebExtRunner } from './web-ext';
+import { createSafariRunner } from './safari';
+import { createManualRunner } from './manual';
+import { isWsl } from '~/core/utils/wsl';
+import { wxt } from '../wxt';
+
+export async function createExtensionRunner(): Promise {
+ if (wxt.config.browser === 'safari') return createSafariRunner();
+
+ if (await isWsl()) return createWslRunner();
+ if (wxt.config.runnerConfig.config?.disabled) return createManualRunner();
+
+ return createWebExtRunner();
+}
diff --git a/packages/wxt/src/core/runners/manual.ts b/packages/wxt/src/core/runners/manual.ts
new file mode 100644
index 0000000..30ce94c
--- /dev/null
+++ b/packages/wxt/src/core/runners/manual.ts
@@ -0,0 +1,22 @@
+import { ExtensionRunner } from '~/types';
+import { relative } from 'node:path';
+import { wxt } from '../wxt';
+
+/**
+ * The manual runner tells the user to load the unpacked extension manually.
+ */
+export function createManualRunner(): ExtensionRunner {
+ return {
+ async openBrowser() {
+ wxt.logger.info(
+ `Load "${relative(
+ process.cwd(),
+ wxt.config.outDir,
+ )}" as an unpacked extension manually`,
+ );
+ },
+ async closeBrowser() {
+ // noop
+ },
+ };
+}
diff --git a/packages/wxt/src/core/runners/safari.ts b/packages/wxt/src/core/runners/safari.ts
new file mode 100644
index 0000000..e334967
--- /dev/null
+++ b/packages/wxt/src/core/runners/safari.ts
@@ -0,0 +1,22 @@
+import { ExtensionRunner } from '~/types';
+import { relative } from 'node:path';
+import { wxt } from '../wxt';
+
+/**
+ * The Safari runner just logs a warning message because `web-ext` doesn't work with Safari.
+ */
+export function createSafariRunner(): ExtensionRunner {
+ return {
+ async openBrowser() {
+ console.warn(
+ `Cannot Safari using web-ext. Load "${relative(
+ process.cwd(),
+ wxt.config.outDir,
+ )}" as an unpacked extension manually`,
+ );
+ },
+ async closeBrowser() {
+ // noop
+ },
+ };
+}
diff --git a/packages/wxt/src/core/runners/web-ext.ts b/packages/wxt/src/core/runners/web-ext.ts
new file mode 100644
index 0000000..0076b48
--- /dev/null
+++ b/packages/wxt/src/core/runners/web-ext.ts
@@ -0,0 +1,99 @@
+import type { WebExtRunInstance } from 'web-ext-run';
+import { ExtensionRunner } from '~/types';
+import { formatDuration } from '../utils/time';
+import defu from 'defu';
+import { wxt } from '../wxt';
+
+/**
+ * Create an `ExtensionRunner` backed by `web-ext`.
+ */
+export function createWebExtRunner(): ExtensionRunner {
+ let runner: WebExtRunInstance | undefined;
+
+ return {
+ async openBrowser() {
+ const startTime = Date.now();
+
+ if (
+ wxt.config.browser === 'firefox' &&
+ wxt.config.manifestVersion === 3
+ ) {
+ throw Error(
+ 'Dev mode does not support Firefox MV3. For alternatives, see https://github.com/wxt-dev/wxt/issues/230#issuecomment-1806881653',
+ );
+ }
+
+ // Use WXT's logger instead of web-ext's built-in one.
+ const webExtLogger = await import('web-ext-run/util/logger');
+ webExtLogger.consoleStream.write = ({ level, msg, name }) => {
+ if (level >= ERROR_LOG_LEVEL) console.error(name, msg);
+ if (level >= WARN_LOG_LEVEL) console.warn(msg);
+ };
+
+ const wxtUserConfig = wxt.config.runnerConfig.config;
+ const userConfig = {
+ console: wxtUserConfig?.openConsole,
+ devtools: wxtUserConfig?.openDevtools,
+ startUrl: wxtUserConfig?.startUrls,
+ keepProfileChanges: wxtUserConfig?.keepProfileChanges,
+ ...(wxt.config.browser === 'firefox'
+ ? {
+ firefox: wxtUserConfig?.binaries?.firefox,
+ firefoxProfile: wxtUserConfig?.firefoxProfile,
+ prefs: wxtUserConfig?.firefoxPrefs,
+ args: wxtUserConfig?.firefoxArgs,
+ }
+ : {
+ chromiumBinary: wxtUserConfig?.binaries?.[wxt.config.browser],
+ chromiumProfile: wxtUserConfig?.chromiumProfile,
+ chromiumPref: defu(
+ wxtUserConfig?.chromiumPref,
+ DEFAULT_CHROMIUM_PREFS,
+ ),
+ args: wxtUserConfig?.chromiumArgs,
+ }),
+ };
+
+ const finalConfig = {
+ ...userConfig,
+ target:
+ wxt.config.browser === 'firefox' ? 'firefox-desktop' : 'chromium',
+ sourceDir: wxt.config.outDir,
+ // WXT handles reloads, so disable auto-reload behaviors in web-ext
+ noReload: true,
+ noInput: true,
+ };
+ const options = {
+ // Don't call `process.exit(0)` after starting web-ext
+ shouldExitProgram: false,
+ };
+ wxt.logger.debug('web-ext config:', finalConfig);
+ wxt.logger.debug('web-ext options:', options);
+
+ const webExt = await import('web-ext-run');
+ runner = await webExt.default.cmd.run(finalConfig, options);
+
+ const duration = Date.now() - startTime;
+ wxt.logger.success(`Opened browser in ${formatDuration(duration)}`);
+ },
+
+ async closeBrowser() {
+ return await runner?.exit();
+ },
+ };
+}
+
+// https://github.com/mozilla/web-ext/blob/e37e60a2738478f512f1255c537133321f301771/src/util/logger.js#L12
+const WARN_LOG_LEVEL = 40;
+const ERROR_LOG_LEVEL = 50;
+
+const DEFAULT_CHROMIUM_PREFS = {
+ devtools: {
+ synced_preferences_sync_disabled: {
+ // Remove content scripts from sourcemap debugger ignore list so stack traces
+ // and log locations show up properly, see:
+ // https://github.com/wxt-dev/wxt/issues/236#issuecomment-1915364520
+ skipContentScripts: false,
+ },
+ },
+};
diff --git a/packages/wxt/src/core/runners/wsl.ts b/packages/wxt/src/core/runners/wsl.ts
new file mode 100644
index 0000000..fe1a5f4
--- /dev/null
+++ b/packages/wxt/src/core/runners/wsl.ts
@@ -0,0 +1,22 @@
+import { ExtensionRunner } from '~/types';
+import { relative } from 'node:path';
+import { wxt } from '../wxt';
+
+/**
+ * The WSL runner just logs a warning message because `web-ext` doesn't work in WSL.
+ */
+export function createWslRunner(): ExtensionRunner {
+ return {
+ async openBrowser() {
+ console.warn(
+ `Cannot open browser when using WSL. Load "${relative(
+ process.cwd(),
+ wxt.config.outDir,
+ )}" as an unpacked extension manually`,
+ );
+ },
+ async closeBrowser() {
+ // noop
+ },
+ };
+}
diff --git a/packages/wxt/src/core/utils/__tests__/arrays.test.ts b/packages/wxt/src/core/utils/__tests__/arrays.test.ts
new file mode 100644
index 0000000..8d353a1
--- /dev/null
+++ b/packages/wxt/src/core/utils/__tests__/arrays.test.ts
@@ -0,0 +1,34 @@
+import { describe, it, expect } from 'vitest';
+import { every, some } from '~/core/utils/arrays';
+
+describe('Array Utils', () => {
+ describe('every', () => {
+ it('should return true when the array is empty', () => {
+ expect(every([], () => false)).toBe(true);
+ });
+
+ it("should return true when all item predicate's return true", () => {
+ expect(every([1, 1, 1], (item) => item === 1)).toBe(true);
+ });
+
+ it("should return false when a single item predicate's return false", () => {
+ expect(every([1, 2, 1], (item) => item === 1)).toBe(false);
+ });
+ });
+
+ describe('some', () => {
+ it('should return true if one value returns true', () => {
+ const array = [1, 2, 3];
+ const predicate = (item: number) => item === 2;
+
+ expect(some(array, predicate)).toBe(true);
+ });
+
+ it('should return false if no values match', () => {
+ const array = [1, 2, 3];
+ const predicate = (item: number) => item === 4;
+
+ expect(some(array, predicate)).toBe(false);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/utils/__tests__/content-scripts.test.ts b/packages/wxt/src/core/utils/__tests__/content-scripts.test.ts
new file mode 100644
index 0000000..bf727ad
--- /dev/null
+++ b/packages/wxt/src/core/utils/__tests__/content-scripts.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, it, beforeEach } from 'vitest';
+import { hashContentScriptOptions } from '~/core/utils/content-scripts';
+import { setFakeWxt } from '~/core/utils/testing/fake-objects';
+
+describe('Content Script Utils', () => {
+ beforeEach(() => {
+ setFakeWxt();
+ });
+
+ describe('hashContentScriptOptions', () => {
+ it('should return a string containing all the options with defaults applied', () => {
+ const hash = hashContentScriptOptions({ matches: [] });
+
+ expect(hash).toMatchInlineSnapshot(
+ `"[["all_frames",false],["exclude_globs",[]],["exclude_matches",[]],["include_globs",[]],["match_about_blank",false],["match_origin_as_fallback",false],["matches",[]],["run_at","document_idle"],["world","ISOLATED"]]"`,
+ );
+ });
+
+ it('should be consistent regardless of the object ordering and default values', () => {
+ const hash1 = hashContentScriptOptions({
+ allFrames: true,
+ matches: ['*://google.com/*', '*://duckduckgo.com/*'],
+ matchAboutBlank: false,
+ });
+ const hash2 = hashContentScriptOptions({
+ matches: ['*://duckduckgo.com/*', '*://google.com/*'],
+ allFrames: true,
+ });
+
+ expect(hash1).toBe(hash2);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/utils/__tests__/content-security-policy.test.ts b/packages/wxt/src/core/utils/__tests__/content-security-policy.test.ts
new file mode 100644
index 0000000..1af9dd7
--- /dev/null
+++ b/packages/wxt/src/core/utils/__tests__/content-security-policy.test.ts
@@ -0,0 +1,40 @@
+import { describe, expect, it } from 'vitest';
+import { ContentSecurityPolicy } from '~/core/utils/content-security-policy';
+
+describe('Content Security Policy Builder', () => {
+ it('should add values to new directives correctly', () => {
+ const csp = new ContentSecurityPolicy();
+
+ csp.add('default-src', "'self'");
+
+ expect(csp.toString()).toEqual("default-src 'self';");
+ });
+
+ it('should add to existing values', () => {
+ const csp = new ContentSecurityPolicy("default-src 'self';");
+
+ csp.add('default-src', 'http://localhost:*');
+
+ expect(csp.toString()).toEqual("default-src 'self' http://localhost:*;");
+ });
+
+ it('should not add duplicates', () => {
+ const csp = new ContentSecurityPolicy("default-src 'self';");
+
+ csp.add('default-src', "'self'");
+
+ expect(csp.toString()).toEqual("default-src 'self';");
+ });
+
+ it('should sort the directives in the correct order', () => {
+ const csp = new ContentSecurityPolicy();
+
+ csp.add('object-src', "'self'");
+ csp.add('script-src', "'self'");
+ csp.add('default-src', "'self'");
+
+ expect(csp.toString()).toEqual(
+ "default-src 'self'; script-src 'self'; object-src 'self';",
+ );
+ });
+});
diff --git a/packages/wxt/src/core/utils/__tests__/entrypoints.test.ts b/packages/wxt/src/core/utils/__tests__/entrypoints.test.ts
new file mode 100644
index 0000000..7a134d6
--- /dev/null
+++ b/packages/wxt/src/core/utils/__tests__/entrypoints.test.ts
@@ -0,0 +1,78 @@
+import { describe, it, expect } from 'vitest';
+import {
+ getEntrypointName,
+ getEntrypointOutputFile,
+ resolvePerBrowserOption,
+} from '~/core/utils/entrypoints';
+import { Entrypoint } from '~/types';
+import { resolve } from 'path';
+
+describe('Entrypoint Utils', () => {
+ describe('getEntrypointName', () => {
+ const entrypointsDir = '/entrypoints';
+
+ it.each<[string, string]>([
+ [resolve(entrypointsDir, 'popup.html'), 'popup'],
+ [resolve(entrypointsDir, 'options/index.html'), 'options'],
+ [resolve(entrypointsDir, 'example.sandbox/index.html'), 'example'],
+ [resolve(entrypointsDir, 'some.content/index.ts'), 'some'],
+ [resolve(entrypointsDir, 'overlay.content.ts'), 'overlay'],
+ ])('should convert %s to %s', (inputPath, expected) => {
+ const actual = getEntrypointName(entrypointsDir, inputPath);
+ expect(actual).toBe(expected);
+ });
+ });
+
+ describe('getEntrypointOutputFile', () => {
+ const outDir = '/.output';
+ it.each<{ expected: string; name: string; ext: string; outputDir: string }>(
+ [
+ {
+ name: 'popup',
+ ext: '.html',
+ outputDir: outDir,
+ expected: resolve(outDir, 'popup.html'),
+ },
+ {
+ name: 'overlay',
+ ext: '.ts',
+ outputDir: resolve(outDir, 'content-scripts'),
+ expected: resolve(outDir, 'content-scripts', 'overlay.ts'),
+ },
+ ],
+ )('should return %s', ({ name, ext, expected, outputDir }) => {
+ const entrypoint: Entrypoint = {
+ type: 'unlisted-page',
+ inputPath: '...',
+ name,
+ outputDir,
+ options: {},
+ skipped: false,
+ };
+
+ const actual = getEntrypointOutputFile(entrypoint, ext);
+ expect(actual).toBe(expected);
+ });
+ });
+
+ describe('resolvePerBrowserOption', () => {
+ it('should return the value directly', () => {
+ expect(resolvePerBrowserOption('some-string', '')).toEqual('some-string');
+ expect(resolvePerBrowserOption(false, '')).toEqual(false);
+ expect(resolvePerBrowserOption([1], '')).toEqual([1]);
+ expect(resolvePerBrowserOption(['string'], '')).toEqual(['string']);
+ });
+
+ it('should return the value for the specific browser', () => {
+ expect(resolvePerBrowserOption({ a: 'one', b: 'two' }, 'a')).toEqual(
+ 'one',
+ );
+ expect(resolvePerBrowserOption({ c: ['one'], d: ['two'] }, 'c')).toEqual([
+ 'one',
+ ]);
+ expect(resolvePerBrowserOption({ c: false, d: true }, 'e')).toEqual(
+ undefined,
+ );
+ });
+ });
+});
diff --git a/packages/wxt/src/core/utils/__tests__/manifest.test.ts b/packages/wxt/src/core/utils/__tests__/manifest.test.ts
new file mode 100644
index 0000000..75aa9f0
--- /dev/null
+++ b/packages/wxt/src/core/utils/__tests__/manifest.test.ts
@@ -0,0 +1,1440 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+import { generateManifest, stripPathFromMatchPattern } from '../manifest';
+import {
+ fakeArray,
+ fakeBackgroundEntrypoint,
+ fakeBuildOutput,
+ fakeContentScriptEntrypoint,
+ fakeEntrypoint,
+ fakeManifestCommand,
+ fakeOptionsEntrypoint,
+ fakePopupEntrypoint,
+ fakeSidepanelEntrypoint,
+ fakeWxtDevServer,
+ setFakeWxt,
+} from '../testing/fake-objects';
+import { Manifest } from 'webextension-polyfill';
+import { BuildOutput, ContentScriptEntrypoint, Entrypoint, OutputAsset } from '~/types';
+import { wxt } from '../../wxt';
+
+const outDir = '/output';
+const contentScriptOutDir = '/output/content-scripts';
+
+describe('Manifest Utils', () => {
+ beforeEach(() => {
+ setFakeWxt();
+ });
+
+ describe('generateManifest', () => {
+ describe('popup', () => {
+ type ActionType = 'browser_action' | 'page_action';
+ const popupEntrypoint = (type?: ActionType) =>
+ fakePopupEntrypoint({
+ options: {
+ // @ts-expect-error: Force this to be undefined instead of inheriting the random value
+ mv2Key: type ?? null,
+ defaultIcon: {
+ '16': '/icon/16.png',
+ },
+ defaultTitle: 'Default Iitle',
+ },
+ outputDir: outDir,
+ });
+
+ it('should include an action for mv3', async () => {
+ const popup = popupEntrypoint();
+ const buildOutput = fakeBuildOutput();
+
+ setFakeWxt({
+ config: {
+ manifestVersion: 3,
+ outDir,
+ },
+ });
+ const expected: Partial = {
+ action: {
+ default_icon: popup.options.defaultIcon,
+ default_title: popup.options.defaultTitle,
+ default_popup: 'popup.html',
+ },
+ };
+
+ const { manifest: actual } = await generateManifest([popup], buildOutput);
+
+ expect(actual).toMatchObject(expected);
+ });
+
+ it.each<{
+ inputType: ActionType | undefined;
+ expectedType: ActionType;
+ }>([
+ { inputType: undefined, expectedType: 'browser_action' },
+ { inputType: 'browser_action', expectedType: 'browser_action' },
+ { inputType: 'page_action', expectedType: 'page_action' },
+ ])('should use the correct action for mv2: %j', async ({ inputType, expectedType }) => {
+ const popup = popupEntrypoint(inputType);
+ const buildOutput = fakeBuildOutput();
+ setFakeWxt({
+ config: {
+ manifestVersion: 2,
+ outDir,
+ },
+ });
+ const expected = {
+ default_icon: popup.options.defaultIcon,
+ default_title: popup.options.defaultTitle,
+ default_popup: 'popup.html',
+ };
+
+ const { manifest: actual } = await generateManifest([popup], buildOutput);
+
+ expect(actual[expectedType]).toEqual(expected);
+ });
+ });
+
+ describe('action without popup', () => {
+ it('should respect the action field in the manifest without a popup', async () => {
+ const buildOutput = fakeBuildOutput();
+ setFakeWxt({
+ config: {
+ outDir,
+ manifestVersion: 3,
+ manifest: {
+ action: {
+ default_icon: 'icon-16.png',
+ default_title: 'Example title',
+ },
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest([], buildOutput);
+
+ expect(actual.action).toEqual(wxt.config.manifest.action);
+ expect(actual.browser_action).toBeUndefined();
+ expect(actual.page_action).toBeUndefined();
+ });
+
+ it('should generate `browser_action` for MV2 when only `action` is defined', async () => {
+ const buildOutput = fakeBuildOutput();
+ setFakeWxt({
+ config: {
+ outDir,
+ manifestVersion: 2,
+ manifest: {
+ action: {
+ default_title: 'Action',
+ },
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest([], buildOutput);
+
+ expect(actual.action).toBeUndefined();
+ expect(actual.browser_action).toEqual(wxt.config.manifest.action);
+ expect(actual.page_action).toBeUndefined();
+ });
+
+ it('should keep the `page_action` for MV2 when both `action` and `page_action` are defined', async () => {
+ const buildOutput = fakeBuildOutput();
+ setFakeWxt({
+ config: {
+ outDir,
+ manifestVersion: 2,
+ manifest: {
+ action: {
+ default_title: 'Action',
+ },
+ page_action: {
+ default_title: 'Page Action',
+ },
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest([], buildOutput);
+
+ expect(actual.action).toBeUndefined();
+ expect(actual.browser_action).toBeUndefined();
+ expect(actual.page_action).toEqual(wxt.config.manifest.page_action);
+ });
+
+ it('should keep the custom `browser_action` for MV2 when both `action` and `browser_action` are defined', async () => {
+ const buildOutput = fakeBuildOutput();
+ setFakeWxt({
+ config: {
+ outDir,
+ manifestVersion: 2,
+ manifest: {
+ action: {
+ default_title: 'Action',
+ },
+ browser_action: {
+ default_title: 'Browser Action',
+ },
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest([], buildOutput);
+
+ expect(actual.action).toBeUndefined();
+ expect(actual.browser_action).toEqual(wxt.config.manifest.browser_action);
+ expect(actual.page_action).toBeUndefined();
+ });
+ });
+
+ describe('options', () => {
+ const options = fakeOptionsEntrypoint({
+ outputDir: outDir,
+ options: {
+ openInTab: false,
+ chromeStyle: true,
+ browserStyle: true,
+ },
+ });
+
+ it('should include a options_ui and chrome_style for chrome', async () => {
+ setFakeWxt({
+ config: {
+ manifestVersion: 3,
+ outDir,
+ browser: 'chrome',
+ },
+ });
+ const buildOutput = fakeBuildOutput();
+ const expected = {
+ open_in_tab: false,
+ chrome_style: true,
+ page: 'options.html',
+ };
+
+ const { manifest: actual } = await generateManifest([options], buildOutput);
+
+ expect(actual.options_ui).toEqual(expected);
+ });
+
+ it('should include a options_ui and browser_style for firefox', async () => {
+ setFakeWxt({
+ config: {
+ manifestVersion: 3,
+ browser: 'firefox',
+ outDir,
+ },
+ });
+ const buildOutput = fakeBuildOutput();
+ const expected = {
+ open_in_tab: false,
+ browser_style: true,
+ page: 'options.html',
+ };
+
+ const { manifest: actual } = await generateManifest([options], buildOutput);
+
+ expect(actual.options_ui).toEqual(expected);
+ });
+ });
+
+ describe('background', () => {
+ const background = fakeBackgroundEntrypoint({
+ outputDir: outDir,
+ options: {
+ persistent: true,
+ type: 'module',
+ },
+ });
+
+ describe('MV3', () => {
+ it.each(['chrome', 'safari'])('should include a service worker and type for %s', async (browser) => {
+ setFakeWxt({
+ config: {
+ outDir,
+ manifestVersion: 3,
+ browser,
+ },
+ });
+ const buildOutput = fakeBuildOutput();
+ const expected = {
+ type: 'module',
+ service_worker: 'background.js',
+ };
+
+ const { manifest: actual } = await generateManifest([background], buildOutput);
+
+ expect(actual.background).toEqual(expected);
+ });
+
+ it('should include a background script and type for firefox', async () => {
+ setFakeWxt({
+ config: {
+ outDir,
+ manifestVersion: 3,
+ browser: 'firefox',
+ },
+ });
+ const buildOutput = fakeBuildOutput();
+ const expected = {
+ type: 'module',
+ scripts: ['background.js'],
+ };
+
+ const { manifest: actual } = await generateManifest([background], buildOutput);
+
+ expect(actual.background).toEqual(expected);
+ });
+ });
+
+ describe('MV2', () => {
+ it.each(['chrome', 'safari'])('should include scripts and persistent for %s', async (browser) => {
+ setFakeWxt({
+ config: {
+ outDir,
+ manifestVersion: 2,
+ browser,
+ },
+ });
+ const buildOutput = fakeBuildOutput();
+ const expected = {
+ persistent: true,
+ scripts: ['background.js'],
+ };
+
+ const { manifest: actual } = await generateManifest([background], buildOutput);
+
+ expect(actual.background).toEqual(expected);
+ });
+
+ it('should include a background script and persistent for firefox mv2', async () => {
+ setFakeWxt({
+ config: {
+ outDir,
+ manifestVersion: 2,
+ browser: 'firefox',
+ },
+ });
+ const buildOutput = fakeBuildOutput();
+ const expected = {
+ persistent: true,
+ scripts: ['background.js'],
+ };
+
+ const { manifest: actual } = await generateManifest([background], buildOutput);
+
+ expect(actual.background).toEqual(expected);
+ });
+ });
+ });
+
+ describe('icons', () => {
+ it('should auto-discover icons with the correct name', async () => {
+ const entrypoints = fakeArray(fakeEntrypoint);
+ const buildOutput = fakeBuildOutput({
+ publicAssets: [
+ { type: 'asset', fileName: 'icon-16.png' },
+ { type: 'asset', fileName: 'icon/32.png' },
+ { type: 'asset', fileName: 'icon@48w.png' },
+ { type: 'asset', fileName: 'icon-64x64.png' },
+ { type: 'asset', fileName: 'icon@96.png' },
+ { type: 'asset', fileName: 'icons/128x128.png' },
+ ],
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.icons).toEqual({
+ 16: 'icon-16.png',
+ 32: 'icon/32.png',
+ 48: 'icon@48w.png',
+ 64: 'icon-64x64.png',
+ 96: 'icon@96.png',
+ 128: 'icons/128x128.png',
+ });
+ });
+
+ it('should return undefined when no icons are found', async () => {
+ const entrypoints = fakeArray(fakeEntrypoint);
+ const buildOutput = fakeBuildOutput({
+ publicAssets: [
+ { type: 'asset', fileName: 'logo.png' },
+ { type: 'asset', fileName: 'icon-16.jpeg' },
+ ],
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.icons).toBeUndefined();
+ });
+
+ it('should allow icons to be overwritten from the wxt.config.ts file', async () => {
+ const entrypoints = fakeArray(fakeEntrypoint);
+ const buildOutput = fakeBuildOutput({
+ publicAssets: [
+ { type: 'asset', fileName: 'icon-16.png' },
+ { type: 'asset', fileName: 'icon-32.png' },
+ { type: 'asset', fileName: 'logo-16.png' },
+ { type: 'asset', fileName: 'logo-32.png' },
+ { type: 'asset', fileName: 'logo-48.png' },
+ ],
+ });
+ const expected = {
+ 16: 'logo-16.png',
+ 32: 'logo-32.png',
+ 48: 'logo-48.png',
+ };
+ setFakeWxt({
+ config: {
+ manifest: {
+ icons: expected,
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.icons).toEqual(expected);
+ });
+ });
+
+ describe('content_scripts', () => {
+ it('should group content scripts and styles together based on their manifest properties', async () => {
+ const cs1: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'one',
+ inputPath: 'entrypoints/one.content/index.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://google.com/*'],
+ },
+ skipped: false,
+ };
+ const cs1Styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/one.css',
+ };
+ const cs2: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'two',
+ inputPath: 'entrypoints/two.content/index.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://google.com/*'],
+ runAt: 'document_end',
+ },
+ skipped: false,
+ };
+ const cs2Styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/two.css',
+ };
+ const cs3: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'three',
+ inputPath: 'entrypoints/three.content/index.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://google.com/*'],
+ runAt: 'document_end',
+ },
+ skipped: false,
+ };
+ const cs3Styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/three.css',
+ };
+ const cs4: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'four',
+ inputPath: 'entrypoints/four.content/index.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://duckduckgo.com/*'],
+ runAt: 'document_end',
+ },
+ skipped: false,
+ };
+ const cs4Styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/four.css',
+ };
+ const cs5: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'five',
+ inputPath: 'entrypoints/five.content/index.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://google.com/*'],
+ world: 'MAIN',
+ },
+ skipped: false,
+ };
+ const cs5Styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/five.css',
+ };
+
+ const entrypoints = [cs1, cs2, cs3, cs4, cs5];
+ setFakeWxt({
+ config: {
+ command: 'build',
+ outDir,
+ manifestVersion: 3,
+ },
+ });
+ const buildOutput: Omit = {
+ publicAssets: [],
+ steps: [
+ { entrypoints: cs1, chunks: [cs1Styles] },
+ { entrypoints: cs2, chunks: [cs2Styles] },
+ { entrypoints: cs3, chunks: [cs3Styles] },
+ { entrypoints: cs4, chunks: [cs4Styles] },
+ { entrypoints: cs5, chunks: [cs5Styles] },
+ ],
+ };
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.content_scripts).toContainEqual({
+ matches: ['*://google.com/*'],
+ css: ['content-scripts/one.css'],
+ js: ['content-scripts/one.js'],
+ });
+ expect(actual.content_scripts).toContainEqual({
+ matches: ['*://google.com/*'],
+ run_at: 'document_end',
+ css: ['content-scripts/two.css', 'content-scripts/three.css'],
+ js: ['content-scripts/two.js', 'content-scripts/three.js'],
+ });
+ expect(actual.content_scripts).toContainEqual({
+ matches: ['*://duckduckgo.com/*'],
+ run_at: 'document_end',
+ css: ['content-scripts/four.css'],
+ js: ['content-scripts/four.js'],
+ });
+ expect(actual.content_scripts).toContainEqual({
+ matches: ['*://google.com/*'],
+ css: ['content-scripts/five.css'],
+ js: ['content-scripts/five.js'],
+ world: 'MAIN',
+ });
+ });
+
+ it('should merge any content scripts declared in wxt.config.ts', async () => {
+ const cs: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'one',
+ inputPath: 'entrypoints/one.content.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://google.com/*'],
+ },
+ skipped: false,
+ };
+ const generatedContentScript = {
+ matches: ['*://google.com/*'],
+ js: ['content-scripts/one.js'],
+ };
+ const userContentScript = {
+ css: ['content-scripts/two.css'],
+ matches: ['*://*.google.com/*'],
+ };
+
+ const entrypoints = [cs];
+ const buildOutput = fakeBuildOutput();
+ setFakeWxt({
+ config: {
+ outDir,
+ command: 'build',
+ manifest: {
+ content_scripts: [userContentScript],
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.content_scripts).toContainEqual(userContentScript);
+ expect(actual.content_scripts).toContainEqual(generatedContentScript);
+ });
+
+ describe('cssInjectionMode', () => {
+ it.each([undefined, 'manifest'] as const)(
+ 'should add a CSS entry when cssInjectionMode is %s',
+ async (cssInjectionMode) => {
+ const cs: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'one',
+ inputPath: 'entrypoints/one.content.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://google.com/*'],
+ cssInjectionMode,
+ },
+ skipped: false,
+ };
+ const styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/one.css',
+ };
+
+ const entrypoints = [cs];
+ const buildOutput: Omit = {
+ publicAssets: [],
+ steps: [{ entrypoints: cs, chunks: [styles] }],
+ };
+ setFakeWxt({
+ config: {
+ outDir,
+ command: 'build',
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.content_scripts).toEqual([
+ {
+ js: ['content-scripts/one.js'],
+ css: ['content-scripts/one.css'],
+ matches: ['*://google.com/*'],
+ },
+ ]);
+ },
+ );
+
+ it.each(['manual', 'ui'] as const)(
+ 'should not add an entry for CSS when cssInjectionMode is %s',
+ async (cssInjectionMode) => {
+ const cs: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'one',
+ inputPath: 'entrypoints/one.content.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://google.com/*'],
+ cssInjectionMode,
+ },
+ skipped: false,
+ };
+ const styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/one.css',
+ };
+
+ const entrypoints = [cs];
+ const buildOutput: Omit = {
+ publicAssets: [],
+ steps: [{ entrypoints: cs, chunks: [styles] }],
+ };
+ setFakeWxt({
+ config: {
+ outDir,
+ command: 'build',
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.content_scripts).toEqual([
+ {
+ js: ['content-scripts/one.js'],
+ matches: ['*://google.com/*'],
+ },
+ ]);
+ },
+ );
+
+ it('should add CSS file to `web_accessible_resources` when cssInjectionMode is "ui" for MV3', async () => {
+ const cs: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'one',
+ inputPath: 'entrypoints/one.content.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://google.com/*'],
+ cssInjectionMode: 'ui',
+ },
+ skipped: false,
+ };
+ const styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/one.css',
+ };
+
+ const entrypoints = [cs];
+ const buildOutput: Omit = {
+ publicAssets: [],
+ steps: [{ entrypoints: cs, chunks: [styles] }],
+ };
+ setFakeWxt({
+ config: {
+ outDir,
+ command: 'build',
+ manifestVersion: 3,
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.web_accessible_resources).toEqual([
+ {
+ matches: ['*://google.com/*'],
+ resources: ['content-scripts/one.css'],
+ },
+ ]);
+ });
+
+ it('should add CSS file to `web_accessible_resources` when cssInjectionMode is "ui" for MV2', async () => {
+ const cs: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'one',
+ inputPath: 'entrypoints/one.content.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://google.com/*'],
+ cssInjectionMode: 'ui',
+ },
+ skipped: false,
+ };
+ const styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/one.css',
+ };
+
+ const entrypoints = [cs];
+ const buildOutput: Omit = {
+ publicAssets: [],
+ steps: [{ entrypoints: cs, chunks: [styles] }],
+ };
+ setFakeWxt({
+ config: {
+ outDir,
+ command: 'build',
+ manifestVersion: 2,
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.web_accessible_resources).toEqual(['content-scripts/one.css']);
+ });
+
+ it('should strip the path off the match pattern so the pattern is valid for `web_accessible_resources`', async () => {
+ const cs: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'one',
+ inputPath: 'entrypoints/one.content.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://play.google.com/books/*'],
+ cssInjectionMode: 'ui',
+ },
+ skipped: false,
+ };
+ const styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/one.css',
+ };
+
+ const entrypoints = [cs];
+ const buildOutput: Omit = {
+ publicAssets: [],
+ steps: [{ entrypoints: cs, chunks: [styles] }],
+ };
+ setFakeWxt({
+ config: {
+ outDir,
+ command: 'build',
+ manifestVersion: 3,
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.web_accessible_resources).toEqual([
+ {
+ matches: ['*://play.google.com/*'],
+ resources: ['content-scripts/one.css'],
+ },
+ ]);
+ });
+ });
+
+ describe('registration', () => {
+ it('should throw an error when registration=runtime for MV2', async () => {
+ const cs: ContentScriptEntrypoint = fakeContentScriptEntrypoint({
+ options: {
+ registration: 'runtime',
+ },
+ });
+
+ const entrypoints = [cs];
+ const buildOutput: Omit = {
+ publicAssets: [],
+ steps: [{ entrypoints: cs, chunks: [] }],
+ };
+ setFakeWxt({
+ config: {
+ manifestVersion: 2,
+ },
+ });
+
+ await expect(generateManifest(entrypoints, buildOutput)).rejects.toThrowError();
+ });
+
+ it('should add host_permissions instead of content_scripts when registration=runtime', async () => {
+ const cs: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'one',
+ inputPath: 'entrypoints/one.content.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://google.com/*'],
+ registration: 'runtime',
+ },
+ skipped: false,
+ };
+ const styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/one.css',
+ };
+
+ const entrypoints = [cs];
+ const buildOutput: Omit = {
+ publicAssets: [],
+ steps: [{ entrypoints: cs, chunks: [styles] }],
+ };
+ setFakeWxt({
+ config: {
+ manifestVersion: 3,
+ outDir,
+ command: 'build',
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.content_scripts).toEqual([]);
+ expect(actual.host_permissions).toEqual(['*://google.com/*']);
+ });
+ });
+ });
+
+ describe('sidepanel', () => {
+ it.each(['chrome', 'safari', 'edge'])(
+ 'should include the side_panel and permission, ignoring all options for %s',
+ async (browser) => {
+ const sidepanel = fakeSidepanelEntrypoint({
+ outputDir: outDir,
+ });
+ const buildOutput = fakeBuildOutput();
+
+ setFakeWxt({
+ config: {
+ manifestVersion: 3,
+ browser,
+ outDir,
+ command: 'build',
+ },
+ });
+ const expected = {
+ side_panel: {
+ default_path: 'sidepanel.html',
+ },
+ permissions: ['sidePanel'],
+ };
+
+ const { manifest: actual } = await generateManifest([sidepanel], buildOutput);
+
+ expect(actual).toMatchObject(expected);
+ },
+ );
+
+ it.each(['firefox'])('should include a sidebar_action for %s', async (browser) => {
+ const sidepanel = fakeSidepanelEntrypoint({
+ outputDir: outDir,
+ });
+ const buildOutput = fakeBuildOutput();
+
+ setFakeWxt({
+ config: {
+ manifestVersion: 3,
+ browser,
+ outDir,
+ },
+ });
+ const expected = {
+ sidebar_action: {
+ default_panel: 'sidepanel.html',
+ open_at_install: sidepanel.options.openAtInstall,
+ default_title: sidepanel.options.defaultTitle,
+ default_icon: sidepanel.options.defaultIcon,
+ browser_style: sidepanel.options.browserStyle,
+ },
+ };
+
+ const { manifest: actual } = await generateManifest([sidepanel], buildOutput);
+
+ expect(actual).toMatchObject(expected);
+ });
+ });
+
+ describe('web_accessible_resources', () => {
+ it('should combine user defined resources and generated resources for MV3', async () => {
+ const cs: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'one',
+ inputPath: 'entrypoints/one.content.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://google.com/*'],
+ cssInjectionMode: 'ui',
+ },
+ skipped: false,
+ };
+ const styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/one.css',
+ };
+
+ const entrypoints = [cs];
+ const buildOutput: Omit = {
+ publicAssets: [],
+ steps: [{ entrypoints: cs, chunks: [styles] }],
+ };
+ setFakeWxt({
+ config: {
+ outDir,
+ command: 'build',
+ manifestVersion: 3,
+ manifest: {
+ web_accessible_resources: [{ resources: ['one.png'], matches: ['*://one.com/*'] }],
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.web_accessible_resources).toEqual([
+ { resources: ['one.png'], matches: ['*://one.com/*'] },
+ {
+ resources: ['content-scripts/one.css'],
+ matches: ['*://google.com/*'],
+ },
+ ]);
+ });
+
+ it('should combine user defined resources and generated resources for MV2', async () => {
+ const cs: ContentScriptEntrypoint = {
+ type: 'content-script',
+ name: 'one',
+ inputPath: 'entrypoints/one.content.ts',
+ outputDir: contentScriptOutDir,
+ options: {
+ matches: ['*://google.com/*'],
+ cssInjectionMode: 'ui',
+ },
+ skipped: false,
+ };
+ const styles: OutputAsset = {
+ type: 'asset',
+ fileName: 'content-scripts/one.css',
+ };
+
+ const entrypoints = [cs];
+ const buildOutput: Omit = {
+ publicAssets: [],
+ steps: [{ entrypoints: cs, chunks: [styles] }],
+ };
+ setFakeWxt({
+ config: {
+ outDir,
+ command: 'build',
+ manifestVersion: 2,
+ manifest: {
+ web_accessible_resources: ['one.png'],
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.web_accessible_resources).toEqual(['one.png', 'content-scripts/one.css']);
+ });
+
+ it('should convert mv3 items to mv2 strings automatically', async () => {
+ setFakeWxt({
+ config: {
+ outDir,
+ manifestVersion: 2,
+ manifest: {
+ web_accessible_resources: [
+ {
+ matches: ['*://*/*'],
+ resources: ['/icon-128.png'],
+ },
+ {
+ matches: ['https://google.com'],
+ resources: ['/icon-128.png', '/icon-32.png'],
+ },
+ ],
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest([], fakeBuildOutput());
+
+ expect(actual.web_accessible_resources).toEqual(['/icon-128.png', '/icon-32.png']);
+ });
+
+ it('should convert mv2 strings to mv3 items with a warning automatically', async () => {
+ setFakeWxt({
+ config: {
+ outDir,
+ manifestVersion: 3,
+ manifest: {
+ web_accessible_resources: ['/icon.svg'],
+ },
+ },
+ });
+
+ await expect(() => generateManifest([], fakeBuildOutput())).rejects.toThrow(
+ 'Non-MV3 web_accessible_resources detected: ["/icon.svg"]. When manually defining web_accessible_resources, define them as MV3 objects ({ matches: [...], resources: [...] }), and WXT will automatically convert them to MV2 when necessary.',
+ );
+ });
+ });
+
+ describe('transformManifest option', () => {
+ it("should call the transformManifest option after the manifest is generated, but before it's returned", async () => {
+ const entrypoints: Entrypoint[] = [];
+ const buildOutput = fakeBuildOutput();
+ const newAuthor = 'Custom Author';
+ setFakeWxt({
+ config: {
+ transformManifest(manifest: any) {
+ manifest.author = newAuthor;
+ },
+ },
+ });
+ const expected = {
+ author: newAuthor,
+ };
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual).toMatchObject(expected);
+ });
+ });
+
+ describe('version', () => {
+ it.each(['chrome', 'safari', 'edge'] as const)(
+ 'should include version and version_name as is on %s',
+ async (browser) => {
+ const version = '1.0.0';
+ const versionName = '1.0.0-alpha1';
+ const entrypoints: Entrypoint[] = [];
+ const buildOutput = fakeBuildOutput();
+ setFakeWxt({
+ config: {
+ browser,
+ manifest: {
+ version,
+ version_name: versionName,
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.version).toBe(version);
+ expect(actual.version_name).toBe(versionName);
+ },
+ );
+
+ it.each(['firefox'] as const)(
+ 'should not include a version_name on %s because it is unsupported',
+ async (browser) => {
+ const version = '1.0.0';
+ const versionName = '1.0.0-alpha1';
+ const entrypoints: Entrypoint[] = [];
+ const buildOutput = fakeBuildOutput();
+ setFakeWxt({
+ config: {
+ browser,
+ manifest: {
+ version,
+ version_name: versionName,
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.version).toBe(version);
+ expect(actual.version_name).toBeUndefined();
+ },
+ );
+
+ it.each(['chrome', 'firefox', 'safari', 'edge'])(
+ 'should not include the version_name if it is equal to version',
+ async (browser) => {
+ const version = '1.0.0';
+ const entrypoints: Entrypoint[] = [];
+ const buildOutput = fakeBuildOutput();
+ setFakeWxt({
+ config: {
+ browser,
+ manifest: {
+ version,
+ version_name: version,
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.version).toBe(version);
+ expect(actual.version_name).toBeUndefined();
+ },
+ );
+
+ it('should log a warning if the version could not be detected', async () => {
+ const entrypoints: Entrypoint[] = [];
+ const buildOutput = fakeBuildOutput();
+ setFakeWxt({
+ config: {
+ manifest: {
+ // @ts-ignore: Purposefully removing version from fake object
+ version: null,
+ },
+ },
+ });
+
+ const { manifest: actual } = await generateManifest(entrypoints, buildOutput);
+
+ expect(actual.version).toBe('0.0.0');
+ expect(actual.version_name).toBeUndefined();
+ expect(console.warn).toBeCalledTimes(1);
+ expect(console.warn).toBeCalledWith(expect.stringContaining('Extension version not found'));
+ });
+ });
+
+ describe('commands', () => {
+ const reloadCommandName = 'wxt:reload-extension';
+ const reloadCommand = {
+ description: expect.any(String),
+ suggested_key: {
+ default: 'Alt+R',
+ },
+ };
+
+ it('should include a command for reloading the extension during development', async () => {
+ setFakeWxt({
+ config: { command: 'serve' },
+ });
+ const output = fakeBuildOutput();
+ const entrypoints = fakeArray(fakeEntrypoint);
+
+ const { manifest: actual } = await generateManifest(entrypoints, output);
+
+ expect(actual.commands).toEqual({
+ [reloadCommandName]: reloadCommand,
+ });
+ });
+
+ it('should customize the reload commands key binding if passing a custom command', async () => {
+ setFakeWxt({
+ config: {
+ command: 'serve',
+ dev: {
+ reloadCommand: 'Ctrl+E',
+ },
+ },
+ });
+ const output = fakeBuildOutput();
+ const entrypoints = fakeArray(fakeEntrypoint);
+
+ const { manifest: actual } = await generateManifest(entrypoints, output);
+
+ expect(actual.commands).toEqual({
+ [reloadCommandName]: {
+ ...reloadCommand,
+ suggested_key: {
+ default: 'Ctrl+E',
+ },
+ },
+ });
+ });
+
+ it("should not include the reload command when it's been disabled", async () => {
+ setFakeWxt({
+ config: {
+ command: 'serve',
+ dev: {
+ reloadCommand: false,
+ },
+ },
+ });
+ const output = fakeBuildOutput();
+ const entrypoints = fakeArray(fakeEntrypoint);
+
+ const { manifest: actual } = await generateManifest(entrypoints, output);
+
+ expect(actual.commands).toBeUndefined();
+ });
+
+ it('should not override any existing commands when adding the one to reload the extension', async () => {
+ const customCommandName = 'custom-command';
+ const customCommand = fakeManifestCommand();
+ setFakeWxt({
+ config: {
+ command: 'serve',
+ manifest: {
+ commands: {
+ [customCommandName]: customCommand,
+ },
+ },
+ },
+ });
+ const output = fakeBuildOutput();
+ const entrypoints = fakeArray(fakeEntrypoint);
+
+ const { manifest: actual } = await generateManifest(entrypoints, output);
+
+ expect(actual.commands).toEqual({
+ [reloadCommandName]: reloadCommand,
+ [customCommandName]: customCommand,
+ });
+ });
+
+ it('should not include the command if there are already 4 others (the max)', async () => {
+ const commands = {
+ command1: fakeManifestCommand(),
+ command2: fakeManifestCommand(),
+ command3: fakeManifestCommand(),
+ command4: fakeManifestCommand(),
+ };
+ setFakeWxt({
+ config: {
+ command: 'serve',
+ manifest: { commands },
+ },
+ });
+ const output = fakeBuildOutput();
+ const entrypoints = fakeArray(fakeEntrypoint);
+
+ const { manifest: actual, warnings } = await generateManifest(entrypoints, output);
+
+ expect(actual.commands).toEqual(commands);
+ expect(warnings).toHaveLength(1);
+ });
+
+ it('should not include the command when building an extension', async () => {
+ setFakeWxt({
+ config: { command: 'build' },
+ });
+ const output = fakeBuildOutput();
+ const entrypoints = fakeArray(fakeEntrypoint);
+
+ const { manifest: actual } = await generateManifest(entrypoints, output);
+
+ expect(actual.commands).toBeUndefined();
+ });
+ });
+
+ describe('Stripping keys', () => {
+ const mv2Manifest = {
+ page_action: {},
+ browser_action: {},
+ automation: {},
+ content_capabilities: {},
+ converted_from_user_script: {},
+ current_locale: {},
+ differential_fingerprint: {},
+ event_rules: {},
+ file_browser_handlers: {},
+ file_system_provider_capabilities: {},
+ input_components: {},
+ nacl_modules: {},
+ natively_connectable: {},
+ offline_enabled: {},
+ platforms: {},
+ replacement_web_app: {},
+ system_indicator: {},
+ user_scripts: {},
+ };
+ const mv3Manifest = {
+ action: {},
+ export: {},
+ optional_host_permissions: {},
+ side_panel: {},
+ };
+ const hostPermissionsManifest = {
+ host_permissions: {},
+ };
+ const manifest: any = {
+ ...mv2Manifest,
+ ...mv3Manifest,
+ ...hostPermissionsManifest,
+ };
+
+ it.each([
+ ['firefox', 2, mv2Manifest],
+ ['chrome', 2, { ...mv2Manifest, ...hostPermissionsManifest }],
+ ['safari', 2, { ...mv2Manifest, ...hostPermissionsManifest }],
+ ['edge', 2, { ...mv2Manifest, ...hostPermissionsManifest }],
+ ['firefox', 3, { ...mv3Manifest, ...hostPermissionsManifest }],
+ ['chrome', 3, { ...mv3Manifest, ...hostPermissionsManifest }],
+ ['safari', 3, { ...mv3Manifest, ...hostPermissionsManifest }],
+ ['edge', 3, { ...mv3Manifest, ...hostPermissionsManifest }],
+ ] as const)("%s MV%s should only include that version's keys", async (browser, manifestVersion, expected) => {
+ setFakeWxt({
+ config: {
+ browser,
+ manifest,
+ manifestVersion,
+ command: 'build',
+ },
+ });
+ const output = fakeBuildOutput();
+
+ const { manifest: actual } = await generateManifest([], output);
+
+ expect(actual).toEqual({
+ name: expect.any(String),
+ version: expect.any(String),
+ manifest_version: manifestVersion,
+ ...expected,
+ });
+ });
+ });
+
+ describe('host_permissions', () => {
+ it('should keep host_permissions as-is for MV3', async () => {
+ const expectedHostPermissions = ['https://google.com/*'];
+ const expectedPermissions = ['scripting'];
+ setFakeWxt({
+ config: {
+ manifest: {
+ host_permissions: expectedHostPermissions,
+ permissions: expectedPermissions,
+ },
+ manifestVersion: 3,
+ command: 'build',
+ },
+ });
+ const output = fakeBuildOutput();
+
+ const { manifest: actual } = await generateManifest([], output);
+
+ expect(actual.permissions).toEqual(expectedPermissions);
+ expect(actual.host_permissions).toEqual(expectedHostPermissions);
+ });
+
+ it('should move host_permissions to permissions for MV2, ignoring duplicates', async () => {
+ const expectedPermissions = ['scripting', '*://*.youtube.com/*', 'https://google.com/*'];
+ setFakeWxt({
+ config: {
+ manifest: {
+ host_permissions: ['https://google.com/*', '*://*.youtube.com/*'],
+ permissions: ['scripting', '*://*.youtube.com/*'],
+ },
+ manifestVersion: 2,
+ command: 'build',
+ },
+ });
+ const output = fakeBuildOutput();
+
+ const { manifest: actual } = await generateManifest([], output);
+
+ expect(actual.permissions).toEqual(expectedPermissions);
+ expect(actual.host_permissions).toBeUndefined();
+ });
+ });
+
+ describe('Dev mode', () => {
+ it('should not add any code for production builds', async () => {
+ setFakeWxt({
+ config: {
+ command: 'build',
+ },
+ server: {
+ hostname: 'localhost',
+ port: 3000,
+ origin: 'http://localhost:3000',
+ },
+ });
+ const output = fakeBuildOutput();
+ const entrypoints: Entrypoint[] = [];
+
+ const { manifest: actual } = await generateManifest(entrypoints, output);
+
+ expect(actual.permissions).toBeUndefined();
+ expect(actual.content_security_policy).toBeUndefined();
+ });
+
+ it('should add required permissions for dev mode to function for MV2', async () => {
+ setFakeWxt({
+ config: {
+ command: 'serve',
+ manifestVersion: 2,
+ },
+ server: fakeWxtDevServer({
+ port: 3000,
+ hostname: 'localhost',
+ origin: 'http://localhost:3000',
+ }),
+ });
+ const output = fakeBuildOutput();
+ const entrypoints: Entrypoint[] = [];
+
+ const { manifest: actual } = await generateManifest(entrypoints, output);
+
+ expect(actual).toMatchObject({
+ content_security_policy: "script-src 'self' http://localhost:3000; object-src 'self';",
+ permissions: ['http://localhost/*', 'tabs'],
+ });
+ });
+
+ it('should add required permissions for dev mode to function for MV3', async () => {
+ setFakeWxt({
+ config: {
+ command: 'serve',
+ manifestVersion: 3,
+ browser: 'chrome',
+ },
+ server: fakeWxtDevServer({
+ hostname: 'localhost',
+ port: 3000,
+ origin: 'http://localhost:3000',
+ }),
+ });
+ const output = fakeBuildOutput();
+ const entrypoints: Entrypoint[] = [];
+
+ const { manifest: actual } = await generateManifest(entrypoints, output);
+
+ expect(actual).toMatchObject({
+ content_security_policy: {
+ extension_pages: "script-src 'self' 'wasm-unsafe-eval' http://localhost:3000; object-src 'self';",
+ sandbox:
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:3000; sandbox allow-scripts allow-forms allow-popups allow-modals; child-src 'self';",
+ },
+ host_permissions: ['http://localhost/*'],
+ permissions: ['tabs', 'scripting'],
+ });
+ });
+ });
+ });
+
+ describe('stripPathFromMatchPattern', () => {
+ it.each([
+ ['', ''],
+ ['*://play.google.com/books/*', '*://play.google.com/*'],
+ ['*://*/*', '*://*/*'],
+ ['https://github.com/wxt-dev/*', 'https://github.com/*'],
+ ])('should convert "%s" to "%s"', (input, expected) => {
+ const actual = stripPathFromMatchPattern(input);
+ expect(actual).toEqual(expected);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/utils/__tests__/package.test.ts b/packages/wxt/src/core/utils/__tests__/package.test.ts
new file mode 100644
index 0000000..dae7061
--- /dev/null
+++ b/packages/wxt/src/core/utils/__tests__/package.test.ts
@@ -0,0 +1,35 @@
+import { describe, it, expect } from 'vitest';
+import { getPackageJson } from '../package';
+import { setFakeWxt } from '../testing/fake-objects';
+import { mock } from 'vitest-mock-extended';
+import { Logger } from '~/types';
+import { WXT_PACKAGE_DIR } from '../../../../e2e/utils';
+
+describe('Package JSON Utils', () => {
+ describe('getPackageJson', () => {
+ it('should return the package.json inside /package.json', async () => {
+ setFakeWxt({
+ config: { root: WXT_PACKAGE_DIR },
+ });
+
+ const actual = await getPackageJson();
+
+ expect(actual).toMatchObject({
+ name: 'wxt',
+ });
+ });
+
+ it("should return an empty object when /package.json doesn't exist", async () => {
+ const root = '/some/path/that/does/not/exist';
+ const logger = mock();
+ setFakeWxt({
+ config: { root, logger },
+ logger,
+ });
+
+ const actual = await getPackageJson();
+
+ expect(actual).toEqual({});
+ });
+ });
+});
diff --git a/packages/wxt/src/core/utils/__tests__/paths.test.ts b/packages/wxt/src/core/utils/__tests__/paths.test.ts
new file mode 100644
index 0000000..16fe6c4
--- /dev/null
+++ b/packages/wxt/src/core/utils/__tests__/paths.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, it } from 'vitest';
+import { normalizePath } from '../paths';
+
+describe('Path Utils', () => {
+ describe('normalizePath', () => {
+ it.each([
+ // Relative paths
+ ['../test.sh', '../test.sh'],
+ ['..\\test.sh', '../test.sh'],
+ ['test.png', 'test.png'],
+ // Absolute paths
+ ['C:\\\\path\\to\\file', 'C:/path/to/file'],
+ ['/path/to/file', '/path/to/file'],
+ // Strip trailing slash
+ ['C:\\\\path\\to\\folder\\', 'C:/path/to/folder'],
+ ['/path/to/folder/', '/path/to/folder'],
+ // Dedupe slashes
+ ['path\\\\\\file', 'path/file'],
+ ['path//file', 'path/file'],
+ ])('should normalize "%s" to "%s"', (input, expected) => {
+ expect(normalizePath(input)).toBe(expected);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/utils/__tests__/strings.test.ts b/packages/wxt/src/core/utils/__tests__/strings.test.ts
new file mode 100644
index 0000000..8653650
--- /dev/null
+++ b/packages/wxt/src/core/utils/__tests__/strings.test.ts
@@ -0,0 +1,67 @@
+import { describe, expect, it } from 'vitest';
+import {
+ kebabCaseAlphanumeric,
+ removeImportStatements,
+ safeVarName,
+} from '../strings';
+
+describe('String utils', () => {
+ describe('kebabCaseAlphanumeric', () => {
+ it.each([
+ ['HELLO', 'hello'],
+ ['Hello, World!', 'hello-world'],
+ ['hello123', 'hello123'],
+ ['Hello World This Is A Test', 'hello-world-this-is-a-test'],
+ ['Hello World', 'hello-world'],
+ ['hello-world', 'hello-world'], // Ensure hyphens are preserved
+ ])('should convert "%s" to "%s"', (input, expected) => {
+ expect(kebabCaseAlphanumeric(input)).toBe(expected);
+ });
+ });
+
+ describe('safeVarName', () => {
+ it.each([
+ ['Hello world!', '_hello_world'],
+ ['123', '_123'],
+ ['abc-123', '_abc_123'],
+ ['', '_'],
+ [' ', '_'],
+ ['_', '_'],
+ ])(
+ "should convert '%s' to '%s', which can be used for a variable name",
+ (input, expected) => {
+ const actual = safeVarName(input);
+ expect(actual).toBe(expected);
+ },
+ );
+ });
+
+ describe('removeImportStatements', () => {
+ it('should remove all import formats', () => {
+ const imports = `
+import { registerGithubService, createGithubApi } from "@/utils/github";
+import {
+ registerGithubService,
+ createGithubApi
+} from "@/utils/github";
+import{ registerGithubService, createGithubApi }from "@/utils/github";
+import GitHub from "@/utils/github";
+import "@/utils/github";
+import '@/utils/github';
+import"@/utils/github"
+ import'@/utils/github';
+import * as abc from "@/utils/github"
+ `;
+ expect(removeImportStatements(imports).trim()).toEqual('');
+ });
+
+ it('should not remove import.meta or inline import statements', () => {
+ const imports = `
+import.meta.env.DEV
+const a = await import("example");
+import("example");
+ `;
+ expect(removeImportStatements(imports)).toEqual(imports);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/utils/__tests__/transform.test.ts b/packages/wxt/src/core/utils/__tests__/transform.test.ts
new file mode 100644
index 0000000..3c8511a
--- /dev/null
+++ b/packages/wxt/src/core/utils/__tests__/transform.test.ts
@@ -0,0 +1,51 @@
+import { describe, it, expect } from 'vitest';
+import { removeMainFunctionCode } from '../transform';
+
+describe('Transform Utils', () => {
+ describe('removeMainFunctionCode', () => {
+ it.each(['defineBackground', 'defineUnlistedScript'])(
+ 'should remove the first arrow function argument for %s',
+ (def) => {
+ const input = `export default ${def}(() => {
+ console.log();
+ })`;
+ const expected = `export default ${def}(() => {})`;
+
+ const actual = removeMainFunctionCode(input).code;
+
+ expect(actual).toEqual(expected);
+ },
+ );
+ it.each(['defineBackground', 'defineUnlistedScript'])(
+ 'should remove the first function argument for %s',
+ (def) => {
+ const input = `export default ${def}(function () {
+ console.log();
+ })`;
+ const expected = `export default ${def}(function () {})`;
+
+ const actual = removeMainFunctionCode(input).code;
+
+ expect(actual).toEqual(expected);
+ },
+ );
+
+ it.each([
+ 'defineBackground',
+ 'defineContentScript',
+ 'defineUnlistedScript',
+ ])('should remove the main field from %s', (def) => {
+ const input = `export default ${def}({
+ asdf: "asdf",
+ main: () => {},
+ })`;
+ const expected = `export default ${def}({
+ asdf: "asdf"
+})`;
+
+ const actual = removeMainFunctionCode(input).code;
+
+ expect(actual).toEqual(expected);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/utils/__tests__/validation.test.ts b/packages/wxt/src/core/utils/__tests__/validation.test.ts
new file mode 100644
index 0000000..9ca603f
--- /dev/null
+++ b/packages/wxt/src/core/utils/__tests__/validation.test.ts
@@ -0,0 +1,100 @@
+import { describe, it, expect } from 'vitest';
+import {
+ fakeArray,
+ fakeContentScriptEntrypoint,
+ fakeEntrypoint,
+ fakeGenericEntrypoint,
+} from '../testing/fake-objects';
+import { validateEntrypoints } from '../validation';
+
+describe('Validation Utils', () => {
+ describe('validateEntrypoints', () => {
+ it('should return no errors when there are no errors', () => {
+ const entrypoints = fakeArray(fakeEntrypoint);
+ const expected = {
+ errors: [],
+ errorCount: 0,
+ warningCount: 0,
+ };
+
+ const actual = validateEntrypoints(entrypoints);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should return an error when exclude is not an array', () => {
+ const entrypoint = fakeGenericEntrypoint({
+ options: {
+ // @ts-expect-error
+ exclude: 0,
+ },
+ });
+ const expected = {
+ errors: [
+ {
+ type: 'error',
+ message: '`exclude` must be an array of browser names',
+ value: 0,
+ entrypoint,
+ },
+ ],
+ errorCount: 1,
+ warningCount: 0,
+ };
+
+ const actual = validateEntrypoints([entrypoint]);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should return an error when include is not an array', () => {
+ const entrypoint = fakeGenericEntrypoint({
+ options: {
+ // @ts-expect-error
+ include: 0,
+ },
+ });
+ const expected = {
+ errors: [
+ {
+ type: 'error',
+ message: '`include` must be an array of browser names',
+ value: 0,
+ entrypoint,
+ },
+ ],
+ errorCount: 1,
+ warningCount: 0,
+ };
+
+ const actual = validateEntrypoints([entrypoint]);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it("should return an error when content scripts don't have a matches", () => {
+ const entrypoint = fakeContentScriptEntrypoint({
+ options: {
+ // @ts-expect-error
+ matches: null,
+ },
+ });
+ const expected = {
+ errors: [
+ {
+ type: 'error',
+ message: '`matches` is required',
+ value: null,
+ entrypoint,
+ },
+ ],
+ errorCount: 1,
+ warningCount: 0,
+ };
+
+ const actual = validateEntrypoints([entrypoint]);
+
+ expect(actual).toEqual(expected);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/utils/__tests__/virtual-modules.test.ts b/packages/wxt/src/core/utils/__tests__/virtual-modules.test.ts
new file mode 100644
index 0000000..6b4e462
--- /dev/null
+++ b/packages/wxt/src/core/utils/__tests__/virtual-modules.test.ts
@@ -0,0 +1,20 @@
+import { describe, it } from 'vitest';
+import {
+ VirtualModuleId,
+ VirtualModuleName,
+ VirtualEntrypointType,
+ VirtualEntrypointModuleName,
+} from '../virtual-modules';
+
+describe('Virtual Modules', () => {
+ it('should resolve types to litteral values, not string', () => {
+ // @ts-expect-error
+ const _c: VirtualEntrypointType = '';
+ // @ts-expect-error
+ const _d: VirtualEntrypointModuleName = '';
+ // @ts-expect-error
+ const _b: VirtualModuleName = '';
+ // @ts-expect-error
+ const _a: VirtualModuleId = '';
+ });
+});
diff --git a/packages/wxt/src/core/utils/arrays.ts b/packages/wxt/src/core/utils/arrays.ts
new file mode 100644
index 0000000..1996d56
--- /dev/null
+++ b/packages/wxt/src/core/utils/arrays.ts
@@ -0,0 +1,34 @@
+/**
+ * Checks if `predicate` returns truthy for all elements of the array.
+ */
+export function every(
+ array: T[],
+ predicate: (item: T, index: number) => boolean,
+): boolean {
+ for (let i = 0; i < array.length; i++)
+ if (!predicate(array[i], i)) return false;
+ return true;
+}
+
+/**
+ * Returns true when any of the predicates return true;
+ */
+export function some(
+ array: T[],
+ predicate: (item: T, index: number) => boolean,
+): boolean {
+ for (let i = 0; i < array.length; i++)
+ if (predicate(array[i], i)) return true;
+ return false;
+}
+
+/**
+ * Convert an item or array to an array.
+ */
+export function toArray(a: T | T[]): T[] {
+ return Array.isArray(a) ? a : [a];
+}
+
+export function filterTruthy(array: Array): T[] {
+ return array.filter((item) => !!item) as T[];
+}
diff --git a/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts b/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts
new file mode 100644
index 0000000..82a0853
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts
@@ -0,0 +1,334 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+import { DevModeChange, detectDevChanges } from '~/core/utils/building';
+import {
+ fakeBackgroundEntrypoint,
+ fakeContentScriptEntrypoint,
+ fakeFile,
+ fakeGenericEntrypoint,
+ fakeManifest,
+ fakeOptionsEntrypoint,
+ fakePopupEntrypoint,
+ fakeOutputAsset,
+ fakeOutputChunk,
+ fakeWxt,
+ setFakeWxt,
+} from '~/core/utils/testing/fake-objects';
+import { BuildOutput, BuildStepOutput } from '~/types';
+import { setWxtForTesting } from '../../../wxt';
+
+describe('Detect Dev Changes', () => {
+ beforeEach(() => {
+ setWxtForTesting(fakeWxt());
+ });
+
+ describe('No changes', () => {
+ it("should return 'no-change' when the changed file isn't used by any of the entrypoints", () => {
+ const changes = ['/some/path.ts'];
+ const currentOutput: BuildOutput = {
+ manifest: fakeManifest(),
+ publicAssets: [],
+ steps: [
+ {
+ entrypoints: fakeContentScriptEntrypoint(),
+ chunks: [fakeOutputChunk(), fakeOutputChunk()],
+ },
+ {
+ entrypoints: fakeContentScriptEntrypoint(),
+ chunks: [fakeOutputChunk(), fakeOutputChunk(), fakeOutputChunk()],
+ },
+ ],
+ };
+
+ const actual = detectDevChanges(changes, currentOutput);
+
+ expect(actual).toEqual({ type: 'no-change' });
+ });
+ });
+
+ describe('wxt.config.ts', () => {
+ it("should return 'full-restart' when one of the changed files is the config file", () => {
+ const configFile = '/root/wxt.config.ts';
+ setFakeWxt({
+ config: {
+ userConfigMetadata: {
+ configFile,
+ },
+ },
+ });
+ const changes = ['/root/src/public/image.svg', configFile];
+ const currentOutput: BuildOutput = {
+ manifest: fakeManifest(),
+ publicAssets: [],
+ steps: [],
+ };
+ const expected: DevModeChange = {
+ type: 'full-restart',
+ };
+
+ const actual = detectDevChanges(changes, currentOutput);
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('web-ext.config.ts', () => {
+ it("should return 'browser-restart' when one of the changed files is the config file", () => {
+ const runnerFile = '/root/web-ext.config.ts';
+ setFakeWxt({
+ config: {
+ runnerConfig: {
+ configFile: runnerFile,
+ },
+ },
+ });
+ const changes = ['/root/src/public/image.svg', runnerFile];
+ const currentOutput: BuildOutput = {
+ manifest: fakeManifest(),
+ publicAssets: [],
+ steps: [],
+ };
+ const expected: DevModeChange = {
+ type: 'browser-restart',
+ };
+
+ const actual = detectDevChanges(changes, currentOutput);
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('Public Assets', () => {
+ it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => {
+ const changes = ['/root/src/public/image.svg'];
+ const asset1 = fakeOutputAsset({
+ fileName: 'image.svg',
+ });
+ const asset2 = fakeOutputAsset({
+ fileName: 'some-other-image.svg',
+ });
+ const currentOutput: BuildOutput = {
+ manifest: fakeManifest(),
+ publicAssets: [asset1, asset2],
+ steps: [],
+ };
+ const expected: DevModeChange = {
+ type: 'extension-reload',
+ rebuildGroups: [],
+ cachedOutput: {
+ ...currentOutput,
+ publicAssets: [asset2],
+ },
+ };
+
+ const actual = detectDevChanges(changes, currentOutput);
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('Background', () => {
+ it("should rebuild the background and reload the extension when the changed file in it's chunks' `moduleIds` field", () => {
+ const changedPath = '/root/utils/shared.ts';
+ const contentScript = fakeContentScriptEntrypoint({
+ inputPath: '/root/overlay.content.ts',
+ });
+ const background = fakeBackgroundEntrypoint({
+ inputPath: '/root/background.ts',
+ });
+
+ const step1: BuildStepOutput = {
+ entrypoints: contentScript,
+ chunks: [
+ fakeOutputChunk({
+ moduleIds: [fakeFile(), fakeFile()],
+ }),
+ ],
+ };
+ const step2: BuildStepOutput = {
+ entrypoints: background,
+ chunks: [
+ fakeOutputChunk({
+ moduleIds: [fakeFile(), changedPath, fakeFile()],
+ }),
+ ],
+ };
+
+ const currentOutput: BuildOutput = {
+ manifest: fakeManifest(),
+ publicAssets: [],
+ steps: [step1, step2],
+ };
+ const expected: DevModeChange = {
+ type: 'extension-reload',
+ cachedOutput: {
+ ...currentOutput,
+ steps: [step1],
+ },
+ rebuildGroups: [background],
+ };
+
+ const actual = detectDevChanges([changedPath], currentOutput);
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('HTML Pages', () => {
+ it('should detect changes to entrypoints/.html files', async () => {
+ const changedPath = '/root/page1.html';
+ const htmlPage1 = fakePopupEntrypoint({
+ inputPath: changedPath,
+ });
+ const htmlPage2 = fakeOptionsEntrypoint({
+ inputPath: '/root/page2.html',
+ });
+ const htmlPage3 = fakeGenericEntrypoint({
+ type: 'sandbox',
+ inputPath: '/root/page3.html',
+ });
+
+ const step1: BuildStepOutput = {
+ entrypoints: [htmlPage1, htmlPage2],
+ chunks: [
+ fakeOutputAsset({
+ fileName: 'page1.html',
+ }),
+ ],
+ };
+ const step2: BuildStepOutput = {
+ entrypoints: [htmlPage3],
+ chunks: [
+ fakeOutputAsset({
+ fileName: 'page2.html',
+ }),
+ ],
+ };
+
+ const currentOutput: BuildOutput = {
+ manifest: fakeManifest(),
+ publicAssets: [],
+ steps: [step1, step2],
+ };
+ const expected: DevModeChange = {
+ type: 'html-reload',
+ cachedOutput: {
+ ...currentOutput,
+ steps: [step2],
+ },
+ rebuildGroups: [[htmlPage1, htmlPage2]],
+ };
+
+ const actual = detectDevChanges([changedPath], currentOutput);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should detect changes to entrypoints//index.html files', async () => {
+ const changedPath = '/root/page1/index.html';
+ const htmlPage1 = fakePopupEntrypoint({
+ inputPath: changedPath,
+ });
+ const htmlPage2 = fakeOptionsEntrypoint({
+ inputPath: '/root/page2/index.html',
+ });
+ const htmlPage3 = fakeGenericEntrypoint({
+ type: 'sandbox',
+ inputPath: '/root/page3/index.html',
+ });
+
+ const step1: BuildStepOutput = {
+ entrypoints: [htmlPage1, htmlPage2],
+ chunks: [
+ fakeOutputAsset({
+ fileName: 'page1.html',
+ }),
+ ],
+ };
+ const step2: BuildStepOutput = {
+ entrypoints: [htmlPage3],
+ chunks: [
+ fakeOutputAsset({
+ fileName: 'page2.html',
+ }),
+ ],
+ };
+
+ const currentOutput: BuildOutput = {
+ manifest: fakeManifest(),
+ publicAssets: [],
+ steps: [step1, step2],
+ };
+ const expected: DevModeChange = {
+ type: 'html-reload',
+ cachedOutput: {
+ ...currentOutput,
+ steps: [step2],
+ },
+ rebuildGroups: [[htmlPage1, htmlPage2]],
+ };
+
+ const actual = detectDevChanges([changedPath], currentOutput);
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('Content Scripts', () => {
+ it('should rebuild then reload only the effected content scripts', async () => {
+ const changedPath = '/root/utils/shared.ts';
+ const script1 = fakeContentScriptEntrypoint({
+ inputPath: '/root/overlay1.content/index.ts',
+ });
+ const script2 = fakeContentScriptEntrypoint({
+ inputPath: '/root/overlay2.ts',
+ });
+ const script3 = fakeContentScriptEntrypoint({
+ inputPath: '/root/overlay3.content/index.ts',
+ });
+
+ const step1: BuildStepOutput = {
+ entrypoints: script1,
+ chunks: [
+ fakeOutputChunk({
+ moduleIds: [fakeFile(), changedPath],
+ }),
+ ],
+ };
+ const step2: BuildStepOutput = {
+ entrypoints: script2,
+ chunks: [
+ fakeOutputChunk({
+ moduleIds: [fakeFile(), fakeFile(), fakeFile()],
+ }),
+ ],
+ };
+ const step3: BuildStepOutput = {
+ entrypoints: script3,
+ chunks: [
+ fakeOutputChunk({
+ moduleIds: [changedPath, fakeFile(), fakeFile()],
+ }),
+ ],
+ };
+
+ const currentOutput: BuildOutput = {
+ manifest: fakeManifest(),
+ publicAssets: [],
+ steps: [step1, step2, step3],
+ };
+ const expected: DevModeChange = {
+ type: 'content-script-reload',
+ cachedOutput: {
+ ...currentOutput,
+ steps: [step2],
+ },
+ changedSteps: [step1, step3],
+ rebuildGroups: [script1, script3],
+ };
+
+ const actual = detectDevChanges([changedPath], currentOutput);
+
+ expect(actual).toEqual(expected);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts b/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts
new file mode 100644
index 0000000..5e4f2a4
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts
@@ -0,0 +1,871 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import {
+ BackgroundEntrypoint,
+ BackgroundEntrypointOptions,
+ BaseEntrypointOptions,
+ ContentScriptEntrypoint,
+ GenericEntrypoint,
+ OptionsEntrypoint,
+ PopupEntrypoint,
+ SidepanelEntrypoint,
+} from '~/types';
+import { resolve } from 'path';
+import { findEntrypoints } from '../find-entrypoints';
+import fs from 'fs-extra';
+import { importEntrypointFile } from '../import-entrypoint';
+import glob from 'fast-glob';
+import {
+ fakeResolvedConfig,
+ setFakeWxt,
+} from '~/core/utils/testing/fake-objects';
+import { unnormalizePath } from '~/core/utils/paths';
+
+vi.mock('../import-entrypoint');
+const importEntrypointFileMock = vi.mocked(importEntrypointFile);
+
+vi.mock('fast-glob');
+const globMock = vi.mocked(glob);
+
+vi.mock('fs-extra');
+const readFileMock = vi.mocked(
+ fs.readFile as (path: string) => Promise,
+);
+
+describe('findEntrypoints', () => {
+ const config = fakeResolvedConfig({
+ manifestVersion: 3,
+ root: '/',
+ entrypointsDir: resolve('/src/entrypoints'),
+ outDir: resolve('.output'),
+ command: 'build',
+ });
+
+ beforeEach(() => {
+ setFakeWxt({ config });
+ });
+
+ it.each<[string, string, PopupEntrypoint]>([
+ [
+ 'popup.html',
+ `
+
+
+
+ Default Title
+
+
+ `,
+ {
+ type: 'popup',
+ name: 'popup',
+ inputPath: resolve(config.entrypointsDir, 'popup.html'),
+ outputDir: config.outDir,
+ options: {
+ defaultIcon: { '16': '/icon/16.png' },
+ defaultTitle: 'Default Title',
+ },
+ skipped: false,
+ },
+ ],
+ [
+ 'popup/index.html',
+ `
+
+
+ Title
+
+
+ `,
+ {
+ type: 'popup',
+ name: 'popup',
+ inputPath: resolve(config.entrypointsDir, 'popup/index.html'),
+ outputDir: config.outDir,
+ options: {
+ defaultTitle: 'Title',
+ },
+ skipped: false,
+ },
+ ],
+ ])(
+ 'should find and load popup entrypoint config from %s',
+ async (path, content, expected) => {
+ globMock.mockResolvedValueOnce([path]);
+ readFileMock.mockResolvedValueOnce(content);
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toHaveLength(1);
+ expect(entrypoints[0]).toEqual(expected);
+ },
+ );
+
+ it.each<[string, string, OptionsEntrypoint]>([
+ [
+ 'options.html',
+ `
+
+
+ Default Title
+
+
+ `,
+ {
+ type: 'options',
+ name: 'options',
+ inputPath: resolve(config.entrypointsDir, 'options.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'options/index.html',
+ `
+
+
+
+ Title
+
+
+ `,
+ {
+ type: 'options',
+ name: 'options',
+ inputPath: resolve(config.entrypointsDir, 'options/index.html'),
+ outputDir: config.outDir,
+ options: {
+ openInTab: true,
+ },
+ skipped: false,
+ },
+ ],
+ ])(
+ 'should find and load options entrypoint config from %s',
+ async (path, content, expected) => {
+ globMock.mockResolvedValueOnce([path]);
+ readFileMock.mockResolvedValueOnce(content);
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toHaveLength(1);
+ expect(entrypoints[0]).toEqual(expected);
+ },
+ );
+
+ it.each<[string, Omit]>([
+ [
+ 'content.ts',
+ {
+ type: 'content-script',
+ name: 'content',
+ inputPath: resolve(config.entrypointsDir, 'content.ts'),
+ outputDir: resolve(config.outDir, 'content-scripts'),
+ skipped: false,
+ },
+ ],
+ [
+ 'overlay.content.ts',
+ {
+ type: 'content-script',
+ name: 'overlay',
+ inputPath: resolve(config.entrypointsDir, 'overlay.content.ts'),
+ outputDir: resolve(config.outDir, 'content-scripts'),
+ skipped: false,
+ },
+ ],
+ [
+ 'content/index.ts',
+ {
+ type: 'content-script',
+ name: 'content',
+ inputPath: resolve(config.entrypointsDir, 'content/index.ts'),
+ outputDir: resolve(config.outDir, 'content-scripts'),
+ skipped: false,
+ },
+ ],
+ [
+ 'overlay.content/index.ts',
+ {
+ type: 'content-script',
+ name: 'overlay',
+ inputPath: resolve(config.entrypointsDir, 'overlay.content/index.ts'),
+ outputDir: resolve(config.outDir, 'content-scripts'),
+ skipped: false,
+ },
+ ],
+ [
+ 'overlay.content.tsx',
+ {
+ type: 'content-script',
+ name: 'overlay',
+ inputPath: resolve(config.entrypointsDir, 'overlay.content.tsx'),
+ outputDir: resolve(config.outDir, 'content-scripts'),
+ skipped: false,
+ },
+ ],
+ ])(
+ 'should find and load content script entrypoint config from %s',
+ async (path, expected) => {
+ const options: ContentScriptEntrypoint['options'] = {
+ matches: [''],
+ };
+ globMock.mockResolvedValueOnce([path]);
+ importEntrypointFileMock.mockResolvedValue(options);
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toHaveLength(1);
+ expect(entrypoints[0]).toEqual({ ...expected, options });
+ expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
+ },
+ );
+
+ it.each<[string, Omit]>([
+ [
+ 'background.ts',
+ {
+ type: 'background',
+ name: 'background',
+ inputPath: resolve(config.entrypointsDir, 'background.ts'),
+ outputDir: config.outDir,
+ skipped: false,
+ },
+ ],
+ [
+ 'background/index.ts',
+ {
+ type: 'background',
+ name: 'background',
+ inputPath: resolve(config.entrypointsDir, 'background/index.ts'),
+ outputDir: config.outDir,
+ skipped: false,
+ },
+ ],
+ ])(
+ 'should find and load background entrypoint config from %s',
+ async (path, expected) => {
+ const options: BackgroundEntrypointOptions = {
+ type: 'module',
+ };
+ globMock.mockResolvedValueOnce([path]);
+ importEntrypointFileMock.mockResolvedValue(options);
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toHaveLength(1);
+ expect(entrypoints[0]).toEqual({ ...expected, options });
+ expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
+ },
+ );
+
+ it.each<[string, string, SidepanelEntrypoint]>([
+ [
+ 'sidepanel.html',
+ `
+
+
+ Default Title
+
+
+
+
+ `,
+ {
+ type: 'sidepanel',
+ name: 'sidepanel',
+ inputPath: resolve(config.entrypointsDir, 'sidepanel.html'),
+ outputDir: config.outDir,
+ options: {
+ defaultTitle: 'Default Title',
+ defaultIcon: { '16': '/icon/16.png' },
+ openAtInstall: true,
+ },
+ skipped: false,
+ },
+ ],
+ [
+ 'sidepanel/index.html',
+ ``,
+ {
+ type: 'sidepanel',
+ name: 'sidepanel',
+ inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'),
+ options: {},
+ outputDir: config.outDir,
+ skipped: false,
+ },
+ ],
+ [
+ 'named.sidepanel.html',
+ ``,
+ {
+ type: 'sidepanel',
+ name: 'named',
+ inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'),
+ options: {},
+ outputDir: config.outDir,
+ skipped: false,
+ },
+ ],
+ [
+ 'named.sidepanel/index.html',
+ ``,
+ {
+ type: 'sidepanel',
+ name: 'named',
+ inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+ ])(
+ 'should find and load sidepanel entrypoint config from %s',
+ async (path, content, expected) => {
+ globMock.mockResolvedValueOnce([path]);
+ readFileMock.mockResolvedValueOnce(content);
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toHaveLength(1);
+ expect(entrypoints[0]).toEqual(expected);
+ },
+ );
+
+ it('should remove type=module from MV2 background scripts', async () => {
+ setFakeWxt({
+ config: {
+ manifestVersion: 2,
+ },
+ });
+ const options: BackgroundEntrypointOptions = {
+ type: 'module',
+ };
+ globMock.mockResolvedValueOnce(['background.ts']);
+ importEntrypointFileMock.mockResolvedValue(options);
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints[0].options).toEqual({});
+ });
+
+ it('should allow type=module for MV3 background service workers', async () => {
+ setFakeWxt({
+ config: {
+ manifestVersion: 3,
+ },
+ });
+ const options: BackgroundEntrypointOptions = {
+ type: 'module',
+ };
+ globMock.mockResolvedValueOnce(['background.ts']);
+ importEntrypointFileMock.mockResolvedValue(options);
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints[0].options).toEqual(options);
+ });
+
+ it("should include a virtual background script so dev reloading works when there isn't a background entrypoint defined by the user", async () => {
+ setFakeWxt({
+ config: {
+ ...config,
+ command: 'serve',
+ },
+ });
+ globMock.mockResolvedValueOnce(['popup.html']);
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toHaveLength(2);
+ expect(entrypoints).toContainEqual({
+ type: 'background',
+ inputPath: 'virtual:user-background',
+ name: 'background',
+ options: {},
+ outputDir: config.outDir,
+ skipped: false,
+ });
+ });
+
+ it.each([
+ 'injected.ts',
+ 'injected.tsx',
+ 'injected.js',
+ 'injected.jsx',
+ 'injected/index.ts',
+ 'injected/index.tsx',
+ 'injected/index.js',
+ 'injected/index.jsx',
+ ])(
+ 'should find and load unlisted-script entrypoint config from %s',
+ async (path) => {
+ const expected = {
+ type: 'unlisted-script',
+ name: 'injected',
+ inputPath: resolve(config.entrypointsDir, path),
+ outputDir: config.outDir,
+ skipped: false,
+ };
+ const options: BaseEntrypointOptions = {};
+ globMock.mockResolvedValueOnce([path]);
+ importEntrypointFileMock.mockResolvedValue(options);
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toHaveLength(1);
+ expect(entrypoints[0]).toEqual({ ...expected, options });
+ expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
+ },
+ );
+
+ it.each<[string, GenericEntrypoint]>([
+ // Sandbox
+ [
+ 'sandbox.html',
+ {
+ type: 'sandbox',
+ name: 'sandbox',
+ inputPath: resolve(config.entrypointsDir, 'sandbox.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'sandbox/index.html',
+ {
+ type: 'sandbox',
+ name: 'sandbox',
+ inputPath: resolve(config.entrypointsDir, 'sandbox/index.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'named.sandbox.html',
+ {
+ type: 'sandbox',
+ name: 'named',
+ inputPath: resolve(config.entrypointsDir, 'named.sandbox.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'named.sandbox/index.html',
+ {
+ type: 'sandbox',
+ name: 'named',
+ inputPath: resolve(config.entrypointsDir, 'named.sandbox/index.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+
+ // bookmarks
+ [
+ 'bookmarks.html',
+ {
+ type: 'bookmarks',
+ name: 'bookmarks',
+ inputPath: resolve(config.entrypointsDir, 'bookmarks.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'bookmarks/index.html',
+ {
+ type: 'bookmarks',
+ name: 'bookmarks',
+ inputPath: resolve(config.entrypointsDir, 'bookmarks/index.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+
+ // history
+ [
+ 'history.html',
+ {
+ type: 'history',
+ name: 'history',
+ inputPath: resolve(config.entrypointsDir, 'history.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'history/index.html',
+ {
+ type: 'history',
+ name: 'history',
+ inputPath: resolve(config.entrypointsDir, 'history/index.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+
+ // newtab
+ [
+ 'newtab.html',
+ {
+ type: 'newtab',
+ name: 'newtab',
+ inputPath: resolve(config.entrypointsDir, 'newtab.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'newtab/index.html',
+ {
+ type: 'newtab',
+ name: 'newtab',
+ inputPath: resolve(config.entrypointsDir, 'newtab/index.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+
+ // devtools
+ [
+ 'devtools.html',
+ {
+ type: 'devtools',
+ name: 'devtools',
+ inputPath: resolve(config.entrypointsDir, 'devtools.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'devtools/index.html',
+ {
+ type: 'devtools',
+ name: 'devtools',
+ inputPath: resolve(config.entrypointsDir, 'devtools/index.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+
+ // unlisted-page
+ [
+ 'onboarding.html',
+ {
+ type: 'unlisted-page',
+ name: 'onboarding',
+ inputPath: resolve(config.entrypointsDir, 'onboarding.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'onboarding/index.html',
+ {
+ type: 'unlisted-page',
+ name: 'onboarding',
+ inputPath: resolve(config.entrypointsDir, 'onboarding/index.html'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+
+ // unlisted-style
+ [
+ 'iframe.scss',
+ {
+ type: 'unlisted-style',
+ name: 'iframe',
+ inputPath: resolve(config.entrypointsDir, 'iframe.scss'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'iframe.css',
+ {
+ type: 'unlisted-style',
+ name: 'iframe',
+ inputPath: resolve(config.entrypointsDir, 'iframe.css'),
+ outputDir: config.outDir,
+ options: {},
+ skipped: false,
+ },
+ ],
+
+ // content-script-style
+ [
+ 'content.css',
+ {
+ type: 'content-script-style',
+ name: 'content',
+ inputPath: resolve(config.entrypointsDir, 'content.css'),
+ outputDir: resolve(config.outDir, 'content-scripts'),
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'overlay.content.css',
+ {
+ type: 'content-script-style',
+ name: 'overlay',
+ inputPath: resolve(config.entrypointsDir, 'overlay.content.css'),
+ outputDir: resolve(config.outDir, 'content-scripts'),
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'content/index.css',
+ {
+ type: 'content-script-style',
+ name: 'content',
+ inputPath: resolve(config.entrypointsDir, 'content/index.css'),
+ outputDir: resolve(config.outDir, 'content-scripts'),
+ options: {},
+ skipped: false,
+ },
+ ],
+ [
+ 'overlay.content/index.css',
+ {
+ type: 'content-script-style',
+ name: 'overlay',
+ inputPath: resolve(config.entrypointsDir, 'overlay.content/index.css'),
+ outputDir: resolve(config.outDir, 'content-scripts'),
+ options: {},
+ skipped: false,
+ },
+ ],
+ ])('should find entrypoint for %s', async (path, expected) => {
+ globMock.mockResolvedValueOnce([path]);
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toHaveLength(1);
+ expect(entrypoints[0]).toEqual(expected);
+ });
+
+ it('should not allow multiple entrypoints with the same name', async () => {
+ globMock.mockResolvedValueOnce([
+ 'options/index.html',
+ 'options/index.jsx',
+ 'popup.html',
+ 'popup/index.html',
+ 'popup/index.ts',
+ 'ui.html',
+ ]);
+
+ await expect(() => findEntrypoints()).rejects.toThrowError(
+ [
+ 'Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.',
+ '',
+ '- options',
+ ` - ${unnormalizePath('src/entrypoints/options/index.html')}`,
+ ` - ${unnormalizePath('src/entrypoints/options/index.jsx')}`,
+ '- popup',
+ ` - ${unnormalizePath('src/entrypoints/popup.html')}`,
+ ` - ${unnormalizePath('src/entrypoints/popup/index.html')}`,
+ ` - ${unnormalizePath('src/entrypoints/popup/index.ts')}`,
+ ].join('\n'),
+ );
+ });
+
+ it('throw an error if there are no entrypoints', async () => {
+ globMock.mockResolvedValueOnce([]);
+
+ await expect(() => findEntrypoints()).rejects.toThrowError(
+ `No entrypoints found in ${unnormalizePath(config.entrypointsDir)}`,
+ );
+ });
+
+ describe('include option', () => {
+ it("should filter out the background when include doesn't contain the target browser", async () => {
+ globMock.mockResolvedValueOnce(['background.ts']);
+ importEntrypointFileMock.mockResolvedValue({
+ include: ['not' + config.browser],
+ });
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toEqual([]);
+ });
+
+ it("should filter out content scripts when include doesn't contain the target browser", async () => {
+ globMock.mockResolvedValueOnce(['example.content.ts']);
+ importEntrypointFileMock.mockResolvedValue({
+ include: ['not' + config.browser],
+ });
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toEqual([]);
+ });
+
+ it("should filter out the popup when include doesn't contain the target browser", async () => {
+ globMock.mockResolvedValueOnce(['popup.html']);
+ readFileMock.mockResolvedValueOnce(
+ `
+
+
+
+ `,
+ );
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toEqual([]);
+ });
+
+ it("should filter out the options page when include doesn't contain the target browser", async () => {
+ globMock.mockResolvedValueOnce(['options.html']);
+ readFileMock.mockResolvedValueOnce(
+ `
+
+
+
+ `,
+ );
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toEqual([]);
+ });
+
+ it("should filter out an unlisted page when include doesn't contain the target browser", async () => {
+ globMock.mockResolvedValueOnce(['unlisted.html']);
+ readFileMock.mockResolvedValueOnce(
+ `
+
+
+
+ `,
+ );
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toEqual([]);
+ });
+ });
+
+ describe('exclude option', () => {
+ it('should filter out the background when exclude contains the target browser', async () => {
+ globMock.mockResolvedValueOnce(['background.ts']);
+ importEntrypointFileMock.mockResolvedValue({
+ exclude: [config.browser],
+ });
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toEqual([]);
+ });
+
+ it('should filter out content scripts when exclude contains the target browser', async () => {
+ globMock.mockResolvedValueOnce(['example.content.ts']);
+ importEntrypointFileMock.mockResolvedValue({
+ exclude: [config.browser],
+ });
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toEqual([]);
+ });
+
+ it('should filter out the popup when exclude contains the target browser', async () => {
+ globMock.mockResolvedValueOnce(['popup.html']);
+ readFileMock.mockResolvedValueOnce(
+ `
+
+
+
+ `,
+ );
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toEqual([]);
+ });
+
+ it('should filter out the options page when exclude contains the target browser', async () => {
+ globMock.mockResolvedValueOnce(['options.html']);
+ readFileMock.mockResolvedValueOnce(
+ `
+
+
+
+ `,
+ );
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toEqual([]);
+ });
+
+ it('should filter out an unlisted page when exclude contains the target browser', async () => {
+ globMock.mockResolvedValueOnce(['unlisted.html']);
+ readFileMock.mockResolvedValueOnce(
+ `
+
+
+
+ `,
+ );
+
+ const entrypoints = await findEntrypoints();
+
+ expect(entrypoints).toEqual([]);
+ });
+ });
+
+ describe('filterEntrypoints option', () => {
+ it('should control entrypoints accessible', async () => {
+ globMock.mockResolvedValue([
+ 'options/index.html',
+ 'popup/index.html',
+ 'ui.content/index.ts',
+ 'injected.content/index.ts',
+ ]);
+ importEntrypointFileMock.mockResolvedValue({});
+ const filterEntrypoints = ['popup', 'ui'];
+ setFakeWxt({
+ config: {
+ root: '/',
+ entrypointsDir: resolve('/src/entrypoints'),
+ outDir: resolve('.output'),
+ command: 'build',
+ filterEntrypoints: new Set(filterEntrypoints),
+ },
+ });
+
+ const entrypoints = await findEntrypoints();
+ const names = entrypoints.map((item) => item.name);
+ expect(names).toHaveLength(2);
+ expect(names).toEqual(filterEntrypoints);
+ });
+ });
+});
diff --git a/packages/wxt/src/core/utils/building/__tests__/group-entrypoints.test.ts b/packages/wxt/src/core/utils/building/__tests__/group-entrypoints.test.ts
new file mode 100644
index 0000000..33c7caf
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/__tests__/group-entrypoints.test.ts
@@ -0,0 +1,174 @@
+import { describe, expect, it } from 'vitest';
+import { Entrypoint } from '~/types';
+import { groupEntrypoints } from '../group-entrypoints';
+import {
+ fakeBackgroundEntrypoint,
+ fakeGenericEntrypoint,
+ fakePopupEntrypoint,
+} from '../../testing/fake-objects';
+
+const background: Entrypoint = {
+ type: 'background',
+ name: 'background',
+ inputPath: '/background.ts',
+ outputDir: '/.output/background',
+ options: {},
+ skipped: false,
+};
+const contentScript: Entrypoint = {
+ type: 'content-script',
+ name: 'overlay',
+ inputPath: '/overlay.content.ts',
+ outputDir: '/.output/content-scripts/overlay',
+ options: {
+ matches: [''],
+ },
+ skipped: false,
+};
+const unlistedScript: Entrypoint = {
+ type: 'unlisted-script',
+ name: 'injected',
+ inputPath: '/injected.ts',
+ outputDir: '/.output/injected',
+ options: {},
+ skipped: false,
+};
+const popup: Entrypoint = {
+ type: 'popup',
+ name: 'popup',
+ inputPath: '/popup.html',
+ outputDir: '/.output/popup',
+ options: {},
+ skipped: false,
+};
+const unlistedPage: Entrypoint = {
+ type: 'unlisted-page',
+ name: 'onboarding',
+ inputPath: '/onboarding.html',
+ outputDir: '/.output/onboarding',
+ options: {},
+ skipped: false,
+};
+const options: Entrypoint = {
+ type: 'options',
+ name: 'options',
+ inputPath: '/options.html',
+ outputDir: '/.output/options',
+ options: {},
+ skipped: false,
+};
+const sandbox1: Entrypoint = {
+ type: 'sandbox',
+ name: 'sandbox',
+ inputPath: '/sandbox1.html',
+ outputDir: '/.output/sandbox1',
+ options: {},
+ skipped: false,
+};
+const sandbox2: Entrypoint = {
+ type: 'sandbox',
+ name: 'sandbox2',
+ inputPath: '/sandbox2.html',
+ outputDir: '/.output/sandbox2',
+ options: {},
+ skipped: false,
+};
+const unlistedStyle: Entrypoint = {
+ type: 'unlisted-style',
+ name: 'injected',
+ inputPath: '/injected.scss',
+ outputDir: '/.output',
+ options: {},
+ skipped: false,
+};
+const contentScriptStyle: Entrypoint = {
+ type: 'content-script-style',
+ name: 'injected',
+ inputPath: '/overlay.content.scss',
+ outputDir: '/.output/content-scripts',
+ options: {},
+ skipped: false,
+};
+
+describe('groupEntrypoints', () => {
+ it('should keep scripts separate', () => {
+ const entrypoints: Entrypoint[] = [
+ contentScript,
+ background,
+ unlistedScript,
+ popup,
+ ];
+ const expected = [contentScript, background, unlistedScript, [popup]];
+
+ const actual = groupEntrypoints(entrypoints);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should keep styles separate', () => {
+ const entrypoints: Entrypoint[] = [
+ unlistedStyle,
+ contentScriptStyle,
+ popup,
+ ];
+ const expected = [unlistedStyle, contentScriptStyle, [popup]];
+
+ const actual = groupEntrypoints(entrypoints);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should group extension pages together', () => {
+ const entrypoints: Entrypoint[] = [
+ popup,
+ background,
+ unlistedPage,
+ options,
+ sandbox1,
+ ];
+ const expected = [[popup, unlistedPage, options], background, [sandbox1]];
+
+ const actual = groupEntrypoints(entrypoints);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should group sandbox pages together', () => {
+ const entrypoints: Entrypoint[] = [
+ sandbox1,
+ popup,
+ sandbox2,
+ contentScript,
+ ];
+ const expected = [[sandbox1, sandbox2], [popup], contentScript];
+
+ const actual = groupEntrypoints(entrypoints);
+
+ expect(actual).toEqual(expected);
+ });
+
+ it('should group ESM compatible scripts with extension pages', () => {
+ const background = fakeBackgroundEntrypoint({
+ options: {
+ type: 'module',
+ },
+ });
+ const popup = fakePopupEntrypoint();
+ const sandbox = fakeGenericEntrypoint({
+ inputPath: '/entrypoints/sandbox.html',
+ name: 'sandbox',
+ type: 'sandbox',
+ });
+
+ const actual = groupEntrypoints([background, popup, sandbox]);
+
+ expect(actual).toEqual([[background, popup], [sandbox]]);
+ });
+
+ it.todo(
+ 'should group ESM compatible sandbox scripts with sandbox pages',
+ () => {
+ // Main world content scripts
+ },
+ );
+});
diff --git a/packages/wxt/src/core/utils/building/__tests__/import-entrypoint.test.ts b/packages/wxt/src/core/utils/building/__tests__/import-entrypoint.test.ts
new file mode 100644
index 0000000..529e170
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/__tests__/import-entrypoint.test.ts
@@ -0,0 +1,52 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+import { importEntrypointFile } from '~/core/utils/building';
+import { resolve } from 'node:path';
+import { setFakeWxt } from '~/core/utils/testing/fake-objects';
+
+const entrypointPath = (filename: string) =>
+ resolve(__dirname, 'test-entrypoints', filename);
+
+describe('importEntrypointFile', () => {
+ beforeEach(() => {
+ setFakeWxt({
+ config: {
+ imports: false,
+ debug: false,
+ // Run inside the demo folder so that wxt is in the node_modules
+ // WXT must also be built for these tests to pass
+ root: 'demo',
+ },
+ });
+ });
+
+ it.each([
+ ['background.ts', { main: expect.any(Function) }],
+ ['content.ts', { main: expect.any(Function), matches: [''] }],
+ ['unlisted.ts', { main: expect.any(Function) }],
+ ['react.tsx', { main: expect.any(Function) }],
+ ['with-named.ts', { main: expect.any(Function) }],
+ ])(
+ 'should return the default export of test-entrypoints/%s',
+ async (file, expected) => {
+ const actual = await importEntrypointFile(entrypointPath(file));
+
+ expect(actual).toEqual(expected);
+ },
+ );
+
+ it('should return undefined when there is no default export', async () => {
+ const actual = await importEntrypointFile(
+ entrypointPath('no-default-export.ts'),
+ );
+
+ expect(actual).toBeUndefined();
+ });
+
+ it('should throw a custom error message when an imported variable is used before main', async () => {
+ await expect(() =>
+ importEntrypointFile(entrypointPath('imported-option.ts')),
+ ).rejects.toThrowError(
+ `imported-option.ts: Cannot use imported variable "faker" outside the main function.`,
+ );
+ });
+});
diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/background.ts b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/background.ts
new file mode 100644
index 0000000..2ea54a2
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/background.ts
@@ -0,0 +1,5 @@
+import { defineBackground } from '~/sandbox';
+
+export default defineBackground({
+ main() {},
+});
diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/content.ts b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/content.ts
new file mode 100644
index 0000000..332c618
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/content.ts
@@ -0,0 +1,6 @@
+import { defineContentScript } from '~/sandbox';
+
+export default defineContentScript({
+ matches: [''],
+ main() {},
+});
diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/imported-option.ts b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/imported-option.ts
new file mode 100644
index 0000000..ba8cf15
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/imported-option.ts
@@ -0,0 +1,7 @@
+import { defineContentScript } from '~/sandbox';
+import { faker } from '@faker-js/faker';
+
+export default defineContentScript({
+ matches: [faker.string.nanoid()],
+ main() {},
+});
diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/no-default-export.ts b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/no-default-export.ts
new file mode 100644
index 0000000..e69de29
diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/react.tsx b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/react.tsx
new file mode 100644
index 0000000..95a4939
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/react.tsx
@@ -0,0 +1,3 @@
+import { defineUnlistedScript } from '~/sandbox';
+
+export default defineUnlistedScript(() => {});
diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/unlisted.ts b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/unlisted.ts
new file mode 100644
index 0000000..95a4939
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/unlisted.ts
@@ -0,0 +1,3 @@
+import { defineUnlistedScript } from '~/sandbox';
+
+export default defineUnlistedScript(() => {});
diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/with-named.ts b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/with-named.ts
new file mode 100644
index 0000000..a344ce1
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/with-named.ts
@@ -0,0 +1,5 @@
+import { defineBackground } from '~/sandbox';
+
+export const a = {};
+
+export default defineBackground(() => {});
diff --git a/packages/wxt/src/core/utils/building/build-entrypoints.ts b/packages/wxt/src/core/utils/building/build-entrypoints.ts
new file mode 100644
index 0000000..aa718f7
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/build-entrypoints.ts
@@ -0,0 +1,52 @@
+import { BuildOutput, BuildStepOutput, EntrypointGroup } from '~/types';
+import { getPublicFiles } from '~/core/utils/fs';
+import fs from 'fs-extra';
+import { dirname, resolve } from 'path';
+import type { Ora } from 'ora';
+import pc from 'picocolors';
+import { wxt } from '../../wxt';
+import { toArray } from '../arrays';
+
+export async function buildEntrypoints(
+ groups: EntrypointGroup[],
+ spinner: Ora,
+): Promise> {
+ const steps: BuildStepOutput[] = [];
+ for (let i = 0; i < groups.length; i++) {
+ const group = groups[i];
+ const groupNames = toArray(group).map((e) => e.name);
+ const groupNameColored = groupNames.join(pc.dim(', '));
+ spinner.text = pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNameColored}`;
+ try {
+ steps.push(await wxt.builder.build(group));
+ } catch (err) {
+ // spinner.stop().clear();
+ console.error(err);
+ // console.error(err);
+ throw Error(`Failed to build ${groupNames.join(', ')}`, { cause: err });
+ }
+ }
+ const publicAssets = await copyPublicDirectory();
+
+ return { publicAssets, steps };
+}
+
+async function copyPublicDirectory(): Promise {
+ const files = await getPublicFiles();
+ if (files.length === 0) return [];
+
+ const publicAssets: BuildOutput['publicAssets'] = [];
+ for (const file of files) {
+ const srcPath = resolve(wxt.config.publicDir, file);
+ const outPath = resolve(wxt.config.outDir, file);
+
+ await fs.ensureDir(dirname(outPath));
+ await fs.copyFile(srcPath, outPath);
+ publicAssets.push({
+ type: 'asset',
+ fileName: file,
+ });
+ }
+
+ return publicAssets;
+}
diff --git a/packages/wxt/src/core/utils/building/detect-dev-changes.ts b/packages/wxt/src/core/utils/building/detect-dev-changes.ts
new file mode 100644
index 0000000..a657264
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/detect-dev-changes.ts
@@ -0,0 +1,203 @@
+import {
+ BuildOutput,
+ BuildStepOutput,
+ EntrypointGroup,
+ OutputAsset,
+ OutputFile,
+} from '~/types';
+import { every, some } from '~/core/utils/arrays';
+import { normalizePath } from '~/core/utils/paths';
+import { wxt } from '../../wxt';
+
+/**
+ * Compare the changed files vs the build output and determine what kind of reload needs to happen:
+ *
+ * - Do nothing
+ * - CSS or JS file associated with an HTML page is changed - this is handled automatically by the
+ * dev server
+ * - Change isn't used by any of the entrypoints
+ * - Reload Content script
+ * - CSS or JS file associated with a content script
+ * - Background script will be told to reload the content script
+ * - Reload HTML file
+ * - HTML file itself is saved - HMR doesn't handle this because the HTML pages are pre-rendered
+ * - Chrome is OK reloading the page when the HTML file is changed without reloading the whole
+ * extension. Not sure about firefox, this might need to change to an extension reload
+ * - Reload extension
+ * - Background script is changed
+ * - Manifest is different
+ * - Restart browser
+ * - Config file changed (wxt.config.ts, .env, web-ext.config.ts, etc)
+ */
+export function detectDevChanges(
+ changedFiles: string[],
+ currentOutput: BuildOutput,
+): DevModeChange {
+ const isConfigChange = some(
+ changedFiles,
+ (file) => file === wxt.config.userConfigMetadata.configFile,
+ );
+ if (isConfigChange) return { type: 'full-restart' };
+
+ const isRunnerChange = some(
+ changedFiles,
+ (file) => file === wxt.config.runnerConfig.configFile,
+ );
+ if (isRunnerChange) return { type: 'browser-restart' };
+
+ const changedSteps = new Set(
+ changedFiles.flatMap((changedFile) =>
+ findEffectedSteps(changedFile, currentOutput),
+ ),
+ );
+ if (changedSteps.size === 0) return { type: 'no-change' };
+
+ const unchangedOutput: BuildOutput = {
+ manifest: currentOutput.manifest,
+ steps: [],
+ publicAssets: [],
+ };
+ const changedOutput: BuildOutput = {
+ manifest: currentOutput.manifest,
+ steps: [],
+ publicAssets: [],
+ };
+
+ for (const step of currentOutput.steps) {
+ if (changedSteps.has(step)) {
+ changedOutput.steps.push(step);
+ } else {
+ unchangedOutput.steps.push(step);
+ }
+ }
+ for (const asset of currentOutput.publicAssets) {
+ if (changedSteps.has(asset)) {
+ changedOutput.publicAssets.push(asset);
+ } else {
+ unchangedOutput.publicAssets.push(asset);
+ }
+ }
+
+ const isOnlyHtmlChanges =
+ changedFiles.length > 0 &&
+ every(changedFiles, (file) => file.endsWith('.html'));
+ if (isOnlyHtmlChanges) {
+ return {
+ type: 'html-reload',
+ cachedOutput: unchangedOutput,
+ rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),
+ };
+ }
+
+ const isOnlyContentScripts =
+ changedOutput.steps.length > 0 &&
+ every(
+ changedOutput.steps.flatMap((step) => step.entrypoints),
+ (entry) => entry.type === 'content-script',
+ );
+ if (isOnlyContentScripts) {
+ return {
+ type: 'content-script-reload',
+ cachedOutput: unchangedOutput,
+ changedSteps: changedOutput.steps,
+ rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),
+ };
+ }
+
+ return {
+ type: 'extension-reload',
+ cachedOutput: unchangedOutput,
+ rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),
+ };
+}
+
+/**
+ * For a single change, return all the step of the build output that were effected by it.
+ */
+function findEffectedSteps(
+ changedFile: string,
+ currentOutput: BuildOutput,
+): DetectedChange[] {
+ const changes: DetectedChange[] = [];
+ const changedPath = normalizePath(changedFile);
+
+ const isChunkEffected = (chunk: OutputFile): boolean =>
+ // If it's an HTML file with the same path, is is effected because HTML files need to be re-rendered
+ // - fileName is normalized, relative bundle path, ".html"
+ (chunk.type === 'asset' &&
+ changedPath.replace('/index.html', '.html').endsWith(chunk.fileName)) ||
+ // If it's a chunk that depends on the changed file, it is effected
+ // - moduleIds are absolute, normalized paths
+ (chunk.type === 'chunk' && chunk.moduleIds.includes(changedPath));
+
+ for (const step of currentOutput.steps) {
+ const effectedChunk = step.chunks.find((chunk) => isChunkEffected(chunk));
+ if (effectedChunk) changes.push(step);
+ }
+
+ const effectedAsset = currentOutput.publicAssets.find((chunk) =>
+ isChunkEffected(chunk),
+ );
+ if (effectedAsset) changes.push(effectedAsset);
+
+ return changes;
+}
+
+/**
+ * Contains information about what files changed, what needs rebuilt, and the type of reload that is
+ * required.
+ */
+export type DevModeChange =
+ | NoChange
+ | HtmlReload
+ | ExtensionReload
+ | ContentScriptReload
+ | FullRestart
+ | BrowserRestart;
+
+interface NoChange {
+ type: 'no-change';
+}
+
+interface RebuildChange {
+ /**
+ * The list of entrypoints that need rebuilt.
+ */
+ rebuildGroups: EntrypointGroup[];
+ /**
+ * The previous output stripped of any files are going to change.
+ */
+ cachedOutput: BuildOutput;
+}
+
+interface FullRestart {
+ type: 'full-restart';
+}
+
+interface BrowserRestart {
+ type: 'browser-restart';
+}
+
+interface HtmlReload extends RebuildChange {
+ type: 'html-reload';
+}
+
+interface ExtensionReload extends RebuildChange {
+ type: 'extension-reload';
+}
+
+// interface BrowserRestart extends RebuildChange {
+// type: 'browser-restart';
+// }
+
+interface ContentScriptReload extends RebuildChange {
+ type: 'content-script-reload';
+ changedSteps: BuildStepOutput[];
+}
+
+/**
+ * When figuring out what needs reloaded, this stores the step that was changed, or the public
+ * directory asset that was changed. It doesn't know what type of change is required yet. Just an
+ * intermediate type.
+ */
+type DetectedChange = BuildStepOutput | OutputAsset;
diff --git a/packages/wxt/src/core/utils/building/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts
new file mode 100644
index 0000000..d4dd3f6
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts
@@ -0,0 +1,441 @@
+import { relative, resolve } from 'path';
+import {
+ BackgroundEntrypoint,
+ BackgroundDefinition,
+ BaseEntrypointOptions,
+ ContentScriptDefinition,
+ ContentScriptEntrypoint,
+ Entrypoint,
+ GenericEntrypoint,
+ OptionsEntrypoint,
+ PopupEntrypoint,
+ UnlistedScriptDefinition,
+ PopupEntrypointOptions,
+ OptionsEntrypointOptions,
+ SidepanelEntrypoint,
+ SidepanelEntrypointOptions,
+} from '~/types';
+import fs from 'fs-extra';
+import { minimatch } from 'minimatch';
+import { parseHTML } from 'linkedom';
+import JSON5 from 'json5';
+import glob from 'fast-glob';
+import { getEntrypointName, resolvePerBrowserOptions } from '~/core/utils/entrypoints';
+import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
+import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
+import pc from 'picocolors';
+import { wxt } from '../../wxt';
+import { importEntrypointFile } from './import-entrypoint';
+
+/**
+ * Return entrypoints and their configuration by looking through the project's files.
+ */
+export async function findEntrypoints(): Promise {
+ // Make sure required TSConfig file exists to load dependencies
+ await fs.mkdir(wxt.config.wxtDir, { recursive: true });
+ await fs.writeJson(resolve(wxt.config.wxtDir, 'tsconfig.json'), {});
+
+ const relativePaths = await glob(Object.keys(PATH_GLOB_TO_TYPE_MAP), {
+ cwd: wxt.config.entrypointsDir,
+ });
+ // Ensure consistent output
+ relativePaths.sort();
+
+ const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP);
+ const entrypointInfos: EntrypointInfo[] = relativePaths.reduce((results, relativePath) => {
+ const inputPath = resolve(wxt.config.entrypointsDir, relativePath);
+ const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);
+ const matchingGlob = pathGlobs.find((glob) => minimatch(relativePath, glob));
+ if (matchingGlob) {
+ const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
+ results.push({
+ name,
+ inputPath,
+ type,
+ skipped: wxt.config.filterEntrypoints != null && !wxt.config.filterEntrypoints.has(name),
+ });
+ }
+ return results;
+ }, []);
+
+ // Validation
+ preventNoEntrypoints(entrypointInfos);
+ preventDuplicateEntrypointNames(entrypointInfos);
+
+ // Import entrypoints to get their config
+ let hasBackground = false;
+ const entrypoints: Entrypoint[] = await Promise.all(
+ entrypointInfos.map(async (info): Promise => {
+ 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,
+ },
+ };
+ }
+ }),
+ );
+
+ if (wxt.config.command === 'serve' && !hasBackground) {
+ entrypoints.push(
+ await getBackgroundEntrypoint({
+ inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
+ name: 'background',
+ type: 'background',
+ skipped: false,
+ }),
+ );
+ }
+
+ wxt.logger.debug('All entrypoints:', entrypoints);
+ const skippedEntrypointNames = entrypointInfos.filter((item) => item.skipped).map((item) => item.name);
+ if (skippedEntrypointNames.length) {
+ console.warn(
+ `Filter excluded the following entrypoints:\n${skippedEntrypointNames
+ .map((item) => `${pc.dim('-')} ${pc.cyan(item)}`)
+ .join('\n')}`,
+ );
+ }
+ const targetEntrypoints = entrypoints.filter((entry) => {
+ const { include, exclude } = entry.options;
+ if (include?.length && exclude?.length) {
+ console.warn(
+ `The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint ignored.`,
+ );
+ return false;
+ }
+ if (exclude?.length && !include?.length) {
+ return !exclude.includes(wxt.config.browser);
+ }
+ if (include?.length && !exclude?.length) {
+ return include.includes(wxt.config.browser);
+ }
+ if (skippedEntrypointNames.includes(entry.name)) {
+ return false;
+ }
+
+ return true;
+ });
+ wxt.logger.debug(`${wxt.config.browser} entrypoints:`, targetEntrypoints);
+ await wxt.hooks.callHook('entrypoints:resolved', wxt, targetEntrypoints);
+
+ return targetEntrypoints;
+}
+
+interface EntrypointInfo {
+ name: string;
+ inputPath: string;
+ type: Entrypoint['type'];
+ /**
+ * @default false
+ */
+ skipped: boolean;
+}
+
+function preventDuplicateEntrypointNames(files: EntrypointInfo[]) {
+ const namesToPaths = files.reduce>((map, { name, inputPath }) => {
+ map[name] ??= [];
+ map[name].push(inputPath);
+ return map;
+ }, {});
+ const errorLines = Object.entries(namesToPaths).reduce((lines, [name, absolutePaths]) => {
+ if (absolutePaths.length > 1) {
+ lines.push(`- ${name}`);
+ absolutePaths.forEach((absolutePath) => {
+ lines.push(` - ${relative(wxt.config.root, absolutePath)}`);
+ });
+ }
+ return lines;
+ }, []);
+ if (errorLines.length > 0) {
+ const errorContent = errorLines.join('\n');
+ throw Error(
+ `Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.\n\n${errorContent}`,
+ );
+ }
+}
+
+function preventNoEntrypoints(files: EntrypointInfo[]) {
+ if (files.length === 0) {
+ throw Error(`No entrypoints found in ${wxt.config.entrypointsDir}`);
+ }
+}
+
+async function getPopupEntrypoint(info: EntrypointInfo): Promise {
+ const options = await getHtmlEntrypointOptions(
+ info,
+ {
+ browserStyle: 'browse_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'),
+ },
+ );
+
+ return {
+ type: 'popup',
+ name: 'popup',
+ options: resolvePerBrowserOptions(options, wxt.config.browser),
+ inputPath: info.inputPath,
+ outputDir: wxt.config.outDir,
+ skipped: info.skipped,
+ };
+}
+
+async function getOptionsEntrypoint(info: EntrypointInfo): Promise {
+ const options = await getHtmlEntrypointOptions(info, {
+ browserStyle: 'browse_style',
+ chromeStyle: 'chrome_style',
+ exclude: 'exclude',
+ include: 'include',
+ openInTab: 'open_in_tab',
+ });
+ return {
+ type: 'options',
+ name: 'options',
+ options: resolvePerBrowserOptions(options, wxt.config.browser),
+ inputPath: info.inputPath,
+ outputDir: wxt.config.outDir,
+ skipped: info.skipped,
+ };
+}
+
+async function getUnlistedPageEntrypoint(info: EntrypointInfo): Promise {
+ const options = await getHtmlEntrypointOptions(info, {
+ exclude: 'exclude',
+ include: 'include',
+ });
+
+ return {
+ type: 'unlisted-page',
+ name: info.name,
+ inputPath: info.inputPath,
+ outputDir: wxt.config.outDir,
+ options,
+ skipped: info.skipped,
+ };
+}
+
+async function getUnlistedScriptEntrypoint({ inputPath, name, skipped }: EntrypointInfo): Promise {
+ const defaultExport = await importEntrypoint(inputPath);
+ if (defaultExport == null) {
+ throw Error(
+ `${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`,
+ );
+ }
+ const { main: _, ...options } = defaultExport;
+ return {
+ type: 'unlisted-script',
+ name,
+ inputPath,
+ outputDir: wxt.config.outDir,
+ options: resolvePerBrowserOptions(options, wxt.config.browser),
+ skipped,
+ };
+}
+
+async function getBackgroundEntrypoint({ inputPath, name, skipped }: EntrypointInfo): Promise {
+ let options: Omit = {};
+ if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
+ const defaultExport = await importEntrypoint(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;
+ }
+
+ if (wxt.config.manifestVersion !== 3) {
+ delete options.type;
+ }
+
+ return {
+ type: 'background',
+ name,
+ inputPath,
+ outputDir: wxt.config.outDir,
+ options: resolvePerBrowserOptions(options, wxt.config.browser),
+ skipped,
+ };
+}
+
+async function getContentScriptEntrypoint({
+ inputPath,
+ name,
+ skipped,
+}: EntrypointInfo): Promise {
+ const { main: _, ...options } = await importEntrypoint(inputPath);
+ if (options == null) {
+ throw Error(`${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`);
+ }
+ return {
+ type: 'content-script',
+ name,
+ inputPath,
+ outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
+ options: resolvePerBrowserOptions(options, wxt.config.browser),
+ skipped,
+ };
+}
+
+async function getSidepanelEntrypoint(info: EntrypointInfo): Promise {
+ const options = await getHtmlEntrypointOptions(
+ info,
+ {
+ browserStyle: 'browse_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),
+ inputPath: info.inputPath,
+ outputDir: wxt.config.outDir,
+ skipped: info.skipped,
+ };
+}
+
+/**
+ * Parse the HTML tags to extract options from them.
+ */
+async function getHtmlEntrypointOptions(
+ info: EntrypointInfo,
+ keyMap: Record,
+ queries?: Partial<{
+ [key in keyof T]: (document: Document, manifestKey: string) => string | undefined;
+ }>,
+ parsers?: Partial<{ [key in keyof T]: (content: string) => T[key] }>,
+): Promise {
+ 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;
+}
+
+const PATH_GLOB_TO_TYPE_MAP: Record = {
+ 'sandbox.html': 'sandbox',
+ 'sandbox/index.html': 'sandbox',
+ '*.sandbox.html': 'sandbox',
+ '*.sandbox/index.html': 'sandbox',
+
+ 'bookmarks.html': 'bookmarks',
+ 'bookmarks/index.html': 'bookmarks',
+
+ 'history.html': 'history',
+ 'history/index.html': 'history',
+
+ 'newtab.html': 'newtab',
+ 'newtab/index.html': 'newtab',
+
+ 'sidepanel.html': 'sidepanel',
+ 'sidepanel/index.html': 'sidepanel',
+ '*.sidepanel.html': 'sidepanel',
+ '*.sidepanel/index.html': 'sidepanel',
+
+ 'devtools.html': 'devtools',
+ 'devtools/index.html': 'devtools',
+
+ 'background.[jt]s': 'background',
+ 'background/index.[jt]s': 'background',
+ [VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background',
+
+ 'content.[jt]s?(x)': 'content-script',
+ 'content/index.[jt]s?(x)': 'content-script',
+ '*.content.[jt]s?(x)': 'content-script',
+ '*.content/index.[jt]s?(x)': 'content-script',
+ [`content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
+ [`*.content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
+ [`content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
+ [`*.content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
+
+ 'popup.html': 'popup',
+ 'popup/index.html': 'popup',
+
+ 'options.html': 'options',
+ 'options/index.html': 'options',
+
+ '*.html': 'unlisted-page',
+ '*/index.html': 'unlisted-page',
+ '*.[jt]s?(x)': 'unlisted-script',
+ '*/index.[jt]s?(x)': 'unlisted-script',
+ [`*.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
+ [`*/index.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
+};
+
+const CONTENT_SCRIPT_OUT_DIR = 'content-scripts';
+
+function importEntrypoint(path: string) {
+ return wxt.config.experimental.viteRuntime ? wxt.builder.importEntrypoint(path) : importEntrypointFile(path);
+}
diff --git a/packages/wxt/src/core/utils/building/generate-wxt-dir.ts b/packages/wxt/src/core/utils/building/generate-wxt-dir.ts
new file mode 100644
index 0000000..a2585e6
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/generate-wxt-dir.ts
@@ -0,0 +1,249 @@
+import { Unimport, createUnimport } from 'unimport';
+import {
+ EslintGlobalsPropValue,
+ Entrypoint,
+ WxtResolvedUnimportOptions,
+} from '~/types';
+import fs from 'fs-extra';
+import { relative, resolve } from 'path';
+import {
+ getEntrypointBundlePath,
+ isHtmlEntrypoint,
+} from '~/core/utils/entrypoints';
+import { getEntrypointGlobals, getGlobals } from '~/core/utils/globals';
+import { normalizePath } from '~/core/utils/paths';
+import path from 'node:path';
+import { Message, parseI18nMessages } from '~/core/utils/i18n';
+import { writeFileIfDifferent, getPublicFiles } from '~/core/utils/fs';
+import { wxt } from '../../wxt';
+
+/**
+ * Generate and write all the files inside the `InternalConfig.typesDir` directory.
+ */
+export async function generateTypesDir(
+ entrypoints: Entrypoint[],
+): Promise {
+ await fs.ensureDir(wxt.config.typesDir);
+
+ const references: string[] = [];
+
+ if (wxt.config.imports !== false) {
+ const unimport = createUnimport(wxt.config.imports);
+ references.push(await writeImportsDeclarationFile(unimport));
+ if (wxt.config.imports.eslintrc.enabled) {
+ await writeImportsEslintFile(unimport, wxt.config.imports);
+ }
+ }
+
+ references.push(await writePathsDeclarationFile(entrypoints));
+ references.push(await writeI18nDeclarationFile());
+ references.push(await writeGlobalsDeclarationFile());
+
+ const mainReference = await writeMainDeclarationFile(references);
+ await writeTsConfigFile(mainReference);
+}
+
+async function writeImportsDeclarationFile(unimport: Unimport) {
+ const filePath = resolve(wxt.config.typesDir, 'imports.d.ts');
+
+ // Load project imports into unimport memory so they are output via generateTypeDeclarations
+ await unimport.scanImportsFromDir(undefined, { cwd: wxt.config.srcDir });
+
+ await writeFileIfDifferent(
+ filePath,
+ ['// Generated by wxt', await unimport.generateTypeDeclarations()].join(
+ '\n',
+ ) + '\n',
+ );
+
+ return filePath;
+}
+
+async function writeImportsEslintFile(
+ unimport: Unimport,
+ options: WxtResolvedUnimportOptions,
+) {
+ const globals: Record = {};
+ const eslintrc = { globals };
+
+ (await unimport.getImports())
+ .map((i) => i.as ?? i.name)
+ .filter(Boolean)
+ .sort()
+ .forEach((name) => {
+ eslintrc.globals[name] = options.eslintrc.globalsPropValue;
+ });
+ await fs.writeJson(options.eslintrc.filePath, eslintrc, { spaces: 2 });
+}
+
+async function writePathsDeclarationFile(
+ entrypoints: Entrypoint[],
+): Promise {
+ const filePath = resolve(wxt.config.typesDir, 'paths.d.ts');
+ const unions = entrypoints
+ .map((entry) =>
+ getEntrypointBundlePath(
+ entry,
+ wxt.config.outDir,
+ isHtmlEntrypoint(entry) ? '.html' : '.js',
+ ),
+ )
+ .concat(await getPublicFiles())
+ .map(normalizePath)
+ .map((path) => ` | "/${path}"`)
+ .sort()
+ .join('\n');
+
+ const template = `// Generated by wxt
+import "wxt/browser";
+
+declare module "wxt/browser" {
+ export type PublicPath =
+{{ union }}
+ type HtmlPublicPath = Extract
+ export interface WxtRuntime extends Runtime.Static {
+ getURL(path: PublicPath): string;
+ getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
+ }
+}
+`;
+
+ await writeFileIfDifferent(
+ filePath,
+ template.replace('{{ union }}', unions || ' | never'),
+ );
+
+ return filePath;
+}
+
+async function writeI18nDeclarationFile(): Promise {
+ const filePath = resolve(wxt.config.typesDir, 'i18n.d.ts');
+ const defaultLocale = wxt.config.manifest.default_locale;
+ const template = `// Generated by wxt
+import "wxt/browser";
+
+declare module "wxt/browser" {
+ /**
+ * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
+ */
+ interface GetMessageOptions {
+ /**
+ * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
+ */
+ escapeLt?: boolean
+ }
+
+ export interface WxtI18n extends I18n.Static {
+{{ overrides }}
+ }
+}
+`;
+
+ let messages: Message[];
+ if (defaultLocale) {
+ const defaultLocalePath = path.resolve(
+ wxt.config.publicDir,
+ '_locales',
+ defaultLocale,
+ 'messages.json',
+ );
+ const content = JSON.parse(await fs.readFile(defaultLocalePath, 'utf-8'));
+ messages = parseI18nMessages(content);
+ } else {
+ messages = parseI18nMessages({});
+ }
+
+ const overrides = messages.map((message) => {
+ return ` /**
+ * ${message.description || 'No message description.'}
+ *
+ * "${message.message}"
+ */
+ getMessage(
+ messageName: "${message.name}",
+ substitutions?: string | string[],
+ options?: GetMessageOptions,
+ ): string;`;
+ });
+ await writeFileIfDifferent(
+ filePath,
+ template.replace('{{ overrides }}', overrides.join('\n')),
+ );
+
+ return filePath;
+}
+
+async function writeGlobalsDeclarationFile(): Promise {
+ const filePath = resolve(wxt.config.typesDir, 'globals.d.ts');
+ const globals = [...getGlobals(wxt.config), ...getEntrypointGlobals('')];
+ await writeFileIfDifferent(
+ filePath,
+ [
+ '// Generated by wxt',
+ 'export {}',
+ 'interface ImportMetaEnv {',
+ ...globals.map((global) => ` readonly ${global.name}: ${global.type};`),
+ '}',
+ 'interface ImportMeta {',
+ ' readonly env: ImportMetaEnv',
+ '}',
+ ].join('\n') + '\n',
+ );
+ return filePath;
+}
+
+async function writeMainDeclarationFile(references: string[]): Promise {
+ const dir = wxt.config.wxtDir;
+ const filePath = resolve(dir, 'wxt.d.ts');
+ await writeFileIfDifferent(
+ filePath,
+ [
+ '// Generated by wxt',
+ `/// `,
+ ...references.map(
+ (ref) =>
+ `/// `,
+ ),
+ ].join('\n') + '\n',
+ );
+ return filePath;
+}
+
+async function writeTsConfigFile(mainReference: string) {
+ const dir = wxt.config.wxtDir;
+ const getTsconfigPath = (path: string) => normalizePath(relative(dir, path));
+ const paths = Object.entries(wxt.config.alias)
+ .flatMap(([alias, absolutePath]) => {
+ const aliasPath = getTsconfigPath(absolutePath);
+ return [
+ ` "${alias}": ["${aliasPath}"]`,
+ ` "${alias}/*": ["${aliasPath}/*"]`,
+ ];
+ })
+ .join(',\n');
+
+ await writeFileIfDifferent(
+ resolve(dir, 'tsconfig.json'),
+ `{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "noEmit": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "paths": {
+${paths}
+ }
+ },
+ "include": [
+ "${getTsconfigPath(wxt.config.root)}/**/*",
+ "./${getTsconfigPath(mainReference)}"
+ ],
+ "exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
+}`,
+ );
+}
diff --git a/packages/wxt/src/core/utils/building/group-entrypoints.ts b/packages/wxt/src/core/utils/building/group-entrypoints.ts
new file mode 100644
index 0000000..28a2f59
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/group-entrypoints.ts
@@ -0,0 +1,52 @@
+import { Entrypoint, EntrypointGroup } from '~/types';
+
+/**
+ * Entrypoints can be build in groups. HTML pages can all be built together in a single step, while
+ * content scripts must be build individually.
+ *
+ * This function returns the entrypoints put into these types of groups.
+ */
+export function groupEntrypoints(entrypoints: Entrypoint[]): EntrypointGroup[] {
+ const groupIndexMap: Partial> = {};
+ const groups: EntrypointGroup[] = [];
+
+ for (const entry of entrypoints) {
+ let group = ENTRY_TYPE_TO_GROUP_MAP[entry.type];
+ if (entry.type === 'background' && entry.options.type === 'module') {
+ group = 'esm';
+ }
+ if (group === 'individual') {
+ groups.push(entry);
+ } else {
+ let groupIndex = groupIndexMap[group];
+ if (groupIndex == null) {
+ groupIndex = groups.push([]) - 1;
+ groupIndexMap[group] = groupIndex;
+ }
+ (groups[groupIndex] as Entrypoint[]).push(entry);
+ }
+ }
+
+ return groups;
+}
+
+const ENTRY_TYPE_TO_GROUP_MAP: Record = {
+ sandbox: 'sandboxed-esm',
+
+ popup: 'esm',
+ newtab: 'esm',
+ history: 'esm',
+ options: 'esm',
+ devtools: 'esm',
+ bookmarks: 'esm',
+ sidepanel: 'esm',
+ 'unlisted-page': 'esm',
+
+ background: 'individual',
+ 'content-script': 'individual',
+ 'unlisted-script': 'individual',
+ 'unlisted-style': 'individual',
+ 'content-script-style': 'individual',
+};
+
+type Group = 'esm' | 'sandboxed-esm' | 'individual';
diff --git a/packages/wxt/src/core/utils/building/import-entrypoint.ts b/packages/wxt/src/core/utils/building/import-entrypoint.ts
new file mode 100644
index 0000000..a45b29e
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/import-entrypoint.ts
@@ -0,0 +1,117 @@
+import createJITI, { TransformOptions as JitiTransformOptions } from 'jiti';
+import { createUnimport } from 'unimport';
+import fs from 'fs-extra';
+import { relative, resolve } from 'node:path';
+import { removeProjectImportStatements } from '~/core/utils/strings';
+import { normalizePath } from '~/core/utils/paths';
+import { TransformOptions, transformSync } from 'esbuild';
+import { fileURLToPath } from 'node:url';
+import { wxt } from '../../wxt';
+
+/**
+ * Get the value from the default export of a `path`.
+ *
+ * It works by:
+ *
+ * 1. Reading the file text
+ * 2. Stripping all imports from it via regex
+ * 3. Auto-import only the client helper functions
+ *
+ * This prevents resolving imports of imports, speeding things up and preventing "xxx is not
+ * defined" errors.
+ *
+ * Downside is that code cannot be executed outside of the main fucntion for the entrypoint,
+ * otherwise you will see "xxx is not defined" errors for any imports used outside of main function.
+ */
+export async function importEntrypointFile(path: string): Promise {
+ wxt.logger.debug('Loading file metadata:', path);
+ // JITI & Babel uses normalized paths.
+ const normalPath = normalizePath(path);
+
+ const unimport = createUnimport({
+ ...wxt.config.imports,
+ // Only allow specific imports, not all from the project
+ dirs: [],
+ });
+ await unimport.init();
+
+ const text = await fs.readFile(path, 'utf-8');
+ const textNoImports = removeProjectImportStatements(text);
+ const { code } = await unimport.injectImports(textNoImports);
+ wxt.logger.debug(
+ ['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'),
+ );
+
+ const jiti = createJITI(
+ typeof __filename !== 'undefined'
+ ? __filename
+ : fileURLToPath(import.meta.url),
+ {
+ cache: false,
+ debug: wxt.config.debug,
+ esmResolve: true,
+ alias: {
+ 'webextension-polyfill': 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.
+ experimentalBun: false,
+ // List of extensions to transform with esbuild
+ extensions: [
+ '.ts',
+ '.cts',
+ '.mts',
+ '.tsx',
+ '.js',
+ '.cjs',
+ '.mjs',
+ '.jsx',
+ ],
+ transform(opts) {
+ const isEntrypoint = opts.filename === normalPath;
+ return transformSync(
+ // Use modified source code for entrypoints
+ isEntrypoint ? code : opts.source,
+ getEsbuildOptions(opts),
+ );
+ },
+ },
+ );
+
+ try {
+ const res = await jiti(path);
+ return res.default;
+ } catch (err) {
+ const filePath = relative(wxt.config.root, path);
+ if (err instanceof ReferenceError) {
+ // "XXX is not defined" - usually due to WXT removing imports
+ const variableName = err.message.replace(' is not defined', '');
+ throw Error(
+ `${filePath}: Cannot use imported variable "${variableName}" outside the main function. See https://wxt.dev/guide/entrypoints.html#side-effects`,
+ { cause: err },
+ );
+ } else {
+ console.error(err);
+ throw Error(`Failed to load entrypoint: ${filePath}`, { cause: err });
+ }
+ }
+}
+
+function getEsbuildOptions(opts: JitiTransformOptions): TransformOptions {
+ const isJsx = opts.filename?.endsWith('x');
+ return {
+ format: 'cjs',
+ loader: isJsx ? 'tsx' : 'ts',
+ ...(isJsx
+ ? {
+ // `h` and `Fragment` are undefined, but that's OK because JSX is never evaluated while
+ // grabbing the entrypoint's options.
+ jsxFactory: 'h',
+ jsxFragment: 'Fragment',
+ }
+ : undefined),
+ };
+}
diff --git a/packages/wxt/src/core/utils/building/index.ts b/packages/wxt/src/core/utils/building/index.ts
new file mode 100644
index 0000000..16bd6d0
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/index.ts
@@ -0,0 +1,9 @@
+export * from './build-entrypoints';
+export * from './detect-dev-changes';
+export * from './find-entrypoints';
+export * from './generate-wxt-dir';
+export * from './resolve-config';
+export * from './group-entrypoints';
+export * from './import-entrypoint';
+export * from './internal-build';
+export * from './rebuild';
diff --git a/packages/wxt/src/core/utils/building/internal-build.ts b/packages/wxt/src/core/utils/building/internal-build.ts
new file mode 100644
index 0000000..aee8373
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/internal-build.ts
@@ -0,0 +1,146 @@
+import { findEntrypoints } from './find-entrypoints';
+import { BuildOutput, Entrypoint } from '~/types';
+import pc from 'picocolors';
+import fs from 'fs-extra';
+import { groupEntrypoints } from './group-entrypoints';
+import { formatDuration } from '~/core/utils/time';
+import { printBuildSummary } from '~/core/utils/log';
+import glob from 'fast-glob';
+import { unnormalizePath } from '~/core/utils/paths';
+import { rebuild } from './rebuild';
+import { relative } from 'node:path';
+import {
+ ValidationError,
+ ValidationResult,
+ ValidationResults,
+ validateEntrypoints,
+} from '../validation';
+import consola from 'consola';
+import { wxt } from '../../wxt';
+import { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer';
+import { isCI } from 'ci-info';
+
+/**
+ * Builds the extension based on an internal config. No more config discovery is performed, the
+ * build is based on exactly what is passed in.
+ *
+ * This function:
+ * 1. Cleans the output directory
+ * 2. Executes the rebuild function with a blank previous output so everything is built (see
+ * `rebuild` for more details)
+ * 3. Prints the summary
+ */
+export async function internalBuild(): Promise {
+ await wxt.hooks.callHook('build:before', wxt);
+
+ const verb = wxt.config.command === 'serve' ? 'Pre-rendering' : 'Building';
+ const target = `${wxt.config.browser}-mv${wxt.config.manifestVersion}`;
+ wxt.logger.info(
+ `${verb} ${pc.cyan(target)} for ${pc.cyan(wxt.config.mode)} with ${pc.green(
+ `${wxt.builder.name} ${wxt.builder.version}`,
+ )}`,
+ );
+ const startTime = Date.now();
+
+ // Cleanup
+ await fs.rm(wxt.config.outDir, { recursive: true, force: true });
+ await fs.ensureDir(wxt.config.outDir);
+
+ const entrypoints = await findEntrypoints();
+ wxt.logger.debug('Detected entrypoints:', entrypoints);
+
+ const validationResults = validateEntrypoints(entrypoints);
+ if (validationResults.errorCount + validationResults.warningCount > 0) {
+ printValidationResults(validationResults);
+ }
+ if (validationResults.errorCount > 0) {
+ throw new ValidationError(`Entrypoint validation failed`, {
+ cause: validationResults,
+ });
+ }
+
+ const groups = groupEntrypoints(entrypoints);
+ await wxt.hooks.callHook('entrypoints:grouped', wxt, groups);
+
+ const { output, warnings } = await rebuild(entrypoints, groups, undefined);
+ await wxt.hooks.callHook('build:done', wxt, output);
+
+ // Post-build
+ await printBuildSummary(
+ wxt.logger.success,
+ `Built extension in ${formatDuration(Date.now() - startTime)}`,
+ output,
+ );
+
+ for (const warning of warnings) {
+ console.warn(...warning);
+ }
+
+ if (wxt.config.analysis.enabled) {
+ await combineAnalysisStats();
+ const statsPath = relative(wxt.config.root, wxt.config.analysis.outputFile);
+ wxt.logger.info(
+ `Analysis complete:\n ${pc.gray('└─')} ${pc.yellow(statsPath)}`,
+ );
+ if (wxt.config.analysis.open) {
+ if (isCI) {
+ wxt.logger.debug(`Skipped opening ${pc.yellow(statsPath)} in CI`);
+ } else {
+ wxt.logger.info(`Opening ${pc.yellow(statsPath)} in browser...`);
+ const { default: open } = await import('open');
+ open(wxt.config.analysis.outputFile);
+ }
+ }
+ }
+
+ return output;
+}
+
+async function combineAnalysisStats(): Promise {
+ const unixFiles = await glob(`${wxt.config.analysis.outputName}-*.json`, {
+ cwd: wxt.config.analysis.outputDir,
+ absolute: true,
+ });
+ const absolutePaths = unixFiles.map(unnormalizePath);
+
+ await mergeJsonOutputs({
+ inputs: absolutePaths,
+ template: wxt.config.analysis.template,
+ filename: wxt.config.analysis.outputFile,
+ });
+
+ if (!wxt.config.analysis.keepArtifacts) {
+ await Promise.all(absolutePaths.map((statsFile) => fs.remove(statsFile)));
+ }
+}
+
+function printValidationResults({
+ errorCount,
+ errors,
+ warningCount,
+}: ValidationResults) {
+ (errorCount > 0 ? console.error : console.warn)(
+ `Entrypoint validation failed: ${errorCount} error${
+ errorCount === 1 ? '' : 's'
+ }, ${warningCount} warning${warningCount === 1 ? '' : 's'}`,
+ );
+
+ const cwd = process.cwd();
+ const entrypointErrors = errors.reduce((map, error) => {
+ const entryErrors = map.get(error.entrypoint) ?? [];
+ entryErrors.push(error);
+ map.set(error.entrypoint, entryErrors);
+ return map;
+ }, new Map());
+
+ Array.from(entrypointErrors.entries()).forEach(([entrypoint, errors]) => {
+ consola.log(relative(cwd, entrypoint.inputPath));
+ console.log();
+ errors.forEach((err) => {
+ const type = err.type === 'error' ? pc.red('ERROR') : pc.yellow('WARN');
+ const recieved = pc.dim(`(recieved: ${JSON.stringify(err.value)})`);
+ consola.log(` - ${type} ${err.message} ${recieved}`);
+ });
+ console.log();
+ });
+}
diff --git a/packages/wxt/src/core/utils/building/rebuild.ts b/packages/wxt/src/core/utils/building/rebuild.ts
new file mode 100644
index 0000000..a1351eb
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/rebuild.ts
@@ -0,0 +1,79 @@
+import type { Manifest } from '~/browser';
+import { BuildOutput, Entrypoint, EntrypointGroup } from '~/types';
+import { generateTypesDir } from './generate-wxt-dir';
+import { buildEntrypoints } from './build-entrypoints';
+import { generateManifest, writeManifest } from '~/core/utils/manifest';
+import { wxt } from '../../wxt';
+
+/**
+ * Given a configuration, list of entrypoints, and an existing, partial output, build the
+ * entrypoints and merge the new output with the existing output.
+ *
+ * This function will:
+ * 1. Generate the .wxt directory's types
+ * 2. Build the `entrypointGroups` (and copies public files)
+ * 3. Generate the latest manifest for all entrypoints
+ * 4. Write the new manifest to the file system
+ *
+ * @param config Internal config containing all the project information.
+ * @param allEntrypoints List of entrypoints used to generate the types inside .wxt directory.
+ * @param entrypointGroups The list of entrypoint groups to build.
+ * @param existingOutput The previous output to combine the rebuild results into. An emptry array if
+ * this is the first build.
+ */
+export async function rebuild(
+ allEntrypoints: Entrypoint[],
+ entrypointGroups: EntrypointGroup[],
+ existingOutput: Omit = {
+ steps: [],
+ publicAssets: [],
+ },
+): Promise<{
+ output: BuildOutput;
+ manifest: Manifest.WebExtensionManifest;
+ warnings: any[][];
+}> {
+ const { default: ora } = await import('ora');
+ const spinner = ora(`Preparing...`).start();
+
+ // Update types directory with new files and types
+ await generateTypesDir(allEntrypoints).catch((err) => {
+ console.error('Failed to update .wxt directory:', err);
+ console.warn('Failed to update .wxt directory:', err);
+ // Throw the error if doing a regular build, don't for dev mode.
+ if (wxt.config.command === 'build') throw err;
+ });
+
+ // Build and merge the outputs
+ const newOutput = await buildEntrypoints(entrypointGroups, spinner);
+ const mergedOutput: Omit = {
+ steps: [...existingOutput.steps, ...newOutput.steps],
+ publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
+ };
+
+ const { manifest: newManifest, warnings: manifestWarnings } =
+ await generateManifest(allEntrypoints, mergedOutput);
+ const finalOutput: BuildOutput = {
+ manifest: newManifest,
+ ...newOutput,
+ };
+
+ // Write manifest
+ await writeManifest(newManifest, finalOutput);
+
+ // Stop the spinner and remove it from the CLI output
+ spinner.clear().stop();
+
+ return {
+ output: {
+ manifest: newManifest,
+ steps: [...existingOutput.steps, ...finalOutput.steps],
+ publicAssets: [
+ ...existingOutput.publicAssets,
+ ...finalOutput.publicAssets,
+ ],
+ },
+ manifest: newManifest,
+ warnings: manifestWarnings,
+ };
+}
diff --git a/packages/wxt/src/core/utils/building/resolve-config.ts b/packages/wxt/src/core/utils/building/resolve-config.ts
new file mode 100644
index 0000000..c013e54
--- /dev/null
+++ b/packages/wxt/src/core/utils/building/resolve-config.ts
@@ -0,0 +1,361 @@
+import { loadConfig } from 'c12';
+import {
+ InlineConfig,
+ ResolvedConfig,
+ UserConfig,
+ ConfigEnv,
+ UserManifestFn,
+ UserManifest,
+ ExtensionRunnerConfig,
+ WxtResolvedUnimportOptions,
+ Logger,
+ WxtCommand,
+} from '~/types';
+import path from 'node:path';
+import { createFsCache } from '~/core/utils/cache';
+import consola, { LogLevels } from 'consola';
+import defu from 'defu';
+import { NullablyRequired } from '../types';
+import { isModuleInstalled } from '../package';
+import fs from 'fs-extra';
+import { normalizePath } from '../paths';
+
+/**
+ * Given an inline config, discover the config file if necessary, merge the results, resolve any
+ * relative paths, and apply any defaults.
+ *
+ * Inline config always has priority over user config. Cli flags are passed as inline config if set.
+ * If unset, undefined is passed in, letting this function decide default values.
+ */
+export async function resolveConfig(
+ inlineConfig: InlineConfig,
+ command: WxtCommand,
+): Promise {
+ // Load user config
+
+ let userConfig: UserConfig = {};
+ let userConfigMetadata: ResolvedConfig['userConfigMetadata'] | undefined;
+ if (inlineConfig.configFile !== false) {
+ const { config: loadedConfig, ...metadata } = await loadConfig({
+ configFile: inlineConfig.configFile,
+ name: 'wxt',
+ cwd: inlineConfig.root ?? process.cwd(),
+ rcFile: false,
+ jitiOptions: {
+ esmResolve: true,
+ },
+ });
+ userConfig = loadedConfig ?? {};
+ userConfigMetadata = metadata;
+ }
+
+ // Merge it into the inline config
+
+ const mergedConfig = await mergeInlineConfig(inlineConfig, userConfig);
+
+ // Apply defaults to make internal config.
+
+ const debug = mergedConfig.debug ?? false;
+ const logger = mergedConfig.logger ?? consola;
+ if (debug) logger.level = LogLevels.debug;
+
+ const browser = mergedConfig.browser ?? 'chrome';
+ const manifestVersion =
+ mergedConfig.manifestVersion ??
+ (browser === 'firefox' || browser === 'safari' ? 2 : 3);
+ const mode = mergedConfig.mode ?? COMMAND_MODES[command];
+ const env: ConfigEnv = { browser, command, manifestVersion, mode };
+
+ const root = path.resolve(
+ inlineConfig.root ?? userConfig.root ?? process.cwd(),
+ );
+ const wxtDir = path.resolve(root, '.wxt');
+ const wxtModuleDir = await resolveWxtModuleDir();
+ const srcDir = path.resolve(root, mergedConfig.srcDir ?? root);
+ const entrypointsDir = path.resolve(
+ srcDir,
+ mergedConfig.entrypointsDir ?? 'entrypoints',
+ );
+ if (await isDirMissing(entrypointsDir)) {
+ logMissingDir(logger, 'Entrypoints', entrypointsDir);
+ }
+ const filterEntrypoints = !!mergedConfig.filterEntrypoints?.length
+ ? new Set(mergedConfig.filterEntrypoints)
+ : undefined;
+ const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
+ if (await isDirMissing(publicDir)) {
+ logMissingDir(logger, 'Public', publicDir);
+ }
+ const typesDir = path.resolve(wxtDir, 'types');
+ const outBaseDir = path.resolve(root, mergedConfig.outDir ?? '.output');
+ const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
+ const reloadCommand = mergedConfig.dev?.reloadCommand ?? 'Alt+R';
+
+ const runnerConfig = await loadConfig({
+ name: 'web-ext',
+ cwd: root,
+ globalRc: true,
+ rcFile: '.webextrc',
+ overrides: inlineConfig.runner,
+ defaults: userConfig.runner,
+ });
+ // Make sure alias are absolute
+ const alias = Object.fromEntries(
+ Object.entries({
+ ...mergedConfig.alias,
+ '@': srcDir,
+ '~': srcDir,
+ '@@': root,
+ '~~': root,
+ }).map(([key, value]) => [key, path.resolve(root, value)]),
+ );
+
+ let devServerConfig: ResolvedConfig['dev']['server'];
+ if (command === 'serve') {
+ let port = mergedConfig.dev?.server?.port;
+ if (port == null || !isFinite(port)) {
+ const { default: getPort, portNumbers } = await import('get-port');
+ port = await getPort({ port: portNumbers(3000, 3010) });
+ }
+ devServerConfig = {
+ port,
+ hostname: 'localhost',
+ };
+ }
+
+ return {
+ browser,
+ command,
+ debug,
+ entrypointsDir,
+ filterEntrypoints,
+ env,
+ fsCache: createFsCache(wxtDir),
+ imports: await getUnimportOptions(wxtDir, logger, mergedConfig),
+ logger,
+ manifest: await resolveManifestConfig(env, mergedConfig.manifest),
+ manifestVersion,
+ mode,
+ outBaseDir,
+ outDir,
+ publicDir,
+ wxtModuleDir,
+ root,
+ runnerConfig,
+ srcDir,
+ typesDir,
+ wxtDir,
+ zip: resolveZipConfig(root, mergedConfig),
+ transformManifest: mergedConfig.transformManifest,
+ analysis: resolveAnalysisConfig(root, mergedConfig),
+ userConfigMetadata: userConfigMetadata ?? {},
+ alias,
+ experimental: defu(mergedConfig.experimental, {
+ includeBrowserPolyfill: true,
+ viteRuntime: false,
+ }),
+ dev: {
+ server: devServerConfig,
+ reloadCommand,
+ },
+ hooks: mergedConfig.hooks ?? {},
+ vite: mergedConfig.vite ?? (() => ({})),
+ };
+}
+
+async function resolveManifestConfig(
+ env: ConfigEnv,
+ manifest: UserManifest | Promise | UserManifestFn | undefined,
+): Promise {
+ return await (typeof manifest === 'function'
+ ? manifest(env)
+ : manifest ?? {});
+}
+
+/**
+ * Merge the inline config and user config. Inline config is given priority. Defaults are not applied here.
+ */
+async function mergeInlineConfig(
+ inlineConfig: InlineConfig,
+ userConfig: UserConfig,
+): Promise {
+ // Merge imports option
+ const imports: InlineConfig['imports'] =
+ inlineConfig.imports === false || userConfig.imports === false
+ ? false
+ : userConfig.imports == null && inlineConfig.imports == null
+ ? undefined
+ : defu(inlineConfig.imports ?? {}, userConfig.imports ?? {});
+
+ // Merge manifest option
+ const manifest: UserManifestFn = async (env) => {
+ const user = await resolveManifestConfig(env, userConfig.manifest);
+ const inline = await resolveManifestConfig(env, inlineConfig.manifest);
+ return defu(inline, user);
+ };
+
+ // Merge transformManifest option
+ const transformManifest: InlineConfig['transformManifest'] = (manifest) => {
+ userConfig.transformManifest?.(manifest);
+ inlineConfig.transformManifest?.(manifest);
+ };
+
+ // Builders
+ const builderConfig = await mergeBuilderConfig(inlineConfig, userConfig);
+
+ return {
+ ...defu(inlineConfig, userConfig),
+ // Custom merge values
+ transformManifest,
+ imports,
+ manifest,
+ ...builderConfig,
+ };
+}
+
+function resolveZipConfig(
+ root: string,
+ mergedConfig: InlineConfig,
+): NullablyRequired {
+ const downloadedPackagesDir = path.resolve(root, '.wxt/local_modules');
+ return {
+ name: undefined,
+ sourcesTemplate: '{{name}}-{{version}}-sources.zip',
+ artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
+ sourcesRoot: root,
+ includeSources: [],
+ compressionLevel: 9,
+ ...mergedConfig.zip,
+ excludeSources: [
+ '**/node_modules',
+ // WXT files
+ '**/web-ext.config.ts',
+ // Hidden files
+ '**/.*',
+ // Tests
+ '**/__tests__/**',
+ '**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
+ // From user
+ ...(mergedConfig.zip?.excludeSources ?? []),
+ ],
+ downloadPackages: mergedConfig.zip?.downloadPackages ?? [],
+ downloadedPackagesDir,
+ };
+}
+
+function resolveAnalysisConfig(
+ root: string,
+ mergedConfig: InlineConfig,
+): NullablyRequired {
+ const analysisOutputFile = path.resolve(
+ root,
+ mergedConfig.analysis?.outputFile ?? 'stats.html',
+ );
+ const analysisOutputDir = path.dirname(analysisOutputFile);
+ const analysisOutputName = path.parse(analysisOutputFile).name;
+
+ return {
+ enabled: mergedConfig.analysis?.enabled ?? false,
+ open: mergedConfig.analysis?.open ?? false,
+ template: mergedConfig.analysis?.template ?? 'treemap',
+ outputFile: analysisOutputFile,
+ outputDir: analysisOutputDir,
+ outputName: analysisOutputName,
+ keepArtifacts: mergedConfig.analysis?.keepArtifacts ?? false,
+ };
+}
+
+async function getUnimportOptions(
+ wxtDir: string,
+ logger: Logger,
+ config: InlineConfig,
+): Promise {
+ if (config.imports === false) return false;
+
+ const enabledConfig = config.imports?.eslintrc?.enabled;
+ let enabled: boolean;
+ switch (enabledConfig) {
+ case undefined:
+ case 'auto':
+ enabled = await isModuleInstalled('eslint');
+ break;
+ default:
+ enabled = enabledConfig;
+ }
+
+ const defaultOptions: WxtResolvedUnimportOptions = {
+ debugLog: logger.debug,
+ imports: [
+ { name: 'defineConfig', from: 'wxt' },
+ { name: 'fakeBrowser', from: 'wxt/testing' },
+ ],
+ presets: [
+ { package: 'wxt/client' },
+ { package: 'wxt/browser' },
+ { package: 'wxt/sandbox' },
+ { package: 'wxt/storage' },
+ ],
+ warn: logger.warn,
+ dirs: ['components', 'composables', 'hooks', 'utils'],
+ eslintrc: {
+ enabled,
+ filePath: path.resolve(wxtDir, 'eslintrc-auto-import.json'),
+ globalsPropValue: true,
+ },
+ };
+
+ return defu(
+ config.imports ?? {},
+ defaultOptions,
+ );
+}
+
+/**
+ * Returns the path to `node_modules/wxt`.
+ */
+async function resolveWxtModuleDir() {
+ const requireResolve =
+ require?.resolve ??
+ (await import('node:module')).default.createRequire(import.meta.url)
+ .resolve;
+ // require.resolve returns the wxt/dist/index file, not the package's root directory, which we want to return
+ return path.resolve(requireResolve('wxt'), '../..');
+}
+
+async function isDirMissing(dir: string) {
+ return !(await fs.exists(dir));
+}
+
+function logMissingDir(logger: Logger, name: string, expected: string) {
+ logger.warn(
+ `${name} directory not found: ./${normalizePath(
+ path.relative(process.cwd(), expected),
+ )}`,
+ );
+}
+
+/**
+ * Map of `ConfigEnv` commands to their default modes.
+ */
+const COMMAND_MODES: Record = {
+ build: 'production',
+ serve: 'development',
+};
+
+export async function mergeBuilderConfig(
+ inlineConfig: InlineConfig,
+ userConfig: UserConfig,
+): Promise> {
+ const vite = await import('vite').catch(() => void 0);
+ if (vite) {
+ return {
+ vite: async (env) => {
+ const resolvedInlineConfig = (await inlineConfig.vite?.(env)) ?? {};
+ const resolvedUserConfig = (await userConfig.vite?.(env)) ?? {};
+ return vite.mergeConfig(resolvedUserConfig, resolvedInlineConfig);
+ },
+ };
+ }
+
+ throw Error('Builder not found. Make sure vite is installed.');
+}
diff --git a/packages/wxt/src/core/utils/cache.ts b/packages/wxt/src/core/utils/cache.ts
new file mode 100644
index 0000000..fa69cfb
--- /dev/null
+++ b/packages/wxt/src/core/utils/cache.ts
@@ -0,0 +1,31 @@
+import fs, { ensureDir } from 'fs-extra';
+import { FsCache } from '~/types';
+import { dirname, resolve } from 'path';
+import { writeFileIfDifferent } from './fs';
+
+/**
+ * A basic file system cache stored at `/.wxt/cache/`. Just caches a string in a
+ * file for the given key.
+ *
+ * @param srcDir Absolute path to source directory. See `InternalConfig.srcDir`
+ */
+export function createFsCache(wxtDir: string): FsCache {
+ const getPath = (key: string) =>
+ resolve(wxtDir, 'cache', encodeURIComponent(key));
+
+ return {
+ async set(key: string, value: string): Promise {
+ const path = getPath(key);
+ await ensureDir(dirname(path));
+ await writeFileIfDifferent(path, value);
+ },
+ async get(key: string): Promise {
+ const path = getPath(key);
+ try {
+ return await fs.readFile(path, 'utf-8');
+ } catch {
+ return undefined;
+ }
+ },
+ };
+}
diff --git a/packages/wxt/src/core/utils/cli.ts b/packages/wxt/src/core/utils/cli.ts
new file mode 100644
index 0000000..05b07d8
--- /dev/null
+++ b/packages/wxt/src/core/utils/cli.ts
@@ -0,0 +1,37 @@
+import { LogLevels, consola } from 'consola';
+import { printHeader } from './log';
+import { formatDuration } from './time';
+
+export function defineCommand(
+ cb: (...args: TArgs) => void | boolean | Promise,
+ options?: {
+ disableFinishedLog?: boolean;
+ },
+) {
+ return async (...args: TArgs) => {
+ // Enable consola's debug mode globally at the start of all commands when the `--debug` flag is
+ // passed
+ const isDebug = !!args.find((arg) => arg?.debug);
+ if (isDebug) {
+ consola.level = LogLevels.debug;
+ }
+
+ const startTime = Date.now();
+ try {
+ printHeader();
+
+ const ongoing = await cb(...args);
+
+ if (!ongoing && !options?.disableFinishedLog)
+ consola.success(
+ `Finished in ${formatDuration(Date.now() - startTime)}`,
+ );
+ } catch (err) {
+ consola.fail(
+ `Command failed after ${formatDuration(Date.now() - startTime)}`,
+ );
+ consola.error(err);
+ process.exit(1);
+ }
+ };
+}
diff --git a/packages/wxt/src/core/utils/constants.ts b/packages/wxt/src/core/utils/constants.ts
new file mode 100644
index 0000000..117b962
--- /dev/null
+++ b/packages/wxt/src/core/utils/constants.ts
@@ -0,0 +1,5 @@
+/**
+ * Module ID used to build the background in dev mode if the extension doesn't include a background
+ * script/service worker.
+ */
+export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';
diff --git a/packages/wxt/src/core/utils/content-scripts.ts b/packages/wxt/src/core/utils/content-scripts.ts
new file mode 100644
index 0000000..2d483e4
--- /dev/null
+++ b/packages/wxt/src/core/utils/content-scripts.ts
@@ -0,0 +1,93 @@
+import type { Manifest, Scripting } from '~/browser';
+import { ContentScriptEntrypoint, ResolvedConfig } from '~/types';
+import { getEntrypointBundlePath } from './entrypoints';
+
+/**
+ * Returns a unique and consistent string hash based on a content scripts options.
+ *
+ * It is able to recognize default values,
+ */
+export function hashContentScriptOptions(
+ options: ContentScriptEntrypoint['options'],
+): string {
+ const simplifiedOptions = mapWxtOptionsToContentScript(
+ options,
+ undefined,
+ undefined,
+ );
+
+ // Remove undefined fields and use defaults to generate hash
+ Object.keys(simplifiedOptions).forEach((key) => {
+ // @ts-expect-error: key not typed as keyof ...
+ if (simplifiedOptions[key] == null) delete simplifiedOptions[key];
+ });
+
+ const withDefaults: Manifest.ContentScript = {
+ exclude_globs: [],
+ exclude_matches: [],
+ include_globs: [],
+ match_about_blank: false,
+ run_at: 'document_idle',
+ all_frames: false,
+ // @ts-expect-error - not in type
+ match_origin_as_fallback: false,
+ world: 'ISOLATED',
+ ...simplifiedOptions,
+ };
+ return JSON.stringify(
+ Object.entries(withDefaults)
+ // Sort any arrays so their values are consistent
+ .map<[string, unknown]>(([key, value]) => {
+ if (Array.isArray(value)) return [key, value.sort()];
+ else return [key, value];
+ })
+ // Sort all the fields alphabetically
+ .sort((l, r) => l[0].localeCompare(r[0])),
+ );
+}
+
+export function mapWxtOptionsToContentScript(
+ options: ContentScriptEntrypoint['options'],
+ js: string[] | undefined,
+ css: string[] | undefined,
+): Manifest.ContentScript {
+ return {
+ matches: options.matches,
+ all_frames: options.allFrames,
+ match_about_blank: options.matchAboutBlank,
+ exclude_globs: options.excludeGlobs,
+ exclude_matches: options.excludeMatches,
+ include_globs: options.includeGlobs,
+ run_at: options.runAt,
+ css,
+ js,
+
+ // @ts-expect-error: untyped chrome options
+ match_origin_as_fallback: options.matchOriginAsFallback,
+ world: options.world,
+ };
+}
+
+export function mapWxtOptionsToRegisteredContentScript(
+ options: ContentScriptEntrypoint['options'],
+ js: string[] | undefined,
+ css: string[] | undefined,
+): Omit {
+ return {
+ allFrames: options.allFrames,
+ excludeMatches: options.excludeMatches,
+ matches: options.matches,
+ runAt: options.runAt,
+ js,
+ css,
+ // @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
+ world: options.world,
+ };
+}
+
+export function getContentScriptJs(
+ config: ResolvedConfig,
+ entrypoint: ContentScriptEntrypoint,
+): string[] {
+ return [getEntrypointBundlePath(entrypoint, config.outDir, '.js')];
+}
diff --git a/packages/wxt/src/core/utils/content-security-policy.ts b/packages/wxt/src/core/utils/content-security-policy.ts
new file mode 100644
index 0000000..5b94ecc
--- /dev/null
+++ b/packages/wxt/src/core/utils/content-security-policy.ts
@@ -0,0 +1,48 @@
+/**
+ * Directive names that make up CSPs. There are more, this is all I need for the plugin.
+ */
+export type CspDirective = 'default-src' | 'script-src' | 'object-src';
+
+export class ContentSecurityPolicy {
+ private static DIRECTIVE_ORDER: Record = {
+ 'default-src': 0,
+ 'script-src': 1,
+ 'object-src': 2,
+ };
+
+ data: Record