Compare commits

..

12 Commits

Author SHA1 Message Date
Aaron c9205bb08f feat: New @wxt-dev/is-background package (#2152)
📼 VHS / Create VHS (push) Waiting to run
Co-authored-by: Smit <sm17p@users.noreply.github.com>
2026-02-22 13:57:09 -06:00
Tam Dang 05e23e70b4 fix(types): include CSS entrypoints in PublicPath generation (#2150)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-22 09:00:58 -06:00
Aaron 68fa36ef2b ci: Fix auto-labeling workflow for forks 2026-02-22 08:57:06 -06:00
Aaron 182d575205 feat(modules): Add support for augumenting entrypoint options (#2149) 2026-02-21 15:57:49 -06:00
Aaron 09eadf6796 docs: Add deep outline to modules page to show recipes 2026-02-21 15:57:25 -06:00
Aaron a6904b6319 ci: Create workflow for labeling and adding authors to PRs (#2148) 2026-02-21 15:29:04 -06:00
Nick Doan 42d65cb10b fix(types): Add type safety to browser.runtime.executeScript files option (#2142)
Co-authored-by: nickbar01234 <nickbar01234gmail.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-21 20:58:15 +00:00
Aaron 636cf1f841 fix: Allow userId option to return undefined 2026-02-21 14:48:28 -06:00
Patryk Kuniczak 3b154508bd chore: Created new types, instead of any for analytics (#2119) 2026-02-21 20:20:42 +00:00
Patryk Kuniczak d34eac9a4c chore: Remove @ts-expect-error from manifest.test.ts and fix typo (#2123) 2026-02-21 14:20:07 -06:00
Patryk Kuniczak 54c6fcca10 chore: Simplify imports in wxt/e2e (#2122) 2026-02-21 18:44:56 +00:00
Patryk Kuniczak 0114ee6a7a chore!: Rename zipdir to zipDir (#2126) 2026-02-21 12:40:34 -06:00
152 changed files with 2416 additions and 3994 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# Set default
* @aklinker1
* @aklinker1 @Timeraa
# Secure Directories
/.github/ @aklinker1
+4 -15
View File
@@ -11,26 +11,19 @@ permissions:
jobs:
vhs:
name: Create VHS
runs-on: macos-latest
runs-on: ubuntu-22.04
if: ${{ github.repository == 'wxt-dev/wxt' }}
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.ref }}
- name: Setup
uses: ./.github/actions/setup
with:
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
- name: Pre-install WXT
run: |
@@ -38,10 +31,9 @@ jobs:
pnpm dlx wxt@latest --version
- name: Record VHS
run: |
brew install ttyd ffmpeg
go install github.com/charmbracelet/vhs@517bcda0faf416728bcf6b7fe489eb0e2469d9b5 # v0.10.0
vhs docs/tapes/init-demo.tape
uses: charmbracelet/vhs-action@59641cdc7fadf3978db65eb8c6937ea2752f4ec3 # v2.1.0
with:
path: 'docs/tapes/init-demo.tape'
- name: Save recorded GIF
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
@@ -49,8 +41,5 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
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
file_pattern: 'docs/assets/*.gif'
-2
View File
@@ -1,4 +1,2 @@
singleQuote: true
endOfLine: lf
plugins:
- prettier-plugin-jsdoc
+6 -2
View File
@@ -4,8 +4,12 @@ While WXT is in prerelease, only the latest version will receive security update
<img alt="npm version" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048">
<!--
## Reporting a Vulnerability
If you discover a security vulnerability, please email me at <aaronklinker1@gmail.com>. I will respond within a few days to acknowledge receipt of your report.
Use this section to tell people how to report a vulnerability.
If the vulnerability is accepted, I will open a public issue to track the fix. If the vulnerability is not accepted, no further action will be taken.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
-->
@@ -119,8 +119,6 @@ const chromeExtensionIds = [
'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
'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);
+3 -1
View File
@@ -77,7 +77,9 @@ export function menuItem(
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[]) {
// skip contents file
const filtered = items.slice(1);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 131 KiB

+12 -11
View File
@@ -16,26 +16,27 @@
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
},
"devDependencies": {
"@aklinker1/buildc": "^1.1.7",
"@aklinker1/buildc": "^1.1.6",
"@aklinker1/check": "^2.2.0",
"@commitlint/config-conventional": "^20.4.3",
"@commitlint/types": "^20.4.3",
"@commitlint/config-conventional": "^20.4.1",
"@commitlint/types": "^20.4.0",
"@types/fs-extra": "^11.0.4",
"@types/semver": "^7.7.1",
"@vitest/coverage-v8": "^4.0.18",
"changelogen": "^0.6.2",
"consola": "^3.4.2",
"fast-glob": "^3.3.3",
"feed": "^5.2.0",
"fs-extra": "^11.3.3",
"lint-staged": "^16.2.7",
"markdown-it-footnote": "^4.0.0",
"markdownlint-cli": "^0.48.0",
"markdownlint-cli": "^0.47.0",
"nano-spawn": "^2.0.0",
"nano-staged": "^0.8.0",
"p-map": "^7.0.4",
"prettier": "^3.8.1",
"prettier-plugin-jsdoc": "^1.8.0",
"semver": "^7.7.4",
"simple-git-hooks": "^2.13.1",
"tinyglobby": "^0.2.15",
"tsdown": "^0.21.0",
"tsdown": "^0.20.3",
"tsx": "4.21.0",
"typedoc": "^0.25.4",
"typedoc-plugin-frontmatter": "^1.3.1",
@@ -47,13 +48,13 @@
"vitepress-plugin-group-icons": "^1.7.1",
"vitepress-plugin-llms": "^1.11.0",
"vitest-mock-extended": "^3.1.0",
"vue": "^3.5.29",
"vue": "^3.5.27",
"wxt": "workspace:*"
},
"simple-git-hooks": {
"pre-commit": "./node_modules/.bin/nano-staged"
"pre-commit": "pnpm lint-staged"
},
"nano-staged": {
"lint-staged": {
"*": "prettier --ignore-unknown --write"
}
}
-33
View File
@@ -1,38 +1,5 @@
# 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
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.2...analytics-v0.5.3)
### 🩹 Fixes
- Add `getAppConfig` as an alias to `useAppConfig` ([#2144](https://github.com/wxt-dev/wxt/pull/2144))
- Allow `userId` option to return `undefined` ([636cf1f8](https://github.com/wxt-dev/wxt/commit/636cf1f8))
- Improve background script detection logic for analytics package ([#1808](https://github.com/wxt-dev/wxt/pull/1808))
### 🏡 Chore
- Created new types, instead of `any` for `analytics` ([#2119](https://github.com/wxt-dev/wxt/pull/2119))
### ❤️ Contributors
- Smit ([@sm17p](https://github.com/sm17p))
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
- Sheng Zhang ([@Arktomson](https://github.com/Arktomson))
## v0.5.2
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.1...analytics-v0.5.2)
@@ -10,7 +10,6 @@ import type {
BaseAnalyticsEvent,
} from './types';
import { browser } from '@wxt-dev/browser';
import { isBackground } from '@wxt-dev/is-background';
type AnalyticsMessage = {
[K in keyof Analytics]: {
@@ -56,14 +55,16 @@ export function createAnalytics(config?: AnalyticsConfig): Analytics {
);
}
if (isBackground()) return createBackgroundAnalytics(config);
// TODO: This only works for standard WXT extensions, add a more generic
// background script detector that works with non-WXT projects.
if (location.pathname === '/background.js')
return createBackgroundAnalytics(config);
return createFrontendAnalytics();
}
/**
* 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(
config: AnalyticsConfig | undefined,
@@ -220,7 +221,9 @@ function createBackgroundAnalytics(
return analytics;
}
/** Creates an analytics client for non-background contexts. */
/**
* Creates an analytics client for non-background contexts.
*/
function createFrontendAnalytics(): Analytics {
const port = browser.runtime.connect({ name: ANALYTICS_PORT });
const sessionId = Date.now();
@@ -44,9 +44,9 @@ export default defineWxtModule({
? clientModuleId
: normalizePath(relative(wxtAnalyticsFolder, clientModuleId))
}';
import { useAppConfig } from '#imports';
import { getAppConfig } from '#imports';
export const analytics = createAnalytics(useAppConfig().analytics);
export const analytics = createAnalytics(getAppConfig().analytics);
`;
addAlias(wxt, '#analytics', wxtAnalyticsIndex);
wxt.hook('prepare:types', async (_, entries) => {
+10 -13
View File
@@ -8,39 +8,36 @@ export interface Analytics {
) => void;
/** Save information about the user. */
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;
/** Calls `config.enabled.setValue`. */
setEnabled: (enabled: boolean) => void;
}
export interface AnalyticsConfig {
/** Array of providers to send analytics to. */
/**
* Array of providers to send analytics to.
*/
providers: AnalyticsProvider[];
/** Enable debug logs and other provider-specific debugging features. */
/**
* Enable debug logs and other provider-specific debugging features.
*/
debug?: boolean;
/**
* Your extension's version, reported alongside events.
*
* @default browser.runtime.getManifest().version`.
*/
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>;
/**
* 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>;
/**
* 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>>;
}
+24 -25
View File
@@ -1,36 +1,14 @@
{
"name": "@wxt-dev/analytics",
"version": "0.5.4",
"version": "0.5.2",
"description": "Add analytics to your web extension",
"type": "module",
"scripts": {
"dev": "buildc --deps-only -- wxt",
"dev:build": "buildc --deps-only -- wxt build",
"check": "pnpm build && check",
"build": "buildc -- tsdown",
"prepack": "pnpm build",
"prepare": "buildc --deps-only -- wxt prepare"
},
"dependencies": {
"@wxt-dev/browser": "workspace:^",
"@wxt-dev/is-background": "workspace:^",
"ua-parser-js": "^1.0.40"
},
"peerDependencies": {
"wxt": ">=0.20.0"
},
"devDependencies": {
"@types/ua-parser-js": "^0.7.39",
"publint": "^0.3.18",
"typescript": "^5.9.3",
"wxt": "workspace:*"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git",
"directory": "packages/analytics"
},
"license": "MIT",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.mts",
@@ -60,5 +38,26 @@
"types": "./dist/index.d.mts",
"files": [
"dist"
]
],
"scripts": {
"dev": "buildc --deps-only -- wxt",
"dev:build": "buildc --deps-only -- wxt build",
"check": "pnpm build && check",
"build": "buildc -- tsdown",
"prepack": "pnpm -s build",
"prepare": "buildc --deps-only -- wxt prepare"
},
"peerDependencies": {
"wxt": ">=0.20.0"
},
"devDependencies": {
"@types/ua-parser-js": "^0.7.39",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"wxt": "workspace:*"
},
"dependencies": {
"@wxt-dev/browser": "workspace:^",
"ua-parser-js": "^1.0.40"
}
}
-29
View File
@@ -1,34 +1,5 @@
# Changelog
## v1.1.1
[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.1.0...auto-icons-v1.1.1)
### 🩹 Fixes
- Auto icons override default icons ([#1616](https://github.com/wxt-dev/wxt/pull/1616))
### 💅 Refactors
- Standardize file existence checks to `pathExists` ([#2083](https://github.com/wxt-dev/wxt/pull/2083))
### 🏡 Chore
- **deps:** Upgrade oxlint from 0.16.8 to 1.14.0 ([a01928e0](https://github.com/wxt-dev/wxt/commit/a01928e0))
- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))
- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))
- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))
- **deps:** Upgrade non-breaking production dependencies ([#1877](https://github.com/wxt-dev/wxt/pull/1877))
- 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))
### ❤️ Contributors
- Omerfardemir <od080624@gmail.com>
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v1.1.0
[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.0.2...auto-icons-v1.1.0)
+5 -7
View File
@@ -24,7 +24,7 @@
}
],
"license": "MIT",
"version": "1.1.1",
"version": "1.1.0",
"type": "module",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
@@ -39,22 +39,20 @@
],
"scripts": {
"build": "buildc -- tsdown",
"check": "pnpm build && check",
"test": "buildc --deps-only -- vitest",
"prepack": "pnpm build"
"check": "pnpm build && check"
},
"peerDependencies": {
"wxt": ">=0.19.0"
},
"dependencies": {
"defu": "^6.1.4",
"fs-extra": "^11.3.3",
"sharp": "^0.34.5"
},
"devDependencies": {
"oxlint": "^1.51.0",
"publint": "^0.3.18",
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
"wxt": "workspace:*"
}
}
@@ -1,630 +0,0 @@
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { resolve } from 'node:path';
import * as fsPromises from 'node:fs/promises';
import sharp from 'sharp';
import type { Wxt, UserManifest } from 'wxt';
// Import the actual module
import autoIconsModule from '../index';
import type { AutoIconsOptions } from '../index';
// Mock dependencies
vi.mock('node:fs/promises', () => ({
mkdir: vi.fn(),
access: vi.fn(),
}));
vi.mock('sharp', () => ({
default: vi.fn(),
}));
// Type definitions for better type safety
interface MockWxt {
config: {
srcDir: string;
outDir: string;
mode: 'development' | 'production';
};
logger: {
warn: Mock;
};
hooks: {
hook: Mock;
};
}
interface PublicAsset {
type: string;
fileName: string;
}
interface BuildOutput {
publicAssets: PublicAsset[];
}
describe('auto-icons module', () => {
const mockWxt: MockWxt = {
config: {
srcDir: '/mock/src',
outDir: '/mock/dist',
mode: 'development',
},
logger: {
warn: vi.fn(),
},
hooks: {
hook: vi.fn(),
},
};
const createMockSharpInstance = () => {
const instance = {
png: vi.fn(),
grayscale: vi.fn(),
resize: vi.fn(),
toFile: vi.fn().mockResolvedValue(undefined),
};
// Make methods chainable
instance.png.mockReturnValue(instance);
instance.grayscale.mockReturnValue(instance);
instance.resize.mockImplementation(() => {
// Create a new instance for each resize to simulate real sharp behavior
const resizedInstance = { ...instance };
resizedInstance.toFile = vi.fn().mockResolvedValue(undefined);
return resizedInstance;
});
return instance;
};
let mockSharpInstance: ReturnType<typeof createMockSharpInstance>;
beforeEach(() => {
vi.clearAllMocks();
mockSharpInstance = createMockSharpInstance();
vi.mocked(sharp).mockReturnValue(
mockSharpInstance as unknown as sharp.Sharp,
);
vi.mocked(fsPromises.access).mockResolvedValue(undefined);
vi.mocked(fsPromises.mkdir).mockResolvedValue(undefined as any);
});
describe('module setup', () => {
it('should have correct module metadata', () => {
expect(autoIconsModule.name).toBe('@wxt-dev/auto-icons');
expect(autoIconsModule.configKey).toBe('autoIcons');
expect(typeof autoIconsModule.setup).toBe('function');
});
});
describe('options handling', () => {
it('should use default options when not provided', async () => {
const options: AutoIconsOptions = {};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
// Verify that the module was set up (hooks were registered)
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
'build:manifestGenerated',
expect.any(Function),
);
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
'build:done',
expect.any(Function),
);
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
'prepare:publicPaths',
expect.any(Function),
);
});
it('should merge custom options with defaults', async () => {
const options: AutoIconsOptions = {
sizes: [64, 32],
grayscaleOnDevelopment: false,
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
// Verify that the module was set up with custom options
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
'build:manifestGenerated',
expect.any(Function),
);
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
'build:done',
expect.any(Function),
);
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
'prepare:publicPaths',
expect.any(Function),
);
});
});
describe('error handling', () => {
it('should warn when disabled', async () => {
const options: AutoIconsOptions = {
enabled: false,
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
expect(mockWxt.logger.warn).toHaveBeenCalledWith(
'`[auto-icons]` @wxt-dev/auto-icons disabled',
);
expect(mockWxt.hooks.hook).not.toHaveBeenCalled();
});
it('should warn when base icon not found', async () => {
vi.mocked(fsPromises.access).mockRejectedValue(new Error('ENOENT'));
const options: AutoIconsOptions = {
enabled: true,
baseIconPath: 'assets/missing-icon.png',
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
expect(mockWxt.logger.warn).toHaveBeenCalledWith(
expect.stringContaining(
'Skipping icon generation, no base icon found at',
),
);
expect(mockWxt.hooks.hook).not.toHaveBeenCalled();
});
});
describe('manifest generation hook', () => {
it('should update manifest with default icons when no custom sizes provided', async () => {
const options: AutoIconsOptions = {
enabled: true,
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const manifestHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
expect(manifestHook).toBeDefined();
const manifest: UserManifest = {};
if (manifestHook) {
await manifestHook(mockWxt as unknown as Wxt, manifest);
}
// Should use default sizes: [128, 48, 32, 16]
expect(manifest.icons).toEqual({
128: 'icons/128.png',
48: 'icons/48.png',
32: 'icons/32.png',
16: 'icons/16.png',
});
});
it('should merge custom sizes with defaults', async () => {
const options: AutoIconsOptions = {
enabled: true,
sizes: [96, 64], // These will be merged with defaults
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const manifestHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
expect(manifestHook).toBeDefined();
const manifest: UserManifest = {};
if (manifestHook) {
await manifestHook(mockWxt as unknown as Wxt, manifest);
}
// defu merges arrays, so we get both custom and default sizes
expect(manifest.icons).toEqual({
96: 'icons/96.png',
64: 'icons/64.png',
128: 'icons/128.png',
48: 'icons/48.png',
32: 'icons/32.png',
16: 'icons/16.png',
});
});
it('should warn when overwriting existing icons in manifest', async () => {
const options: AutoIconsOptions = {
enabled: true,
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const manifestHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
const manifest: UserManifest = {
icons: {
128: 'existing-icon.png',
},
};
if (manifestHook) {
await manifestHook(mockWxt as unknown as Wxt, manifest);
}
expect(mockWxt.logger.warn).toHaveBeenCalledWith(
'`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons',
);
});
});
describe('icon generation hook', () => {
it('should generate icons with default sizes', async () => {
const options: AutoIconsOptions = {
enabled: true,
};
const output: BuildOutput = {
publicAssets: [],
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
expect(buildHook).toBeDefined();
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
}
expect(sharp).toHaveBeenCalledWith(
resolve('/mock/src', 'assets/icon.png'),
);
expect(mockSharpInstance.png).toHaveBeenCalled();
// Should resize to default sizes
expect(mockSharpInstance.resize).toHaveBeenCalledWith(128);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(48);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(32);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(16);
expect(fsPromises.mkdir).toHaveBeenCalledWith(
resolve('/mock/dist', 'icons'),
{ recursive: true },
);
expect(output.publicAssets).toEqual([
{ type: 'asset', fileName: 'icons/128.png' },
{ type: 'asset', fileName: 'icons/48.png' },
{ type: 'asset', fileName: 'icons/32.png' },
{ type: 'asset', fileName: 'icons/16.png' },
]);
});
it('should generate icons with custom sizes merged with defaults', async () => {
const options: AutoIconsOptions = {
enabled: true,
sizes: [96, 64],
};
const output: BuildOutput = {
publicAssets: [],
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
expect(buildHook).toBeDefined();
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
}
// Should include both custom and default sizes
expect(mockSharpInstance.resize).toHaveBeenCalledWith(96);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(64);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(128);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(48);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(32);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(16);
expect(output.publicAssets).toEqual([
{ type: 'asset', fileName: 'icons/96.png' },
{ type: 'asset', fileName: 'icons/64.png' },
{ type: 'asset', fileName: 'icons/128.png' },
{ type: 'asset', fileName: 'icons/48.png' },
{ type: 'asset', fileName: 'icons/32.png' },
{ type: 'asset', fileName: 'icons/16.png' },
]);
});
it('should apply grayscale in development mode', async () => {
const options: AutoIconsOptions = {
enabled: true,
grayscaleOnDevelopment: true,
sizes: [128],
};
const output: BuildOutput = { publicAssets: [] };
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
}
expect(mockSharpInstance.grayscale).toHaveBeenCalled();
});
it('should not apply grayscale in production mode', async () => {
const prodMockWxt = {
...mockWxt,
config: {
...mockWxt.config,
mode: 'production' as const,
},
};
const options: AutoIconsOptions = {
enabled: true,
grayscaleOnDevelopment: true,
sizes: [128],
};
const output: BuildOutput = { publicAssets: [] };
await autoIconsModule.setup!(prodMockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(prodMockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
if (buildHook) {
await buildHook(prodMockWxt as unknown as Wxt, output);
}
expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();
});
it('should not apply grayscale when disabled', async () => {
const options: AutoIconsOptions = {
enabled: true,
grayscaleOnDevelopment: false,
sizes: [128],
};
const output: BuildOutput = { publicAssets: [] };
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
}
expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();
});
});
describe('public paths hook', () => {
it('should add default icon paths to public paths', async () => {
const options: AutoIconsOptions = {
enabled: true,
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const pathsHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'prepare:publicPaths')?.[1];
expect(pathsHook).toBeDefined();
const paths: string[] = [];
if (pathsHook) {
pathsHook(mockWxt as unknown as Wxt, paths);
}
expect(paths).toEqual([
'icons/128.png',
'icons/48.png',
'icons/32.png',
'icons/16.png',
]);
});
});
describe('edge cases and error handling', () => {
it('should handle empty sizes array', async () => {
const options: AutoIconsOptions = {
enabled: true,
sizes: [], // Empty array should still merge with defaults
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const manifestHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
const manifest: UserManifest = {};
if (manifestHook) {
await manifestHook(mockWxt as unknown as Wxt, manifest);
}
// Should still have default sizes due to defu merge
expect(manifest.icons).toEqual({
128: 'icons/128.png',
48: 'icons/48.png',
32: 'icons/32.png',
16: 'icons/16.png',
});
});
it('should handle sharp processing errors gracefully', async () => {
const options: AutoIconsOptions = {
enabled: true,
};
// Make toFile throw an error - need to properly chain resize -> png -> toFile
const errorInstance = {
toFile: vi.fn().mockRejectedValue(new Error('Sharp processing failed')),
grayscale: vi.fn(),
composite: vi.fn(),
};
errorInstance.grayscale.mockReturnValue(errorInstance);
errorInstance.composite.mockReturnValue(errorInstance);
mockSharpInstance.resize = vi.fn().mockImplementation(() => ({
png: vi.fn().mockReturnValue(errorInstance),
}));
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
const output: BuildOutput = { publicAssets: [] };
// Should throw the sharp error
if (buildHook) {
await expect(
buildHook(mockWxt as unknown as Wxt, output),
).rejects.toThrow('Sharp processing failed');
}
});
it('should handle file system errors during directory creation', async () => {
const options: AutoIconsOptions = {
enabled: true,
};
// Make ensureDir throw an error
vi.mocked(fsPromises.mkdir).mockRejectedValue(
new Error('Directory creation failed'),
);
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
const output: BuildOutput = { publicAssets: [] };
// The module doesn't await ensureDir, so it won't throw
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
// But ensureDir should have been called
expect(fsPromises.mkdir).toHaveBeenCalled();
}
});
it('should handle custom base icon path correctly', async () => {
const customPath = 'custom/icon.png';
const options: AutoIconsOptions = {
enabled: true,
baseIconPath: customPath,
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
const output: BuildOutput = { publicAssets: [] };
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
}
// Should resolve the path relative to srcDir
expect(sharp).toHaveBeenCalledWith(resolve('/mock/src', customPath));
});
});
describe('integration test', () => {
it('should handle full workflow correctly', async () => {
const options: AutoIconsOptions = {
enabled: true,
baseIconPath: 'assets/custom-icon.png',
sizes: [96], // Will be merged with defaults
grayscaleOnDevelopment: false,
};
const manifest: UserManifest = {};
const output: BuildOutput = { publicAssets: [] };
const paths: string[] = [];
// Setup the module
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
// Execute all hooks
const manifestHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
const pathsHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'prepare:publicPaths')?.[1];
if (manifestHook) {
await manifestHook(mockWxt as unknown as Wxt, manifest);
}
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
}
if (pathsHook) {
pathsHook(mockWxt as unknown as Wxt, paths);
}
// Verify results - defu merges arrays
expect(manifest.icons).toEqual({
96: 'icons/96.png',
128: 'icons/128.png',
48: 'icons/48.png',
32: 'icons/32.png',
16: 'icons/16.png',
});
expect(output.publicAssets).toEqual([
{ type: 'asset', fileName: 'icons/96.png' },
{ type: 'asset', fileName: 'icons/128.png' },
{ type: 'asset', fileName: 'icons/48.png' },
{ type: 'asset', fileName: 'icons/32.png' },
{ type: 'asset', fileName: 'icons/16.png' },
]);
expect(paths).toEqual([
'icons/96.png',
'icons/128.png',
'icons/48.png',
'icons/32.png',
'icons/16.png',
]);
expect(sharp).toHaveBeenCalledWith(
resolve('/mock/src', 'assets/custom-icon.png'),
);
expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();
});
});
});
+13 -19
View File
@@ -3,7 +3,7 @@ import { defineWxtModule } from 'wxt/modules';
import { resolve, relative } from 'node:path';
import defu from 'defu';
import sharp from 'sharp';
import { access, mkdir } from 'node:fs/promises';
import { ensureDir, pathExists } from 'fs-extra';
export default defineWxtModule<AutoIconsOptions>({
name: '@wxt-dev/auto-icons',
@@ -37,11 +37,7 @@ export default defineWxtModule<AutoIconsOptions>({
if (!parsedOptions.enabled)
return wxt.logger.warn(`\`[auto-icons]\` ${this.name} disabled`);
const iconExists = await access(resolvedPath).then(
() => true,
() => false,
);
if (!iconExists) {
if (!(await pathExists(resolvedPath))) {
return wxt.logger.warn(
`\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`,
);
@@ -49,7 +45,7 @@ export default defineWxtModule<AutoIconsOptions>({
wxt.hooks.hook('build:manifestGenerated', async (wxt, manifest) => {
if (manifest.icons)
wxt.logger.warn(
return wxt.logger.warn(
'`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons',
);
@@ -95,7 +91,7 @@ export default defineWxtModule<AutoIconsOptions>({
}
}
mkdir(resolve(outputFolder, 'icons'), { recursive: true });
ensureDir(resolve(outputFolder, 'icons'));
await resizedImage.toFile(resolve(outputFolder, `icons/${size}.png`));
output.publicAssets.push({
@@ -113,11 +109,12 @@ export default defineWxtModule<AutoIconsOptions>({
},
});
/** Options for the auto-icons module */
/**
* Options for the auto-icons module
*/
export interface AutoIconsOptions {
/**
* Enable auto-icons generation
*
* @default true
*/
enabled?: boolean;
@@ -125,30 +122,27 @@ export interface AutoIconsOptions {
* Path to the image to use.
*
* Path is relative to the project's src directory.
*
* @default '<srcDir>/assets/icon.png'
* @default "<srcDir>/assets/icon.png"
*/
baseIconPath?: string;
/**
* Apply a visual indicator to the icon when running in development mode.
*
* "grayscale" converts the icon to grayscale. "overlay" covers the bottom
* half with a yellow rectangle and writes "DEV" in black text. Set to `false`
* to disable any indicator.
* "grayscale" converts the icon to grayscale.
* "overlay" covers the bottom half with a yellow rectangle and writes "DEV" in black text.
* Set to `false` to disable any indicator.
*
* @default 'grayscale'
* @default "grayscale"
*/
developmentIndicator?: 'grayscale' | 'overlay' | false;
/**
* Grayscale the image when in development mode to indicate development
*
* @deprecated Use `developmentIndicator` instead
* @default true
* @deprecated Use `developmentIndicator` instead
*/
grayscaleOnDevelopment?: boolean;
/**
* Sizes to generate icons for
*
* @default [128, 48, 32, 16]
*/
sizes?: number[];
-8
View File
@@ -1,8 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
mockReset: true,
restoreMocks: true,
},
});
+1 -1
View File
@@ -24,7 +24,7 @@
],
"devDependencies": {
"@types/chrome": "0.1.37",
"@types/node": "^20.0.0",
"fs-extra": "^11.3.3",
"nano-spawn": "^2.0.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
+8 -8
View File
@@ -1,5 +1,5 @@
import spawn from 'nano-spawn';
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
import fs from 'fs-extra';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve, sep } from 'node:path';
import { sep as posixSep } from 'node:path/posix';
@@ -17,8 +17,8 @@ const pkgJsonPath = fileURLToPath(
import.meta.resolve('@types/chrome/package.json'),
);
const pkgDir = dirname(pkgJsonPath);
const pkgJson = JSON.parse(await readFile(pkgJsonPath, 'utf-8'));
const pkgJsonTemplate = await readFile('templates/package.json', 'utf8');
const pkgJson = await fs.readJson(pkgJsonPath);
const pkgJsonTemplate = await fs.readFile('templates/package.json', 'utf8');
const newPkgJson = JSON.parse(
pkgJsonTemplate.replaceAll('{{chromeTypesVersion}}', pkgJson.version),
);
@@ -27,7 +27,7 @@ newPkgJson.peerDependencies = pkgJson.peerDependencies;
newPkgJson.peerDependenciesMeta = pkgJson.peerDependenciesMeta;
const outPkgJsonPath = resolve('package.json');
await writeFile(outPkgJsonPath, JSON.stringify(newPkgJson, null, 2));
await fs.writeJson(outPkgJsonPath, newPkgJson);
await spawn('pnpm', ['-w', 'prettier', '--write', outPkgJsonPath]);
// Generate declaration files
@@ -35,7 +35,7 @@ await spawn('pnpm', ['-w', 'prettier', '--write', outPkgJsonPath]);
console.log('Generating declaration files');
const outDir = resolve('src/gen');
const declarationFileMapping = (
await readdir(pkgDir, {
await fs.readdir(pkgDir, {
recursive: true,
encoding: 'utf8',
})
@@ -50,11 +50,11 @@ const declarationFileMapping = (
}));
for (const { file, srcPath, destPath } of declarationFileMapping) {
const content = await readFile(srcPath, 'utf8');
const content = await fs.readFile(srcPath, 'utf8');
const transformedContent = transformFile(file, content);
const destDir = dirname(destPath);
await mkdir(destDir, { recursive: true });
await writeFile(destPath, transformedContent);
await fs.mkdir(destDir, { recursive: true });
await fs.writeFile(destPath, transformedContent);
console.log(` \x1b[2m-\x1b[0m \x1b[36m${file}\x1b[0m`);
}
+1 -1
View File
@@ -24,7 +24,7 @@
],
"devDependencies": {
"@types/chrome": "{{chromeTypesVersion}}",
"@types/node": "^20.0.0",
"fs-extra": "^11.3.3",
"nano-spawn": "^2.0.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
-26
View File
@@ -1,31 +1,5 @@
# Changelog
## v0.2.5
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.4...i18n-v0.2.5)
### 🩹 Fixes
- Add `.jsonc` support for locale files to match docs ([#2066](https://github.com/wxt-dev/wxt/pull/2066))
### 🏡 Chore
- Manually fix markdownlint errors ([#1711](https://github.com/wxt-dev/wxt/pull/1711))
- **deps:** Upgrade oxlint from 0.16.8 to 1.14.0 ([a01928e0](https://github.com/wxt-dev/wxt/commit/a01928e0))
- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))
- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))
- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))
- 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))
- Upgrade major deps ([#2070](https://github.com/wxt-dev/wxt/pull/2070))
### ❤️ Contributors
- Ilya Kubariev ([@gymnasy55](https://github.com/gymnasy55))
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.2.4
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.3...i18n-v0.2.4)
+6 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/i18n",
"description": "Type-safe wrapper around browser.i18n.getMessage with additional features",
"version": "0.2.5",
"version": "0.2.4",
"type": "module",
"repository": {
"type": "git",
@@ -23,14 +23,13 @@
"scripts": {
"build": "buildc -- tsdown",
"check": "buildc --deps-only -- check",
"test": "buildc --deps-only -- vitest",
"prepack": "pnpm build"
"test": "buildc --deps-only -- vitest"
},
"dependencies": {
"@wxt-dev/browser": "workspace:^",
"chokidar": "^5.0.0",
"confbox": "^0.2.4",
"tinyglobby": "^0.2.15"
"confbox": "^0.1.8 || ^0.2.2",
"fast-glob": "^3.3.3"
},
"peerDependencies": {
"wxt": ">=0.19.7"
@@ -42,8 +41,8 @@
},
"devDependencies": {
"@types/node": "^20.17.6",
"oxlint": "^1.51.0",
"publint": "^0.3.18",
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
"vitest-plugin-random-seed": "^1.1.2",
+10 -5
View File
@@ -51,8 +51,7 @@ 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> = {
'@@extension_id': {
@@ -111,7 +110,9 @@ const ALLOWED_CHROME_MESSAGE_KEYS: Set<string> = new Set<keyof ChromeMessage>([
// 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(
file: string,
): Promise<ParsedMessage[]> {
@@ -120,7 +121,9 @@ export async function parseMessagesFile(
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(
text: string,
format: 'JSON5' | 'YAML' | 'TOML',
@@ -128,7 +131,9 @@ export function parseMessagesText(
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[] {
return _parseMessagesObject(
[],
+3 -1
View File
@@ -1,4 +1,6 @@
/** @module @wxt-dev/i18n */
/**
* @module @wxt-dev/i18n
*/
import {
I18nStructure,
DefaultI18nStructure,
+6 -6
View File
@@ -3,7 +3,7 @@
*
* ```ts
* export default defineConfig({
* modules: ['@wxt-dev/i18n/module'],
* modules: ["@wxt-dev/i18n/module"],
* });
* ```
*
@@ -18,7 +18,7 @@ import {
generateTypeText,
SUPPORTED_LOCALES,
} from './build';
import { glob } from 'tinyglobby';
import glob from 'fast-glob';
import { basename, extname, join, resolve } from 'node:path';
import { watch } from 'chokidar';
import { GeneratedPublicFile, WxtDirFileEntry } from 'wxt';
@@ -47,7 +47,6 @@ export default defineWxtModule<I18nOptions>({
const files = await glob('*.{json,json5,jsonc,yml,yaml,toml}', {
cwd: localesDir,
absolute: true,
expandDirectories: false,
});
const unsupportedLocales: string[] = [];
@@ -181,12 +180,13 @@ export { type GeneratedI18nStructure }
},
});
/** Options for the i18n module */
/**
* Options for the i18n module
*/
export interface I18nOptions {
/**
* Directory containing files that define the translations.
*
* @default '${config.srcDir}/locales'
* @default "${config.srcDir}/locales"
*/
localesDir?: string;
}
+2 -2
View File
@@ -15,8 +15,8 @@
"@wxt-dev/browser": "workspace:^"
},
"devDependencies": {
"oxlint": "^1.51.0",
"publint": "^0.3.18",
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
+6 -10
View File
@@ -1,14 +1,10 @@
/**
* 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:
*
* 1. Easier to mock in tests
* 2. Safe to import in NodeJS environments (but it should be safe to run just
* in-case)
* 3. Keeps startup fast by waiting to run the slow functions (`instanceof` or
* `browser.extension.getBackgroundPage`) until needed
* 2. Safe to import in NodeJS environments (but it should be safe to run just 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
*/
@@ -20,10 +16,10 @@ let cached: boolean | undefined;
* Getter that returns if the current context is apart of an extension's
* background or not.
*
* > This function caches the result when called for the first time so it doesn't
* > have to recalculate.
* > This function caches the result when called for the first time so it
* > doesn't 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 {
if (cached == null) cached = getIsBackground();
-46
View File
@@ -1,51 +1,5 @@
# 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
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.4...module-react-v1.1.5)
+2 -9
View File
@@ -1,16 +1,15 @@
import 'wxt';
import { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules';
import react, { Options as PluginOptions } from '@vitejs/plugin-react';
import type { PluginOption } from 'vite';
export default defineWxtModule<ReactModuleOptions>({
name: '@wxt-dev/module-react',
configKey: 'react',
setup(wxt, options) {
const { vite, vitePluginsBefore } = options ?? {};
const { vite } = options ?? {};
addViteConfig(wxt, () => ({
plugins: [...(vitePluginsBefore ?? []), react(vite)],
plugins: [react(vite)],
}));
addImportPreset(wxt, 'react');
@@ -31,12 +30,6 @@ export default defineWxtModule<ReactModuleOptions>({
export interface ReactModuleOptions {
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' {
+5 -8
View File
@@ -18,7 +18,7 @@
},
"license": "MIT",
"funding": "https://github.com/sponsors/wxt-dev",
"version": "1.2.2",
"version": "1.1.5",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
@@ -42,24 +42,21 @@
"dev": "wxt",
"check": "pnpm build && check",
"build": "buildc -- tsdown",
"prepare": "buildc --deps-only -- wxt prepare",
"prepack": "pnpm build"
"prepare": "buildc --deps-only -- wxt prepare"
},
"peerDependencies": {
"vite": "^5.4.19 || ^6.3.4 || ^7.0.0 || ^8.0.0-0",
"wxt": ">=0.19.16"
},
"dependencies": {
"@vitejs/plugin-react": "^4.4.1 || ^5.0.0 || ^6.0.0"
"@vitejs/plugin-react": "^4.4.1 || ^5.0.0"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
"publint": "^0.3.18",
"publint": "^0.3.17",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"typescript": "^5.9.3",
"vite": "^7.0.0",
"wxt": "workspace:*"
}
}
+2 -3
View File
@@ -42,8 +42,7 @@
"dev": "wxt",
"check": "pnpm build && check",
"build": "buildc -- tsdown",
"prepare": "buildc --deps-only -- wxt prepare",
"prepack": "pnpm build"
"prepare": "buildc --deps-only -- wxt prepare"
},
"peerDependencies": {
"wxt": ">=0.19.16"
@@ -52,7 +51,7 @@
"vite-plugin-solid": "^2.11.10"
},
"devDependencies": {
"publint": "^0.3.18",
"publint": "^0.3.17",
"solid-js": "^1.9.11",
"typescript": "^5.9.3",
"wxt": "workspace:*"
+2 -3
View File
@@ -40,8 +40,7 @@
],
"scripts": {
"build": "buildc -- tsdown",
"check": "pnpm build && check",
"prepack": "pnpm build"
"check": "pnpm build && check"
},
"peerDependencies": {
"wxt": ">=0.18.6",
@@ -51,7 +50,7 @@
"@sveltejs/vite-plugin-svelte": "^4.0.0 || ^5.0.0 || ^6.0.0"
},
"devDependencies": {
"publint": "^0.3.18",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"wxt": "workspace:*"
}
+2 -3
View File
@@ -40,8 +40,7 @@
],
"scripts": {
"build": "buildc -- tsdown",
"check": "pnpm build && check",
"prepack": "pnpm build"
"check": "pnpm build && check"
},
"peerDependencies": {
"wxt": ">=0.19.16"
@@ -50,7 +49,7 @@
"@vitejs/plugin-vue": "^5.2.3 || ^6.0.0"
},
"devDependencies": {
"publint": "^0.3.18",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"wxt": "workspace:*"
}
+3 -3
View File
@@ -25,11 +25,11 @@
"test": "buildc --deps-only -- vitest",
"dev": "tsx --trace-warnings dev.ts",
"build": "buildc -- tsdown",
"prepack": "pnpm build"
"prepublishOnly": "pnpm build"
},
"devDependencies": {
"oxlint": "^1.51.0",
"publint": "^0.3.18",
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
+1 -2
View File
@@ -124,8 +124,7 @@ 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[]>> = {
chrome: [
+3 -7
View File
@@ -4,11 +4,8 @@ import { createCdpConnection } from './cdp';
/**
* 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(
debuggerUrl: string,
@@ -36,8 +33,7 @@ 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`
* - `--user-data-dir=...`
+10 -36
View File
@@ -17,46 +17,21 @@ export type Target = KnownTarget | UnknownTarget;
export type RunOptions = {
/** Paths to binaries to use for each target. */
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[];
/**
* 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';
/**
* 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;
/**
* 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;
/**
* 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;
/**
* 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[];
/**
* 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;
/**
* 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;
};
@@ -244,9 +219,8 @@ async function pathExists(path: string): Promise<boolean> {
}
/**
* Copied from
* https://github.com/GoogleChrome/chrome-launcher/blob/main/src/flags.ts with
* some flags commented out. Run tests after updating to compare.
* Copied from 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 = [
'--disable-features=' +
-12
View File
@@ -1,17 +1,5 @@
# Changelog
## v1.2.8
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.7...storage-v1.2.8)
### 🩹 Fixes
- Correctly update version metadata when setting a value for the first time ([#2139](https://github.com/wxt-dev/wxt/pull/2139))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v1.2.7
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.6...storage-v1.2.7)
+4 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/storage",
"description": "Web extension storage API provided by WXT, supports all browsers.",
"version": "1.2.8",
"version": "1.2.7",
"type": "module",
"repository": {
"type": "git",
@@ -27,8 +27,7 @@
"scripts": {
"build": "buildc -- tsdown",
"check": "buildc --deps-only -- check",
"test": "buildc --deps-only -- vitest",
"prepack": "pnpm build"
"test": "buildc --deps-only -- vitest"
},
"dependencies": {
"@wxt-dev/browser": "workspace:^",
@@ -37,8 +36,8 @@
},
"devDependencies": {
"@webext-core/fake-browser": "^1.3.4",
"oxlint": "^1.51.0",
"publint": "^0.3.18",
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"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';
/**
* This works because fakeBrowser is synchronous, and is will finish any number
* of chained calls within a single tick of the event loop, ie: a timeout of 0.
* This works because fakeBrowser is synchronous, and is will finish any number of chained
* calls within a single tick of the event loop, ie: a timeout of 0.
*/
async function waitForMigrations() {
return new Promise((res) => setTimeout(res));
+100 -64
View File
@@ -1,9 +1,7 @@
/**
* 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.
*
* @module @wxt-dev/storage
*/
import { browser, type Browser } from '@wxt-dev/browser';
@@ -691,7 +689,7 @@ export interface WxtStorage {
* Get an item from storage, or return `null` if it doesn't exist.
*
* @example
* await storage.getItem<number>('local:installDate');
* await storage.getItem<number>("local:installDate");
*/
getItem<TValue>(
key: StorageItemKey,
@@ -704,11 +702,11 @@ export interface WxtStorage {
): Promise<TValue | null>;
/**
* Get multiple items from storage. The return order is guaranteed to be the
* same as the order requested.
* Get multiple items from storage. The return order is guaranteed to be the same as the order
* requested.
*
* @example
* await storage.getItems(['local:installDate', 'session:someCounter']);
* await storage.getItems(["local:installDate", "session:someCounter"]);
*/
getItems(
keys: Array<
@@ -719,11 +717,11 @@ export interface WxtStorage {
): Promise<Array<{ key: StorageItemKey; value: any }>>;
/**
* Return an object containing metadata about the key. Object is stored at
* `key + "$"`. If value is not an object, it returns an empty object.
* Return an object containing metadata about the key. Object is stored at `key + "$"`. If value
* is not an object, it returns an empty object.
*
* @example
* await storage.getMeta('local:installDate');
* await storage.getMeta("local:installDate");
*/
getMeta<T extends Record<string, unknown>>(key: StorageItemKey): Promise<T>;
@@ -738,23 +736,22 @@ export interface WxtStorage {
): Promise<Array<{ key: StorageItemKey; meta: any }>>;
/**
* Set a value in storage. Setting a value to `null` or `undefined` is
* equivalent to calling `removeItem`.
* Set a value in storage. Setting a value to `null` or `undefined` is equivalent to calling
* `removeItem`.
*
* @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>;
/**
* 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
* await storage.setItem([
* await storage.setItem([
* { key: "local:installDate", value: Date.now() },
* { key: "session:someCounter, value: 5 },
* ]);
* ]);
*/
setItems(
values: Array<
@@ -764,11 +761,11 @@ export interface WxtStorage {
): Promise<void>;
/**
* Sets metadata properties. If some properties are already set, but are not
* included in the `properties` parameter, they will not be removed.
* Sets metadata properties. If some properties are already set, but are not included in the
* `properties` parameter, they will not be removed.
*
* @example
* await storage.setMeta('local:installDate', { appVersion });
* await storage.setMeta("local:installDate", { appVersion });
*/
setMeta<T extends Record<string, unknown>>(
key: StorageItemKey,
@@ -791,11 +788,13 @@ export interface WxtStorage {
* Removes an item from storage.
*
* @example
* await storage.removeItem('local:installDate');
* await storage.removeItem("local:installDate");
*/
removeItem(key: StorageItemKey, opts?: RemoveItemOptions): Promise<void>;
/** Remove a list of keys from storage. */
/**
* Remove a list of keys from storage.
*/
removeItems(
keys: Array<
| StorageItemKey
@@ -805,41 +804,48 @@ export interface WxtStorage {
>,
): Promise<void>;
/** Removes all items from the provided storage area. */
/**
* Removes all items from the provided storage area.
*/
clear(base: StorageArea): Promise<void>;
/**
* Remove the entire metadata for a key, or specific properties by name.
*
* @example
* // Remove all metadata properties from the item
* await storage.removeMeta('local:installDate');
* // Remove all metadata properties from the item
* await storage.removeMeta("local:installDate");
*
* // Remove only specific the "v" field
* await storage.removeMeta('local:installDate', 'v');
* // Remove only specific the "v" field
* await storage.removeMeta("local:installDate", "v")
*/
removeMeta(
key: StorageItemKey,
properties?: string | string[],
): Promise<void>;
/** Return all the items in storage. */
/**
* Return all the items in storage.
*/
snapshot(
base: StorageArea,
opts?: SnapshotOptions,
): Promise<Record<string, unknown>>;
/**
* Restores the results of `snapshot`. If new properties have been saved since
* the snapshot, they are not overridden. Only values existing in the snapshot
* are overridden.
* Restores the results of `snapshot`. If new properties have been saved since the snapshot, they are
* not overridden. Only values existing in the snapshot are overridden.
*/
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;
/** Remove all watch listeners. */
/**
* Remove all watch listeners.
*/
unwatch(): void;
/**
@@ -888,42 +894,61 @@ export interface WxtStorageItem<
TValue,
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;
/** @deprecated Renamed to fallback, use it instead. */
/**
* @deprecated Renamed to fallback, use it instead.
*/
defaultValue: TValue;
/** The value provided by the `fallback` option. */
/**
* The value provided by the `fallback` option.
*/
fallback: TValue;
/** Get the latest value from storage. */
/**
* Get the latest value from storage.
*/
getValue(): Promise<TValue>;
/** Get metadata. */
/**
* Get metadata.
*/
getMeta(): Promise<NullablePartial<TMetadata>>;
/** Set the value in storage. */
/**
* Set the value in storage.
*/
setValue(value: TValue): Promise<void>;
/** Set metadata properties. */
/**
* Set metadata properties.
*/
setMeta(properties: NullablePartial<TMetadata>): Promise<void>;
/** Remove the value from storage. */
/**
* Remove the value from storage.
*/
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>;
/** Listen for changes to the value in storage. */
/**
* Listen for changes to the value in storage.
*/
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 manually.
* **This function is ran automatically whenever the extension updates**, so you don't have to call it
* manually.
*/
migrate(): Promise<void>;
}
@@ -932,10 +957,14 @@ export type StorageArea = 'local' | 'session' | 'sync' | 'managed';
export type StorageItemKey = `${StorageArea}:${string}`;
export interface GetItemOptions<T> {
/** @deprecated Renamed to `fallback`, use it instead. */
/**
* @deprecated Renamed to `fallback`, use it instead.
*/
defaultValue?: T;
/** Default value returned when `getItem` would otherwise return `null`. */
/**
* Default value returned when `getItem` would otherwise return `null`.
*/
fallback?: T;
}
@@ -950,17 +979,21 @@ export interface RemoveItemOptions {
export interface SnapshotOptions {
/**
* Exclude a list of keys. The storage area prefix should be removed since the
* snapshot is for a specific storage area already.
* Exclude a list of keys. The storage area prefix should be removed since the snapshot is for a
* specific storage area already.
*/
excludeKeys?: string[];
}
export interface WxtStorageItemOptions<T> {
/** @deprecated Renamed to `fallback`, use it instead. */
/**
* @deprecated Renamed to `fallback`, use it instead.
*/
defaultValue?: T;
/** Default value returned when `getValue` would otherwise return `null`. */
/**
* Default value returned when `getValue` would otherwise return `null`.
*/
fallback?: T;
/**
@@ -972,26 +1005,25 @@ export interface WxtStorageItemOptions<T> {
init?: () => T | Promise<T>;
/**
* Provide a version number for the storage item to enable migrations. When
* changing the version in the future, migration functions will be ran on
* application startup.
* Provide a version number for the storage item to enable migrations. When changing the version
* in the future, migration functions will be ran on application startup.
*/
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>;
/**
* Print debug logs, such as migration process.
*
* @default false
*/
debug?: boolean;
/** A callback function that runs on migration complete. */
/**
* A callback function that runs on migration complete.
*/
onMigrationComplete?: (migratedValue: T, targetVersion: number) => void;
}
@@ -1000,17 +1032,21 @@ export type StorageAreaChanges = {
};
/**
* Same as `Partial`, but includes `| null`. It makes all the properties of an
* object optional and nullable.
* Same as `Partial`, but includes `| null`. It makes all the properties of an object optional and
* nullable.
*/
type NullablePartial<T> = {
[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;
/** Call to remove a watch listener */
/**
* Call to remove a watch listener
*/
export type Unwatch = () => void;
export class MigrationError extends Error {
+4 -5
View File
@@ -33,22 +33,21 @@
],
"scripts": {
"build": "buildc -- tsdown",
"check": "buildc --deps-only -- check",
"prepack": "pnpm build"
"check": "buildc --deps-only -- check"
},
"peerDependencies": {
"unocss": ">=0.60.0",
"wxt": ">=0.19.0"
},
"devDependencies": {
"oxlint": "^1.51.0",
"publint": "^0.3.18",
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"unocss": "^0.64.0 || ^0.65.0 || ^65.0.0 || ^66.0.0",
"wxt": "workspace:*"
},
"dependencies": {
"defu": "^6.1.4",
"tinyglobby": "^0.2.15"
"fast-glob": "^3.3.3"
}
}
+6 -10
View File
@@ -38,11 +38,12 @@ export default defineWxtModule<UnoCSSOptions>({
},
});
/** Options for the UnoCSS module */
/**
* Options for the UnoCSS module
*/
export interface UnoCSSOptions<Theme extends object = object> {
/**
* Enable UnoCSS
*
* @default true
*/
enabled?: boolean;
@@ -50,17 +51,12 @@ export interface UnoCSSOptions<Theme extends object = object> {
* 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
* it from specific builds.
*
* @example
* {undefined} ('popup',
* 'options');
*
* @default [ ]
* @example ["popup", "options"]
* @default []
*/
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];
}
+3 -4
View File
@@ -38,16 +38,15 @@
"dev": "wxt",
"check": "pnpm build && check",
"build": "buildc -- tsdown",
"prepare": "buildc --deps-only -- wxt prepare",
"prepack": "pnpm build"
"prepare": "buildc --deps-only -- wxt prepare"
},
"peerDependencies": {
"webextension-polyfill": "*",
"wxt": ">=0.20.0"
},
"devDependencies": {
"@types/webextension-polyfill": "^0.12.5",
"publint": "^0.3.18",
"@types/webextension-polyfill": "^0.12.4",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"webextension-polyfill": "^0.12.0",
"wxt": "workspace:*"
+1 -1
View File
@@ -22,7 +22,7 @@
"react-dom": "^19.2.4"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
"@wxt-dev/auto-icons": "workspace:*",
"@wxt-dev/unocss": "workspace:*",
-90
View File
@@ -1,95 +1,5 @@
# 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
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.16...wxt-v0.20.17)
+4 -5
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env node
/**
* A alias around `publish-extension` that is always installed on the path
* without having to install `publish-browser-extension` as a direct dependency
* (like for PNPM, which doesn't link sub-dependency binaries to
* "node_modules/.bin")
* A alias around `publish-extension` that is always installed on the path without having to install
* `publish-browser-extension` as a direct dependency (like for PNPM, which doesn't link
* sub-dependency binaries to "node_modules/.bin")
*/
await import('publish-browser-extension/cli');
import 'publish-browser-extension/cli';
+5 -6
View File
@@ -1,8 +1,8 @@
import spawn from 'nano-spawn';
import { mkdir, writeFile } from 'node:fs/promises';
import { glob } from 'tinyglobby';
import { describe, expect, it } from 'vitest';
import { describe, it, expect } from 'vitest';
import { TestProject, WXT_PACKAGE_DIR } from '../utils';
import spawn from 'nano-spawn';
import glob from 'fast-glob';
import { mkdir, writeJson } from 'fs-extra';
describe('Init command', () => {
it('should download and create a template', async () => {
@@ -21,7 +21,6 @@ describe('Init command', () => {
cwd: project.root,
onlyFiles: true,
dot: true,
expandDirectories: false,
});
expect(files.sort()).toMatchInlineSnapshot(`
@@ -53,7 +52,7 @@ describe('Init command', () => {
it('should throw an error if the directory is not empty', async () => {
const project = new TestProject();
await mkdir(project.root, { recursive: true });
await writeFile(project.resolvePath('package.json'), JSON.stringify({}));
await writeJson(project.resolvePath('package.json'), {});
await expect(() =>
spawn(
+3 -3
View File
@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { TestProject } from '../utils';
import type { InlineConfig, UnlistedScriptEntrypoint } from '../../src';
import { readFile } from 'node:fs/promises';
import type { GenericEntrypoint, InlineConfig } from '../../src';
import { readFile } from 'fs-extra';
import { normalizePath } from '../../src';
describe('Module Helpers', () => {
@@ -49,7 +49,7 @@ describe('Module Helpers', () => {
'export default defineBackground(() => {})',
);
const entrypoint: UnlistedScriptEntrypoint = {
const entrypoint: GenericEntrypoint = {
type: 'unlisted-script',
inputPath: project.resolvePath('modules/test/injected.ts'),
name: 'injected',
@@ -459,104 +459,4 @@ describe('Output Directory Structure', () => {
"
`);
});
describe('globalName option', () => {
it('generates an IIFE with a default name', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.js',
`export default defineContentScript({
matches: ["*://*/*"],
main() {},
})`,
);
await project.build({ vite: () => ({ build: { minify: false } }) });
const output = await project.serializeFile(
'.output/chrome-mv3/content-scripts/content.js',
);
expect(output).toMatch(/^var content\s?=[\s\S]*^content;$/gm);
});
it('generates an IIFE with a specific name', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.js',
`export default defineContentScript({
globalName: "MyContentScript",
matches: ["*://*/*"],
main() {},
})`,
);
await project.build({ vite: () => ({ build: { minify: false } }) });
const output = await project.serializeFile(
'.output/chrome-mv3/content-scripts/content.js',
);
expect(output).toMatch(
/^var MyContentScript =[\s\S]*^MyContentScript;$/gm,
);
});
it('generates an IIFE with a specific name provided by a function', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.js',
`export default defineContentScript({
globalName: () => "MyContentScript",
matches: ["*://*/*"],
main() {},
})`,
);
await project.build({ vite: () => ({ build: { minify: false } }) });
const output = await project.serializeFile(
'.output/chrome-mv3/content-scripts/content.js',
);
expect(output).toMatch(
/^var MyContentScript =[\s\S]*^MyContentScript;$/gm,
);
});
it('generates an anonymous IIFE when not minified', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.js',
`export default defineContentScript({
globalName: false,
matches: ["*://*/*"],
main() {},
})`,
);
await project.build({ vite: () => ({ build: { minify: false } }) });
const output = await project.serializeFile(
'.output/chrome-mv3/content-scripts/content.js',
);
expect(output).toMatch(/^\(function\(\) {[\s\S]*^}\)\(\);$/gm);
});
it('generates an anonymous IIFE when minified', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.js',
`export default defineContentScript({
globalName: false,
matches: ["*://*/*"],
main() {},
})`,
);
await project.build({ vite: () => ({ build: { minify: true } }) });
const output = await project.serializeFile(
'.output/chrome-mv3/content-scripts/content.js',
);
expect(output).toMatch(/^\(function\(\){[\s\S]*}\)\(\);$/gm);
});
});
});
+1 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
import { TestProject } from '../utils';
import extract from 'extract-zip';
import spawn from 'nano-spawn';
import { readFile, writeFile } from 'node:fs/promises';
import { readFile, writeFile } from 'fs-extra';
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 spawn from 'nano-spawn';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, relative, resolve } from 'path';
import { glob } from 'tinyglobby';
import {
InlineConfig,
UserConfig,
@@ -12,7 +12,6 @@ import {
zip,
} from '../src';
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
// wxt package. This uses the TS files instead of the compiled JS package
@@ -60,7 +59,9 @@ 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 = {}) {
this.config = config;
this.files.push([
@@ -108,7 +109,9 @@ export class TestProject {
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 {
return resolve(this.root, ...path);
}
@@ -120,8 +123,8 @@ export class TestProject {
const [name, content] = file;
const filePath = this.resolvePath(name);
const fileDir = dirname(filePath);
await mkdir(fileDir, { recursive: true });
await writeFile(filePath, content ?? '', 'utf-8');
await fs.ensureDir(fileDir);
await fs.writeFile(filePath, content ?? '', 'utf-8');
}
// Only install dependencies if the project has custom ones - otherwise the
@@ -138,11 +141,11 @@ export class TestProject {
}
/**
* Read all the files from the test project's `.output` directory and combine
* them into a string that can be used in a snapshot.
* Read all the files from the test project's `.output` directory and combine them into a string
* that can be used in a snapshot.
*
* Optionally, provide a list of filenames whose content is not printed
* (because it's inconsistent or not relevant to a test).
* Optionally, provide a list of filenames whose content is not printed (because it's inconsistent
* or not relevant to a test).
*/
serializeOutput(ignoreContentsOfFilenames?: string[]): Promise<string> {
return this.serializeDir('.output', ignoreContentsOfFilenames);
@@ -151,8 +154,8 @@ export class TestProject {
/**
* Deeply print the filename and contents of all files in a directory.
*
* Optionally, provide a list of filenames whose content is not printed
* (because it's inconsistent or not relevant to a test).
* Optionally, provide a list of filenames whose content is not printed (because it's inconsistent
* or not relevant to a test).
*/
private async serializeDir(
dir: string,
@@ -161,7 +164,6 @@ export class TestProject {
const outputFiles = await glob('**/*', {
cwd: this.resolvePath(dir),
ignore: ['**/node_modules', '**/.output'],
expandDirectories: false,
});
outputFiles.sort();
const fileContents = [];
@@ -177,26 +179,24 @@ export class TestProject {
/**
* @param path An absolute path to a file or a path relative to the root.
* @param ignoreContents An optional boolean that, when true, causes this
* function to not print the file contents.
* @param ignoreContents An optional boolean that, when true, causes this function to not print
* the file contents.
*/
async serializeFile(path: string, ignoreContents?: boolean): Promise<string> {
const absolutePath = this.resolvePath(path);
return [
normalizePath(relative(this.root, absolutePath)),
ignoreContents
? '<contents-ignored>'
: await readFile(absolutePath, 'utf-8'),
ignoreContents ? '<contents-ignored>' : await fs.readFile(absolutePath),
].join(`\n${''.padEnd(40, '-')}\n`);
}
pathExists(...path: string[]): Promise<boolean> {
return pathExists(this.resolvePath(...path));
return fs.pathExists(this.resolvePath(...path));
}
getOutputManifest(
path: string = '.output/chrome-mv3/manifest.json',
): Promise<any> {
return readJson(this.resolvePath(path));
return fs.readJson(this.resolvePath(path));
}
}
-1
View File
@@ -1,4 +1,3 @@
// oxlint-disable-next-line eslint(no-shadow-restricted-names)
declare namespace globalThis {
var __ENTRYPOINT__: string;
}
+18 -16
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.20.19",
"version": "0.20.17",
"description": "⚡ Next-gen Web Extension Framework",
"license": "MIT",
"scripts": {
@@ -25,44 +25,45 @@
"@wxt-dev/storage": "workspace:^1.0.0",
"async-mutex": "^0.5.0",
"c12": "^3.3.3",
"cac": "^6.7.14 || ^7.0.0",
"cac": "^6.7.14",
"chokidar": "^5.0.0",
"ci-info": "^4.4.0",
"consola": "^3.4.2",
"defu": "^6.1.4",
"dotenv": "^17.3.1",
"dotenv": "^17.2.4",
"dotenv-expand": "^12.0.3",
"esbuild": "^0.27.1",
"fast-glob": "^3.3.3",
"filesize": "^11.0.13",
"fs-extra": "^11.3.3",
"get-port-please": "^3.2.0",
"giget": "^1.2.3 || ^2.0.0 || ^3.0.0",
"hookable": "^6.0.1",
"import-meta-resolve": "^4.2.0",
"is-wsl": "^3.1.1",
"is-wsl": "^3.1.0",
"json5": "^2.2.3",
"jszip": "^3.10.1",
"linkedom": "^0.18.12",
"magicast": "^0.5.2",
"minimatch": "^10.2.4",
"minimatch": "^10.1.2",
"nano-spawn": "^2.0.0",
"nanospinner": "^1.2.2",
"normalize-path": "^3.0.0",
"nypm": "^0.6.5",
"ohash": "^2.0.11",
"open": "^11.0.0",
"ora": "^9.3.0",
"perfect-debounce": "^2.1.0",
"picocolors": "^1.1.1",
"prompts": "^2.4.2",
"publish-browser-extension": "^2.3.0 || ^3.0.2 || ^4.0.4",
"publish-browser-extension": "^2.3.0 || ^3.0.2 || ^4.0.0",
"scule": "^1.3.0",
"tinyglobby": "^0.2.15",
"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",
"unimport": "^3.13.1 || ^4.0.0 || ^5.0.0",
"vite": "^5.4.19 || ^6.3.4 || ^7.0.0",
"vite-node": "^3.2.4 || ^5.0.0",
"web-ext-run": "^0.2.4"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
"eslint": "^8.57.0 || ^9.0.0"
},
"peerDependenciesMeta": {
"eslint": {
@@ -70,17 +71,18 @@
}
},
"devDependencies": {
"@faker-js/faker": "^10.3.0",
"@faker-js/faker": "^10.2.0",
"@types/fs-extra": "^11.0.4",
"@types/lodash.merge": "^4.6.9",
"@types/node": "^20.17.6",
"@types/normalize-path": "^3.0.2",
"@types/prompts": "^2.4.9",
"eslint": "^10.0.0",
"eslint": "^9.38.0",
"extract-zip": "^2.0.1",
"happy-dom": "^20.8.3",
"happy-dom": "^20.5.0",
"lodash.merge": "^4.6.2",
"oxlint": "^1.51.0",
"publint": "^0.3.18",
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
"vitest-plugin-random-seed": "^1.1.2"
+7 -12
View File
@@ -1,32 +1,25 @@
/**
* 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
* import { browser } from 'wxt/browser';
*
* browser.runtime.onInstalled.addListener(() => {
* // ...
* });
* })
* ```
*
* @module wxt/browser
*/
import { browser as _browser, type Browser } from '@wxt-dev/browser';
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`
*/
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`
*/
export interface WxtI18n {}
@@ -43,7 +36,9 @@ type InjectionResult<Result> = Array<
export interface WxtScripting {
executeScript: {
/** @see {@link Browser.scripting.executeScript} */
/**
* @see {@link Browser.scripting.executeScript}
*/
<Args extends any[], Result>(
injection: ScriptInjection<Args, Result>,
): Promise<InjectionResult<Result>>;
+3 -4
View File
@@ -8,8 +8,7 @@ import { registerWxt } from '../core/wxt';
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(
cb: (
@@ -55,8 +54,8 @@ export function wrapAction(
}
/**
* Array flags, when not passed, are either `undefined` or `[undefined]`. This
* function filters out the
* Array flags, when not passed, are either `undefined` or `[undefined]`. This function filters out
* the
*/
export function getArrayFromFlags<T>(
flags: any,
+8 -9
View File
@@ -3,18 +3,17 @@ import { internalBuild } from './utils/building';
import { registerWxt } from './wxt';
/**
* Bundles the extension for production. Returns a promise of the build result.
* Discovers the `wxt.config.ts` file in the root directory, and merges that
* config with what is passed in.
* Bundles the extension for production. Returns a promise of the build result. Discovers the `wxt.config.ts` file in
* the root directory, and merges that config with what is passed in.
*
* @example
* // Use config from `wxt.config.ts`
* const res = await build();
* // Use config from `wxt.config.ts`
* const res = await build()
*
* // or override config `from wxt.config.ts`
* const res = await build({
* // Override config...
* });
* // or override config `from wxt.config.ts`
* const res = await build({
* // Override config...
* })
*/
export async function build(config?: InlineConfig): Promise<BuildOutput> {
await registerWxt('build', config);
+24 -38
View File
@@ -26,7 +26,7 @@ import { ViteNodeRunner } from 'vite-node/client';
import { installSourcemapsSupport } from 'vite-node/source-map';
import { createExtensionEnvironment } from '../../utils/environments';
import { dirname, extname, join, relative } from 'node:path';
import { mkdir, readdir, rename, rmdir, stat } from 'node:fs/promises';
import fs from 'fs-extra';
import { normalizePath } from '../../utils';
export async function createViteBuilder(
@@ -37,8 +37,7 @@ export async function createViteBuilder(
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?: {
excludeAnalysisPlugin?: boolean;
@@ -103,15 +102,14 @@ 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 entry = getRollupEntry(entrypoint);
const plugins: NonNullable<vite.UserConfig['plugins']> = [
wxtPlugins.entrypointGroupGlobals(entrypoint),
];
let iifeReturnValueName = safeVarName(entrypoint.name);
const iifeReturnValueName = safeVarName(entrypoint.name);
if (
entrypoint.type === 'content-script-style' ||
@@ -124,17 +122,7 @@ export async function createViteBuilder(
entrypoint.type === 'content-script' ||
entrypoint.type === 'unlisted-script'
) {
if (typeof entrypoint.options.globalName === 'string') {
iifeReturnValueName = entrypoint.options.globalName;
} else if (typeof entrypoint.options.globalName === 'function') {
iifeReturnValueName = entrypoint.options.globalName(entrypoint);
}
if (entrypoint.options.globalName === false) {
plugins.push(wxtPlugins.iifeAnonymous(iifeReturnValueName));
} else {
plugins.push(wxtPlugins.iifeFooter(iifeReturnValueName));
}
plugins.push(wxtPlugins.iifeFooter(iifeReturnValueName));
}
return {
@@ -179,8 +167,7 @@ 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 htmlEntrypoints = new Set(
@@ -213,8 +200,7 @@ 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 => {
return {
@@ -391,8 +377,8 @@ function getBuildOutputChunks(
}
/**
* Returns the input module ID (virtual or real file) for an entrypoint. The
* returned string should be passed as an input to rollup.
* Returns the input module ID (virtual or real file) for an entrypoint. The returned string should
* be passed as an input to rollup.
*/
function getRollupEntry(entrypoint: Entrypoint): string {
let virtualEntrypointType: VirtualEntrypointType | undefined;
@@ -417,17 +403,15 @@ function getRollupEntry(entrypoint: Entrypoint): string {
}
/**
* Ensures the HTML files output by a multipage build are in the correct
* location. This does two things:
* Ensures the HTML files output by a multipage build are in the correct location. This does two
* things:
*
* 1. Moves the HTML files to their final location at
* `<outDir>/<entrypoint.name>.html`.
* 2. Updates the bundle so it summarizes the files correctly in the returned build
* output.
* 1. Moves the HTML files to their final location at `<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. HTML files access them via absolute URLs, so we don't
* need to update any import paths in the HTML files either.
* Assets (JS and CSS) are output to the `<outDir>/assets` directory, and don't need to be modified.
* HTML files access them via absolute URLs, so we don't need to update any import paths in the HTML
* files either.
*/
async function moveHtmlFiles(
config: ResolvedConfig,
@@ -455,8 +439,8 @@ async function moveHtmlFiles(
);
const oldAbsPath = join(config.outDir, oldBundlePath);
const newAbsPath = join(config.outDir, newBundlePath);
await mkdir(dirname(newAbsPath), { recursive: true });
await rename(oldAbsPath, newAbsPath);
await fs.ensureDir(dirname(newAbsPath));
await fs.move(oldAbsPath, newAbsPath, { overwrite: true });
return {
...chunk,
@@ -471,19 +455,21 @@ async function moveHtmlFiles(
return movedChunks;
}
/** Recursively remove all directories that are empty/ */
/**
* Recursively remove all directories that are empty/
*/
export async function removeEmptyDirs(dir: string): Promise<void> {
const files = await readdir(dir);
const files = await fs.readdir(dir);
for (const file of files) {
const filePath = join(dir, file);
const stats = await stat(filePath);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
await removeEmptyDirs(filePath);
}
}
try {
await rmdir(dir);
await fs.rmdir(dir);
} catch {
// noop on failure - this means the directory was not empty.
}
@@ -15,7 +15,9 @@ export function bundleAnalysis(config: ResolvedConfig): vite.Plugin {
}) as vite.Plugin;
}
/** @internal FOR TESTING ONLY. */
/**
* @internal FOR TESTING ONLY.
*/
export function resetBundleIncrement() {
increment = 0;
}
@@ -3,16 +3,14 @@ import { Entrypoint, ResolvedConfig } from '../../../../types';
import { getEntrypointBundlePath } from '../../../utils/entrypoints';
/**
* Rename CSS entrypoint outputs to ensure a JS file is not generated, and that
* the CSS file is placed in the correct place.
* Rename CSS entrypoint outputs to ensure a JS file is not generated, and that the CSS file is
* placed in the correct place.
*
* It:
*
* 1. Renames CSS files to their final paths
* 2. Removes the JS file that get's output by lib mode
*
* THIS PLUGIN SHOULD ONLY BE APPLIED TO CSS LIB MODE BUILDS. It should not be
* added to every build.
* THIS PLUGIN SHOULD ONLY BE APPLIED TO CSS LIB MODE BUILDS. It should not be added to every build.
*/
export function cssEntrypoints(
entrypoint: Entrypoint,
@@ -1,8 +1,8 @@
/**
* Overrides definitions for `import.meta.*`
*
* - `import.meta.url`: Without this, background service workers crash trying to
* access `document.location`, see https://github.com/wxt-dev/wxt/issues/392
* - `import.meta.url`: Without this, background service workers crash trying to access
* `document.location`, see https://github.com/wxt-dev/wxt/issues/392
*/
export function defineImportMeta() {
return {
@@ -10,8 +10,7 @@ import { hash } from 'ohash';
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(
config: ResolvedConfig,
@@ -41,32 +40,32 @@ export function devHtmlPrerender(
},
// Convert scripts like src="./main.tsx" -> src="http://localhost:3000/entrypoints/popup/main.tsx"
// before the paths are replaced with their bundled path
transform: {
filter: {
id: /\.html$/,
},
handler(code, id) {
if (config.command !== 'serve' || server == null) return;
transform(code, id) {
if (
config.command !== 'serve' ||
server == null ||
!id.endsWith('.html')
)
return;
const { document } = parseHTML(code);
const { document } = parseHTML(code);
const _pointToDevServer = (querySelector: string, attr: string) =>
pointToDevServer(config, server, id, document, querySelector, attr);
_pointToDevServer('script[type=module]', 'src');
_pointToDevServer('link[rel=stylesheet]', 'href');
const _pointToDevServer = (querySelector: string, attr: string) =>
pointToDevServer(config, server, id, document, querySelector, attr);
_pointToDevServer('script[type=module]', 'src');
_pointToDevServer('link[rel=stylesheet]', 'href');
// Add a script to add page reloading
const reloader = document.createElement('script');
reloader.src = htmlReloadId;
reloader.type = 'module';
document.head.appendChild(reloader);
// Add a script to add page reloading
const reloader = document.createElement('script');
reloader.src = htmlReloadId;
reloader.type = 'module';
document.head.appendChild(reloader);
const newHtml = document.toString();
config.logger.debug('transform ' + id);
config.logger.debug('Old HTML:\n' + code);
config.logger.debug('New HTML:\n' + newHtml);
return newHtml;
},
const newHtml = document.toString();
config.logger.debug('transform ' + id);
config.logger.debug('Old HTML:\n' + code);
config.logger.debug('New HTML:\n' + newHtml);
return newHtml;
},
// Pass the HTML through the dev server to add dev-mode specific code
@@ -114,37 +113,29 @@ export function devHtmlPrerender(
{
name: 'wxt:virtualize-inline-scripts',
apply: 'serve',
resolveId: {
filter: {
id: [new RegExp(`^${virtualInlineScript}`), new RegExp('^/chunks/')],
},
handler(id) {
// Ignore chunks during HTML file pre-rendering
if (id.startsWith('/chunks/')) {
return '\0noop';
}
resolveId(id) {
// Resolve inline scripts
if (id.startsWith(virtualInlineScript)) {
return '\0' + id;
}
return `\0${id}`;
},
// Ignore chunks during HTML file pre-rendering
if (id.startsWith('/chunks/')) {
return '\0noop';
}
},
load: {
filter: {
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 '';
}
load(id) {
// Resolve virtualized inline scripts
if (id.startsWith(resolvedVirtualInlineScript)) {
// id="virtual:wxt-inline-script?<key>"
const key = id.substring(id.indexOf('?') + 1);
return inlineScriptContents[key];
},
}
// Ignore chunks during HTML file pre-rendering
if (id === '\0noop') {
return '';
}
},
},
];
@@ -2,8 +2,7 @@ import type { Plugin } from 'vite';
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(
config: ResolvedConfig,
@@ -3,33 +3,24 @@ import type { ResolvedConfig } from '../../../../types';
import { fetchCached } from '../../../utils/network';
/**
* Downloads any URL imports, like Google Analytics, into virtual modules so
* they are bundled with the extension instead of depending on remote code at
* runtime.
* Downloads any URL imports, like Google Analytics, into virtual modules so they are bundled with
* the extension instead of depending on remote code at runtime.
*
* @example
* import 'url:https://google-tagmanager.com/gtag?id=XYZ';
* import "url:https://google-tagmanager.com/gtag?id=XYZ";
*/
export function download(config: ResolvedConfig): Plugin {
return {
name: 'wxt:download',
resolveId: {
filter: {
id: /^url:/,
},
handler(id) {
return `\0${id}`;
},
resolveId(id) {
if (id.startsWith('url:')) return '\0' + id;
},
load: {
filter: {
//eslint-disable-next-line no-control-regex
id: /^\x00url:/,
},
handler(id) {
const url = id.replace('\0url:', '');
return fetchCached(url, config);
},
load(id) {
if (!id.startsWith('\0url:')) return;
// Load file from network or cache
const url = id.replace('\0url:', '');
return fetchCached(url, config);
},
};
}
@@ -2,7 +2,9 @@ import type * as vite from 'vite';
import { EntrypointGroup } from '../../../../types';
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(
entrypointGroup: EntrypointGroup,
): vite.PluginOption {
@@ -3,8 +3,7 @@ import type * as vite from 'vite';
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 {
const virtualSetupModule = 'virtual:wxt-setup';
@@ -33,21 +32,11 @@ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
},
};
},
resolveId: {
filter: {
id: new RegExp(`${virtualSetupModule}$`),
},
handler() {
return resolvedVirtualSetupModule;
},
resolveId(id) {
if (id.endsWith(virtualSetupModule)) return resolvedVirtualSetupModule;
},
load: {
filter: {
id: new RegExp(`^${resolvedVirtualSetupModule}$`),
},
handler() {
return setupTemplate;
},
load(id) {
if (id === resolvedVirtualSetupModule) return setupTemplate;
},
};
}
@@ -1,17 +0,0 @@
import type { Plugin } from 'vite';
export function iifeAnonymous(iifeReturnValueName: string): Plugin {
return {
name: 'wxt:iife-anonymous',
generateBundle(_, bundle) {
for (const chunk of Object.values(bundle)) {
if (chunk.type === 'chunk' && chunk.isEntry) {
const namedIIFEPrefix = new RegExp(
`^var ${iifeReturnValueName}\\s*=\\s*(\\(function)`,
);
chunk.code = chunk.code.replace(namedIIFEPrefix, '$1');
}
}
},
};
}
@@ -1,11 +1,10 @@
import type { Plugin } from 'vite';
/**
* Add a footer with the returned value so it can return values to
* `scripting.executeScript` Footer is added a part of esbuild to make sure it's
* not minified. It get's removed if added to
* `build.rollupOptions.output.footer` See
* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript#return_value
* Add a footer with the returned value so it can return values to `scripting.executeScript`
* Footer is added a part of esbuild to make sure it's not minified. It
* get's removed if added to `build.rollupOptions.output.footer`
* See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript#return_value
*/
export function iifeFooter(iifeReturnValueName: string): Plugin {
return {
@@ -14,4 +14,3 @@ export * from './removeEntrypointMainFunction';
export * from './wxtPluginLoader';
export * from './resolveAppConfig';
export * from './iifeFooter';
export * from './iifeAnonymous';
@@ -2,30 +2,21 @@ import type { Plugin } from 'vite';
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 connection is setup and the extension reloads HTML pages
* and content scripts correctly.
* In dev mode, if there's not a background script listed, we need to add one so that the web socket
* connection is setup and the extension reloads HTML pages and content scripts correctly.
*/
export function noopBackground(): Plugin {
const virtualModuleId = VIRTUAL_NOOP_BACKGROUND_MODULE_ID;
const resolvedVirtualModuleId = '\0' + virtualModuleId;
return {
name: 'wxt:noop-background',
resolveId: {
filter: {
id: new RegExp(`^${virtualModuleId}$`),
},
handler() {
return resolvedVirtualModuleId;
},
resolveId(id) {
if (id === virtualModuleId) return resolvedVirtualModuleId;
},
load: {
filter: {
id: new RegExp(`^${resolvedVirtualModuleId}$`),
},
handler() {
load(id) {
if (id === resolvedVirtualModuleId) {
return `import { defineBackground } from 'wxt/utils/define-background';\nexport default defineBackground(() => void 0)`;
},
}
},
};
}
@@ -5,8 +5,7 @@ import { removeMainFunctionCode } from '../../../utils/transform';
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(
config: ResolvedConfig,
@@ -17,15 +16,14 @@ export function removeEntrypointMainFunction(
name: 'wxt:remove-entrypoint-main-function',
transform: {
order: 'pre',
filter: {
id: new RegExp(`^${absPath}$`),
},
handler(code) {
const newCode = removeMainFunctionCode(code);
config.logger.debug('vite-node transformed entrypoint', path);
config.logger.debug(`Original:\n---\n${code}\n---`);
config.logger.debug(`Transformed:\n---\n${newCode.code}\n---`);
return newCode;
handler(code, id) {
if (id === absPath) {
const newCode = removeMainFunctionCode(code);
config.logger.debug('vite-node transformed entrypoint', path);
config.logger.debug(`Original:\n---\n${code}\n---`);
config.logger.debug(`Transformed:\n---\n${newCode.code}\n---`);
return newCode;
}
},
},
};
@@ -1,11 +1,10 @@
import { pathExists } from '../../../utils/fs';
import { pathExists } from 'fs-extra';
import { resolve } from 'node:path';
import type * as vite from 'vite';
import { ResolvedConfig } from '../../../../types';
/**
* When importing `virtual:app-config`, resolve it to the `app.config.ts` file
* in the project.
* When importing `virtual:app-config`, resolve it to the `app.config.ts` file in the project.
*/
export function resolveAppConfig(config: ResolvedConfig): vite.Plugin {
const virtualModuleId = 'virtual:app-config';
@@ -23,23 +22,15 @@ export function resolveAppConfig(config: ResolvedConfig): vite.Plugin {
},
};
},
resolveId: {
filter: {
id: new RegExp(`^${virtualModuleId}$`),
},
async handler() {
return (await pathExists(appConfigFile))
? appConfigFile
: resolvedVirtualModuleId;
},
async resolveId(id) {
if (id !== virtualModuleId) return;
return (await pathExists(appConfigFile))
? appConfigFile
: resolvedVirtualModuleId;
},
load: {
filter: {
id: new RegExp(`^${resolvedVirtualModuleId}$`),
},
handler() {
return `export default {}`;
},
load(id) {
if (id === resolvedVirtualModuleId) return `export default {}`;
},
};
}
@@ -1,5 +1,3 @@
import { readFile } from 'node:fs/promises';
import { resolve } from 'path';
import type { Plugin } from 'vite';
import { ResolvedConfig } from '../../../../types';
import { normalizePath } from '../../../utils';
@@ -7,41 +5,36 @@ import {
VirtualModuleId,
virtualModuleNames,
} 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[] {
return virtualModuleNames.map((name) => {
const virtualId: `${VirtualModuleId}?` = `virtual:wxt-${name}?`;
const resolvedVirtualId = '\0' + virtualId;
return {
name: `wxt:resolve-virtual-${name}`,
resolveId: {
filter: {
id: new RegExp(virtualId),
},
handler(id) {
const inputPath = normalizePath(
id.substring(id.indexOf(virtualId) + virtualId.length),
);
return resolvedVirtualId + inputPath;
},
resolveId(id) {
// Id doesn't start with prefix, it looks like this:
// /path/to/project/virtual:wxt-background?/path/to/project/entrypoints/background.ts
const index = id.indexOf(virtualId);
if (index === -1) return;
const inputPath = normalizePath(id.substring(index + virtualId.length));
return resolvedVirtualId + inputPath;
},
load: {
filter: {
id: new RegExp(`^${resolvedVirtualId}`),
},
async handler(id) {
const inputPath = id.replace(resolvedVirtualId, '');
const template = await readFile(
resolve(config.wxtModuleDir, `dist/virtual/${name}.mjs`),
'utf-8',
);
return template.replace(`virtual:user-${name}`, inputPath);
},
async load(id) {
if (!id.startsWith(resolvedVirtualId)) return;
const inputPath = id.replace(resolvedVirtualId, '');
const template = await fs.readFile(
resolve(config.wxtModuleDir, `dist/virtual/${name}.mjs`),
'utf-8',
);
return template.replace(`virtual:user-${name}`, inputPath);
},
};
});
@@ -4,9 +4,7 @@ import { normalizePath } from '../../../utils';
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 {
const virtualModuleId = 'virtual:wxt-plugins';
@@ -16,50 +14,33 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin {
return {
name: 'wxt:plugin-loader',
resolveId: {
filter: {
id: [
new RegExp(`^${virtualModuleId}$`),
new RegExp(`^${virtualHtmlModuleId}$`),
],
},
handler(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
return resolvedVirtualHtmlModuleId;
},
resolveId(id) {
if (id === virtualModuleId) return resolvedVirtualModuleId;
if (id === virtualHtmlModuleId) return resolvedVirtualHtmlModuleId;
},
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);
}`;
}
},
load(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}`;
}
if (id === resolvedVirtualHtmlModuleId) {
return `import { initPlugins } from '${virtualModuleId}';
try {
initPlugins();
} catch (err) {
console.error("[wxt] Failed to initialize plugins", err);
}`;
}
},
transformIndexHtml: {
// Use "pre" so the new script is added before vite bundles all the scripts
@@ -82,8 +63,7 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin {
const newHead = document.createElement('head');
document.documentElement.prepend(newHead);
}
document.head?.prepend(script);
document.head.prepend(script);
return document.toString();
},
},
+10 -11
View File
@@ -1,28 +1,28 @@
import { rm } from 'node:fs/promises';
import path from 'node:path';
import glob from 'fast-glob';
import fs from 'fs-extra';
import pc from 'picocolors';
import { glob } from 'tinyglobby';
import { InlineConfig } from '../types';
import { registerWxt, wxt } from './wxt';
/**
* Remove generated/temp files from the directory.
*
* @example
* await clean();
*
* @param config Optional config that will override your `<root>/wxt.config.ts`.
*
* @example
* await clean();
*/
export async function clean(config?: InlineConfig): Promise<void>;
/**
* Remove generated/temp files from the directory.
*
* @deprecated
* @example
* 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.
* @param root The directory to look for generated/temp files in. Defaults to `process.cwd()`. Can be relative to `process.cwd()` or absolute.
*
* @example
* await clean();
*/
export async function clean(root?: string): Promise<void>;
@@ -48,7 +48,6 @@ export async function clean(config?: string | InlineConfig) {
absolute: true,
onlyDirectories: true,
deep: 2,
expandDirectories: false,
});
if (directories.length === 0) {
wxt.logger.debug('No generated files found.');
@@ -60,7 +59,7 @@ export async function clean(config?: string | InlineConfig) {
directories.map((dir) => pc.cyan(path.relative(root, dir))).join(', '),
);
for (const directory of directories) {
await rm(directory, { force: true, recursive: true });
await fs.rm(directory, { force: true, recursive: true });
wxt.logger.debug('Deleted ' + pc.cyan(path.relative(root, directory)));
}
}
+8 -11
View File
@@ -32,14 +32,13 @@ import { createKeyboardShortcuts } from './keyboard-shortcuts';
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
* const server = await wxt.createServer({
* // Enter config...
* });
* await server.start();
* const server = await wxt.createServer({
* // Enter config...
* });
* await server.start();
*/
export async function createServer(
inlineConfig?: InlineConfig,
@@ -200,8 +199,8 @@ async function createServerInternal(): Promise<WxtDevServer> {
}
/**
* Returns a function responsible for reloading different parts of the extension
* when a file changes.
* Returns a function responsible for reloading different parts of the extension when a file
* changes.
*/
function createFileReloader(server: WxtDevServer) {
const fileChangedMutex = new Mutex();
@@ -296,8 +295,7 @@ 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) {
if (wxt.config.manifestVersion === 3) {
@@ -352,7 +350,6 @@ function getFilenameList(names: string[]): string {
/**
* Based on the current build output, return a list of files that are:
*
* 1. Not in node_modules
* 2. Not inside project root
*/
@@ -1,7 +1,9 @@
import consola from 'consola';
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 {
consola.warn(
'`defineRunnerConfig` is deprecated, use `defineWebExtConfig` instead. See https://wxt.dev/guide/resources/upgrading.html#v0-19-0-rarr-v0-20-0',
@@ -10,8 +12,7 @@ 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 {
return config;
+7 -8
View File
@@ -1,20 +1,19 @@
import { Entrypoint, WxtDirEntry, WxtDirFileEntry } from '../types';
import { mkdir, readFile } from 'node:fs/promises';
import fs from 'fs-extra';
import { dirname, relative, resolve } from 'node:path';
import { getEntrypointBundlePath, isHtmlEntrypoint } from './utils/entrypoints';
import { getEntrypointGlobals, getGlobals } from './utils/globals';
import { normalizePath } from './utils';
import path from 'node:path';
import { Message, parseI18nMessages } from './utils/i18n';
import { pathExists, writeFileIfDifferent, getPublicFiles } from './utils/fs';
import { writeFileIfDifferent, getPublicFiles } from './utils/fs';
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> {
await mkdir(wxt.config.typesDir, { recursive: true });
await fs.ensureDir(wxt.config.typesDir);
const entries: WxtDirEntry[] = [
// Hard-coded entries
@@ -57,7 +56,7 @@ export async function generateWxtDir(entrypoints: Entrypoint[]): Promise<void> {
await Promise.all(
absoluteFileEntries.map(async (file) => {
await mkdir(dirname(file.path), { recursive: true });
await fs.ensureDir(dirname(file.path));
await writeFileIfDifferent(file.path, file.text);
}),
);
@@ -149,8 +148,8 @@ declare module "wxt/browser" {
'messages.json',
);
let messages: Message[];
if (await pathExists(defaultLocalePath)) {
const content = JSON.parse(await readFile(defaultLocalePath, 'utf-8'));
if (await fs.pathExists(defaultLocalePath)) {
const content = JSON.parse(await fs.readFile(defaultLocalePath, 'utf-8'));
messages = parseI18nMessages(content);
} else {
messages = parseI18nMessages({});
+21 -16
View File
@@ -1,8 +1,7 @@
import prompts from 'prompts';
import { consola } from 'consola';
import { downloadTemplate } from 'giget';
import { readdir, rename } from 'node:fs/promises';
import { pathExists } from './utils/fs';
import fs from 'fs-extra';
import path from 'node:path';
import pc from 'picocolors';
import { Formatter } from 'picocolors/types';
@@ -57,10 +56,10 @@ export async function initialize(options: {
input.template ??= defaultTemplate;
input.packageManager ??= options.packageManager;
const isExists = await pathExists(input.directory);
const isExists = await fs.pathExists(input.directory);
if (isExists) {
const isEmpty =
(await readdir(input.directory)).filter((dir) => dir !== '.git')
(await fs.readdir(input.directory)).filter((dir) => dir !== '.git')
.length === 0;
if (!isEmpty) {
consola.error(
@@ -88,9 +87,13 @@ export async function initialize(options: {
}
interface Template {
/** Template's name. */
/**
* Template's name.
*/
name: string;
/** Path to template directory in github repo. */
/**
* Path to template directory in github repo.
*/
path: string;
}
@@ -158,8 +161,8 @@ async function cloneProject({
directory: string;
template: Template;
}) {
const { createSpinner } = await import('nanospinner');
const spinner = createSpinner('Downloading template').start();
const { default: ora } = await import('ora');
const spinner = ora('Downloading template').start();
try {
// 1. Clone repo
await downloadTemplate(`gh:${REPO}/${template.path}`, {
@@ -168,16 +171,18 @@ async function cloneProject({
});
// 2. Move _gitignore -> .gitignore
await rename(
path.join(directory, '_gitignore'),
path.join(directory, '.gitignore'),
).catch((err) =>
consola.warn('Failed to move _gitignore to .gitignore:', err),
);
await fs
.move(
path.join(directory, '_gitignore'),
path.join(directory, '.gitignore'),
)
.catch((err) =>
consola.warn('Failed to move _gitignore to .gitignore:', err),
);
spinner.success();
spinner.succeed();
} catch (err) {
spinner.error();
spinner.fail();
throw Error(`Failed to setup new project: ${JSON.stringify(err, null, 2)}`);
}
}
+3 -1
View File
@@ -9,7 +9,9 @@ export interface KeyboardShortcutWatcher {
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(
server: WxtDevServer,
): KeyboardShortcutWatcher {
@@ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
import path from 'node:path';
import { npm } from '../npm';
import spawn from 'nano-spawn';
import { pathExists } from '../../utils/fs';
import { pathExists } from 'fs-extra';
describe('NPM Package Management Utils', () => {
describe('listDependencies', () => {
@@ -1,13 +1,13 @@
import { Dependency } from '../../types';
import { WxtPackageManagerImpl } from './types';
import path from 'node:path';
import { mkdir } from 'node:fs/promises';
import { ensureDir } from 'fs-extra';
import spawn from 'nano-spawn';
export const npm: WxtPackageManagerImpl = {
overridesKey: 'overrides',
async downloadDependency(id, downloadDir) {
await mkdir(downloadDir, { recursive: true });
await ensureDir(downloadDir);
const res = await spawn('npm', ['pack', id, '--json'], {
cwd: downloadDir,
});
+14 -13
View File
@@ -20,9 +20,9 @@ import { createFsCache } from './utils/cache';
import consola, { LogLevels } from 'consola';
import defu from 'defu';
import { NullablyRequired } from './utils/types';
import { pathExists } from './utils/fs';
import fs from 'fs-extra';
import { normalizePath } from './utils';
import { glob } from 'tinyglobby';
import glob from 'fast-glob';
import { builtinModules } from '../builtin-modules';
import { getEslintVersion } from './utils/eslint';
import { safeStringToNumber } from './utils/number';
@@ -31,12 +31,11 @@ import { getPort } from 'get-port-please';
import { fileURLToPath, pathToFileURL } from 'node:url';
/**
* Given an inline config, discover the config file if necessary, merge the
* results, resolve any relative paths, and apply any defaults.
* Given an inline config, discover the config file if necessary, merge the results, resolve any
* relative paths, and apply any defaults.
*
* Inline config always has priority over user config. Cli flags are passed as
* inline config if set. If unset, undefined is passed in, letting this function
* decide default values.
* Inline config always has priority over user config. Cli flags are passed as inline config if set.
* If unset, undefined is passed in, letting this function decide default values.
*/
export async function resolveConfig(
inlineConfig: InlineConfig,
@@ -250,8 +249,7 @@ 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(
inlineConfig: InlineConfig,
@@ -530,7 +528,9 @@ async function getUnimportEslintOptions(
};
}
/** Returns the path to `node_modules/wxt`. */
/**
* Returns the path to `node_modules/wxt`.
*/
function resolveWxtModuleDir() {
// 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
@@ -543,7 +543,7 @@ function resolveWxtModuleDir() {
}
async function isDirMissing(dir: string) {
return !(await pathExists(dir));
return !(await fs.pathExists(dir));
}
function logMissingDir(logger: Logger, name: string, expected: string) {
@@ -554,7 +554,9 @@ 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> = {
build: 'production',
serve: 'development',
@@ -612,7 +614,6 @@ export async function resolveWxtUserModules(
const localModulePaths = await glob(['*.[tj]s', '*/index.[tj]s'], {
cwd: modulesDir,
onlyFiles: true,
expandDirectories: false,
}).catch(() => []);
// Sort modules to ensure a consistent execution order
localModulePaths.sort();
+3 -1
View File
@@ -2,7 +2,9 @@ import { ExtensionRunner } from '../../types';
import { relative } from 'node:path';
import { wxt } from '../wxt';
/** The manual runner tells the user to load the unpacked extension manually. */
/**
* The manual runner tells the user to load the unpacked extension manually.
*/
export function createManualRunner(): ExtensionRunner {
return {
async openBrowser() {
+1 -2
View File
@@ -3,8 +3,7 @@ import { relative } from 'node:path';
import { wxt } from '../wxt';
/**
* The Safari runner just logs a warning message because `web-ext` doesn't work
* with Safari.
* The Safari runner just logs a warning message because `web-ext` doesn't work with Safari.
*/
export function createSafariRunner(): ExtensionRunner {
return {
+3 -1
View File
@@ -4,7 +4,9 @@ import { formatDuration } from '../utils/time';
import defu from 'defu';
import { wxt } from '../wxt';
/** Create an `ExtensionRunner` backed by `web-ext`. */
/**
* Create an `ExtensionRunner` backed by `web-ext`.
*/
export function createWebExtRunner(): ExtensionRunner {
let runner: WebExtRunInstance | undefined;
+1 -2
View File
@@ -3,8 +3,7 @@ import { relative } from 'node:path';
import { wxt } from '../wxt';
/**
* The WSL runner just logs a warning message because `web-ext` doesn't work in
* WSL.
* The WSL runner just logs a warning message because `web-ext` doesn't work in WSL.
*/
export function createWslRunner(): ExtensionRunner {
return {
+9 -3
View File
@@ -1,4 +1,6 @@
/** 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>(
array: T[],
predicate: (item: T, index: number) => boolean,
@@ -8,7 +10,9 @@ export function every<T>(
return true;
}
/** Returns true when any of the predicates return true; */
/**
* Returns true when any of the predicates return true;
*/
export function some<T>(
array: T[],
predicate: (item: T, index: number) => boolean,
@@ -18,7 +22,9 @@ export function some<T>(
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[] {
return Array.isArray(a) ? a : [a];
}
@@ -1,7 +1,4 @@
import { readFile } from 'node:fs/promises';
import { resolve } from 'path';
import { glob } from 'tinyglobby';
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import {
BackgroundEntrypoint,
BackgroundEntrypointOptions,
@@ -12,16 +9,21 @@ import {
PopupEntrypoint,
SidepanelEntrypoint,
} from '../../../../types';
import { wxt } from '../../../wxt';
import { unnormalizePath } from '../../paths';
import { fakeResolvedConfig, setFakeWxt } from '../../testing/fake-objects';
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';
vi.mock('tinyglobby');
vi.mock('fast-glob');
const globMock = vi.mocked(glob);
vi.mock('node:fs/promises');
const readFileMock = vi.mocked(readFile);
vi.mock('fs-extra');
const readFileMock = vi.mocked(
fs.readFile as (path: string) => Promise<string>,
);
describe('findEntrypoints', () => {
const config = fakeResolvedConfig({
@@ -5,30 +5,28 @@ import {
ResolvedPublicFile,
} from '../../../types';
import { getPublicFiles } from '../fs';
import { copyFile, mkdir, writeFile } from 'node:fs/promises';
import fs from 'fs-extra';
import { dirname, resolve } from 'path';
import type { Spinner } from 'nanospinner';
import type { Ora } from 'ora';
import pc from 'picocolors';
import { wxt } from '../../wxt';
import { toArray } from '../arrays';
export async function buildEntrypoints(
groups: EntrypointGroup[],
spinner: Spinner,
spinner: Ora,
): Promise<Omit<BuildOutput, 'manifest'>> {
const steps: BuildStepOutput[] = [];
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
const groupNames = toArray(group).map((e) => e.name);
const groupNameColored = groupNames.join(pc.dim(', '));
spinner.update({
text: pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNameColored}`,
});
spinner.text =
pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNameColored}`;
try {
steps.push(await wxt.builder.build(group));
} catch (err) {
spinner.stop();
spinner.clear();
spinner.stop().clear();
wxt.logger.error(err);
throw Error(`Failed to build ${groupNames.join(', ')}`, { cause: err });
}
@@ -50,11 +48,11 @@ async function copyPublicDirectory(): Promise<BuildOutput['publicAssets']> {
for (const file of files) {
const absoluteDest = resolve(wxt.config.outDir, file.relativeDest);
await mkdir(dirname(absoluteDest), { recursive: true });
await fs.ensureDir(dirname(absoluteDest));
if ('absoluteSrc' in file) {
await copyFile(file.absoluteSrc, absoluteDest);
await fs.copyFile(file.absoluteSrc, absoluteDest);
} else {
await writeFile(absoluteDest, file.contents, 'utf8');
await fs.writeFile(absoluteDest, file.contents, 'utf8');
}
publicAssets.push({
type: 'asset',
@@ -9,36 +9,27 @@ import { normalizePath } from '../paths';
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
*
* - 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
* - Reload Content script
*
* - CSS or JS file associated with a content script
* - Background script will be told to reload the content script
* - Reload HTML file
*
* - HTML file itself is saved - HMR doesn't handle this because the HTML pages
* are pre-rendered
* - Chrome is OK reloading the page when the HTML file is changed without
* reloading the whole extension. Not sure about firefox, this might need
* to change to an extension reload
* - HTML file itself is saved - HMR doesn't handle this because the HTML pages are pre-rendered
* - Chrome is OK reloading the page when the HTML file is changed without reloading the whole
* extension. Not sure about firefox, this might need to change to an extension reload
* - Reload extension
*
* - Background script is changed
* - Manifest is different
* - Restart browser
*
* - Web-ext.config.ts (runner config changes)
* - web-ext.config.ts (runner config changes)
* - 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)
*/
export function detectDevChanges(
@@ -135,8 +126,7 @@ 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(
changedFile: string,
@@ -175,8 +165,8 @@ function findEffectedSteps(
}
/**
* Contains information about what files changed, what needs rebuilt, and the
* type of reload that is required.
* Contains information about what files changed, what needs rebuilt, and the type of reload that is
* required.
*/
export type DevModeChange =
| NoChange
@@ -191,9 +181,13 @@ interface NoChange {
}
interface RebuildChange {
/** The list of entrypoints that need rebuilt. */
/**
* The list of entrypoints that need rebuilt.
*/
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;
}
@@ -10,13 +10,12 @@ import {
SidepanelEntrypoint,
MainWorldContentScriptEntrypointOptions,
IsolatedWorldContentScriptEntrypointOptions,
UnlistedScriptEntrypoint,
} from '../../../types';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import fs from 'fs-extra';
import { minimatch } from 'minimatch';
import { parseHTML } from 'linkedom';
import JSON5 from 'json5';
import { glob } from 'tinyglobby';
import glob from 'fast-glob';
import {
getEntrypointName,
isHtmlEntrypoint,
@@ -30,16 +29,15 @@ import { wxt } from '../../wxt';
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[]> {
// Make sure required TSConfig file exists to load dependencies
await mkdir(wxt.config.wxtDir, { recursive: true });
await fs.mkdir(wxt.config.wxtDir, { recursive: true });
try {
await writeFile(
await fs.writeJson(
resolve(wxt.config.wxtDir, 'tsconfig.json'),
JSON.stringify({}),
{},
{ flag: 'wx' },
);
} catch (err) {
@@ -50,7 +48,6 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
const relativePaths = await glob(Object.keys(PATH_GLOB_TO_TYPE_MAP), {
cwd: wxt.config.entrypointsDir,
expandDirectories: false,
});
// Ensure consistent output
relativePaths.sort();
@@ -197,14 +194,11 @@ async function importEntrypoints(infos: EntrypointInfo[]) {
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(
info: EntrypointInfo,
): Promise<Record<string, any>> {
const content = await readFile(info.inputPath, 'utf-8');
const content = await fs.readFile(info.inputPath, 'utf-8');
const { document } = parseHTML(content);
const metaTags = document.querySelectorAll('meta');
@@ -325,7 +319,7 @@ async function getUnlistedPageEntrypoint(
async function getUnlistedScriptEntrypoint(
{ inputPath, name }: EntrypointInfo,
options: Record<string, any>,
): Promise<UnlistedScriptEntrypoint> {
): Promise<GenericEntrypoint> {
return {
type: 'unlisted-script',
name,
@@ -1,8 +1,8 @@
import { Entrypoint, EntrypointGroup } from '../../../types';
/**
* Entrypoints are built in groups. HTML pages can all be built together in a
* single step, content scripts must be build individually, etc.
* Entrypoints are built in groups. HTML pages can all be built together in a single step,
* content scripts must be build individually, etc.
*
* This function returns the entrypoints put into these types of groups.
*/
@@ -1,11 +1,11 @@
import { findEntrypoints } from './find-entrypoints';
import { BuildOutput, Entrypoint } from '../../../types';
import pc from 'picocolors';
import { mkdir, rm } from 'node:fs/promises';
import fs from 'fs-extra';
import { groupEntrypoints } from './group-entrypoints';
import { formatDuration } from '../time';
import { printBuildSummary } from '../log';
import { glob } from 'tinyglobby';
import glob from 'fast-glob';
import { unnormalizePath } from '../paths';
import { rebuild } from './rebuild';
import { relative } from 'node:path';
@@ -20,14 +20,13 @@ import { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer';
import { isCI } from 'ci-info';
/**
* Builds the extension based on an internal config. No more config discovery is
* performed, the build is based on exactly what is passed in.
* Builds the extension based on an internal config. No more config discovery is performed, the
* build is based on exactly what is passed in.
*
* This function:
*
* 1. Cleans the output directory
* 2. Executes the rebuild function with a blank previous output so everything is
* built (see `rebuild` for more details)
* 2. Executes the rebuild function with a blank previous output so everything is built (see
* `rebuild` for more details)
* 3. Prints the summary
*/
export async function internalBuild(): Promise<BuildOutput> {
@@ -43,8 +42,8 @@ export async function internalBuild(): Promise<BuildOutput> {
const startTime = Date.now();
// Cleanup
await rm(wxt.config.outDir, { recursive: true, force: true });
await mkdir(wxt.config.outDir, { recursive: true });
await fs.rm(wxt.config.outDir, { recursive: true, force: true });
await fs.ensureDir(wxt.config.outDir);
const entrypoints = await findEntrypoints();
wxt.logger.debug('Detected entrypoints:', entrypoints);
@@ -100,7 +99,6 @@ async function combineAnalysisStats(): Promise<void> {
const unixFiles = await glob(`${wxt.config.analysis.outputName}-*.json`, {
cwd: wxt.config.analysis.outputDir,
absolute: true,
expandDirectories: false,
});
const absolutePaths = unixFiles.map(unnormalizePath);
@@ -111,9 +109,7 @@ async function combineAnalysisStats(): Promise<void> {
});
if (!wxt.config.analysis.keepArtifacts) {
await Promise.all(
absolutePaths.map((statsFile) => rm(statsFile, { force: true })),
);
await Promise.all(absolutePaths.map((statsFile) => fs.remove(statsFile)));
}
}
@@ -1,28 +1,25 @@
import type { Browser } from '@wxt-dev/browser';
import { createSpinner } from 'nanospinner';
import { BuildOutput, Entrypoint, EntrypointGroup } from '../../../types';
import { generateWxtDir } from '../../generate-wxt-dir';
import { buildEntrypoints } from './build-entrypoints';
import { generateManifest, writeManifest } from '../../utils/manifest';
import { wxt } from '../../wxt';
import { buildEntrypoints } from './build-entrypoints';
import type { Browser } from '@wxt-dev/browser';
/**
* Given a configuration, list of entrypoints, and an existing, partial output,
* build the entrypoints and merge the new output with the existing output.
* Given a configuration, list of entrypoints, and an existing, partial output, build the
* entrypoints and merge the new output with the existing output.
*
* This function will:
*
* 1. Generate the .wxt directory's types
* 2. Build the `entrypointGroups` (and copies public files)
* 3. Generate the latest manifest for all entrypoints
* 4. Write the new manifest to the file system
*
* @param config Internal config containing all the project information.
* @param allEntrypoints List of entrypoints used to generate the types inside
* .wxt directory.
* @param allEntrypoints List of entrypoints used to generate the types inside .wxt directory.
* @param entrypointGroups The list of entrypoint groups to build.
* @param existingOutput The previous output to combine the rebuild results
* into. An empty array if this is the first build.
* @param existingOutput The previous output to combine the rebuild results into. An emptry array if
* this is the first build.
*/
export async function rebuild(
allEntrypoints: Entrypoint[],
@@ -36,7 +33,8 @@ export async function rebuild(
manifest: Browser.runtime.Manifest;
warnings: any[][];
}> {
const spinner = createSpinner('Preparing...').start();
const { default: ora } = await import('ora');
const spinner = ora(`Preparing...`).start();
// Update types directory with new files and types
await generateWxtDir(allEntrypoints).catch((err) => {
+5 -5
View File
@@ -1,11 +1,11 @@
import { mkdir, readFile } from 'node:fs/promises';
import fs, { ensureDir } from 'fs-extra';
import { FsCache } from '../../types';
import { dirname, resolve } from 'path';
import { writeFileIfDifferent } from './fs';
/**
* A basic file system cache stored at `<srcDir>/.wxt/cache/<key>`. Just caches
* a string in a file for the given key.
* A basic file system cache stored at `<srcDir>/.wxt/cache/<key>`. Just caches a string in a
* file for the given key.
*
* @param srcDir Absolute path to source directory. See `InternalConfig.srcDir`
*/
@@ -16,13 +16,13 @@ export function createFsCache(wxtDir: string): FsCache {
return {
async set(key: string, value: string): Promise<void> {
const path = getPath(key);
await mkdir(dirname(path), { recursive: true });
await ensureDir(dirname(path));
await writeFileIfDifferent(path, value);
},
async get(key: string): Promise<string | undefined> {
const path = getPath(key);
try {
return await readFile(path, 'utf-8');
return await fs.readFile(path, 'utf-8');
} catch {
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 script/service worker.
* Module ID used to build the background in dev mode if the extension doesn't include a background
* script/service worker.
*/
export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';
@@ -4,8 +4,7 @@ import { getEntrypointBundlePath } from './entrypoints';
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,
*/
@@ -1,6 +1,5 @@
/**
* 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';
@@ -26,7 +25,9 @@ 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 {
const values = this.data[directive] ?? [];
newValues.forEach((newValue) => {

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