Compare commits

...

29 Commits

Author SHA1 Message Date
github-actions[bot] e4598a5e37 chore(release): @wxt-dev/module-react v1.2.2
📼 VHS / Create VHS (push) Cancelled after 0s
2026-03-14 23:48:01 +00:00
github-actions[bot] db5c22809c chore(release): wxt v0.20.19
📼 VHS / Create VHS (push) Cancelled after 0s
2026-03-14 23:43:04 +00:00
Aaron c61fa8f858 fix: Add @vitejs/plugin-react v6 support 2026-03-14 18:38:33 -05:00
Florian Metz 5fe4681620 chore: replace fs-extra with node:fs/promises (#2174)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-03-06 09:29:49 -06:00
Aaron 80d384361b chore: Update nano-staged precommit command 2026-03-06 09:17:22 -06:00
dagangtj a1e72c0749 chore: replace lint-staged with nano-staged (#2179)
Co-authored-by: Dean <dagangtj@gmail.com>
2026-03-06 15:13:41 +00:00
Aaron e3a80b8247 ci: Re-enable react template validation 2026-03-06 08:54:08 -06:00
github-actions[bot] c0013acadc chore(release): @wxt-dev/module-react v1.2.1
📼 VHS / Create VHS (push) Cancelled after 0s
2026-03-06 14:51:43 +00:00
Aaron f472c53da7 ci: Temporarily disable react template validation 2026-03-06 08:47:58 -06:00
Aaron 032f7931e8 chore: Add prepack script to all packages 2026-03-06 08:43:51 -06:00
github-actions[bot] 57a12c1711 chore(release): @wxt-dev/module-react v1.2.0
📼 VHS / Create VHS (push) Cancelled after 0s
2026-03-06 14:24:19 +00:00
Aaron d13ca3983f chore(deps): Upgdate fs-extra in browser package 2026-03-06 08:19:54 -06:00
aklinker1 9769ce4ac9 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-03-06 00:44:23 +00:00
Aaron 55dc263dc9 fix: List eslint 10 as supported 2026-03-05 10:08:02 -06:00
Aaron e56ae23747 chore(deps): Upgrade deps (#2175) 2026-03-05 10:00:41 -06:00
Aaron bfd4af5d69 feat: Vite 8 support 2026-03-05 08:59:58 -06:00
Patryk Kuniczak 9673604878 perf: Use filter to improve plugin performance with rolldown (#1787)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-03-05 08:55:36 -06:00
Florian Metz ae55aecb0d chore: replace fast-glob and ora with lighter alternatives (#2173) 2026-03-04 14:26:11 -06:00
Martin Broder a14d0b4f2f feat: add vitePluginsBefore option (#2170) 2026-03-04 11:39:00 -06:00
Aaron 301e4a17da chore: Add prettier-plugin-jsdoc to project (#2171) 2026-03-03 08:43:04 -06:00
etoome 19cebbf673 docs: Added "isTrust" to the homepage (#2161) 2026-02-27 00:49:06 +00:00
Kuba ef73c07dfe docs: Added "Scrape Similar" to the homepage (#2158) 2026-02-23 13:57:08 +00:00
Aaron ca5ad7e51b chore: Update codeowners 2026-02-22 18:56:12 -06:00
github-actions[bot] 5dab944abc chore(release): wxt v0.20.18
📼 VHS / Create VHS (push) Cancelled after 0s
2026-02-22 23:45:06 +00:00
Aaron adaae6e1a4 fix: Fix submit command (#2157) 2026-02-22 17:29:53 -06:00
Aaron 4524cbdf76 ci: Fix VHS workflow 2026-02-22 17:29:53 -06:00
github-actions[bot] 1171cf52f0 chore(release): @wxt-dev/analytics v0.5.4
📼 VHS / Create VHS (push) Cancelled after 0s
2026-02-22 17:29:53 -06:00
Aaron c5978b1072 fix: Continue using useAppConfig to support older versions of WXT 2026-02-22 17:29:53 -06:00
Aaron e5ef9367ed ci: Fix VHS action (#2155) 2026-02-22 17:29:53 -06:00
143 changed files with 3082 additions and 2387 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# Set default # Set default
* @aklinker1 @Timeraa * @aklinker1
# Secure Directories # Secure Directories
/.github/ @aklinker1 /.github/ @aklinker1
+15 -4
View File
@@ -11,19 +11,26 @@ permissions:
jobs: jobs:
vhs: vhs:
name: Create VHS name: Create VHS
runs-on: ubuntu-22.04 runs-on: macos-latest
if: ${{ github.repository == 'wxt-dev/wxt' }} if: ${{ github.repository == 'wxt-dev/wxt' }}
permissions: permissions:
contents: write contents: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.ref }}
- name: Setup - name: Setup
uses: ./.github/actions/setup uses: ./.github/actions/setup
with: with:
install: false install: false
- name: Setup Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version: '1.25.1'
# This prevents pnpm dlx from downloading WXT in the video # This prevents pnpm dlx from downloading WXT in the video
- name: Pre-install WXT - name: Pre-install WXT
run: | run: |
@@ -31,9 +38,10 @@ jobs:
pnpm dlx wxt@latest --version pnpm dlx wxt@latest --version
- name: Record VHS - name: Record VHS
uses: charmbracelet/vhs-action@59641cdc7fadf3978db65eb8c6937ea2752f4ec3 # v2.1.0 run: |
with: brew install ttyd ffmpeg
path: 'docs/tapes/init-demo.tape' go install github.com/charmbracelet/vhs@517bcda0faf416728bcf6b7fe489eb0e2469d9b5 # v0.10.0
vhs docs/tapes/init-demo.tape
- name: Save recorded GIF - name: Save recorded GIF
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0 uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
@@ -41,5 +49,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
commit_message: 'docs: Update `wxt init` GIF' commit_message: 'docs: Update `wxt init` GIF'
commit_user_name: github-actions[bot]
commit_user_email: github-actions[bot]@users.noreply.github.com
commit_author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
# https://github.com/charmbracelet/vhs#output # https://github.com/charmbracelet/vhs#output
file_pattern: 'docs/assets/*.gif' file_pattern: 'docs/assets/*.gif'
+2
View File
@@ -1,2 +1,4 @@
singleQuote: true singleQuote: true
endOfLine: lf endOfLine: lf
plugins:
- prettier-plugin-jsdoc
@@ -119,6 +119,8 @@ const chromeExtensionIds = [
'mfjdonmgmgcijagclnkfhmjiblbfjaid', // KeyFloat - Floating multilingual keyboard with native key mappings, drag, dark mode, sounds, and dynamic layouts for macOS & Windows 'mfjdonmgmgcijagclnkfhmjiblbfjaid', // KeyFloat - Floating multilingual keyboard with native key mappings, drag, dark mode, sounds, and dynamic layouts for macOS & Windows
'dhiekgdaipindoapjmcnpompdknjeijf', // Glossy New Tab - Say Goodbye to Boring Tabs with live wallpapers 'dhiekgdaipindoapjmcnpompdknjeijf', // Glossy New Tab - Say Goodbye to Boring Tabs with live wallpapers
'lapnciffpekdengooeolaienkeoilfeo', // All API Hub AI Relay & New API Manager - https://github.com/qixing-jk/all-api-hub 'lapnciffpekdengooeolaienkeoilfeo', // All API Hub AI Relay & New API Manager - https://github.com/qixing-jk/all-api-hub
'bhgobenflkkhfcgkikejaaejenoddcmo', // Scrape Similar - Extract data from websites into spreadsheets - https://github.com/zizzfizzix/scrape-similar
'kinlknncggaihnhdcalijdmpbhbflalm', // isTrust - https://github.com/Internet-Society-Belgium/isTrust/
]; ];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds); const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
+1 -3
View File
@@ -77,9 +77,7 @@ export function menuItem(
return { text, link }; return { text, link };
} }
/** /** Clean up and add badges to typedoc leaf sections */
* Clean up and add badges to typedoc leaf sections
*/
export function prepareTypedocSidebar(items: SidebarItem[]) { export function prepareTypedocSidebar(items: SidebarItem[]) {
// skip contents file // skip contents file
const filtered = items.slice(1); const filtered = items.slice(1);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 136 KiB

+11 -12
View File
@@ -16,27 +16,26 @@
"docs:preview": "pnpm -s docs:gen && vitepress preview docs" "docs:preview": "pnpm -s docs:gen && vitepress preview docs"
}, },
"devDependencies": { "devDependencies": {
"@aklinker1/buildc": "^1.1.6", "@aklinker1/buildc": "^1.1.7",
"@aklinker1/check": "^2.2.0", "@aklinker1/check": "^2.2.0",
"@commitlint/config-conventional": "^20.4.1", "@commitlint/config-conventional": "^20.4.3",
"@commitlint/types": "^20.4.0", "@commitlint/types": "^20.4.3",
"@types/fs-extra": "^11.0.4",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.0.18",
"changelogen": "^0.6.2", "changelogen": "^0.6.2",
"consola": "^3.4.2", "consola": "^3.4.2",
"fast-glob": "^3.3.3",
"feed": "^5.2.0", "feed": "^5.2.0",
"fs-extra": "^11.3.3",
"lint-staged": "^16.2.7",
"markdown-it-footnote": "^4.0.0", "markdown-it-footnote": "^4.0.0",
"markdownlint-cli": "^0.47.0", "markdownlint-cli": "^0.48.0",
"nano-spawn": "^2.0.0", "nano-spawn": "^2.0.0",
"nano-staged": "^0.8.0",
"p-map": "^7.0.4", "p-map": "^7.0.4",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"prettier-plugin-jsdoc": "^1.8.0",
"semver": "^7.7.4", "semver": "^7.7.4",
"simple-git-hooks": "^2.13.1", "simple-git-hooks": "^2.13.1",
"tsdown": "^0.20.3", "tinyglobby": "^0.2.15",
"tsdown": "^0.21.0",
"tsx": "4.21.0", "tsx": "4.21.0",
"typedoc": "^0.25.4", "typedoc": "^0.25.4",
"typedoc-plugin-frontmatter": "^1.3.1", "typedoc-plugin-frontmatter": "^1.3.1",
@@ -48,13 +47,13 @@
"vitepress-plugin-group-icons": "^1.7.1", "vitepress-plugin-group-icons": "^1.7.1",
"vitepress-plugin-llms": "^1.11.0", "vitepress-plugin-llms": "^1.11.0",
"vitest-mock-extended": "^3.1.0", "vitest-mock-extended": "^3.1.0",
"vue": "^3.5.27", "vue": "^3.5.29",
"wxt": "workspace:*" "wxt": "workspace:*"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"pre-commit": "pnpm lint-staged" "pre-commit": "./node_modules/.bin/nano-staged"
}, },
"lint-staged": { "nano-staged": {
"*": "prettier --ignore-unknown --write" "*": "prettier --ignore-unknown --write"
} }
} }
+12
View File
@@ -1,5 +1,17 @@
# Changelog # Changelog
## v0.5.4
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.3...analytics-v0.5.4)
### 🩹 Fixes
- Continue using `useAppConfig` to support older versions of WXT ([bfd94556](https://github.com/wxt-dev/wxt/commit/bfd94556))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.5.3 ## v0.5.3
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.2...analytics-v0.5.3) [compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.2...analytics-v0.5.3)
@@ -62,7 +62,8 @@ export function createAnalytics(config?: AnalyticsConfig): Analytics {
} }
/** /**
* Creates an analytics client in the background responsible for uploading events to the server to avoid CORS errors. * Creates an analytics client in the background responsible for uploading
* events to the server to avoid CORS errors.
*/ */
function createBackgroundAnalytics( function createBackgroundAnalytics(
config: AnalyticsConfig | undefined, config: AnalyticsConfig | undefined,
@@ -219,9 +220,7 @@ function createBackgroundAnalytics(
return analytics; return analytics;
} }
/** /** Creates an analytics client for non-background contexts. */
* Creates an analytics client for non-background contexts.
*/
function createFrontendAnalytics(): Analytics { function createFrontendAnalytics(): Analytics {
const port = browser.runtime.connect({ name: ANALYTICS_PORT }); const port = browser.runtime.connect({ name: ANALYTICS_PORT });
const sessionId = Date.now(); const sessionId = Date.now();
@@ -44,9 +44,9 @@ export default defineWxtModule({
? clientModuleId ? clientModuleId
: normalizePath(relative(wxtAnalyticsFolder, clientModuleId)) : normalizePath(relative(wxtAnalyticsFolder, clientModuleId))
}'; }';
import { getAppConfig } from '#imports'; import { useAppConfig } from '#imports';
export const analytics = createAnalytics(getAppConfig().analytics); export const analytics = createAnalytics(useAppConfig().analytics);
`; `;
addAlias(wxt, '#analytics', wxtAnalyticsIndex); addAlias(wxt, '#analytics', wxtAnalyticsIndex);
wxt.hook('prepare:types', async (_, entries) => { wxt.hook('prepare:types', async (_, entries) => {
+13 -10
View File
@@ -8,36 +8,39 @@ export interface Analytics {
) => void; ) => void;
/** Save information about the user. */ /** Save information about the user. */
identify: (userId: string, userProperties?: Record<string, string>) => void; identify: (userId: string, userProperties?: Record<string, string>) => void;
/** Automatically setup and track user interactions, returning a function to remove any listeners that were setup. */ /**
* Automatically setup and track user interactions, returning a function to
* remove any listeners that were setup.
*/
autoTrack: (root: Document | ShadowRoot | Element) => () => void; autoTrack: (root: Document | ShadowRoot | Element) => () => void;
/** Calls `config.enabled.setValue`. */ /** Calls `config.enabled.setValue`. */
setEnabled: (enabled: boolean) => void; setEnabled: (enabled: boolean) => void;
} }
export interface AnalyticsConfig { export interface AnalyticsConfig {
/** /** Array of providers to send analytics to. */
* Array of providers to send analytics to.
*/
providers: AnalyticsProvider[]; providers: AnalyticsProvider[];
/** /** Enable debug logs and other provider-specific debugging features. */
* Enable debug logs and other provider-specific debugging features.
*/
debug?: boolean; debug?: boolean;
/** /**
* Your extension's version, reported alongside events. * Your extension's version, reported alongside events.
*
* @default browser.runtime.getManifest().version`. * @default browser.runtime.getManifest().version`.
*/ */
version?: string; version?: string;
/** /**
* Configure how the enabled flag is persisted. Defaults to using `browser.storage.local`. * Configure how the enabled flag is persisted. Defaults to using
* `browser.storage.local`.
*/ */
enabled?: AnalyticsStorageItem<boolean>; enabled?: AnalyticsStorageItem<boolean>;
/** /**
* Configure how the user Id is persisted. Defaults to using `browser.storage.local`. * Configure how the user Id is persisted. Defaults to using
* `browser.storage.local`.
*/ */
userId?: AnalyticsStorageItem<string | undefined>; userId?: AnalyticsStorageItem<string | undefined>;
/** /**
* Configure how user properties are persisted. Defaults to using `browser.storage.local`. * Configure how user properties are persisted. Defaults to using
* `browser.storage.local`.
*/ */
userProperties?: AnalyticsStorageItem<Record<string, string>>; userProperties?: AnalyticsStorageItem<Record<string, string>>;
} }
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wxt-dev/analytics", "name": "@wxt-dev/analytics",
"version": "0.5.3", "version": "0.5.4",
"description": "Add analytics to your web extension", "description": "Add analytics to your web extension",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -8,7 +8,7 @@
"dev:build": "buildc --deps-only -- wxt build", "dev:build": "buildc --deps-only -- wxt build",
"check": "pnpm build && check", "check": "pnpm build && check",
"build": "buildc -- tsdown", "build": "buildc -- tsdown",
"prepack": "pnpm -s build", "prepack": "pnpm build",
"prepare": "buildc --deps-only -- wxt prepare" "prepare": "buildc --deps-only -- wxt prepare"
}, },
"dependencies": { "dependencies": {
@@ -21,7 +21,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"publint": "^0.3.17", "publint": "^0.3.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"wxt": "workspace:*" "wxt": "workspace:*"
}, },
+4 -4
View File
@@ -40,19 +40,19 @@
"scripts": { "scripts": {
"build": "buildc -- tsdown", "build": "buildc -- tsdown",
"check": "pnpm build && check", "check": "pnpm build && check",
"test": "buildc --deps-only -- vitest" "test": "buildc --deps-only -- vitest",
"prepack": "pnpm build"
}, },
"peerDependencies": { "peerDependencies": {
"wxt": ">=0.19.0" "wxt": ">=0.19.0"
}, },
"dependencies": { "dependencies": {
"defu": "^6.1.4", "defu": "^6.1.4",
"fs-extra": "^11.3.3",
"sharp": "^0.34.5" "sharp": "^0.34.5"
}, },
"devDependencies": { "devDependencies": {
"oxlint": "^1.43.0", "oxlint": "^1.51.0",
"publint": "^0.3.17", "publint": "^0.3.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.18", "vitest": "^4.0.18",
"wxt": "workspace:*" "wxt": "workspace:*"
+11 -10
View File
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import * as fsExtra from 'fs-extra'; import * as fsPromises from 'node:fs/promises';
import sharp from 'sharp'; import sharp from 'sharp';
import type { Wxt, UserManifest } from 'wxt'; import type { Wxt, UserManifest } from 'wxt';
@@ -9,9 +9,9 @@ import autoIconsModule from '../index';
import type { AutoIconsOptions } from '../index'; import type { AutoIconsOptions } from '../index';
// Mock dependencies // Mock dependencies
vi.mock('fs-extra', () => ({ vi.mock('node:fs/promises', () => ({
ensureDir: vi.fn(), mkdir: vi.fn(),
pathExists: vi.fn(), access: vi.fn(),
})); }));
vi.mock('sharp', () => ({ vi.mock('sharp', () => ({
@@ -86,8 +86,8 @@ describe('auto-icons module', () => {
vi.mocked(sharp).mockReturnValue( vi.mocked(sharp).mockReturnValue(
mockSharpInstance as unknown as sharp.Sharp, mockSharpInstance as unknown as sharp.Sharp,
); );
vi.mocked(fsExtra.pathExists).mockResolvedValue(true as any); vi.mocked(fsPromises.access).mockResolvedValue(undefined);
vi.mocked(fsExtra.ensureDir).mockResolvedValue(undefined as any); vi.mocked(fsPromises.mkdir).mockResolvedValue(undefined as any);
}); });
describe('module setup', () => { describe('module setup', () => {
@@ -158,7 +158,7 @@ describe('auto-icons module', () => {
}); });
it('should warn when base icon not found', async () => { it('should warn when base icon not found', async () => {
vi.mocked(fsExtra.pathExists).mockResolvedValue(false as any); vi.mocked(fsPromises.access).mockRejectedValue(new Error('ENOENT'));
const options: AutoIconsOptions = { const options: AutoIconsOptions = {
enabled: true, enabled: true,
@@ -293,8 +293,9 @@ describe('auto-icons module', () => {
expect(mockSharpInstance.resize).toHaveBeenCalledWith(32); expect(mockSharpInstance.resize).toHaveBeenCalledWith(32);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(16); expect(mockSharpInstance.resize).toHaveBeenCalledWith(16);
expect(fsExtra.ensureDir).toHaveBeenCalledWith( expect(fsPromises.mkdir).toHaveBeenCalledWith(
resolve('/mock/dist', 'icons'), resolve('/mock/dist', 'icons'),
{ recursive: true },
); );
expect(output.publicAssets).toEqual([ expect(output.publicAssets).toEqual([
@@ -514,7 +515,7 @@ describe('auto-icons module', () => {
}; };
// Make ensureDir throw an error // Make ensureDir throw an error
vi.mocked(fsExtra.ensureDir).mockRejectedValue( vi.mocked(fsPromises.mkdir).mockRejectedValue(
new Error('Directory creation failed'), new Error('Directory creation failed'),
); );
@@ -530,7 +531,7 @@ describe('auto-icons module', () => {
if (buildHook) { if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output); await buildHook(mockWxt as unknown as Wxt, output);
// But ensureDir should have been called // But ensureDir should have been called
expect(fsExtra.ensureDir).toHaveBeenCalled(); expect(fsPromises.mkdir).toHaveBeenCalled();
} }
}); });
+18 -12
View File
@@ -3,7 +3,7 @@ import { defineWxtModule } from 'wxt/modules';
import { resolve, relative } from 'node:path'; import { resolve, relative } from 'node:path';
import defu from 'defu'; import defu from 'defu';
import sharp from 'sharp'; import sharp from 'sharp';
import { ensureDir, pathExists } from 'fs-extra'; import { access, mkdir } from 'node:fs/promises';
export default defineWxtModule<AutoIconsOptions>({ export default defineWxtModule<AutoIconsOptions>({
name: '@wxt-dev/auto-icons', name: '@wxt-dev/auto-icons',
@@ -37,7 +37,11 @@ export default defineWxtModule<AutoIconsOptions>({
if (!parsedOptions.enabled) if (!parsedOptions.enabled)
return wxt.logger.warn(`\`[auto-icons]\` ${this.name} disabled`); return wxt.logger.warn(`\`[auto-icons]\` ${this.name} disabled`);
if (!(await pathExists(resolvedPath))) { const iconExists = await access(resolvedPath).then(
() => true,
() => false,
);
if (!iconExists) {
return wxt.logger.warn( return wxt.logger.warn(
`\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`, `\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`,
); );
@@ -91,7 +95,7 @@ export default defineWxtModule<AutoIconsOptions>({
} }
} }
ensureDir(resolve(outputFolder, 'icons')); mkdir(resolve(outputFolder, 'icons'), { recursive: true });
await resizedImage.toFile(resolve(outputFolder, `icons/${size}.png`)); await resizedImage.toFile(resolve(outputFolder, `icons/${size}.png`));
output.publicAssets.push({ output.publicAssets.push({
@@ -109,12 +113,11 @@ export default defineWxtModule<AutoIconsOptions>({
}, },
}); });
/** /** Options for the auto-icons module */
* Options for the auto-icons module
*/
export interface AutoIconsOptions { export interface AutoIconsOptions {
/** /**
* Enable auto-icons generation * Enable auto-icons generation
*
* @default true * @default true
*/ */
enabled?: boolean; enabled?: boolean;
@@ -122,27 +125,30 @@ export interface AutoIconsOptions {
* Path to the image to use. * Path to the image to use.
* *
* Path is relative to the project's src directory. * Path is relative to the project's src directory.
* @default "<srcDir>/assets/icon.png" *
* @default '<srcDir>/assets/icon.png'
*/ */
baseIconPath?: string; baseIconPath?: string;
/** /**
* Apply a visual indicator to the icon when running in development mode. * Apply a visual indicator to the icon when running in development mode.
* *
* "grayscale" converts the icon to grayscale. * "grayscale" converts the icon to grayscale. "overlay" covers the bottom
* "overlay" covers the bottom half with a yellow rectangle and writes "DEV" in black text. * half with a yellow rectangle and writes "DEV" in black text. Set to `false`
* Set to `false` to disable any indicator. * to disable any indicator.
* *
* @default "grayscale" * @default 'grayscale'
*/ */
developmentIndicator?: 'grayscale' | 'overlay' | false; developmentIndicator?: 'grayscale' | 'overlay' | false;
/** /**
* Grayscale the image when in development mode to indicate development * Grayscale the image when in development mode to indicate development
* @default true *
* @deprecated Use `developmentIndicator` instead * @deprecated Use `developmentIndicator` instead
* @default true
*/ */
grayscaleOnDevelopment?: boolean; grayscaleOnDevelopment?: boolean;
/** /**
* Sizes to generate icons for * Sizes to generate icons for
*
* @default [128, 48, 32, 16] * @default [128, 48, 32, 16]
*/ */
sizes?: number[]; sizes?: number[];
+1 -1
View File
@@ -24,7 +24,7 @@
], ],
"devDependencies": { "devDependencies": {
"@types/chrome": "0.1.37", "@types/chrome": "0.1.37",
"fs-extra": "^11.3.3", "@types/node": "^20.0.0",
"nano-spawn": "^2.0.0", "nano-spawn": "^2.0.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.18" "vitest": "^4.0.18"
+8 -8
View File
@@ -1,5 +1,5 @@
import spawn from 'nano-spawn'; import spawn from 'nano-spawn';
import fs from 'fs-extra'; import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { dirname, join, resolve, sep } from 'node:path'; import { dirname, join, resolve, sep } from 'node:path';
import { sep as posixSep } from 'node:path/posix'; import { sep as posixSep } from 'node:path/posix';
@@ -17,8 +17,8 @@ const pkgJsonPath = fileURLToPath(
import.meta.resolve('@types/chrome/package.json'), import.meta.resolve('@types/chrome/package.json'),
); );
const pkgDir = dirname(pkgJsonPath); const pkgDir = dirname(pkgJsonPath);
const pkgJson = await fs.readJson(pkgJsonPath); const pkgJson = JSON.parse(await readFile(pkgJsonPath, 'utf-8'));
const pkgJsonTemplate = await fs.readFile('templates/package.json', 'utf8'); const pkgJsonTemplate = await readFile('templates/package.json', 'utf8');
const newPkgJson = JSON.parse( const newPkgJson = JSON.parse(
pkgJsonTemplate.replaceAll('{{chromeTypesVersion}}', pkgJson.version), pkgJsonTemplate.replaceAll('{{chromeTypesVersion}}', pkgJson.version),
); );
@@ -27,7 +27,7 @@ newPkgJson.peerDependencies = pkgJson.peerDependencies;
newPkgJson.peerDependenciesMeta = pkgJson.peerDependenciesMeta; newPkgJson.peerDependenciesMeta = pkgJson.peerDependenciesMeta;
const outPkgJsonPath = resolve('package.json'); const outPkgJsonPath = resolve('package.json');
await fs.writeJson(outPkgJsonPath, newPkgJson); await writeFile(outPkgJsonPath, JSON.stringify(newPkgJson, null, 2));
await spawn('pnpm', ['-w', 'prettier', '--write', outPkgJsonPath]); await spawn('pnpm', ['-w', 'prettier', '--write', outPkgJsonPath]);
// Generate declaration files // Generate declaration files
@@ -35,7 +35,7 @@ await spawn('pnpm', ['-w', 'prettier', '--write', outPkgJsonPath]);
console.log('Generating declaration files'); console.log('Generating declaration files');
const outDir = resolve('src/gen'); const outDir = resolve('src/gen');
const declarationFileMapping = ( const declarationFileMapping = (
await fs.readdir(pkgDir, { await readdir(pkgDir, {
recursive: true, recursive: true,
encoding: 'utf8', encoding: 'utf8',
}) })
@@ -50,11 +50,11 @@ const declarationFileMapping = (
})); }));
for (const { file, srcPath, destPath } of declarationFileMapping) { for (const { file, srcPath, destPath } of declarationFileMapping) {
const content = await fs.readFile(srcPath, 'utf8'); const content = await readFile(srcPath, 'utf8');
const transformedContent = transformFile(file, content); const transformedContent = transformFile(file, content);
const destDir = dirname(destPath); const destDir = dirname(destPath);
await fs.mkdir(destDir, { recursive: true }); await mkdir(destDir, { recursive: true });
await fs.writeFile(destPath, transformedContent); await writeFile(destPath, transformedContent);
console.log(` \x1b[2m-\x1b[0m \x1b[36m${file}\x1b[0m`); console.log(` \x1b[2m-\x1b[0m \x1b[36m${file}\x1b[0m`);
} }
+1 -1
View File
@@ -24,7 +24,7 @@
], ],
"devDependencies": { "devDependencies": {
"@types/chrome": "{{chromeTypesVersion}}", "@types/chrome": "{{chromeTypesVersion}}",
"fs-extra": "^11.3.3", "@types/node": "^20.0.0",
"nano-spawn": "^2.0.0", "nano-spawn": "^2.0.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.18" "vitest": "^4.0.18"
+6 -5
View File
@@ -23,13 +23,14 @@
"scripts": { "scripts": {
"build": "buildc -- tsdown", "build": "buildc -- tsdown",
"check": "buildc --deps-only -- check", "check": "buildc --deps-only -- check",
"test": "buildc --deps-only -- vitest" "test": "buildc --deps-only -- vitest",
"prepack": "pnpm build"
}, },
"dependencies": { "dependencies": {
"@wxt-dev/browser": "workspace:^", "@wxt-dev/browser": "workspace:^",
"chokidar": "^5.0.0", "chokidar": "^5.0.0",
"confbox": "^0.1.8 || ^0.2.2", "confbox": "^0.2.4",
"fast-glob": "^3.3.3" "tinyglobby": "^0.2.15"
}, },
"peerDependencies": { "peerDependencies": {
"wxt": ">=0.19.7" "wxt": ">=0.19.7"
@@ -41,8 +42,8 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.17.6", "@types/node": "^20.17.6",
"oxlint": "^1.43.0", "oxlint": "^1.51.0",
"publint": "^0.3.17", "publint": "^0.3.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.18", "vitest": "^4.0.18",
"vitest-plugin-random-seed": "^1.1.2", "vitest-plugin-random-seed": "^1.1.2",
+5 -10
View File
@@ -51,7 +51,8 @@ export type MessageFormat = 'JSON5' | 'YAML' | 'TOML';
// //
/** /**
* See https://developer.chrome.com/docs/extensions/reference/api/i18n#overview-predefined * See
* https://developer.chrome.com/docs/extensions/reference/api/i18n#overview-predefined
*/ */
const PREDEFINED_MESSAGES: Record<string, ChromeMessage> = { const PREDEFINED_MESSAGES: Record<string, ChromeMessage> = {
'@@extension_id': { '@@extension_id': {
@@ -110,9 +111,7 @@ const ALLOWED_CHROME_MESSAGE_KEYS: Set<string> = new Set<keyof ChromeMessage>([
// PARSING // PARSING
// //
/** /** Parse a messages file, extract the messages. Supports JSON, JSON5, and YAML. */
* Parse a messages file, extract the messages. Supports JSON, JSON5, and YAML.
*/
export async function parseMessagesFile( export async function parseMessagesFile(
file: string, file: string,
): Promise<ParsedMessage[]> { ): Promise<ParsedMessage[]> {
@@ -121,9 +120,7 @@ export async function parseMessagesFile(
return parseMessagesText(text, EXT_FORMATS_MAP[ext] ?? 'JSON5'); return parseMessagesText(text, EXT_FORMATS_MAP[ext] ?? 'JSON5');
} }
/** /** Parse a string, extracting the messages. Supports JSON, JSON5, and YAML. */
* Parse a string, extracting the messages. Supports JSON, JSON5, and YAML.
*/
export function parseMessagesText( export function parseMessagesText(
text: string, text: string,
format: 'JSON5' | 'YAML' | 'TOML', format: 'JSON5' | 'YAML' | 'TOML',
@@ -131,9 +128,7 @@ export function parseMessagesText(
return parseMessagesObject(PARSERS[format](text)); return parseMessagesObject(PARSERS[format](text));
} }
/** /** Given the JS object form of a raw messages file, extract the messages. */
* Given the JS object form of a raw messages file, extract the messages.
*/
export function parseMessagesObject(object: any): ParsedMessage[] { export function parseMessagesObject(object: any): ParsedMessage[] {
return _parseMessagesObject( return _parseMessagesObject(
[], [],
+1 -3
View File
@@ -1,6 +1,4 @@
/** /** @module @wxt-dev/i18n */
* @module @wxt-dev/i18n
*/
import { import {
I18nStructure, I18nStructure,
DefaultI18nStructure, DefaultI18nStructure,
+6 -6
View File
@@ -3,7 +3,7 @@
* *
* ```ts * ```ts
* export default defineConfig({ * export default defineConfig({
* modules: ["@wxt-dev/i18n/module"], * modules: ['@wxt-dev/i18n/module'],
* }); * });
* ``` * ```
* *
@@ -18,7 +18,7 @@ import {
generateTypeText, generateTypeText,
SUPPORTED_LOCALES, SUPPORTED_LOCALES,
} from './build'; } from './build';
import glob from 'fast-glob'; import { glob } from 'tinyglobby';
import { basename, extname, join, resolve } from 'node:path'; import { basename, extname, join, resolve } from 'node:path';
import { watch } from 'chokidar'; import { watch } from 'chokidar';
import { GeneratedPublicFile, WxtDirFileEntry } from 'wxt'; import { GeneratedPublicFile, WxtDirFileEntry } from 'wxt';
@@ -47,6 +47,7 @@ export default defineWxtModule<I18nOptions>({
const files = await glob('*.{json,json5,jsonc,yml,yaml,toml}', { const files = await glob('*.{json,json5,jsonc,yml,yaml,toml}', {
cwd: localesDir, cwd: localesDir,
absolute: true, absolute: true,
expandDirectories: false,
}); });
const unsupportedLocales: string[] = []; const unsupportedLocales: string[] = [];
@@ -180,13 +181,12 @@ export { type GeneratedI18nStructure }
}, },
}); });
/** /** Options for the i18n module */
* Options for the i18n module
*/
export interface I18nOptions { export interface I18nOptions {
/** /**
* Directory containing files that define the translations. * Directory containing files that define the translations.
* @default "${config.srcDir}/locales" *
* @default '${config.srcDir}/locales'
*/ */
localesDir?: string; localesDir?: string;
} }
+2 -2
View File
@@ -15,8 +15,8 @@
"@wxt-dev/browser": "workspace:^" "@wxt-dev/browser": "workspace:^"
}, },
"devDependencies": { "devDependencies": {
"oxlint": "^1.43.0", "oxlint": "^1.51.0",
"publint": "^0.3.17", "publint": "^0.3.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.18" "vitest": "^4.0.18"
}, },
+10 -6
View File
@@ -1,10 +1,14 @@
/** /**
* This module uses a lazy getter function so the logic isn't ran until it's needed. * This module uses a lazy getter function so the logic isn't ran until it's
* needed.
* *
* This has a few benefits: * This has a few benefits:
*
* 1. Easier to mock in tests * 1. Easier to mock in tests
* 2. Safe to import in NodeJS environments (but it should be safe to run just in-case) * 2. Safe to import in NodeJS environments (but it should be safe to run just
* 3. Keeps startup fast by waiting to run the slow functions (`instanceof` or `browser.extension.getBackgroundPage`) until needed * in-case)
* 3. Keeps startup fast by waiting to run the slow functions (`instanceof` or
* `browser.extension.getBackgroundPage`) until needed
* *
* @module @wxt-dev/is-background * @module @wxt-dev/is-background
*/ */
@@ -16,10 +20,10 @@ let cached: boolean | undefined;
* Getter that returns if the current context is apart of an extension's * Getter that returns if the current context is apart of an extension's
* background or not. * background or not.
* *
* > This function caches the result when called for the first time so it * > This function caches the result when called for the first time so it doesn't
* > doesn't have to recalculate. * > have to recalculate.
* *
* @returns true when in a background page or service worker. * @returns True when in a background page or service worker.
*/ */
export function isBackground(): boolean { export function isBackground(): boolean {
if (cached == null) cached = getIsBackground(); if (cached == null) cached = getIsBackground();
+46
View File
@@ -1,5 +1,51 @@
# Changelog # Changelog
## v1.2.2
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.2.1...module-react-v1.2.2)
### 🩹 Fixes
- Add `@vitejs/plugin-react` v6 support ([c61fa8f8](https://github.com/wxt-dev/wxt/commit/c61fa8f8))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v1.2.1
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.2.0...module-react-v1.2.1)
### 🏡 Chore
- Add prepack script to all packages ([032f7931](https://github.com/wxt-dev/wxt/commit/032f7931))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v1.2.0
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.5...module-react-v1.2.0)
### 🚀 Enhancements
- Add `vitePluginsBefore` option ([#2170](https://github.com/wxt-dev/wxt/pull/2170))
- Vite 8 support ([bfd4af5d](https://github.com/wxt-dev/wxt/commit/bfd4af5d))
### 🏡 Chore
- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))
- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))
- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))
- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))
- **deps:** Upgrade deps ([#2175](https://github.com/wxt-dev/wxt/pull/2175))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Martin Broder <hello@martinbroder.com>
## v1.1.5 ## v1.1.5
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.4...module-react-v1.1.5) [compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.4...module-react-v1.1.5)
+9 -2
View File
@@ -1,15 +1,16 @@
import 'wxt'; import 'wxt';
import { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules'; import { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules';
import react, { Options as PluginOptions } from '@vitejs/plugin-react'; import react, { Options as PluginOptions } from '@vitejs/plugin-react';
import type { PluginOption } from 'vite';
export default defineWxtModule<ReactModuleOptions>({ export default defineWxtModule<ReactModuleOptions>({
name: '@wxt-dev/module-react', name: '@wxt-dev/module-react',
configKey: 'react', configKey: 'react',
setup(wxt, options) { setup(wxt, options) {
const { vite } = options ?? {}; const { vite, vitePluginsBefore } = options ?? {};
addViteConfig(wxt, () => ({ addViteConfig(wxt, () => ({
plugins: [react(vite)], plugins: [...(vitePluginsBefore ?? []), react(vite)],
})); }));
addImportPreset(wxt, 'react'); addImportPreset(wxt, 'react');
@@ -30,6 +31,12 @@ export default defineWxtModule<ReactModuleOptions>({
export interface ReactModuleOptions { export interface ReactModuleOptions {
vite?: PluginOptions; vite?: PluginOptions;
/**
* Vite plugins to add before the `react()` plugin. Some plugins like the
* `@tanstack/router-plugin` need to be added before `react()` to work
* correctly.
*/
vitePluginsBefore?: PluginOption[];
} }
declare module 'wxt' { declare module 'wxt' {
+8 -5
View File
@@ -18,7 +18,7 @@
}, },
"license": "MIT", "license": "MIT",
"funding": "https://github.com/sponsors/wxt-dev", "funding": "https://github.com/sponsors/wxt-dev",
"version": "1.1.5", "version": "1.2.2",
"type": "module", "type": "module",
"main": "./dist/index.cjs", "main": "./dist/index.cjs",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",
@@ -42,21 +42,24 @@
"dev": "wxt", "dev": "wxt",
"check": "pnpm build && check", "check": "pnpm build && check",
"build": "buildc -- tsdown", "build": "buildc -- tsdown",
"prepare": "buildc --deps-only -- wxt prepare" "prepare": "buildc --deps-only -- wxt prepare",
"prepack": "pnpm build"
}, },
"peerDependencies": { "peerDependencies": {
"vite": "^5.4.19 || ^6.3.4 || ^7.0.0 || ^8.0.0-0",
"wxt": ">=0.19.16" "wxt": ">=0.19.16"
}, },
"dependencies": { "dependencies": {
"@vitejs/plugin-react": "^4.4.1 || ^5.0.0" "@vitejs/plugin-react": "^4.4.1 || ^5.0.0 || ^6.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.2.13", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"publint": "^0.3.17", "publint": "^0.3.18",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.0.0",
"wxt": "workspace:*" "wxt": "workspace:*"
} }
} }
+3 -2
View File
@@ -42,7 +42,8 @@
"dev": "wxt", "dev": "wxt",
"check": "pnpm build && check", "check": "pnpm build && check",
"build": "buildc -- tsdown", "build": "buildc -- tsdown",
"prepare": "buildc --deps-only -- wxt prepare" "prepare": "buildc --deps-only -- wxt prepare",
"prepack": "pnpm build"
}, },
"peerDependencies": { "peerDependencies": {
"wxt": ">=0.19.16" "wxt": ">=0.19.16"
@@ -51,7 +52,7 @@
"vite-plugin-solid": "^2.11.10" "vite-plugin-solid": "^2.11.10"
}, },
"devDependencies": { "devDependencies": {
"publint": "^0.3.17", "publint": "^0.3.18",
"solid-js": "^1.9.11", "solid-js": "^1.9.11",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"wxt": "workspace:*" "wxt": "workspace:*"
+3 -2
View File
@@ -40,7 +40,8 @@
], ],
"scripts": { "scripts": {
"build": "buildc -- tsdown", "build": "buildc -- tsdown",
"check": "pnpm build && check" "check": "pnpm build && check",
"prepack": "pnpm build"
}, },
"peerDependencies": { "peerDependencies": {
"wxt": ">=0.18.6", "wxt": ">=0.18.6",
@@ -50,7 +51,7 @@
"@sveltejs/vite-plugin-svelte": "^4.0.0 || ^5.0.0 || ^6.0.0" "@sveltejs/vite-plugin-svelte": "^4.0.0 || ^5.0.0 || ^6.0.0"
}, },
"devDependencies": { "devDependencies": {
"publint": "^0.3.17", "publint": "^0.3.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"wxt": "workspace:*" "wxt": "workspace:*"
} }
+3 -2
View File
@@ -40,7 +40,8 @@
], ],
"scripts": { "scripts": {
"build": "buildc -- tsdown", "build": "buildc -- tsdown",
"check": "pnpm build && check" "check": "pnpm build && check",
"prepack": "pnpm build"
}, },
"peerDependencies": { "peerDependencies": {
"wxt": ">=0.19.16" "wxt": ">=0.19.16"
@@ -49,7 +50,7 @@
"@vitejs/plugin-vue": "^5.2.3 || ^6.0.0" "@vitejs/plugin-vue": "^5.2.3 || ^6.0.0"
}, },
"devDependencies": { "devDependencies": {
"publint": "^0.3.17", "publint": "^0.3.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"wxt": "workspace:*" "wxt": "workspace:*"
} }
+3 -3
View File
@@ -25,11 +25,11 @@
"test": "buildc --deps-only -- vitest", "test": "buildc --deps-only -- vitest",
"dev": "tsx --trace-warnings dev.ts", "dev": "tsx --trace-warnings dev.ts",
"build": "buildc -- tsdown", "build": "buildc -- tsdown",
"prepublishOnly": "pnpm build" "prepack": "pnpm build"
}, },
"devDependencies": { "devDependencies": {
"oxlint": "^1.43.0", "oxlint": "^1.51.0",
"publint": "^0.3.17", "publint": "^0.3.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.18" "vitest": "^4.0.18"
}, },
+2 -1
View File
@@ -124,7 +124,8 @@ export const KNOWN_BROWSER_PATHS: Record<
}; };
/** /**
* When targeting a browser, this map contains the other targets to fall back on when a binary could not be found for the primary target. * When targeting a browser, this map contains the other targets to fall back on
* when a binary could not be found for the primary target.
*/ */
export const FALLBACK_TARGETS: Partial<Record<KnownTarget, KnownTarget[]>> = { export const FALLBACK_TARGETS: Partial<Record<KnownTarget, KnownTarget[]>> = {
chrome: [ chrome: [
+7 -3
View File
@@ -4,8 +4,11 @@ import { createCdpConnection } from './cdp';
/** /**
* Install an extension to an already running instance of Firefox. * Install an extension to an already running instance of Firefox.
* @param debuggerUrl The URL of the Firefox BiDi server (ex: `ws://127.0.0.1:45912`). *
* @param extensionDir Absolute path to the directory containing the extension to be installed. * @param debuggerUrl The URL of the Firefox BiDi server (ex:
* `ws://127.0.0.1:45912`).
* @param extensionDir Absolute path to the directory containing the extension
* to be installed.
*/ */
export async function installFirefox( export async function installFirefox(
debuggerUrl: string, debuggerUrl: string,
@@ -33,7 +36,8 @@ export type BidiWebExtensionInstallResponse = {
}; };
/** /**
* Given a child process of Chrome, install an extension. The process must be started with the following flags: * Given a child process of Chrome, install an extension. The process must be
* started with the following flags:
* *
* - `--remote-debugging-pipe` * - `--remote-debugging-pipe`
* - `--user-data-dir=...` * - `--user-data-dir=...`
+36 -10
View File
@@ -17,21 +17,46 @@ export type Target = KnownTarget | UnknownTarget;
export type RunOptions = { export type RunOptions = {
/** Paths to binaries to use for each target. */ /** Paths to binaries to use for each target. */
browserBinaries?: Record<string, string>; browserBinaries?: Record<string, string>;
/** Customize the arguments passed to the chromium binary. Conflicting arguments with required ones to install extensions are ignored. */ /**
* Customize the arguments passed to the chromium binary. Conflicting
* arguments with required ones to install extensions are ignored.
*/
chromiumArgs?: string[]; chromiumArgs?: string[];
/** Control how data is persisted between launches. Either save data at a user level, project level, or don't persist data at all. Defaults to `project`. */ /**
* Control how data is persisted between launches. Either save data at a user
* level, project level, or don't persist data at all. Defaults to `project`.
*/
dataPersistence?: 'user' | 'project' | 'none'; dataPersistence?: 'user' | 'project' | 'none';
/** Customize where your profile's data is stored when using `dataPersistence: 'project'`. Can be absolute or relative to the current working directory. */ /**
* Customize where your profile's data is stored when using `dataPersistence:
* 'project'`. Can be absolute or relative to the current working directory.
*/
projectDataDir?: string; projectDataDir?: string;
/** Customize the port Chrome's debugger is listening on. Defaults to a random open port. */ /**
* Customize the port Chrome's debugger is listening on. Defaults to a random
* open port.
*/
chromiumRemoteDebuggingPort?: number; chromiumRemoteDebuggingPort?: number;
/** Directory where the extension will be installed from. Should contain a `manifest.json` file. Can be relative to the current working directory. Defaults to the current working directory. */ /**
* Directory where the extension will be installed from. Should contain a
* `manifest.json` file. Can be relative to the current working directory.
* Defaults to the current working directory.
*/
extensionDir?: string; extensionDir?: string;
/** Customize the arguments passed to the firefox binary. Conflicting arguments with required ones to install extensions are ignored. */ /**
* Customize the arguments passed to the firefox binary. Conflicting arguments
* with required ones to install extensions are ignored.
*/
firefoxArgs?: string[]; firefoxArgs?: string[];
/** Customize the port Firefox's debugger is listening on. Defaults to a random open port. */ /**
* Customize the port Firefox's debugger is listening on. Defaults to a random
* open port.
*/
firefoxRemoteDebuggingPort?: number; firefoxRemoteDebuggingPort?: number;
/** Specify the browser to open. Defaults to `"chrome"`, but you can pass any string. */ /**
* Specify the browser to open. Defaults to `"chrome"`, but you can pass any
* string.
*/
target?: Target; target?: Target;
}; };
@@ -219,8 +244,9 @@ async function pathExists(path: string): Promise<boolean> {
} }
/** /**
* Copied from https://github.com/GoogleChrome/chrome-launcher/blob/main/src/flags.ts * Copied from
* with some flags commented out. Run tests after updating to compare. * https://github.com/GoogleChrome/chrome-launcher/blob/main/src/flags.ts with
* some flags commented out. Run tests after updating to compare.
*/ */
const CHROME_LAUNCHER_DEFAULT_FLAGS = [ const CHROME_LAUNCHER_DEFAULT_FLAGS = [
'--disable-features=' + '--disable-features=' +
+4 -3
View File
@@ -27,7 +27,8 @@
"scripts": { "scripts": {
"build": "buildc -- tsdown", "build": "buildc -- tsdown",
"check": "buildc --deps-only -- check", "check": "buildc --deps-only -- check",
"test": "buildc --deps-only -- vitest" "test": "buildc --deps-only -- vitest",
"prepack": "pnpm build"
}, },
"dependencies": { "dependencies": {
"@wxt-dev/browser": "workspace:^", "@wxt-dev/browser": "workspace:^",
@@ -36,8 +37,8 @@
}, },
"devDependencies": { "devDependencies": {
"@webext-core/fake-browser": "^1.3.4", "@webext-core/fake-browser": "^1.3.4",
"oxlint": "^1.43.0", "oxlint": "^1.51.0",
"publint": "^0.3.17", "publint": "^0.3.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.18" "vitest": "^4.0.18"
}, },
+2 -2
View File
@@ -4,8 +4,8 @@ import { beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest';
import { MigrationError, type WxtStorageItem, storage } from '../index'; import { MigrationError, type WxtStorageItem, storage } from '../index';
/** /**
* This works because fakeBrowser is synchronous, and is will finish any number of chained * This works because fakeBrowser is synchronous, and is will finish any number
* calls within a single tick of the event loop, ie: a timeout of 0. * of chained calls within a single tick of the event loop, ie: a timeout of 0.
*/ */
async function waitForMigrations() { async function waitForMigrations() {
return new Promise((res) => setTimeout(res)); return new Promise((res) => setTimeout(res));
+64 -100
View File
@@ -1,7 +1,9 @@
/** /**
* Simplified storage APIs with support for versioned fields, snapshots, metadata, and item definitions. * Simplified storage APIs with support for versioned fields, snapshots,
* metadata, and item definitions.
* *
* See [the guide](https://wxt.dev/storage.html) for more information. * See [the guide](https://wxt.dev/storage.html) for more information.
*
* @module @wxt-dev/storage * @module @wxt-dev/storage
*/ */
import { browser, type Browser } from '@wxt-dev/browser'; import { browser, type Browser } from '@wxt-dev/browser';
@@ -689,7 +691,7 @@ export interface WxtStorage {
* Get an item from storage, or return `null` if it doesn't exist. * Get an item from storage, or return `null` if it doesn't exist.
* *
* @example * @example
* await storage.getItem<number>("local:installDate"); * await storage.getItem<number>('local:installDate');
*/ */
getItem<TValue>( getItem<TValue>(
key: StorageItemKey, key: StorageItemKey,
@@ -702,11 +704,11 @@ export interface WxtStorage {
): Promise<TValue | null>; ): Promise<TValue | null>;
/** /**
* Get multiple items from storage. The return order is guaranteed to be the same as the order * Get multiple items from storage. The return order is guaranteed to be the
* requested. * same as the order requested.
* *
* @example * @example
* await storage.getItems(["local:installDate", "session:someCounter"]); * await storage.getItems(['local:installDate', 'session:someCounter']);
*/ */
getItems( getItems(
keys: Array< keys: Array<
@@ -717,11 +719,11 @@ export interface WxtStorage {
): Promise<Array<{ key: StorageItemKey; value: any }>>; ): Promise<Array<{ key: StorageItemKey; value: any }>>;
/** /**
* Return an object containing metadata about the key. Object is stored at `key + "$"`. If value * Return an object containing metadata about the key. Object is stored at
* is not an object, it returns an empty object. * `key + "$"`. If value is not an object, it returns an empty object.
* *
* @example * @example
* await storage.getMeta("local:installDate"); * await storage.getMeta('local:installDate');
*/ */
getMeta<T extends Record<string, unknown>>(key: StorageItemKey): Promise<T>; getMeta<T extends Record<string, unknown>>(key: StorageItemKey): Promise<T>;
@@ -736,22 +738,23 @@ export interface WxtStorage {
): Promise<Array<{ key: StorageItemKey; meta: any }>>; ): Promise<Array<{ key: StorageItemKey; meta: any }>>;
/** /**
* Set a value in storage. Setting a value to `null` or `undefined` is equivalent to calling * Set a value in storage. Setting a value to `null` or `undefined` is
* `removeItem`. * equivalent to calling `removeItem`.
* *
* @example * @example
* await storage.setItem<number>("local:installDate", Date.now()); * await storage.setItem<number>('local:installDate', Date.now());
*/ */
setItem<T>(key: StorageItemKey, value: T | null): Promise<void>; setItem<T>(key: StorageItemKey, value: T | null): Promise<void>;
/** /**
* Set multiple values in storage. If a value is set to `null` or `undefined`, the key is removed. * Set multiple values in storage. If a value is set to `null` or `undefined`,
* the key is removed.
* *
* @example * @example
* await storage.setItem([ * await storage.setItem([
* { key: "local:installDate", value: Date.now() }, * { key: "local:installDate", value: Date.now() },
* { key: "session:someCounter, value: 5 }, * { key: "session:someCounter, value: 5 },
* ]); * ]);
*/ */
setItems( setItems(
values: Array< values: Array<
@@ -761,11 +764,11 @@ export interface WxtStorage {
): Promise<void>; ): Promise<void>;
/** /**
* Sets metadata properties. If some properties are already set, but are not included in the * Sets metadata properties. If some properties are already set, but are not
* `properties` parameter, they will not be removed. * included in the `properties` parameter, they will not be removed.
* *
* @example * @example
* await storage.setMeta("local:installDate", { appVersion }); * await storage.setMeta('local:installDate', { appVersion });
*/ */
setMeta<T extends Record<string, unknown>>( setMeta<T extends Record<string, unknown>>(
key: StorageItemKey, key: StorageItemKey,
@@ -788,13 +791,11 @@ export interface WxtStorage {
* Removes an item from storage. * Removes an item from storage.
* *
* @example * @example
* await storage.removeItem("local:installDate"); * await storage.removeItem('local:installDate');
*/ */
removeItem(key: StorageItemKey, opts?: RemoveItemOptions): Promise<void>; removeItem(key: StorageItemKey, opts?: RemoveItemOptions): Promise<void>;
/** /** Remove a list of keys from storage. */
* Remove a list of keys from storage.
*/
removeItems( removeItems(
keys: Array< keys: Array<
| StorageItemKey | StorageItemKey
@@ -804,48 +805,41 @@ export interface WxtStorage {
>, >,
): Promise<void>; ): Promise<void>;
/** /** Removes all items from the provided storage area. */
* Removes all items from the provided storage area.
*/
clear(base: StorageArea): Promise<void>; clear(base: StorageArea): Promise<void>;
/** /**
* Remove the entire metadata for a key, or specific properties by name. * Remove the entire metadata for a key, or specific properties by name.
* *
* @example * @example
* // Remove all metadata properties from the item * // Remove all metadata properties from the item
* await storage.removeMeta("local:installDate"); * await storage.removeMeta('local:installDate');
* *
* // Remove only specific the "v" field * // Remove only specific the "v" field
* await storage.removeMeta("local:installDate", "v") * await storage.removeMeta('local:installDate', 'v');
*/ */
removeMeta( removeMeta(
key: StorageItemKey, key: StorageItemKey,
properties?: string | string[], properties?: string | string[],
): Promise<void>; ): Promise<void>;
/** /** Return all the items in storage. */
* Return all the items in storage.
*/
snapshot( snapshot(
base: StorageArea, base: StorageArea,
opts?: SnapshotOptions, opts?: SnapshotOptions,
): Promise<Record<string, unknown>>; ): Promise<Record<string, unknown>>;
/** /**
* Restores the results of `snapshot`. If new properties have been saved since the snapshot, they are * Restores the results of `snapshot`. If new properties have been saved since
* not overridden. Only values existing in the snapshot are overridden. * the snapshot, they are not overridden. Only values existing in the snapshot
* are overridden.
*/ */
restoreSnapshot(base: StorageArea, data: any): Promise<void>; restoreSnapshot(base: StorageArea, data: any): Promise<void>;
/** /** Watch for changes to a specific key in storage. */
* Watch for changes to a specific key in storage.
*/
watch<T>(key: StorageItemKey, cb: WatchCallback<T | null>): Unwatch; watch<T>(key: StorageItemKey, cb: WatchCallback<T | null>): Unwatch;
/** /** Remove all watch listeners. */
* Remove all watch listeners.
*/
unwatch(): void; unwatch(): void;
/** /**
@@ -894,61 +888,42 @@ export interface WxtStorageItem<
TValue, TValue,
TMetadata extends Record<string, unknown>, TMetadata extends Record<string, unknown>,
> { > {
/** /** The storage key passed when creating the storage item. */
* The storage key passed when creating the storage item.
*/
key: StorageItemKey; key: StorageItemKey;
/** /** @deprecated Renamed to fallback, use it instead. */
* @deprecated Renamed to fallback, use it instead.
*/
defaultValue: TValue; defaultValue: TValue;
/** /** The value provided by the `fallback` option. */
* The value provided by the `fallback` option.
*/
fallback: TValue; fallback: TValue;
/** /** Get the latest value from storage. */
* Get the latest value from storage.
*/
getValue(): Promise<TValue>; getValue(): Promise<TValue>;
/** /** Get metadata. */
* Get metadata.
*/
getMeta(): Promise<NullablePartial<TMetadata>>; getMeta(): Promise<NullablePartial<TMetadata>>;
/** /** Set the value in storage. */
* Set the value in storage.
*/
setValue(value: TValue): Promise<void>; setValue(value: TValue): Promise<void>;
/** /** Set metadata properties. */
* Set metadata properties.
*/
setMeta(properties: NullablePartial<TMetadata>): Promise<void>; setMeta(properties: NullablePartial<TMetadata>): Promise<void>;
/** /** Remove the value from storage. */
* Remove the value from storage.
*/
removeValue(opts?: RemoveItemOptions): Promise<void>; removeValue(opts?: RemoveItemOptions): Promise<void>;
/** /** Remove all metadata or certain properties from metadata. */
* Remove all metadata or certain properties from metadata.
*/
removeMeta(properties?: string[]): Promise<void>; removeMeta(properties?: string[]): Promise<void>;
/** /** Listen for changes to the value in storage. */
* Listen for changes to the value in storage.
*/
watch(cb: WatchCallback<TValue>): Unwatch; watch(cb: WatchCallback<TValue>): Unwatch;
/** /**
* If there are migrations defined on the storage item, migrate to the latest version. * If there are migrations defined on the storage item, migrate to the latest
* version.
* *
* **This function is ran automatically whenever the extension updates**, so you don't have to call it * **This function is ran automatically whenever the extension updates**, so
* manually. * you don't have to call it manually.
*/ */
migrate(): Promise<void>; migrate(): Promise<void>;
} }
@@ -957,14 +932,10 @@ export type StorageArea = 'local' | 'session' | 'sync' | 'managed';
export type StorageItemKey = `${StorageArea}:${string}`; export type StorageItemKey = `${StorageArea}:${string}`;
export interface GetItemOptions<T> { export interface GetItemOptions<T> {
/** /** @deprecated Renamed to `fallback`, use it instead. */
* @deprecated Renamed to `fallback`, use it instead.
*/
defaultValue?: T; defaultValue?: T;
/** /** Default value returned when `getItem` would otherwise return `null`. */
* Default value returned when `getItem` would otherwise return `null`.
*/
fallback?: T; fallback?: T;
} }
@@ -979,21 +950,17 @@ export interface RemoveItemOptions {
export interface SnapshotOptions { export interface SnapshotOptions {
/** /**
* Exclude a list of keys. The storage area prefix should be removed since the snapshot is for a * Exclude a list of keys. The storage area prefix should be removed since the
* specific storage area already. * snapshot is for a specific storage area already.
*/ */
excludeKeys?: string[]; excludeKeys?: string[];
} }
export interface WxtStorageItemOptions<T> { export interface WxtStorageItemOptions<T> {
/** /** @deprecated Renamed to `fallback`, use it instead. */
* @deprecated Renamed to `fallback`, use it instead.
*/
defaultValue?: T; defaultValue?: T;
/** /** Default value returned when `getValue` would otherwise return `null`. */
* Default value returned when `getValue` would otherwise return `null`.
*/
fallback?: T; fallback?: T;
/** /**
@@ -1005,25 +972,26 @@ export interface WxtStorageItemOptions<T> {
init?: () => T | Promise<T>; init?: () => T | Promise<T>;
/** /**
* Provide a version number for the storage item to enable migrations. When changing the version * Provide a version number for the storage item to enable migrations. When
* in the future, migration functions will be ran on application startup. * changing the version in the future, migration functions will be ran on
* application startup.
*/ */
version?: number; version?: number;
/** /**
* A map of version numbers to the functions used to migrate the data to that version. * A map of version numbers to the functions used to migrate the data to that
* version.
*/ */
migrations?: Record<number, (oldValue: any) => any>; migrations?: Record<number, (oldValue: any) => any>;
/** /**
* Print debug logs, such as migration process. * Print debug logs, such as migration process.
*
* @default false * @default false
*/ */
debug?: boolean; debug?: boolean;
/** /** A callback function that runs on migration complete. */
* A callback function that runs on migration complete.
*/
onMigrationComplete?: (migratedValue: T, targetVersion: number) => void; onMigrationComplete?: (migratedValue: T, targetVersion: number) => void;
} }
@@ -1032,21 +1000,17 @@ export type StorageAreaChanges = {
}; };
/** /**
* Same as `Partial`, but includes `| null`. It makes all the properties of an object optional and * Same as `Partial`, but includes `| null`. It makes all the properties of an
* nullable. * object optional and nullable.
*/ */
type NullablePartial<T> = { type NullablePartial<T> = {
[key in keyof T]+?: T[key] | undefined | null; [key in keyof T]+?: T[key] | undefined | null;
}; };
/** /** Callback called when a value in storage is changed. */
* Callback called when a value in storage is changed.
*/
export type WatchCallback<T> = (newValue: T, oldValue: T) => void; export type WatchCallback<T> = (newValue: T, oldValue: T) => void;
/** /** Call to remove a watch listener */
* Call to remove a watch listener
*/
export type Unwatch = () => void; export type Unwatch = () => void;
export class MigrationError extends Error { export class MigrationError extends Error {
+5 -4
View File
@@ -33,21 +33,22 @@
], ],
"scripts": { "scripts": {
"build": "buildc -- tsdown", "build": "buildc -- tsdown",
"check": "buildc --deps-only -- check" "check": "buildc --deps-only -- check",
"prepack": "pnpm build"
}, },
"peerDependencies": { "peerDependencies": {
"unocss": ">=0.60.0", "unocss": ">=0.60.0",
"wxt": ">=0.19.0" "wxt": ">=0.19.0"
}, },
"devDependencies": { "devDependencies": {
"oxlint": "^1.43.0", "oxlint": "^1.51.0",
"publint": "^0.3.17", "publint": "^0.3.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"unocss": "^0.64.0 || ^0.65.0 || ^65.0.0 || ^66.0.0", "unocss": "^0.64.0 || ^0.65.0 || ^65.0.0 || ^66.0.0",
"wxt": "workspace:*" "wxt": "workspace:*"
}, },
"dependencies": { "dependencies": {
"defu": "^6.1.4", "defu": "^6.1.4",
"fast-glob": "^3.3.3" "tinyglobby": "^0.2.15"
} }
} }
+10 -6
View File
@@ -38,12 +38,11 @@ export default defineWxtModule<UnoCSSOptions>({
}, },
}); });
/** /** Options for the UnoCSS module */
* Options for the UnoCSS module
*/
export interface UnoCSSOptions<Theme extends object = object> { export interface UnoCSSOptions<Theme extends object = object> {
/** /**
* Enable UnoCSS * Enable UnoCSS
*
* @default true * @default true
*/ */
enabled?: boolean; enabled?: boolean;
@@ -51,12 +50,17 @@ export interface UnoCSSOptions<Theme extends object = object> {
* List of entrypoint names that UnoCSS is not used in. By default, the UnoCSS * List of entrypoint names that UnoCSS is not used in. By default, the UnoCSS
* vite plugin is added to all build steps, but this option is used to exclude * vite plugin is added to all build steps, but this option is used to exclude
* it from specific builds. * it from specific builds.
* @example ["popup", "options"] *
* @default [] * @example
* {undefined} ('popup',
* 'options');
*
* @default [ ]
*/ */
excludeEntrypoints?: string[]; excludeEntrypoints?: string[];
/** /**
* The path to your `unocss.config.ts` file, relative to <rootDir>, or inline configuration. * The path to your `unocss.config.ts` file, relative to <rootDir>, or inline
* configuration.
*/ */
configOrPath?: Parameters<typeof UnoCSS<Theme>>[0]; configOrPath?: Parameters<typeof UnoCSS<Theme>>[0];
} }
+4 -3
View File
@@ -38,15 +38,16 @@
"dev": "wxt", "dev": "wxt",
"check": "pnpm build && check", "check": "pnpm build && check",
"build": "buildc -- tsdown", "build": "buildc -- tsdown",
"prepare": "buildc --deps-only -- wxt prepare" "prepare": "buildc --deps-only -- wxt prepare",
"prepack": "pnpm build"
}, },
"peerDependencies": { "peerDependencies": {
"webextension-polyfill": "*", "webextension-polyfill": "*",
"wxt": ">=0.20.0" "wxt": ">=0.20.0"
}, },
"devDependencies": { "devDependencies": {
"@types/webextension-polyfill": "^0.12.4", "@types/webextension-polyfill": "^0.12.5",
"publint": "^0.3.17", "publint": "^0.3.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"webextension-polyfill": "^0.12.0", "webextension-polyfill": "^0.12.0",
"wxt": "workspace:*" "wxt": "workspace:*"
+1 -1
View File
@@ -22,7 +22,7 @@
"react-dom": "^19.2.4" "react-dom": "^19.2.4"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.2.13", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@wxt-dev/auto-icons": "workspace:*", "@wxt-dev/auto-icons": "workspace:*",
"@wxt-dev/unocss": "workspace:*", "@wxt-dev/unocss": "workspace:*",
+90
View File
@@ -1,5 +1,95 @@
# Changelog # Changelog
## v0.20.19
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.18...wxt-v0.20.19)
### 🚀 Enhancements
- Vite 8 support ([bfd4af5d](https://github.com/wxt-dev/wxt/commit/bfd4af5d))
### 🔥 Performance
- Use filter to improve plugin performance with rolldown ([#1787](https://github.com/wxt-dev/wxt/pull/1787))
### 🩹 Fixes
- List eslint 10 as supported ([55dc263d](https://github.com/wxt-dev/wxt/commit/55dc263d))
### 📖 Documentation
- Added "Scrape Similar" to the homepage ([#2158](https://github.com/wxt-dev/wxt/pull/2158))
- Added "isTrust" to the homepage ([#2161](https://github.com/wxt-dev/wxt/pull/2161))
### 🏡 Chore
- Add `prettier-plugin-jsdoc` to project ([#2171](https://github.com/wxt-dev/wxt/pull/2171))
- Replace fast-glob and ora with lighter alternatives ([#2173](https://github.com/wxt-dev/wxt/pull/2173))
- **deps:** Upgrade deps ([#2175](https://github.com/wxt-dev/wxt/pull/2175))
- Replace `fs-extra` with `node:fs/promises` ([#2174](https://github.com/wxt-dev/wxt/pull/2174))
### ❤️ Contributors
- Florian Metz ([@Timeraa](https://github.com/Timeraa))
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
- Etoome ([@etoome](https://github.com/etoome))
- Kuba ([@zizzfizzix](https://github.com/zizzfizzix))
## v0.20.18
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.17...wxt-v0.20.18)
### 🚀 Enhancements
- Create tests for all func of `network.ts` ([#2132](https://github.com/wxt-dev/wxt/pull/2132))
- Use Navigation API for location change detection with polling fallback ([#2136](https://github.com/wxt-dev/wxt/pull/2136))
- **modules:** Add support for augumenting entrypoint options ([#2149](https://github.com/wxt-dev/wxt/pull/2149))
- New `@wxt-dev/is-background` package ([#2152](https://github.com/wxt-dev/wxt/pull/2152))
- Add `globalName` entrypoint option. ([#2017](https://github.com/wxt-dev/wxt/pull/2017))
### 🩹 Fixes
- Add `getAppConfig` as an alias to `useAppConfig` ([#2144](https://github.com/wxt-dev/wxt/pull/2144))
- Enable mv3 dev mode with firefox 147 ([#2135](https://github.com/wxt-dev/wxt/pull/2135))
- **types:** Add type safety to `browser.runtime.executeScript` `files` option ([#2142](https://github.com/wxt-dev/wxt/pull/2142))
- **types:** Include CSS entrypoints in PublicPath generation ([#2150](https://github.com/wxt-dev/wxt/pull/2150))
- Fix submit command ([#2157](https://github.com/wxt-dev/wxt/pull/2157))
### 📖 Documentation
- Added "Glossy New Tab" to the homepage ([#2133](https://github.com/wxt-dev/wxt/pull/2133))
- Add All API Hub to showcase extensions ([#2137](https://github.com/wxt-dev/wxt/pull/2137))
- Add `wxt-local-analytics` to community resources ([#2141](https://github.com/wxt-dev/wxt/pull/2141))
- Add deep outline to modules page to show recipes ([653608c9](https://github.com/wxt-dev/wxt/commit/653608c9))
### 🏡 Chore
- Add missing `async` to network.ts ([#2107](https://github.com/wxt-dev/wxt/pull/2107))
- Make type for `__ENTRYPOINT__` of vitest.globalSetup.ts ([#2128](https://github.com/wxt-dev/wxt/pull/2128))
- Remove unnecessary `@ts-ignore` of `builders/vite` index.ts ([#2127](https://github.com/wxt-dev/wxt/pull/2127))
- Rename `zipdir` to `zipDir` ([#2126](https://github.com/wxt-dev/wxt/pull/2126))
- Simplify imports in `wxt/e2e` ([#2122](https://github.com/wxt-dev/wxt/pull/2122))
- Remove `@ts-expect-error` from manifest.test.ts and fix typo ([#2123](https://github.com/wxt-dev/wxt/pull/2123))
### 🤖 CI
- Fix VHS action ([#2155](https://github.com/wxt-dev/wxt/pull/2155))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Jonathan Viney ([@jviney](https://github.com/jviney))
- Tam Dang ([@dahomita](https://github.com/dahomita))
- Nick Doan ([@nickbar01234](https://github.com/nickbar01234))
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
- Anthony Ciccarello ([@aciccarello](https://github.com/aciccarello))
- Henrik Wenz ([@HaNdTriX](https://github.com/HaNdTriX))
- Sheng Zhang ([@Arktomson](https://github.com/Arktomson))
- Oleksii ([@Aler1x](https://github.com/Aler1x))
- Qixing-jk <vq3d5d8c@duck.com>
- Muzammil ([@oyzamil](https://github.com/oyzamil))
## v0.20.17 ## v0.20.17
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.16...wxt-v0.20.17) [compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.16...wxt-v0.20.17)
+5 -4
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* A alias around `publish-extension` that is always installed on the path without having to install * A alias around `publish-extension` that is always installed on the path
* `publish-browser-extension` as a direct dependency (like for PNPM, which doesn't link * without having to install `publish-browser-extension` as a direct dependency
* sub-dependency binaries to "node_modules/.bin") * (like for PNPM, which doesn't link sub-dependency binaries to
* "node_modules/.bin")
*/ */
import 'publish-browser-extension/cli'; await import('publish-browser-extension/cli');
+6 -5
View File
@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest';
import { TestProject, WXT_PACKAGE_DIR } from '../utils';
import spawn from 'nano-spawn'; import spawn from 'nano-spawn';
import glob from 'fast-glob'; import { mkdir, writeFile } from 'node:fs/promises';
import { mkdir, writeJson } from 'fs-extra'; import { glob } from 'tinyglobby';
import { describe, expect, it } from 'vitest';
import { TestProject, WXT_PACKAGE_DIR } from '../utils';
describe('Init command', () => { describe('Init command', () => {
it('should download and create a template', async () => { it('should download and create a template', async () => {
@@ -21,6 +21,7 @@ describe('Init command', () => {
cwd: project.root, cwd: project.root,
onlyFiles: true, onlyFiles: true,
dot: true, dot: true,
expandDirectories: false,
}); });
expect(files.sort()).toMatchInlineSnapshot(` expect(files.sort()).toMatchInlineSnapshot(`
@@ -52,7 +53,7 @@ describe('Init command', () => {
it('should throw an error if the directory is not empty', async () => { it('should throw an error if the directory is not empty', async () => {
const project = new TestProject(); const project = new TestProject();
await mkdir(project.root, { recursive: true }); await mkdir(project.root, { recursive: true });
await writeJson(project.resolvePath('package.json'), {}); await writeFile(project.resolvePath('package.json'), JSON.stringify({}));
await expect(() => await expect(() =>
spawn( spawn(
+1 -1
View File
@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { TestProject } from '../utils'; import { TestProject } from '../utils';
import type { InlineConfig, UnlistedScriptEntrypoint } from '../../src'; import type { InlineConfig, UnlistedScriptEntrypoint } from '../../src';
import { readFile } from 'fs-extra'; import { readFile } from 'node:fs/promises';
import { normalizePath } from '../../src'; import { normalizePath } from '../../src';
describe('Module Helpers', () => { describe('Module Helpers', () => {
+1 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
import { TestProject } from '../utils'; import { TestProject } from '../utils';
import extract from 'extract-zip'; import extract from 'extract-zip';
import spawn from 'nano-spawn'; import spawn from 'nano-spawn';
import { readFile, writeFile } from 'fs-extra'; import { readFile, writeFile } from 'node:fs/promises';
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true'; process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
+21 -21
View File
@@ -1,8 +1,8 @@
import glob from 'fast-glob';
import fs, { mkdir } from 'fs-extra';
import merge from 'lodash.merge'; import merge from 'lodash.merge';
import spawn from 'nano-spawn'; import spawn from 'nano-spawn';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, relative, resolve } from 'path'; import { dirname, relative, resolve } from 'path';
import { glob } from 'tinyglobby';
import { import {
InlineConfig, InlineConfig,
UserConfig, UserConfig,
@@ -12,6 +12,7 @@ import {
zip, zip,
} from '../src'; } from '../src';
import { normalizePath } from '../src/core/utils'; import { normalizePath } from '../src/core/utils';
import { pathExists, readJson } from '../src/core/utils/fs';
// Run "pnpm wxt" to use the "wxt" dev script, not the "wxt" binary from the // 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 // wxt package. This uses the TS files instead of the compiled JS package
@@ -59,9 +60,7 @@ export class TestProject {
]); ]);
} }
/** /** Add a `wxt.config.ts` to the project with specific contents. */
* Add a `wxt.config.ts` to the project with specific contents.
*/
setConfigFileConfig(config: UserConfig = {}) { setConfigFileConfig(config: UserConfig = {}) {
this.config = config; this.config = config;
this.files.push([ this.files.push([
@@ -109,9 +108,7 @@ export class TestProject {
return server; return server;
} }
/** /** Call `path.resolve` relative to the project's root directory. */
* Call `path.resolve` relative to the project's root directory.
*/
resolvePath(...path: string[]): string { resolvePath(...path: string[]): string {
return resolve(this.root, ...path); return resolve(this.root, ...path);
} }
@@ -123,8 +120,8 @@ export class TestProject {
const [name, content] = file; const [name, content] = file;
const filePath = this.resolvePath(name); const filePath = this.resolvePath(name);
const fileDir = dirname(filePath); const fileDir = dirname(filePath);
await fs.ensureDir(fileDir); await mkdir(fileDir, { recursive: true });
await fs.writeFile(filePath, content ?? '', 'utf-8'); await writeFile(filePath, content ?? '', 'utf-8');
} }
// Only install dependencies if the project has custom ones - otherwise the // Only install dependencies if the project has custom ones - otherwise the
@@ -141,11 +138,11 @@ export class TestProject {
} }
/** /**
* Read all the files from the test project's `.output` directory and combine them into a string * Read all the files from the test project's `.output` directory and combine
* that can be used in a snapshot. * 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 * Optionally, provide a list of filenames whose content is not printed
* or not relevant to a test). * (because it's inconsistent or not relevant to a test).
*/ */
serializeOutput(ignoreContentsOfFilenames?: string[]): Promise<string> { serializeOutput(ignoreContentsOfFilenames?: string[]): Promise<string> {
return this.serializeDir('.output', ignoreContentsOfFilenames); return this.serializeDir('.output', ignoreContentsOfFilenames);
@@ -154,8 +151,8 @@ export class TestProject {
/** /**
* Deeply print the filename and contents of all files in a directory. * 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 * Optionally, provide a list of filenames whose content is not printed
* or not relevant to a test). * (because it's inconsistent or not relevant to a test).
*/ */
private async serializeDir( private async serializeDir(
dir: string, dir: string,
@@ -164,6 +161,7 @@ export class TestProject {
const outputFiles = await glob('**/*', { const outputFiles = await glob('**/*', {
cwd: this.resolvePath(dir), cwd: this.resolvePath(dir),
ignore: ['**/node_modules', '**/.output'], ignore: ['**/node_modules', '**/.output'],
expandDirectories: false,
}); });
outputFiles.sort(); outputFiles.sort();
const fileContents = []; const fileContents = [];
@@ -179,24 +177,26 @@ export class TestProject {
/** /**
* @param path An absolute path to a file or a path relative to the root. * @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 * @param ignoreContents An optional boolean that, when true, causes this
* the file contents. * function to not print the file contents.
*/ */
async serializeFile(path: string, ignoreContents?: boolean): Promise<string> { async serializeFile(path: string, ignoreContents?: boolean): Promise<string> {
const absolutePath = this.resolvePath(path); const absolutePath = this.resolvePath(path);
return [ return [
normalizePath(relative(this.root, absolutePath)), normalizePath(relative(this.root, absolutePath)),
ignoreContents ? '<contents-ignored>' : await fs.readFile(absolutePath), ignoreContents
? '<contents-ignored>'
: await readFile(absolutePath, 'utf-8'),
].join(`\n${''.padEnd(40, '-')}\n`); ].join(`\n${''.padEnd(40, '-')}\n`);
} }
pathExists(...path: string[]): Promise<boolean> { pathExists(...path: string[]): Promise<boolean> {
return fs.pathExists(this.resolvePath(...path)); return pathExists(this.resolvePath(...path));
} }
getOutputManifest( getOutputManifest(
path: string = '.output/chrome-mv3/manifest.json', path: string = '.output/chrome-mv3/manifest.json',
): Promise<any> { ): Promise<any> {
return fs.readJson(this.resolvePath(path)); return readJson(this.resolvePath(path));
} }
} }
+1
View File
@@ -1,3 +1,4 @@
// oxlint-disable-next-line eslint(no-shadow-restricted-names)
declare namespace globalThis { declare namespace globalThis {
var __ENTRYPOINT__: string; var __ENTRYPOINT__: string;
} }
+16 -18
View File
@@ -1,7 +1,7 @@
{ {
"name": "wxt", "name": "wxt",
"type": "module", "type": "module",
"version": "0.20.17", "version": "0.20.19",
"description": "⚡ Next-gen Web Extension Framework", "description": "⚡ Next-gen Web Extension Framework",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@@ -25,45 +25,44 @@
"@wxt-dev/storage": "workspace:^1.0.0", "@wxt-dev/storage": "workspace:^1.0.0",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"c12": "^3.3.3", "c12": "^3.3.3",
"cac": "^6.7.14", "cac": "^6.7.14 || ^7.0.0",
"chokidar": "^5.0.0", "chokidar": "^5.0.0",
"ci-info": "^4.4.0", "ci-info": "^4.4.0",
"consola": "^3.4.2", "consola": "^3.4.2",
"defu": "^6.1.4", "defu": "^6.1.4",
"dotenv": "^17.2.4", "dotenv": "^17.3.1",
"dotenv-expand": "^12.0.3", "dotenv-expand": "^12.0.3",
"esbuild": "^0.27.1", "esbuild": "^0.27.1",
"fast-glob": "^3.3.3",
"filesize": "^11.0.13", "filesize": "^11.0.13",
"fs-extra": "^11.3.3",
"get-port-please": "^3.2.0", "get-port-please": "^3.2.0",
"giget": "^1.2.3 || ^2.0.0 || ^3.0.0", "giget": "^1.2.3 || ^2.0.0 || ^3.0.0",
"hookable": "^6.0.1", "hookable": "^6.0.1",
"import-meta-resolve": "^4.2.0", "import-meta-resolve": "^4.2.0",
"is-wsl": "^3.1.0", "is-wsl": "^3.1.1",
"json5": "^2.2.3", "json5": "^2.2.3",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"linkedom": "^0.18.12", "linkedom": "^0.18.12",
"magicast": "^0.5.2", "magicast": "^0.5.2",
"minimatch": "^10.1.2", "minimatch": "^10.2.4",
"nano-spawn": "^2.0.0", "nano-spawn": "^2.0.0",
"nanospinner": "^1.2.2",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"nypm": "^0.6.5", "nypm": "^0.6.5",
"ohash": "^2.0.11", "ohash": "^2.0.11",
"open": "^11.0.0", "open": "^11.0.0",
"ora": "^9.3.0",
"perfect-debounce": "^2.1.0", "perfect-debounce": "^2.1.0",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"publish-browser-extension": "^2.3.0 || ^3.0.2 || ^4.0.0", "publish-browser-extension": "^2.3.0 || ^3.0.2 || ^4.0.4",
"scule": "^1.3.0", "scule": "^1.3.0",
"unimport": "^3.13.1 || ^4.0.0 || ^5.0.0", "tinyglobby": "^0.2.15",
"vite": "^5.4.19 || ^6.3.4 || ^7.0.0", "unimport": "^3.13.1 || ^4.0.0 || ^5.0.0 || ^6.0.0",
"vite": "^5.4.19 || ^6.3.4 || ^7.0.0 || ^8.0.0-0",
"vite-node": "^3.2.4 || ^5.0.0", "vite-node": "^3.2.4 || ^5.0.0",
"web-ext-run": "^0.2.4" "web-ext-run": "^0.2.4"
}, },
"peerDependencies": { "peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0" "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"eslint": { "eslint": {
@@ -71,18 +70,17 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^10.2.0", "@faker-js/faker": "^10.3.0",
"@types/fs-extra": "^11.0.4",
"@types/lodash.merge": "^4.6.9", "@types/lodash.merge": "^4.6.9",
"@types/node": "^20.17.6", "@types/node": "^20.17.6",
"@types/normalize-path": "^3.0.2", "@types/normalize-path": "^3.0.2",
"@types/prompts": "^2.4.9", "@types/prompts": "^2.4.9",
"eslint": "^9.38.0", "eslint": "^10.0.0",
"extract-zip": "^2.0.1", "extract-zip": "^2.0.1",
"happy-dom": "^20.5.0", "happy-dom": "^20.8.3",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"oxlint": "^1.43.0", "oxlint": "^1.51.0",
"publint": "^0.3.17", "publint": "^0.3.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.18", "vitest": "^4.0.18",
"vitest-plugin-random-seed": "^1.1.2" "vitest-plugin-random-seed": "^1.1.2"
+12 -7
View File
@@ -1,25 +1,32 @@
/** /**
* Contains the `browser` export which you should use to access the extension APIs in your project: * Contains the `browser` export which you should use to access the extension
* APIs in your project:
*
* ```ts * ```ts
* import { browser } from 'wxt/browser'; * import { browser } from 'wxt/browser';
* *
* browser.runtime.onInstalled.addListener(() => { * browser.runtime.onInstalled.addListener(() => {
* // ... * // ...
* }) * });
* ``` * ```
*
* @module wxt/browser * @module wxt/browser
*/ */
import { browser as _browser, type Browser } from '@wxt-dev/browser'; import { browser as _browser, type Browser } from '@wxt-dev/browser';
import type { ScriptPublicPath } from './utils/inject-script'; import type { ScriptPublicPath } from './utils/inject-script';
/** /**
* This interface is empty because it is generated per-project when running `wxt prepare`. See: * This interface is empty because it is generated per-project when running `wxt
* prepare`. See:
*
* - `.wxt/types/paths.d.ts` * - `.wxt/types/paths.d.ts`
*/ */
export interface WxtRuntime {} export interface WxtRuntime {}
/** /**
* This interface is empty because it is generated per-project when running `wxt prepare`. See: * This interface is empty because it is generated per-project when running `wxt
* prepare`. See:
*
* - `.wxt/types/i18n.d.ts` * - `.wxt/types/i18n.d.ts`
*/ */
export interface WxtI18n {} export interface WxtI18n {}
@@ -36,9 +43,7 @@ type InjectionResult<Result> = Array<
export interface WxtScripting { export interface WxtScripting {
executeScript: { executeScript: {
/** /** @see {@link Browser.scripting.executeScript} */
* @see {@link Browser.scripting.executeScript}
*/
<Args extends any[], Result>( <Args extends any[], Result>(
injection: ScriptInjection<Args, Result>, injection: ScriptInjection<Args, Result>,
): Promise<InjectionResult<Result>>; ): Promise<InjectionResult<Result>>;
+4 -3
View File
@@ -8,7 +8,8 @@ import { registerWxt } from '../core/wxt';
import spawn from 'nano-spawn'; import spawn from 'nano-spawn';
/** /**
* Wrap an action handler to add a timer, error handling, and maybe enable debug mode. * Wrap an action handler to add a timer, error handling, and maybe enable debug
* mode.
*/ */
export function wrapAction( export function wrapAction(
cb: ( cb: (
@@ -54,8 +55,8 @@ export function wrapAction(
} }
/** /**
* Array flags, when not passed, are either `undefined` or `[undefined]`. This function filters out * Array flags, when not passed, are either `undefined` or `[undefined]`. This
* the * function filters out the
*/ */
export function getArrayFromFlags<T>( export function getArrayFromFlags<T>(
flags: any, flags: any,
+9 -8
View File
@@ -3,17 +3,18 @@ import { internalBuild } from './utils/building';
import { registerWxt } from './wxt'; import { registerWxt } from './wxt';
/** /**
* Bundles the extension for production. Returns a promise of the build result. Discovers the `wxt.config.ts` file in * Bundles the extension for production. Returns a promise of the build result.
* the root directory, and merges that config with what is passed in. * Discovers the `wxt.config.ts` file in the root directory, and merges that
* config with what is passed in.
* *
* @example * @example
* // Use config from `wxt.config.ts` * // Use config from `wxt.config.ts`
* const res = await build() * const res = await build();
* *
* // or override config `from wxt.config.ts` * // or override config `from wxt.config.ts`
* const res = await build({ * const res = await build({
* // Override config... * // Override config...
* }) * });
*/ */
export async function build(config?: InlineConfig): Promise<BuildOutput> { export async function build(config?: InlineConfig): Promise<BuildOutput> {
await registerWxt('build', config); await registerWxt('build', config);
+26 -22
View File
@@ -26,7 +26,7 @@ import { ViteNodeRunner } from 'vite-node/client';
import { installSourcemapsSupport } from 'vite-node/source-map'; import { installSourcemapsSupport } from 'vite-node/source-map';
import { createExtensionEnvironment } from '../../utils/environments'; import { createExtensionEnvironment } from '../../utils/environments';
import { dirname, extname, join, relative } from 'node:path'; import { dirname, extname, join, relative } from 'node:path';
import fs from 'fs-extra'; import { mkdir, readdir, rename, rmdir, stat } from 'node:fs/promises';
import { normalizePath } from '../../utils'; import { normalizePath } from '../../utils';
export async function createViteBuilder( export async function createViteBuilder(
@@ -37,7 +37,8 @@ export async function createViteBuilder(
const vite = await import('vite'); const vite = await import('vite');
/** /**
* Returns the base vite config shared by all builds based on the inline and user config. * Returns the base vite config shared by all builds based on the inline and
* user config.
*/ */
const getBaseConfig = async (baseConfigOptions?: { const getBaseConfig = async (baseConfigOptions?: {
excludeAnalysisPlugin?: boolean; excludeAnalysisPlugin?: boolean;
@@ -102,7 +103,8 @@ export async function createViteBuilder(
}; };
/** /**
* Return the basic config for building an entrypoint in [lib mode](https://vitejs.dev/guide/build.html#library-mode). * 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 getLibModeConfig = (entrypoint: Entrypoint): vite.InlineConfig => {
const entry = getRollupEntry(entrypoint); const entry = getRollupEntry(entrypoint);
@@ -177,7 +179,8 @@ export async function createViteBuilder(
}; };
/** /**
* Return the basic config for building multiple entrypoints in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app). * 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 getMultiPageConfig = (entrypoints: Entrypoint[]): vite.InlineConfig => {
const htmlEntrypoints = new Set( const htmlEntrypoints = new Set(
@@ -210,7 +213,8 @@ export async function createViteBuilder(
}; };
/** /**
* Return the basic config for building a single CSS entrypoint in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app). * Return the basic config for building a single CSS entrypoint in [multi-page
* mode](https://vitejs.dev/guide/build.html#multi-page-app).
*/ */
const getCssConfig = (entrypoint: Entrypoint): vite.InlineConfig => { const getCssConfig = (entrypoint: Entrypoint): vite.InlineConfig => {
return { return {
@@ -387,8 +391,8 @@ function getBuildOutputChunks(
} }
/** /**
* Returns the input module ID (virtual or real file) for an entrypoint. The returned string should * Returns the input module ID (virtual or real file) for an entrypoint. The
* be passed as an input to rollup. * returned string should be passed as an input to rollup.
*/ */
function getRollupEntry(entrypoint: Entrypoint): string { function getRollupEntry(entrypoint: Entrypoint): string {
let virtualEntrypointType: VirtualEntrypointType | undefined; let virtualEntrypointType: VirtualEntrypointType | undefined;
@@ -413,15 +417,17 @@ function getRollupEntry(entrypoint: Entrypoint): string {
} }
/** /**
* Ensures the HTML files output by a multipage build are in the correct location. This does two * Ensures the HTML files output by a multipage build are in the correct
* things: * location. This does two things:
* *
* 1. Moves the HTML files to their final location at `<outDir>/<entrypoint.name>.html`. * 1. Moves the HTML files to their final location at
* 2. Updates the bundle so it summarizes the files correctly in the returned build output. * `<outDir>/<entrypoint.name>.html`.
* 2. Updates the bundle so it summarizes the files correctly in the returned build
* output.
* *
* Assets (JS and CSS) are output to the `<outDir>/assets` directory, and don't need to be modified. * Assets (JS and CSS) are output to the `<outDir>/assets` directory, and don't
* HTML files access them via absolute URLs, so we don't need to update any import paths in the HTML * need to be modified. HTML files access them via absolute URLs, so we don't
* files either. * need to update any import paths in the HTML files either.
*/ */
async function moveHtmlFiles( async function moveHtmlFiles(
config: ResolvedConfig, config: ResolvedConfig,
@@ -449,8 +455,8 @@ async function moveHtmlFiles(
); );
const oldAbsPath = join(config.outDir, oldBundlePath); const oldAbsPath = join(config.outDir, oldBundlePath);
const newAbsPath = join(config.outDir, newBundlePath); const newAbsPath = join(config.outDir, newBundlePath);
await fs.ensureDir(dirname(newAbsPath)); await mkdir(dirname(newAbsPath), { recursive: true });
await fs.move(oldAbsPath, newAbsPath, { overwrite: true }); await rename(oldAbsPath, newAbsPath);
return { return {
...chunk, ...chunk,
@@ -465,21 +471,19 @@ async function moveHtmlFiles(
return movedChunks; return movedChunks;
} }
/** /** Recursively remove all directories that are empty/ */
* Recursively remove all directories that are empty/
*/
export async function removeEmptyDirs(dir: string): Promise<void> { export async function removeEmptyDirs(dir: string): Promise<void> {
const files = await fs.readdir(dir); const files = await readdir(dir);
for (const file of files) { for (const file of files) {
const filePath = join(dir, file); const filePath = join(dir, file);
const stats = await fs.stat(filePath); const stats = await stat(filePath);
if (stats.isDirectory()) { if (stats.isDirectory()) {
await removeEmptyDirs(filePath); await removeEmptyDirs(filePath);
} }
} }
try { try {
await fs.rmdir(dir); await rmdir(dir);
} catch { } catch {
// noop on failure - this means the directory was not empty. // noop on failure - this means the directory was not empty.
} }
@@ -15,9 +15,7 @@ export function bundleAnalysis(config: ResolvedConfig): vite.Plugin {
}) as vite.Plugin; }) as vite.Plugin;
} }
/** /** @internal FOR TESTING ONLY. */
* @internal FOR TESTING ONLY.
*/
export function resetBundleIncrement() { export function resetBundleIncrement() {
increment = 0; increment = 0;
} }
@@ -3,14 +3,16 @@ import { Entrypoint, ResolvedConfig } from '../../../../types';
import { getEntrypointBundlePath } from '../../../utils/entrypoints'; import { getEntrypointBundlePath } from '../../../utils/entrypoints';
/** /**
* Rename CSS entrypoint outputs to ensure a JS file is not generated, and that the CSS file is * Rename CSS entrypoint outputs to ensure a JS file is not generated, and that
* placed in the correct place. * the CSS file is placed in the correct place.
* *
* It: * It:
*
* 1. Renames CSS files to their final paths * 1. Renames CSS files to their final paths
* 2. Removes the JS file that get's output by lib mode * 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. * THIS PLUGIN SHOULD ONLY BE APPLIED TO CSS LIB MODE BUILDS. It should not be
* added to every build.
*/ */
export function cssEntrypoints( export function cssEntrypoints(
entrypoint: Entrypoint, entrypoint: Entrypoint,
@@ -1,8 +1,8 @@
/** /**
* Overrides definitions for `import.meta.*` * Overrides definitions for `import.meta.*`
* *
* - `import.meta.url`: Without this, background service workers crash trying to access * - `import.meta.url`: Without this, background service workers crash trying to
* `document.location`, see https://github.com/wxt-dev/wxt/issues/392 * access `document.location`, see https://github.com/wxt-dev/wxt/issues/392
*/ */
export function defineImportMeta() { export function defineImportMeta() {
return { return {
@@ -10,7 +10,8 @@ import { hash } from 'ohash';
const inlineScriptContents: Record<string, string> = {}; const inlineScriptContents: Record<string, string> = {};
/** /**
* Pre-renders the HTML entrypoints when building the extension to connect to the dev server. * Pre-renders the HTML entrypoints when building the extension to connect to
* the dev server.
*/ */
export function devHtmlPrerender( export function devHtmlPrerender(
config: ResolvedConfig, config: ResolvedConfig,
@@ -40,32 +41,32 @@ export function devHtmlPrerender(
}, },
// Convert scripts like src="./main.tsx" -> src="http://localhost:3000/entrypoints/popup/main.tsx" // Convert scripts like src="./main.tsx" -> src="http://localhost:3000/entrypoints/popup/main.tsx"
// before the paths are replaced with their bundled path // before the paths are replaced with their bundled path
transform(code, id) { transform: {
if ( filter: {
config.command !== 'serve' || id: /\.html$/,
server == null || },
!id.endsWith('.html') handler(code, id) {
) if (config.command !== 'serve' || server == null) return;
return;
const { document } = parseHTML(code); const { document } = parseHTML(code);
const _pointToDevServer = (querySelector: string, attr: string) => const _pointToDevServer = (querySelector: string, attr: string) =>
pointToDevServer(config, server, id, document, querySelector, attr); pointToDevServer(config, server, id, document, querySelector, attr);
_pointToDevServer('script[type=module]', 'src'); _pointToDevServer('script[type=module]', 'src');
_pointToDevServer('link[rel=stylesheet]', 'href'); _pointToDevServer('link[rel=stylesheet]', 'href');
// Add a script to add page reloading // Add a script to add page reloading
const reloader = document.createElement('script'); const reloader = document.createElement('script');
reloader.src = htmlReloadId; reloader.src = htmlReloadId;
reloader.type = 'module'; reloader.type = 'module';
document.head.appendChild(reloader); document.head.appendChild(reloader);
const newHtml = document.toString(); const newHtml = document.toString();
config.logger.debug('transform ' + id); config.logger.debug('transform ' + id);
config.logger.debug('Old HTML:\n' + code); config.logger.debug('Old HTML:\n' + code);
config.logger.debug('New HTML:\n' + newHtml); config.logger.debug('New HTML:\n' + newHtml);
return newHtml; return newHtml;
},
}, },
// Pass the HTML through the dev server to add dev-mode specific code // Pass the HTML through the dev server to add dev-mode specific code
@@ -113,29 +114,37 @@ export function devHtmlPrerender(
{ {
name: 'wxt:virtualize-inline-scripts', name: 'wxt:virtualize-inline-scripts',
apply: 'serve', apply: 'serve',
resolveId(id) { resolveId: {
// Resolve inline scripts filter: {
if (id.startsWith(virtualInlineScript)) { id: [new RegExp(`^${virtualInlineScript}`), new RegExp('^/chunks/')],
return '\0' + id; },
} handler(id) {
// Ignore chunks during HTML file pre-rendering
if (id.startsWith('/chunks/')) {
return '\0noop';
}
// Ignore chunks during HTML file pre-rendering return `\0${id}`;
if (id.startsWith('/chunks/')) { },
return '\0noop';
}
}, },
load(id) { load: {
// Resolve virtualized inline scripts filter: {
if (id.startsWith(resolvedVirtualInlineScript)) { id: [
new RegExp(`^${resolvedVirtualInlineScript}`),
//eslint-disable-next-line no-control-regex
new RegExp('^\x00noop'),
],
},
handler(id) {
// Ignore chunks during HTML file pre-rendering
if (id === '\0noop') {
return '';
}
// id="virtual:wxt-inline-script?<key>" // id="virtual:wxt-inline-script?<key>"
const key = id.substring(id.indexOf('?') + 1); const key = id.substring(id.indexOf('?') + 1);
return inlineScriptContents[key]; return inlineScriptContents[key];
} },
// Ignore chunks during HTML file pre-rendering
if (id === '\0noop') {
return '';
}
}, },
}, },
]; ];
@@ -2,7 +2,8 @@ import type { Plugin } from 'vite';
import type { ResolvedConfig, WxtDevServer } from '../../../../types'; import type { ResolvedConfig, WxtDevServer } from '../../../../types';
/** /**
* Defines global constants about the dev server. Helps scripts connect to the server's web socket. * Defines global constants about the dev server. Helps scripts connect to the
* server's web socket.
*/ */
export function devServerGlobals( export function devServerGlobals(
config: ResolvedConfig, config: ResolvedConfig,
@@ -3,24 +3,33 @@ import type { ResolvedConfig } from '../../../../types';
import { fetchCached } from '../../../utils/network'; import { fetchCached } from '../../../utils/network';
/** /**
* Downloads any URL imports, like Google Analytics, into virtual modules so they are bundled with * Downloads any URL imports, like Google Analytics, into virtual modules so
* the extension instead of depending on remote code at runtime. * they are bundled with the extension instead of depending on remote code at
* runtime.
* *
* @example * @example
* import "url:https://google-tagmanager.com/gtag?id=XYZ"; * import 'url:https://google-tagmanager.com/gtag?id=XYZ';
*/ */
export function download(config: ResolvedConfig): Plugin { export function download(config: ResolvedConfig): Plugin {
return { return {
name: 'wxt:download', name: 'wxt:download',
resolveId(id) { resolveId: {
if (id.startsWith('url:')) return '\0' + id; filter: {
id: /^url:/,
},
handler(id) {
return `\0${id}`;
},
}, },
load(id) { load: {
if (!id.startsWith('\0url:')) return; filter: {
//eslint-disable-next-line no-control-regex
// Load file from network or cache id: /^\x00url:/,
const url = id.replace('\0url:', ''); },
return fetchCached(url, config); handler(id) {
const url = id.replace('\0url:', '');
return fetchCached(url, config);
},
}, },
}; };
} }
@@ -2,9 +2,7 @@ import type * as vite from 'vite';
import { EntrypointGroup } from '../../../../types'; import { EntrypointGroup } from '../../../../types';
import { getEntrypointGlobals } from '../../../utils/globals'; import { getEntrypointGlobals } from '../../../utils/globals';
/** /** Define a set of global variables specific to an entrypoint. */
* Define a set of global variables specific to an entrypoint.
*/
export function entrypointGroupGlobals( export function entrypointGroupGlobals(
entrypointGroup: EntrypointGroup, entrypointGroup: EntrypointGroup,
): vite.PluginOption { ): vite.PluginOption {
@@ -3,7 +3,8 @@ import type * as vite from 'vite';
import { ResolvedConfig } from '../../../../types'; import { ResolvedConfig } from '../../../../types';
/** /**
* Mock `wxt/browser` and stub the global `browser`/`chrome` types with a fake version of the extension APIs * Mock `wxt/browser` and stub the global `browser`/`chrome` types with a fake
* version of the extension APIs
*/ */
export function extensionApiMock(config: ResolvedConfig): vite.PluginOption { export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
const virtualSetupModule = 'virtual:wxt-setup'; const virtualSetupModule = 'virtual:wxt-setup';
@@ -32,11 +33,21 @@ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
}, },
}; };
}, },
resolveId(id) { resolveId: {
if (id.endsWith(virtualSetupModule)) return resolvedVirtualSetupModule; filter: {
id: new RegExp(`${virtualSetupModule}$`),
},
handler() {
return resolvedVirtualSetupModule;
},
}, },
load(id) { load: {
if (id === resolvedVirtualSetupModule) return setupTemplate; filter: {
id: new RegExp(`^${resolvedVirtualSetupModule}$`),
},
handler() {
return setupTemplate;
},
}, },
}; };
} }
@@ -1,10 +1,11 @@
import type { Plugin } from 'vite'; import type { Plugin } from 'vite';
/** /**
* Add a footer with the returned value so it can return values to `scripting.executeScript` * Add a footer with the returned value so it can return values to
* Footer is added a part of esbuild to make sure it's not minified. It * `scripting.executeScript` Footer is added a part of esbuild to make sure it's
* get's removed if added to `build.rollupOptions.output.footer` * not minified. It get's removed if added to
* See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript#return_value * `build.rollupOptions.output.footer` See
* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript#return_value
*/ */
export function iifeFooter(iifeReturnValueName: string): Plugin { export function iifeFooter(iifeReturnValueName: string): Plugin {
return { return {
@@ -2,21 +2,30 @@ import type { Plugin } from 'vite';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../../../utils/constants'; import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../../../utils/constants';
/** /**
* In dev mode, if there's not a background script listed, we need to add one so that the web socket * In dev mode, if there's not a background script listed, we need to add one so
* connection is setup and the extension reloads HTML pages and content scripts correctly. * that the web socket connection is setup and the extension reloads HTML pages
* and content scripts correctly.
*/ */
export function noopBackground(): Plugin { export function noopBackground(): Plugin {
const virtualModuleId = VIRTUAL_NOOP_BACKGROUND_MODULE_ID; const virtualModuleId = VIRTUAL_NOOP_BACKGROUND_MODULE_ID;
const resolvedVirtualModuleId = '\0' + virtualModuleId; const resolvedVirtualModuleId = '\0' + virtualModuleId;
return { return {
name: 'wxt:noop-background', name: 'wxt:noop-background',
resolveId(id) { resolveId: {
if (id === virtualModuleId) return resolvedVirtualModuleId; filter: {
id: new RegExp(`^${virtualModuleId}$`),
},
handler() {
return resolvedVirtualModuleId;
},
}, },
load(id) { load: {
if (id === resolvedVirtualModuleId) { filter: {
id: new RegExp(`^${resolvedVirtualModuleId}$`),
},
handler() {
return `import { defineBackground } from 'wxt/utils/define-background';\nexport default defineBackground(() => void 0)`; return `import { defineBackground } from 'wxt/utils/define-background';\nexport default defineBackground(() => void 0)`;
} },
}, },
}; };
} }
@@ -5,7 +5,8 @@ import { removeMainFunctionCode } from '../../../utils/transform';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
/** /**
* Transforms entrypoints, removing the main function from the entrypoint if it exists. * Transforms entrypoints, removing the main function from the entrypoint if it
* exists.
*/ */
export function removeEntrypointMainFunction( export function removeEntrypointMainFunction(
config: ResolvedConfig, config: ResolvedConfig,
@@ -16,14 +17,15 @@ export function removeEntrypointMainFunction(
name: 'wxt:remove-entrypoint-main-function', name: 'wxt:remove-entrypoint-main-function',
transform: { transform: {
order: 'pre', order: 'pre',
handler(code, id) { filter: {
if (id === absPath) { id: new RegExp(`^${absPath}$`),
const newCode = removeMainFunctionCode(code); },
config.logger.debug('vite-node transformed entrypoint', path); handler(code) {
config.logger.debug(`Original:\n---\n${code}\n---`); const newCode = removeMainFunctionCode(code);
config.logger.debug(`Transformed:\n---\n${newCode.code}\n---`); config.logger.debug('vite-node transformed entrypoint', path);
return newCode; config.logger.debug(`Original:\n---\n${code}\n---`);
} config.logger.debug(`Transformed:\n---\n${newCode.code}\n---`);
return newCode;
}, },
}, },
}; };
@@ -1,10 +1,11 @@
import { pathExists } from 'fs-extra'; import { pathExists } from '../../../utils/fs';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import type * as vite from 'vite'; import type * as vite from 'vite';
import { ResolvedConfig } from '../../../../types'; import { ResolvedConfig } from '../../../../types';
/** /**
* When importing `virtual:app-config`, resolve it to the `app.config.ts` file in the project. * When importing `virtual:app-config`, resolve it to the `app.config.ts` file
* in the project.
*/ */
export function resolveAppConfig(config: ResolvedConfig): vite.Plugin { export function resolveAppConfig(config: ResolvedConfig): vite.Plugin {
const virtualModuleId = 'virtual:app-config'; const virtualModuleId = 'virtual:app-config';
@@ -22,15 +23,23 @@ export function resolveAppConfig(config: ResolvedConfig): vite.Plugin {
}, },
}; };
}, },
async resolveId(id) { resolveId: {
if (id !== virtualModuleId) return; filter: {
id: new RegExp(`^${virtualModuleId}$`),
return (await pathExists(appConfigFile)) },
? appConfigFile async handler() {
: resolvedVirtualModuleId; return (await pathExists(appConfigFile))
? appConfigFile
: resolvedVirtualModuleId;
},
}, },
load(id) { load: {
if (id === resolvedVirtualModuleId) return `export default {}`; filter: {
id: new RegExp(`^${resolvedVirtualModuleId}$`),
},
handler() {
return `export default {}`;
},
}, },
}; };
} }
@@ -1,3 +1,5 @@
import { readFile } from 'node:fs/promises';
import { resolve } from 'path';
import type { Plugin } from 'vite'; import type { Plugin } from 'vite';
import { ResolvedConfig } from '../../../../types'; import { ResolvedConfig } from '../../../../types';
import { normalizePath } from '../../../utils'; import { normalizePath } from '../../../utils';
@@ -5,36 +7,41 @@ import {
VirtualModuleId, VirtualModuleId,
virtualModuleNames, virtualModuleNames,
} from '../../../utils/virtual-modules'; } from '../../../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. * Resolve all the virtual modules to the `node_modules/wxt/dist/virtual`
* directory.
*/ */
export function resolveVirtualModules(config: ResolvedConfig): Plugin[] { export function resolveVirtualModules(config: ResolvedConfig): Plugin[] {
return virtualModuleNames.map((name) => { return virtualModuleNames.map((name) => {
const virtualId: `${VirtualModuleId}?` = `virtual:wxt-${name}?`; const virtualId: `${VirtualModuleId}?` = `virtual:wxt-${name}?`;
const resolvedVirtualId = '\0' + virtualId; const resolvedVirtualId = '\0' + virtualId;
return { return {
name: `wxt:resolve-virtual-${name}`, name: `wxt:resolve-virtual-${name}`,
resolveId(id) { resolveId: {
// Id doesn't start with prefix, it looks like this: filter: {
// /path/to/project/virtual:wxt-background?/path/to/project/entrypoints/background.ts id: new RegExp(virtualId),
const index = id.indexOf(virtualId); },
if (index === -1) return; handler(id) {
const inputPath = normalizePath(
const inputPath = normalizePath(id.substring(index + virtualId.length)); id.substring(id.indexOf(virtualId) + virtualId.length),
return resolvedVirtualId + inputPath; );
return resolvedVirtualId + inputPath;
},
}, },
async load(id) { load: {
if (!id.startsWith(resolvedVirtualId)) return; filter: {
id: new RegExp(`^${resolvedVirtualId}`),
const inputPath = id.replace(resolvedVirtualId, ''); },
const template = await fs.readFile( async handler(id) {
resolve(config.wxtModuleDir, `dist/virtual/${name}.mjs`), const inputPath = id.replace(resolvedVirtualId, '');
'utf-8', const template = await readFile(
); resolve(config.wxtModuleDir, `dist/virtual/${name}.mjs`),
return template.replace(`virtual:user-${name}`, inputPath); 'utf-8',
);
return template.replace(`virtual:user-${name}`, inputPath);
},
}, },
}; };
}); });
@@ -4,7 +4,9 @@ import { normalizePath } from '../../../utils';
import { ResolvedConfig } from '../../../../types'; import { ResolvedConfig } from '../../../../types';
/** /**
* Resolve and load plugins for each entrypoint. This handles both JS entrypoints via the `virtual:wxt-plugins` import, and HTML files by adding `virtual:wxt-html-plugins` to the document's `<head>` * Resolve and load plugins for each entrypoint. This handles both JS
* entrypoints via the `virtual:wxt-plugins` import, and HTML files by adding
* `virtual:wxt-html-plugins` to the document's `<head>`
*/ */
export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin {
const virtualModuleId = 'virtual:wxt-plugins'; const virtualModuleId = 'virtual:wxt-plugins';
@@ -14,33 +16,50 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin {
return { return {
name: 'wxt:plugin-loader', name: 'wxt:plugin-loader',
resolveId(id) { resolveId: {
if (id === virtualModuleId) return resolvedVirtualModuleId; filter: {
if (id === virtualHtmlModuleId) return resolvedVirtualHtmlModuleId; id: [
}, new RegExp(`^${virtualModuleId}$`),
load(id) { new RegExp(`^${virtualHtmlModuleId}$`),
if (id === resolvedVirtualModuleId) { ],
// Import and init all plugins },
const imports = config.plugins handler(id) {
.map( if (id === virtualModuleId) {
(plugin, i) => return resolvedVirtualModuleId;
`import initPlugin${i} from '${normalizePath(plugin)}';`, }
)
.join('\n');
const initCalls = config.plugins
.map((_, i) => ` initPlugin${i}();`)
.join('\n');
return `${imports}\n\nexport function initPlugins() {\n${initCalls}\n}`;
}
if (id === resolvedVirtualHtmlModuleId) {
return `import { initPlugins } from '${virtualModuleId}';
try { return resolvedVirtualHtmlModuleId;
initPlugins(); },
} catch (err) { },
console.error("[wxt] Failed to initialize plugins", err); load: {
}`; filter: {
} id: [
new RegExp(`^${resolvedVirtualModuleId}$`),
new RegExp(`^${resolvedVirtualHtmlModuleId}$`),
],
},
handler(id) {
if (id === resolvedVirtualModuleId) {
// Import and init all plugins
const imports = config.plugins
.map(
(plugin, i) =>
`import initPlugin${i} from '${normalizePath(plugin)}';`,
)
.join('\n');
const initCalls = config.plugins
.map((_, i) => ` initPlugin${i}();`)
.join('\n');
return `${imports}\n\nexport function initPlugins() {\n${initCalls}\n}`;
} else {
return `import { initPlugins } from '${virtualModuleId}';
try {
initPlugins();
} catch (err) {
console.error("[wxt] Failed to initialize plugins", err);
}`;
}
},
}, },
transformIndexHtml: { transformIndexHtml: {
// Use "pre" so the new script is added before vite bundles all the scripts // Use "pre" so the new script is added before vite bundles all the scripts
@@ -63,7 +82,8 @@ try {
const newHead = document.createElement('head'); const newHead = document.createElement('head');
document.documentElement.prepend(newHead); document.documentElement.prepend(newHead);
} }
document.head.prepend(script);
document.head?.prepend(script);
return document.toString(); return document.toString();
}, },
}, },
+11 -10
View File
@@ -1,28 +1,28 @@
import { rm } from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import glob from 'fast-glob';
import fs from 'fs-extra';
import pc from 'picocolors'; import pc from 'picocolors';
import { glob } from 'tinyglobby';
import { InlineConfig } from '../types'; import { InlineConfig } from '../types';
import { registerWxt, wxt } from './wxt'; import { registerWxt, wxt } from './wxt';
/** /**
* Remove generated/temp files from the directory. * Remove generated/temp files from the directory.
* *
* @param config Optional config that will override your `<root>/wxt.config.ts`.
*
* @example * @example
* await clean(); * await clean();
*
* @param config Optional config that will override your `<root>/wxt.config.ts`.
*/ */
export async function clean(config?: InlineConfig): Promise<void>; export async function clean(config?: InlineConfig): Promise<void>;
/** /**
* Remove generated/temp files from the directory. * Remove generated/temp files from the directory.
* *
* @deprecated * @deprecated
*
* @param root The directory to look for generated/temp files in. Defaults to `process.cwd()`. Can be relative to `process.cwd()` or absolute.
*
* @example * @example
* await clean(); * await clean();
*
* @param root The directory to look for generated/temp files in. Defaults to
* `process.cwd()`. Can be relative to `process.cwd()` or absolute.
*/ */
export async function clean(root?: string): Promise<void>; export async function clean(root?: string): Promise<void>;
@@ -48,6 +48,7 @@ export async function clean(config?: string | InlineConfig) {
absolute: true, absolute: true,
onlyDirectories: true, onlyDirectories: true,
deep: 2, deep: 2,
expandDirectories: false,
}); });
if (directories.length === 0) { if (directories.length === 0) {
wxt.logger.debug('No generated files found.'); wxt.logger.debug('No generated files found.');
@@ -59,7 +60,7 @@ export async function clean(config?: string | InlineConfig) {
directories.map((dir) => pc.cyan(path.relative(root, dir))).join(', '), directories.map((dir) => pc.cyan(path.relative(root, dir))).join(', '),
); );
for (const directory of directories) { for (const directory of directories) {
await fs.rm(directory, { force: true, recursive: true }); await rm(directory, { force: true, recursive: true });
wxt.logger.debug('Deleted ' + pc.cyan(path.relative(root, directory))); wxt.logger.debug('Deleted ' + pc.cyan(path.relative(root, directory)));
} }
} }
+11 -8
View File
@@ -32,13 +32,14 @@ import { createKeyboardShortcuts } from './keyboard-shortcuts';
import { isBabelSyntaxError, logBabelSyntaxError } from './utils/syntax-errors'; import { isBabelSyntaxError, logBabelSyntaxError } from './utils/syntax-errors';
/** /**
* Creates a dev server and pre-builds all the files that need to exist before loading the extension. * Creates a dev server and pre-builds all the files that need to exist before
* loading the extension.
* *
* @example * @example
* const server = await wxt.createServer({ * const server = await wxt.createServer({
* // Enter config... * // Enter config...
* }); * });
* await server.start(); * await server.start();
*/ */
export async function createServer( export async function createServer(
inlineConfig?: InlineConfig, inlineConfig?: InlineConfig,
@@ -199,8 +200,8 @@ async function createServerInternal(): Promise<WxtDevServer> {
} }
/** /**
* Returns a function responsible for reloading different parts of the extension when a file * Returns a function responsible for reloading different parts of the extension
* changes. * when a file changes.
*/ */
function createFileReloader(server: WxtDevServer) { function createFileReloader(server: WxtDevServer) {
const fileChangedMutex = new Mutex(); const fileChangedMutex = new Mutex();
@@ -295,7 +296,8 @@ function createFileReloader(server: WxtDevServer) {
} }
/** /**
* From the server, tell the client to reload content scripts from the provided build step outputs. * From the server, tell the client to reload content scripts from the provided
* build step outputs.
*/ */
function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) { function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) {
if (wxt.config.manifestVersion === 3) { if (wxt.config.manifestVersion === 3) {
@@ -350,6 +352,7 @@ function getFilenameList(names: string[]): string {
/** /**
* Based on the current build output, return a list of files that are: * Based on the current build output, return a list of files that are:
*
* 1. Not in node_modules * 1. Not in node_modules
* 2. Not inside project root * 2. Not inside project root
*/ */
@@ -1,9 +1,7 @@
import consola from 'consola'; import consola from 'consola';
import { WebExtConfig } from '../types'; import { WebExtConfig } from '../types';
/** /** @deprecated Use `defineWebExtConfig` instead. Same function, different name. */
* @deprecated Use `defineWebExtConfig` instead. Same function, different name.
*/
export function defineRunnerConfig(config: WebExtConfig): WebExtConfig { export function defineRunnerConfig(config: WebExtConfig): WebExtConfig {
consola.warn( consola.warn(
'`defineRunnerConfig` is deprecated, use `defineWebExtConfig` instead. See https://wxt.dev/guide/resources/upgrading.html#v0-19-0-rarr-v0-20-0', '`defineRunnerConfig` is deprecated, use `defineWebExtConfig` instead. See https://wxt.dev/guide/resources/upgrading.html#v0-19-0-rarr-v0-20-0',
@@ -12,7 +10,8 @@ export function defineRunnerConfig(config: WebExtConfig): WebExtConfig {
} }
/** /**
* Configure how [`web-ext`](https://github.com/mozilla/web-ext) starts the browser during development. * Configure how [`web-ext`](https://github.com/mozilla/web-ext) starts the
* browser during development.
*/ */
export function defineWebExtConfig(config: WebExtConfig): WebExtConfig { export function defineWebExtConfig(config: WebExtConfig): WebExtConfig {
return config; return config;
+8 -7
View File
@@ -1,19 +1,20 @@
import { Entrypoint, WxtDirEntry, WxtDirFileEntry } from '../types'; import { Entrypoint, WxtDirEntry, WxtDirFileEntry } from '../types';
import fs from 'fs-extra'; import { mkdir, readFile } from 'node:fs/promises';
import { dirname, relative, resolve } from 'node:path'; import { dirname, relative, resolve } from 'node:path';
import { getEntrypointBundlePath, isHtmlEntrypoint } from './utils/entrypoints'; import { getEntrypointBundlePath, isHtmlEntrypoint } from './utils/entrypoints';
import { getEntrypointGlobals, getGlobals } from './utils/globals'; import { getEntrypointGlobals, getGlobals } from './utils/globals';
import { normalizePath } from './utils'; import { normalizePath } from './utils';
import path from 'node:path'; import path from 'node:path';
import { Message, parseI18nMessages } from './utils/i18n'; import { Message, parseI18nMessages } from './utils/i18n';
import { writeFileIfDifferent, getPublicFiles } from './utils/fs'; import { pathExists, writeFileIfDifferent, getPublicFiles } from './utils/fs';
import { wxt } from './wxt'; import { wxt } from './wxt';
/** /**
* Generate and write all the files inside the `InternalConfig.typesDir` directory. * Generate and write all the files inside the `InternalConfig.typesDir`
* directory.
*/ */
export async function generateWxtDir(entrypoints: Entrypoint[]): Promise<void> { export async function generateWxtDir(entrypoints: Entrypoint[]): Promise<void> {
await fs.ensureDir(wxt.config.typesDir); await mkdir(wxt.config.typesDir, { recursive: true });
const entries: WxtDirEntry[] = [ const entries: WxtDirEntry[] = [
// Hard-coded entries // Hard-coded entries
@@ -56,7 +57,7 @@ export async function generateWxtDir(entrypoints: Entrypoint[]): Promise<void> {
await Promise.all( await Promise.all(
absoluteFileEntries.map(async (file) => { absoluteFileEntries.map(async (file) => {
await fs.ensureDir(dirname(file.path)); await mkdir(dirname(file.path), { recursive: true });
await writeFileIfDifferent(file.path, file.text); await writeFileIfDifferent(file.path, file.text);
}), }),
); );
@@ -148,8 +149,8 @@ declare module "wxt/browser" {
'messages.json', 'messages.json',
); );
let messages: Message[]; let messages: Message[];
if (await fs.pathExists(defaultLocalePath)) { if (await pathExists(defaultLocalePath)) {
const content = JSON.parse(await fs.readFile(defaultLocalePath, 'utf-8')); const content = JSON.parse(await readFile(defaultLocalePath, 'utf-8'));
messages = parseI18nMessages(content); messages = parseI18nMessages(content);
} else { } else {
messages = parseI18nMessages({}); messages = parseI18nMessages({});
+16 -21
View File
@@ -1,7 +1,8 @@
import prompts from 'prompts'; import prompts from 'prompts';
import { consola } from 'consola'; import { consola } from 'consola';
import { downloadTemplate } from 'giget'; import { downloadTemplate } from 'giget';
import fs from 'fs-extra'; import { readdir, rename } from 'node:fs/promises';
import { pathExists } from './utils/fs';
import path from 'node:path'; import path from 'node:path';
import pc from 'picocolors'; import pc from 'picocolors';
import { Formatter } from 'picocolors/types'; import { Formatter } from 'picocolors/types';
@@ -56,10 +57,10 @@ export async function initialize(options: {
input.template ??= defaultTemplate; input.template ??= defaultTemplate;
input.packageManager ??= options.packageManager; input.packageManager ??= options.packageManager;
const isExists = await fs.pathExists(input.directory); const isExists = await pathExists(input.directory);
if (isExists) { if (isExists) {
const isEmpty = const isEmpty =
(await fs.readdir(input.directory)).filter((dir) => dir !== '.git') (await readdir(input.directory)).filter((dir) => dir !== '.git')
.length === 0; .length === 0;
if (!isEmpty) { if (!isEmpty) {
consola.error( consola.error(
@@ -87,13 +88,9 @@ export async function initialize(options: {
} }
interface Template { interface Template {
/** /** Template's name. */
* Template's name.
*/
name: string; name: string;
/** /** Path to template directory in github repo. */
* Path to template directory in github repo.
*/
path: string; path: string;
} }
@@ -161,8 +158,8 @@ async function cloneProject({
directory: string; directory: string;
template: Template; template: Template;
}) { }) {
const { default: ora } = await import('ora'); const { createSpinner } = await import('nanospinner');
const spinner = ora('Downloading template').start(); const spinner = createSpinner('Downloading template').start();
try { try {
// 1. Clone repo // 1. Clone repo
await downloadTemplate(`gh:${REPO}/${template.path}`, { await downloadTemplate(`gh:${REPO}/${template.path}`, {
@@ -171,18 +168,16 @@ async function cloneProject({
}); });
// 2. Move _gitignore -> .gitignore // 2. Move _gitignore -> .gitignore
await fs await rename(
.move( path.join(directory, '_gitignore'),
path.join(directory, '_gitignore'), path.join(directory, '.gitignore'),
path.join(directory, '.gitignore'), ).catch((err) =>
) consola.warn('Failed to move _gitignore to .gitignore:', err),
.catch((err) => );
consola.warn('Failed to move _gitignore to .gitignore:', err),
);
spinner.succeed(); spinner.success();
} catch (err) { } catch (err) {
spinner.fail(); spinner.error();
throw Error(`Failed to setup new project: ${JSON.stringify(err, null, 2)}`); throw Error(`Failed to setup new project: ${JSON.stringify(err, null, 2)}`);
} }
} }
+1 -3
View File
@@ -9,9 +9,7 @@ export interface KeyboardShortcutWatcher {
printHelp(flags: { canReopenBrowser: boolean }): void; printHelp(flags: { canReopenBrowser: boolean }): void;
} }
/** /** Function that creates a keyboard shortcut handler for the extension. */
* Function that creates a keyboard shortcut handler for the extension.
*/
export function createKeyboardShortcuts( export function createKeyboardShortcuts(
server: WxtDevServer, server: WxtDevServer,
): KeyboardShortcutWatcher { ): KeyboardShortcutWatcher {
@@ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
import path from 'node:path'; import path from 'node:path';
import { npm } from '../npm'; import { npm } from '../npm';
import spawn from 'nano-spawn'; import spawn from 'nano-spawn';
import { pathExists } from 'fs-extra'; import { pathExists } from '../../utils/fs';
describe('NPM Package Management Utils', () => { describe('NPM Package Management Utils', () => {
describe('listDependencies', () => { describe('listDependencies', () => {
@@ -1,13 +1,13 @@
import { Dependency } from '../../types'; import { Dependency } from '../../types';
import { WxtPackageManagerImpl } from './types'; import { WxtPackageManagerImpl } from './types';
import path from 'node:path'; import path from 'node:path';
import { ensureDir } from 'fs-extra'; import { mkdir } from 'node:fs/promises';
import spawn from 'nano-spawn'; import spawn from 'nano-spawn';
export const npm: WxtPackageManagerImpl = { export const npm: WxtPackageManagerImpl = {
overridesKey: 'overrides', overridesKey: 'overrides',
async downloadDependency(id, downloadDir) { async downloadDependency(id, downloadDir) {
await ensureDir(downloadDir); await mkdir(downloadDir, { recursive: true });
const res = await spawn('npm', ['pack', id, '--json'], { const res = await spawn('npm', ['pack', id, '--json'], {
cwd: downloadDir, cwd: downloadDir,
}); });
+13 -14
View File
@@ -20,9 +20,9 @@ import { createFsCache } from './utils/cache';
import consola, { LogLevels } from 'consola'; import consola, { LogLevels } from 'consola';
import defu from 'defu'; import defu from 'defu';
import { NullablyRequired } from './utils/types'; import { NullablyRequired } from './utils/types';
import fs from 'fs-extra'; import { pathExists } from './utils/fs';
import { normalizePath } from './utils'; import { normalizePath } from './utils';
import glob from 'fast-glob'; import { glob } from 'tinyglobby';
import { builtinModules } from '../builtin-modules'; import { builtinModules } from '../builtin-modules';
import { getEslintVersion } from './utils/eslint'; import { getEslintVersion } from './utils/eslint';
import { safeStringToNumber } from './utils/number'; import { safeStringToNumber } from './utils/number';
@@ -31,11 +31,12 @@ import { getPort } from 'get-port-please';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
/** /**
* Given an inline config, discover the config file if necessary, merge the results, resolve any * Given an inline config, discover the config file if necessary, merge the
* relative paths, and apply any defaults. * 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. * Inline config always has priority over user config. Cli flags are passed as
* If unset, undefined is passed in, letting this function decide default values. * inline config if set. If unset, undefined is passed in, letting this function
* decide default values.
*/ */
export async function resolveConfig( export async function resolveConfig(
inlineConfig: InlineConfig, inlineConfig: InlineConfig,
@@ -249,7 +250,8 @@ async function resolveManifestConfig(
} }
/** /**
* Merge the inline config and user config. Inline config is given priority. Defaults are not applied here. * Merge the inline config and user config. Inline config is given priority.
* Defaults are not applied here.
*/ */
async function mergeInlineConfig( async function mergeInlineConfig(
inlineConfig: InlineConfig, inlineConfig: InlineConfig,
@@ -528,9 +530,7 @@ async function getUnimportEslintOptions(
}; };
} }
/** /** Returns the path to `node_modules/wxt`. */
* Returns the path to `node_modules/wxt`.
*/
function resolveWxtModuleDir() { function resolveWxtModuleDir() {
// TODO: Switch to import.meta.resolve() once the parent argument is unflagged // TODO: Switch to import.meta.resolve() once the parent argument is unflagged
// (e.g. --experimental-import-meta-resolve) and all Node.js versions we support // (e.g. --experimental-import-meta-resolve) and all Node.js versions we support
@@ -543,7 +543,7 @@ function resolveWxtModuleDir() {
} }
async function isDirMissing(dir: string) { async function isDirMissing(dir: string) {
return !(await fs.pathExists(dir)); return !(await pathExists(dir));
} }
function logMissingDir(logger: Logger, name: string, expected: string) { function logMissingDir(logger: Logger, name: string, expected: string) {
@@ -554,9 +554,7 @@ function logMissingDir(logger: Logger, name: string, expected: string) {
); );
} }
/** /** Map of `ConfigEnv` commands to their default modes. */
* Map of `ConfigEnv` commands to their default modes.
*/
const COMMAND_MODES: Record<WxtCommand, string> = { const COMMAND_MODES: Record<WxtCommand, string> = {
build: 'production', build: 'production',
serve: 'development', serve: 'development',
@@ -614,6 +612,7 @@ export async function resolveWxtUserModules(
const localModulePaths = await glob(['*.[tj]s', '*/index.[tj]s'], { const localModulePaths = await glob(['*.[tj]s', '*/index.[tj]s'], {
cwd: modulesDir, cwd: modulesDir,
onlyFiles: true, onlyFiles: true,
expandDirectories: false,
}).catch(() => []); }).catch(() => []);
// Sort modules to ensure a consistent execution order // Sort modules to ensure a consistent execution order
localModulePaths.sort(); localModulePaths.sort();
+1 -3
View File
@@ -2,9 +2,7 @@ import { ExtensionRunner } from '../../types';
import { relative } from 'node:path'; import { relative } from 'node:path';
import { wxt } from '../wxt'; import { wxt } from '../wxt';
/** /** The manual runner tells the user to load the unpacked extension manually. */
* The manual runner tells the user to load the unpacked extension manually.
*/
export function createManualRunner(): ExtensionRunner { export function createManualRunner(): ExtensionRunner {
return { return {
async openBrowser() { async openBrowser() {
+2 -1
View File
@@ -3,7 +3,8 @@ import { relative } from 'node:path';
import { wxt } from '../wxt'; import { wxt } from '../wxt';
/** /**
* The Safari runner just logs a warning message because `web-ext` doesn't work with Safari. * The Safari runner just logs a warning message because `web-ext` doesn't work
* with Safari.
*/ */
export function createSafariRunner(): ExtensionRunner { export function createSafariRunner(): ExtensionRunner {
return { return {
+1 -3
View File
@@ -4,9 +4,7 @@ import { formatDuration } from '../utils/time';
import defu from 'defu'; import defu from 'defu';
import { wxt } from '../wxt'; import { wxt } from '../wxt';
/** /** Create an `ExtensionRunner` backed by `web-ext`. */
* Create an `ExtensionRunner` backed by `web-ext`.
*/
export function createWebExtRunner(): ExtensionRunner { export function createWebExtRunner(): ExtensionRunner {
let runner: WebExtRunInstance | undefined; let runner: WebExtRunInstance | undefined;
+2 -1
View File
@@ -3,7 +3,8 @@ import { relative } from 'node:path';
import { wxt } from '../wxt'; import { wxt } from '../wxt';
/** /**
* The WSL runner just logs a warning message because `web-ext` doesn't work in WSL. * The WSL runner just logs a warning message because `web-ext` doesn't work in
* WSL.
*/ */
export function createWslRunner(): ExtensionRunner { export function createWslRunner(): ExtensionRunner {
return { return {
+3 -9
View File
@@ -1,6 +1,4 @@
/** /** Checks if `predicate` returns truthy for all elements of the array. */
* Checks if `predicate` returns truthy for all elements of the array.
*/
export function every<T>( export function every<T>(
array: T[], array: T[],
predicate: (item: T, index: number) => boolean, predicate: (item: T, index: number) => boolean,
@@ -10,9 +8,7 @@ export function every<T>(
return true; return true;
} }
/** /** Returns true when any of the predicates return true; */
* Returns true when any of the predicates return true;
*/
export function some<T>( export function some<T>(
array: T[], array: T[],
predicate: (item: T, index: number) => boolean, predicate: (item: T, index: number) => boolean,
@@ -22,9 +18,7 @@ export function some<T>(
return false; return false;
} }
/** /** Convert an item or array to an array. */
* Convert an item or array to an array.
*/
export function toArray<T>(a: T | T[]): T[] { export function toArray<T>(a: T | T[]): T[] {
return Array.isArray(a) ? a : [a]; return Array.isArray(a) ? a : [a];
} }
@@ -1,4 +1,7 @@
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { readFile } from 'node:fs/promises';
import { resolve } from 'path';
import { glob } from 'tinyglobby';
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { import {
BackgroundEntrypoint, BackgroundEntrypoint,
BackgroundEntrypointOptions, BackgroundEntrypointOptions,
@@ -9,21 +12,16 @@ import {
PopupEntrypoint, PopupEntrypoint,
SidepanelEntrypoint, SidepanelEntrypoint,
} from '../../../../types'; } from '../../../../types';
import { resolve } from 'path';
import { findEntrypoints } from '../find-entrypoints';
import fs from 'fs-extra';
import glob from 'fast-glob';
import { fakeResolvedConfig, setFakeWxt } from '../../testing/fake-objects';
import { unnormalizePath } from '../../paths';
import { wxt } from '../../../wxt'; import { wxt } from '../../../wxt';
import { unnormalizePath } from '../../paths';
import { fakeResolvedConfig, setFakeWxt } from '../../testing/fake-objects';
import { findEntrypoints } from '../find-entrypoints';
vi.mock('fast-glob'); vi.mock('tinyglobby');
const globMock = vi.mocked(glob); const globMock = vi.mocked(glob);
vi.mock('fs-extra'); vi.mock('node:fs/promises');
const readFileMock = vi.mocked( const readFileMock = vi.mocked(readFile);
fs.readFile as (path: string) => Promise<string>,
);
describe('findEntrypoints', () => { describe('findEntrypoints', () => {
const config = fakeResolvedConfig({ const config = fakeResolvedConfig({
@@ -5,28 +5,30 @@ import {
ResolvedPublicFile, ResolvedPublicFile,
} from '../../../types'; } from '../../../types';
import { getPublicFiles } from '../fs'; import { getPublicFiles } from '../fs';
import fs from 'fs-extra'; import { copyFile, mkdir, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'path'; import { dirname, resolve } from 'path';
import type { Ora } from 'ora'; import type { Spinner } from 'nanospinner';
import pc from 'picocolors'; import pc from 'picocolors';
import { wxt } from '../../wxt'; import { wxt } from '../../wxt';
import { toArray } from '../arrays'; import { toArray } from '../arrays';
export async function buildEntrypoints( export async function buildEntrypoints(
groups: EntrypointGroup[], groups: EntrypointGroup[],
spinner: Ora, spinner: Spinner,
): Promise<Omit<BuildOutput, 'manifest'>> { ): Promise<Omit<BuildOutput, 'manifest'>> {
const steps: BuildStepOutput[] = []; const steps: BuildStepOutput[] = [];
for (let i = 0; i < groups.length; i++) { for (let i = 0; i < groups.length; i++) {
const group = groups[i]; const group = groups[i];
const groupNames = toArray(group).map((e) => e.name); const groupNames = toArray(group).map((e) => e.name);
const groupNameColored = groupNames.join(pc.dim(', ')); const groupNameColored = groupNames.join(pc.dim(', '));
spinner.text = spinner.update({
pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNameColored}`; text: pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNameColored}`,
});
try { try {
steps.push(await wxt.builder.build(group)); steps.push(await wxt.builder.build(group));
} catch (err) { } catch (err) {
spinner.stop().clear(); spinner.stop();
spinner.clear();
wxt.logger.error(err); wxt.logger.error(err);
throw Error(`Failed to build ${groupNames.join(', ')}`, { cause: err }); throw Error(`Failed to build ${groupNames.join(', ')}`, { cause: err });
} }
@@ -48,11 +50,11 @@ async function copyPublicDirectory(): Promise<BuildOutput['publicAssets']> {
for (const file of files) { for (const file of files) {
const absoluteDest = resolve(wxt.config.outDir, file.relativeDest); const absoluteDest = resolve(wxt.config.outDir, file.relativeDest);
await fs.ensureDir(dirname(absoluteDest)); await mkdir(dirname(absoluteDest), { recursive: true });
if ('absoluteSrc' in file) { if ('absoluteSrc' in file) {
await fs.copyFile(file.absoluteSrc, absoluteDest); await copyFile(file.absoluteSrc, absoluteDest);
} else { } else {
await fs.writeFile(absoluteDest, file.contents, 'utf8'); await writeFile(absoluteDest, file.contents, 'utf8');
} }
publicAssets.push({ publicAssets.push({
type: 'asset', type: 'asset',
@@ -9,27 +9,36 @@ import { normalizePath } from '../paths';
import { wxt } from '../../wxt'; import { wxt } from '../../wxt';
/** /**
* Compare the changed files vs the build output and determine what kind of reload needs to happen: * Compare the changed files vs the build output and determine what kind of
* reload needs to happen:
* *
* - Do nothing * - Do nothing
* - CSS or JS file associated with an HTML page is changed - this is handled automatically by the *
* dev server * - 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 * - Change isn't used by any of the entrypoints
* - Reload Content script * - Reload Content script
*
* - CSS or JS file associated with a content script * - CSS or JS file associated with a content script
* - Background script will be told to reload the content script * - Background script will be told to reload the content script
* - Reload HTML file * - 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 * - HTML file itself is saved - HMR doesn't handle this because the HTML pages
* extension. Not sure about firefox, this might need to change to an extension reload * 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 * - Reload extension
*
* - Background script is changed * - Background script is changed
* - Manifest is different * - Manifest is different
* - Restart browser * - Restart browser
* - web-ext.config.ts (runner config changes) *
* - Web-ext.config.ts (runner config changes)
* - Full dev server restart * - Full dev server restart
* - wxt.config.ts (main config file) *
* - modules/* (any file related to WXT modules) * - Wxt.config.ts (main config file)
* - Modules/* (any file related to WXT modules)
* - .env (environment variable changed could effect build) * - .env (environment variable changed could effect build)
*/ */
export function detectDevChanges( export function detectDevChanges(
@@ -126,7 +135,8 @@ export function detectDevChanges(
} }
/** /**
* For a single change, return all the step of the build output that were effected by it. * For a single change, return all the step of the build output that were
* effected by it.
*/ */
function findEffectedSteps( function findEffectedSteps(
changedFile: string, changedFile: string,
@@ -165,8 +175,8 @@ function findEffectedSteps(
} }
/** /**
* Contains information about what files changed, what needs rebuilt, and the type of reload that is * Contains information about what files changed, what needs rebuilt, and the
* required. * type of reload that is required.
*/ */
export type DevModeChange = export type DevModeChange =
| NoChange | NoChange
@@ -181,13 +191,9 @@ interface NoChange {
} }
interface RebuildChange { interface RebuildChange {
/** /** The list of entrypoints that need rebuilt. */
* The list of entrypoints that need rebuilt.
*/
rebuildGroups: EntrypointGroup[]; rebuildGroups: EntrypointGroup[];
/** /** The previous output stripped of any files are going to change. */
* The previous output stripped of any files are going to change.
*/
cachedOutput: BuildOutput; cachedOutput: BuildOutput;
} }
@@ -12,11 +12,11 @@ import {
IsolatedWorldContentScriptEntrypointOptions, IsolatedWorldContentScriptEntrypointOptions,
UnlistedScriptEntrypoint, UnlistedScriptEntrypoint,
} from '../../../types'; } from '../../../types';
import fs from 'fs-extra'; import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import { parseHTML } from 'linkedom'; import { parseHTML } from 'linkedom';
import JSON5 from 'json5'; import JSON5 from 'json5';
import glob from 'fast-glob'; import { glob } from 'tinyglobby';
import { import {
getEntrypointName, getEntrypointName,
isHtmlEntrypoint, isHtmlEntrypoint,
@@ -30,15 +30,16 @@ import { wxt } from '../../wxt';
import { camelCase } from 'scule'; import { camelCase } from 'scule';
/** /**
* Return entrypoints and their configuration by looking through the project's files. * Return entrypoints and their configuration by looking through the project's
* files.
*/ */
export async function findEntrypoints(): Promise<Entrypoint[]> { export async function findEntrypoints(): Promise<Entrypoint[]> {
// Make sure required TSConfig file exists to load dependencies // Make sure required TSConfig file exists to load dependencies
await fs.mkdir(wxt.config.wxtDir, { recursive: true }); await mkdir(wxt.config.wxtDir, { recursive: true });
try { try {
await fs.writeJson( await writeFile(
resolve(wxt.config.wxtDir, 'tsconfig.json'), resolve(wxt.config.wxtDir, 'tsconfig.json'),
{}, JSON.stringify({}),
{ flag: 'wx' }, { flag: 'wx' },
); );
} catch (err) { } catch (err) {
@@ -49,6 +50,7 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
const relativePaths = await glob(Object.keys(PATH_GLOB_TO_TYPE_MAP), { const relativePaths = await glob(Object.keys(PATH_GLOB_TO_TYPE_MAP), {
cwd: wxt.config.entrypointsDir, cwd: wxt.config.entrypointsDir,
expandDirectories: false,
}); });
// Ensure consistent output // Ensure consistent output
relativePaths.sort(); relativePaths.sort();
@@ -195,11 +197,14 @@ async function importEntrypoints(infos: EntrypointInfo[]) {
return resMap; return resMap;
} }
/** Extract `manifest.` and `wxt.` options from meta tags, converting snake_case keys to camelCase */ /**
* Extract `manifest.` and `wxt.` options from meta tags, converting snake_case
* keys to camelCase
*/
async function importHtmlEntrypoint( async function importHtmlEntrypoint(
info: EntrypointInfo, info: EntrypointInfo,
): Promise<Record<string, any>> { ): Promise<Record<string, any>> {
const content = await fs.readFile(info.inputPath, 'utf-8'); const content = await readFile(info.inputPath, 'utf-8');
const { document } = parseHTML(content); const { document } = parseHTML(content);
const metaTags = document.querySelectorAll('meta'); const metaTags = document.querySelectorAll('meta');
@@ -1,8 +1,8 @@
import { Entrypoint, EntrypointGroup } from '../../../types'; import { Entrypoint, EntrypointGroup } from '../../../types';
/** /**
* Entrypoints are built in groups. HTML pages can all be built together in a single step, * Entrypoints are built in groups. HTML pages can all be built together in a
* content scripts must be build individually, etc. * single step, content scripts must be build individually, etc.
* *
* This function returns the entrypoints put into these types of groups. * This function returns the entrypoints put into these types of groups.
*/ */
@@ -1,11 +1,11 @@
import { findEntrypoints } from './find-entrypoints'; import { findEntrypoints } from './find-entrypoints';
import { BuildOutput, Entrypoint } from '../../../types'; import { BuildOutput, Entrypoint } from '../../../types';
import pc from 'picocolors'; import pc from 'picocolors';
import fs from 'fs-extra'; import { mkdir, rm } from 'node:fs/promises';
import { groupEntrypoints } from './group-entrypoints'; import { groupEntrypoints } from './group-entrypoints';
import { formatDuration } from '../time'; import { formatDuration } from '../time';
import { printBuildSummary } from '../log'; import { printBuildSummary } from '../log';
import glob from 'fast-glob'; import { glob } from 'tinyglobby';
import { unnormalizePath } from '../paths'; import { unnormalizePath } from '../paths';
import { rebuild } from './rebuild'; import { rebuild } from './rebuild';
import { relative } from 'node:path'; import { relative } from 'node:path';
@@ -20,13 +20,14 @@ import { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer';
import { isCI } from 'ci-info'; import { isCI } from 'ci-info';
/** /**
* Builds the extension based on an internal config. No more config discovery is performed, the * Builds the extension based on an internal config. No more config discovery is
* build is based on exactly what is passed in. * performed, the build is based on exactly what is passed in.
* *
* This function: * This function:
*
* 1. Cleans the output directory * 1. Cleans the output directory
* 2. Executes the rebuild function with a blank previous output so everything is built (see * 2. Executes the rebuild function with a blank previous output so everything is
* `rebuild` for more details) * built (see `rebuild` for more details)
* 3. Prints the summary * 3. Prints the summary
*/ */
export async function internalBuild(): Promise<BuildOutput> { export async function internalBuild(): Promise<BuildOutput> {
@@ -42,8 +43,8 @@ export async function internalBuild(): Promise<BuildOutput> {
const startTime = Date.now(); const startTime = Date.now();
// Cleanup // Cleanup
await fs.rm(wxt.config.outDir, { recursive: true, force: true }); await rm(wxt.config.outDir, { recursive: true, force: true });
await fs.ensureDir(wxt.config.outDir); await mkdir(wxt.config.outDir, { recursive: true });
const entrypoints = await findEntrypoints(); const entrypoints = await findEntrypoints();
wxt.logger.debug('Detected entrypoints:', entrypoints); wxt.logger.debug('Detected entrypoints:', entrypoints);
@@ -99,6 +100,7 @@ async function combineAnalysisStats(): Promise<void> {
const unixFiles = await glob(`${wxt.config.analysis.outputName}-*.json`, { const unixFiles = await glob(`${wxt.config.analysis.outputName}-*.json`, {
cwd: wxt.config.analysis.outputDir, cwd: wxt.config.analysis.outputDir,
absolute: true, absolute: true,
expandDirectories: false,
}); });
const absolutePaths = unixFiles.map(unnormalizePath); const absolutePaths = unixFiles.map(unnormalizePath);
@@ -109,7 +111,9 @@ async function combineAnalysisStats(): Promise<void> {
}); });
if (!wxt.config.analysis.keepArtifacts) { if (!wxt.config.analysis.keepArtifacts) {
await Promise.all(absolutePaths.map((statsFile) => fs.remove(statsFile))); await Promise.all(
absolutePaths.map((statsFile) => rm(statsFile, { force: true })),
);
} }
} }
@@ -1,25 +1,28 @@
import type { Browser } from '@wxt-dev/browser';
import { createSpinner } from 'nanospinner';
import { BuildOutput, Entrypoint, EntrypointGroup } from '../../../types'; import { BuildOutput, Entrypoint, EntrypointGroup } from '../../../types';
import { generateWxtDir } from '../../generate-wxt-dir'; import { generateWxtDir } from '../../generate-wxt-dir';
import { buildEntrypoints } from './build-entrypoints';
import { generateManifest, writeManifest } from '../../utils/manifest'; import { generateManifest, writeManifest } from '../../utils/manifest';
import { wxt } from '../../wxt'; import { wxt } from '../../wxt';
import type { Browser } from '@wxt-dev/browser'; import { buildEntrypoints } from './build-entrypoints';
/** /**
* Given a configuration, list of entrypoints, and an existing, partial output, build the * Given a configuration, list of entrypoints, and an existing, partial output,
* entrypoints and merge the new output with the existing output. * build the entrypoints and merge the new output with the existing output.
* *
* This function will: * This function will:
*
* 1. Generate the .wxt directory's types * 1. Generate the .wxt directory's types
* 2. Build the `entrypointGroups` (and copies public files) * 2. Build the `entrypointGroups` (and copies public files)
* 3. Generate the latest manifest for all entrypoints * 3. Generate the latest manifest for all entrypoints
* 4. Write the new manifest to the file system * 4. Write the new manifest to the file system
* *
* @param config Internal config containing all the project information. * @param config Internal config containing all the project information.
* @param allEntrypoints List of entrypoints used to generate the types inside .wxt directory. * @param allEntrypoints List of entrypoints used to generate the types inside
* .wxt directory.
* @param entrypointGroups The list of entrypoint groups to build. * @param entrypointGroups The list of entrypoint groups to build.
* @param existingOutput The previous output to combine the rebuild results into. An emptry array if * @param existingOutput The previous output to combine the rebuild results
* this is the first build. * into. An empty array if this is the first build.
*/ */
export async function rebuild( export async function rebuild(
allEntrypoints: Entrypoint[], allEntrypoints: Entrypoint[],
@@ -33,8 +36,7 @@ export async function rebuild(
manifest: Browser.runtime.Manifest; manifest: Browser.runtime.Manifest;
warnings: any[][]; warnings: any[][];
}> { }> {
const { default: ora } = await import('ora'); const spinner = createSpinner('Preparing...').start();
const spinner = ora(`Preparing...`).start();
// Update types directory with new files and types // Update types directory with new files and types
await generateWxtDir(allEntrypoints).catch((err) => { await generateWxtDir(allEntrypoints).catch((err) => {
+5 -5
View File
@@ -1,11 +1,11 @@
import fs, { ensureDir } from 'fs-extra'; import { mkdir, readFile } from 'node:fs/promises';
import { FsCache } from '../../types'; import { FsCache } from '../../types';
import { dirname, resolve } from 'path'; import { dirname, resolve } from 'path';
import { writeFileIfDifferent } from './fs'; import { writeFileIfDifferent } from './fs';
/** /**
* A basic file system cache stored at `<srcDir>/.wxt/cache/<key>`. Just caches a string in a * A basic file system cache stored at `<srcDir>/.wxt/cache/<key>`. Just caches
* file for the given key. * a string in a file for the given key.
* *
* @param srcDir Absolute path to source directory. See `InternalConfig.srcDir` * @param srcDir Absolute path to source directory. See `InternalConfig.srcDir`
*/ */
@@ -16,13 +16,13 @@ export function createFsCache(wxtDir: string): FsCache {
return { return {
async set(key: string, value: string): Promise<void> { async set(key: string, value: string): Promise<void> {
const path = getPath(key); const path = getPath(key);
await ensureDir(dirname(path)); await mkdir(dirname(path), { recursive: true });
await writeFileIfDifferent(path, value); await writeFileIfDifferent(path, value);
}, },
async get(key: string): Promise<string | undefined> { async get(key: string): Promise<string | undefined> {
const path = getPath(key); const path = getPath(key);
try { try {
return await fs.readFile(path, 'utf-8'); return await readFile(path, 'utf-8');
} catch { } catch {
return undefined; return undefined;
} }
+2 -2
View File
@@ -1,5 +1,5 @@
/** /**
* Module ID used to build the background in dev mode if the extension doesn't include a background * Module ID used to build the background in dev mode if the extension doesn't
* script/service worker. * include a background script/service worker.
*/ */
export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background'; export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';
@@ -4,7 +4,8 @@ import { getEntrypointBundlePath } from './entrypoints';
import { ManifestContentScript } from './types'; import { ManifestContentScript } from './types';
/** /**
* Returns a unique and consistent string hash based on a content scripts options. * Returns a unique and consistent string hash based on a content scripts
* options.
* *
* It is able to recognize default values, * It is able to recognize default values,
*/ */
@@ -1,5 +1,6 @@
/** /**
* Directive names that make up CSPs. There are more, this is all I need for the plugin. * 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 type CspDirective = 'default-src' | 'script-src' | 'object-src';
@@ -25,9 +26,7 @@ export class ContentSecurityPolicy {
} }
} }
/** /** Ensure a set of values are listed under a directive. */
* Ensure a set of values are listed under a directive.
*/
add(directive: CspDirective, ...newValues: string[]): ContentSecurityPolicy { add(directive: CspDirective, ...newValues: string[]): ContentSecurityPolicy {
const values = this.data[directive] ?? []; const values = this.data[directive] ?? [];
newValues.forEach((newValue) => { newValues.forEach((newValue) => {
+5 -7
View File
@@ -27,8 +27,8 @@ export function getEntrypointOutputFile(
} }
/** /**
* Return's the entrypoint's output path relative to the output directory. Used for paths in the * Return's the entrypoint's output path relative to the output directory. Used
* manifest and rollup's bundle. * for paths in the manifest and rollup's bundle.
*/ */
export function getEntrypointBundlePath( export function getEntrypointBundlePath(
entrypoint: Entrypoint, entrypoint: Entrypoint,
@@ -40,9 +40,7 @@ export function getEntrypointBundlePath(
); );
} }
/** /** Given an entrypoint option, resolve it's value based on a target browser. */
* Given an entrypoint option, resolve it's value based on a target browser.
*/
export function resolvePerBrowserOption<T>( export function resolvePerBrowserOption<T>(
option: PerBrowserOption<T>, option: PerBrowserOption<T>,
browser: TargetBrowser, browser: TargetBrowser,
@@ -55,8 +53,8 @@ export function resolvePerBrowserOption<T>(
/** /**
* Given an entrypoint option, resolve it's value based on a target browser. * Given an entrypoint option, resolve it's value based on a target browser.
* *
* defaultIcon is special, it's the only key that's a record, which can confuse this function. So * DefaultIcon is special, it's the only key that's a record, which can confuse
* it's been manually excluded from resolution. * this function. So it's been manually excluded from resolution.
*/ */
export function resolvePerBrowserOptions< export function resolvePerBrowserOptions<
T extends Record<string, any>, T extends Record<string, any>,
+1 -3
View File
@@ -2,9 +2,7 @@ import { config } from 'dotenv';
import { expand } from 'dotenv-expand'; import { expand } from 'dotenv-expand';
import type { TargetBrowser } from '../../types'; import type { TargetBrowser } from '../../types';
/** /** Load environment files based on the current mode and browser. */
* Load environment files based on the current mode and browser.
*/
export function loadEnv(mode: string, browser: TargetBrowser) { export function loadEnv(mode: string, browser: TargetBrowser) {
return expand( return expand(
config({ config({
+26 -15
View File
@@ -1,11 +1,25 @@
import fs from 'fs-extra'; import { access, readFile, writeFile } from 'node:fs/promises';
import glob from 'fast-glob'; import { glob } from 'tinyglobby';
import { unnormalizePath } from './paths';
import { wxt } from '../wxt'; import { wxt } from '../wxt';
import { unnormalizePath } from './paths';
export async function pathExists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}
export async function readJson<T = any>(path: string): Promise<T> {
return JSON.parse(await readFile(path, 'utf-8'));
}
/** /**
* Only write the contents to a file if it results in a change. This prevents unnecessary file * Only write the contents to a file if it results in a change. This prevents
* watchers from being triggered, like WXT's dev server or the TS language server in editors. * unnecessary file watchers from being triggered, like WXT's dev server or the
* TS language server in editors.
* *
* @param file The file to write to. * @param file The file to write to.
* @param newContents The new text content to write. * @param newContents The new text content to write.
@@ -14,22 +28,19 @@ export async function writeFileIfDifferent(
file: string, file: string,
newContents: string, newContents: string,
): Promise<void> { ): Promise<void> {
const existingContents = await fs const existingContents = await readFile(file, 'utf-8').catch(() => undefined);
.readFile(file, 'utf-8')
.catch(() => undefined);
if (existingContents !== newContents) { if (existingContents !== newContents) {
await fs.writeFile(file, newContents); await writeFile(file, newContents);
} }
} }
/**
* Get all the files in the project's public directory. Returned paths are relative to the
* `config.publicDir`.
*/
export async function getPublicFiles(): Promise<string[]> { export async function getPublicFiles(): Promise<string[]> {
if (!(await fs.pathExists(wxt.config.publicDir))) return []; if (!(await pathExists(wxt.config.publicDir))) return [];
const files = await glob('**/*', { cwd: wxt.config.publicDir }); const files = await glob('**/*', {
cwd: wxt.config.publicDir,
expandDirectories: false,
});
return files.map(unnormalizePath); return files.map(unnormalizePath);
} }
@@ -1,6 +1,6 @@
import path from 'node:path'; import path from 'node:path';
import pc from 'picocolors'; import pc from 'picocolors';
import fs from 'fs-extra'; import { lstat } from 'node:fs/promises';
import { filesize } from 'filesize'; import { filesize } from 'filesize';
import { printTable } from './printTable'; import { printTable } from './printTable';
@@ -20,7 +20,7 @@ export async function printFileList(
]; ];
const prefix = i === files.length - 1 ? ' └─' : ' ├─'; const prefix = i === files.length - 1 ? ' └─' : ' ├─';
const color = getChunkColor(file); const color = getChunkColor(file);
const stats = await fs.lstat(file); const stats = await lstat(file);
totalSize += stats.size; totalSize += stats.size;
const size = String(filesize(stats.size)); const size = String(filesize(stats.size));
return [ return [
+22 -28
View File
@@ -7,7 +7,7 @@ import {
PopupEntrypoint, PopupEntrypoint,
SidepanelEntrypoint, SidepanelEntrypoint,
} from '../../types'; } from '../../types';
import fs from 'fs-extra'; import { mkdir } from 'node:fs/promises';
import { resolve } from 'path'; import { resolve } from 'path';
import { getEntrypointBundlePath } from './entrypoints'; import { getEntrypointBundlePath } from './entrypoints';
import { ContentSecurityPolicy } from './content-security-policy'; import { ContentSecurityPolicy } from './content-security-policy';
@@ -23,9 +23,7 @@ import { wxt } from '../wxt';
import { ManifestV3WebAccessibleResource } from './types'; import { ManifestV3WebAccessibleResource } from './types';
import type { Browser } from '@wxt-dev/browser'; import type { Browser } from '@wxt-dev/browser';
/** /** Writes the manifest to the output directory and the build output. */
* Writes the manifest to the output directory and the build output.
*/
export async function writeManifest( export async function writeManifest(
manifest: Browser.runtime.Manifest, manifest: Browser.runtime.Manifest,
output: BuildOutput, output: BuildOutput,
@@ -35,7 +33,7 @@ export async function writeManifest(
? JSON.stringify(manifest) ? JSON.stringify(manifest)
: JSON.stringify(manifest, null, 2); : JSON.stringify(manifest, null, 2);
await fs.ensureDir(wxt.config.outDir); await mkdir(wxt.config.outDir, { recursive: true });
await writeFileIfDifferent(resolve(wxt.config.outDir, 'manifest.json'), str); await writeFileIfDifferent(resolve(wxt.config.outDir, 'manifest.json'), str);
output.publicAssets.unshift({ output.publicAssets.unshift({
@@ -44,9 +42,7 @@ export async function writeManifest(
}); });
} }
/** /** Generates the manifest based on the config and entrypoints. */
* Generates the manifest based on the config and entrypoints.
*/
export async function generateManifest( export async function generateManifest(
allEntrypoints: Entrypoint[], allEntrypoints: Entrypoint[],
buildOutput: Omit<BuildOutput, 'manifest'>, buildOutput: Omit<BuildOutput, 'manifest'>,
@@ -155,8 +151,9 @@ export async function generateManifest(
} }
/** /**
* Removes suffixes from the version, like X.Y.Z-alpha1 (which browsers don't allow), so it's a * Removes suffixes from the version, like X.Y.Z-alpha1 (which browsers don't
* simple version number, like X or X.Y or X.Y.Z, which browsers allow. * allow), so it's a simple version number, like X or X.Y or X.Y.Z, which
* browsers allow.
*/ */
function simplifyVersion(versionName: string): string { function simplifyVersion(versionName: string): string {
// Regex adapted from here: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version#version_format // Regex adapted from here: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version#version_format
@@ -514,8 +511,8 @@ function addDevModePermissions(manifest: Browser.runtime.Manifest) {
} }
/** /**
* Returns the bundle paths to CSS files associated with a list of content scripts, or undefined if * Returns the bundle paths to CSS files associated with a list of content
* there is no associated CSS. * scripts, or undefined if there is no associated CSS.
*/ */
export function getContentScriptCssFiles( export function getContentScriptCssFiles(
contentScripts: ContentScriptEntrypoint[], contentScripts: ContentScriptEntrypoint[],
@@ -541,9 +538,9 @@ export function getContentScriptCssFiles(
} }
/** /**
* Content scripts configured with `cssInjectionMode: "ui"` need to add their CSS files to web * Content scripts configured with `cssInjectionMode: "ui"` need to add their
* accessible resources so they can be fetched as text and added to shadow roots that the UI is * CSS files to web accessible resources so they can be fetched as text and
* added to. * added to shadow roots that the UI is added to.
*/ */
export function getContentScriptCssWebAccessibleResources( export function getContentScriptCssWebAccessibleResources(
contentScripts: ContentScriptEntrypoint[], contentScripts: ContentScriptEntrypoint[],
@@ -571,8 +568,8 @@ export function getContentScriptCssWebAccessibleResources(
} }
/** /**
* Based on the build output, return a Record of each content script's name to it CSS file if the * Based on the build output, return a Record of each content script's name to
* script includes one. * it CSS file if the script includes one.
*/ */
export function getContentScriptsCssMap( export function getContentScriptsCssMap(
buildOutput: Omit<BuildOutput, 'manifest'>, buildOutput: Omit<BuildOutput, 'manifest'>,
@@ -610,8 +607,8 @@ function addHostPermission(
} }
/** /**
* - "<all_urls>" &rarr; "<all_urls>" * - "<all_urls>" "<all_urls>"
* - "*://play.google.com/books/*" &rarr; "*://play.google.com/*" * - "_://play.google.com/books/_" "_://play.google.com/_"
*/ */
export function stripPathFromMatchPattern(pattern: string) { export function stripPathFromMatchPattern(pattern: string) {
const protocolSepIndex = pattern.indexOf('://'); const protocolSepIndex = pattern.indexOf('://');
@@ -622,9 +619,10 @@ export function stripPathFromMatchPattern(pattern: string) {
} }
/** /**
* Converts all MV3 web accessible resources to their MV2 forms. MV3 web accessible resources are * Converts all MV3 web accessible resources to their MV2 forms. MV3 web
* generated in this file, and may be defined by the user in their manifest. In both cases, when * accessible resources are generated in this file, and may be defined by the
* targeting MV2, automatically convert their definitions down to the basic MV2 array. * user in their manifest. In both cases, when targeting MV2, automatically
* convert their definitions down to the basic MV2 array.
*/ */
export function convertWebAccessibleResourcesToMv2( export function convertWebAccessibleResourcesToMv2(
manifest: Browser.runtime.Manifest, manifest: Browser.runtime.Manifest,
@@ -674,9 +672,7 @@ function convertCspToMv2(manifest: Browser.runtime.Manifest): void {
manifest.content_security_policy.extension_pages; manifest.content_security_policy.extension_pages;
} }
/** /** Make sure all resources are in MV3 format. If not, add a warning. */
* Make sure all resources are in MV3 format. If not, add a warning.
*/
function validateMv3WebAccessibleResources( function validateMv3WebAccessibleResources(
manifest: Browser.runtime.Manifest, manifest: Browser.runtime.Manifest,
): void { ): void {
@@ -694,9 +690,7 @@ function validateMv3WebAccessibleResources(
} }
} }
/** /** Remove keys from the manifest based on the build target. */
* Remove keys from the manifest based on the build target.
*/
function stripKeys(manifest: Browser.runtime.Manifest): void { function stripKeys(manifest: Browser.runtime.Manifest): void {
let keysToRemove: string[] = []; let keysToRemove: string[] = [];
if (wxt.config.manifestVersion === 2) { if (wxt.config.manifestVersion === 2) {
@@ -8,10 +8,10 @@ import { minimatch, MinimatchOptions } from 'minimatch';
* matches any of the negative patterns, it will return `false`. * matches any of the negative patterns, it will return `false`.
* *
* @example * @example
* ```ts * ```ts
* minimatchMultiple('a.json', ['*.json', '!b.json']); // => true * minimatchMultiple('a.json', ['*.json', '!b.json']); // => true
* minimatchMultiple('b.json', ['*.json', '!b.json']); // => false * minimatchMultiple('b.json', ['*.json', '!b.json']); // => false
* ``` * ```;
*/ */
export function minimatchMultiple( export function minimatchMultiple(
search: string, search: string,
+2 -2
View File
@@ -21,8 +21,8 @@ export async function isOnline(): Promise<boolean> {
} }
/** /**
* Fetches a URL with a simple GET request. Grabs it from cache if it doesn't exist, or throws an * Fetches a URL with a simple GET request. Grabs it from cache if it doesn't
* error if it can't be resolved via the network or cache. * exist, or throws an error if it can't be resolved via the network or cache.
*/ */
export async function fetchCached( export async function fetchCached(
url: string, url: string,
+2 -2
View File
@@ -1,5 +1,5 @@
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import fs from 'fs-extra'; import { readJson } from './fs';
import { wxt } from '../wxt'; import { wxt } from '../wxt';
/** /**
@@ -12,7 +12,7 @@ export async function getPackageJson(): Promise<
> { > {
const file = resolve(wxt.config.root, 'package.json'); const file = resolve(wxt.config.root, 'package.json');
try { try {
return await fs.readJson(file); return await readJson(file);
} catch (err) { } catch (err) {
wxt.logger.debug( wxt.logger.debug(
`Failed to read package.json at: ${file}. Returning undefined.`, `Failed to read package.json at: ${file}. Returning undefined.`,

Some files were not shown because too many files have changed in this diff Show More