Compare commits

..

20 Commits

Author SHA1 Message Date
github-actions[bot] e20c012eb3 chore(release): @wxt-dev/storage v1.2.8
📼 VHS / Create VHS (push) Cancelled after 0s
2026-02-22 17:29:53 -06:00
github-actions[bot] e88df26d7c chore(release): @wxt-dev/auto-icons v1.1.1
📼 VHS / Create VHS (push) Cancelled after 0s
2026-02-22 17:29:53 -06:00
github-actions[bot] 9491e6845e chore(release): @wxt-dev/i18n v0.2.5
📼 VHS / Create VHS (push) Cancelled after 0s
2026-02-22 17:29:53 -06:00
Aaron e4433c4212 ci: Fix scripts/list_unreleased_commits.sh (#2153) 2026-02-22 17:29:53 -06:00
Jonathan Viney c6fdc5a5d5 feat: Add globalName entrypoint option. (#2017) 2026-02-22 17:29:53 -06:00
Aaron 68dd3fc027 docs: Update section in SECURITY.md around reporting vulnerabilities (#2147) 2026-02-22 17:29:53 -06:00
omerfardemir cd7aec6752 fix: auto icons override default icons (#1616)
Co-authored-by: Florian Metz <me@timeraa.dev>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-22 17:29:53 -06:00
Smit b80556498d fix: Improve background script detection logic for analytics package (#1808)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-22 17:29:53 -06:00
Aaron 38530cdb39 feat: New @wxt-dev/is-background package (#2152)
Co-authored-by: Smit <sm17p@users.noreply.github.com>
2026-02-22 17:29:53 -06:00
Tam Dang 078fd5990e fix(types): include CSS entrypoints in PublicPath generation (#2150)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-22 17:29:53 -06:00
Aaron b4be5feaa6 ci: Fix auto-labeling workflow for forks 2026-02-22 17:29:53 -06:00
Aaron d982f97bd8 feat(modules): Add support for augumenting entrypoint options (#2149) 2026-02-22 17:29:53 -06:00
Aaron 653608c98c docs: Add deep outline to modules page to show recipes 2026-02-22 17:29:53 -06:00
Aaron d63be2d6f3 ci: Create workflow for labeling and adding authors to PRs (#2148) 2026-02-22 17:29:53 -06:00
Nick Doan 4500ca577f 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-22 17:29:53 -06:00
Aaron cc935daec0 fix: Allow userId option to return undefined 2026-02-22 17:29:53 -06:00
Patryk Kuniczak 51ff46ec58 chore: Created new types, instead of any for analytics (#2119) 2026-02-22 17:29:53 -06:00
Patryk Kuniczak 206b7731cf chore: Remove @ts-expect-error from manifest.test.ts and fix typo (#2123) 2026-02-22 17:29:53 -06:00
Patryk Kuniczak d16f59d33d chore: Simplify imports in wxt/e2e (#2122) 2026-02-22 17:29:53 -06:00
Patryk Kuniczak a5ec046346 chore: Rename zipdir to zipDir (#2126) 2026-02-22 17:29:49 -06:00
22 changed files with 913 additions and 51 deletions
+2 -6
View File
@@ -4,12 +4,8 @@ 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
Use this section to tell people how to report 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.
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.
-->
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.
@@ -10,6 +10,7 @@ import type {
BaseAnalyticsEvent,
} from './types';
import { browser } from '@wxt-dev/browser';
import { isBackground } from '@wxt-dev/is-background';
type AnalyticsMessage = {
[K in keyof Analytics]: {
@@ -55,10 +56,7 @@ export function createAnalytics(config?: AnalyticsConfig): Analytics {
);
}
// 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);
if (isBackground()) return createBackgroundAnalytics(config);
return createFrontendAnalytics();
}
+24 -23
View File
@@ -2,13 +2,35 @@
"name": "@wxt-dev/analytics",
"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 -s 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.17",
"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",
@@ -38,26 +60,5 @@
"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,5 +1,34 @@
# 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)
+4 -2
View File
@@ -24,7 +24,7 @@
}
],
"license": "MIT",
"version": "1.1.0",
"version": "1.1.1",
"type": "module",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
@@ -39,7 +39,8 @@
],
"scripts": {
"build": "buildc -- tsdown",
"check": "pnpm build && check"
"check": "pnpm build && check",
"test": "buildc --deps-only -- vitest"
},
"peerDependencies": {
"wxt": ">=0.19.0"
@@ -53,6 +54,7 @@
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
"wxt": "workspace:*"
}
}
@@ -0,0 +1,629 @@
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { resolve } from 'node:path';
import * as fsExtra from 'fs-extra';
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('fs-extra', () => ({
ensureDir: vi.fn(),
pathExists: 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(fsExtra.pathExists).mockResolvedValue(true as any);
vi.mocked(fsExtra.ensureDir).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(fsExtra.pathExists).mockResolvedValue(false as any);
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(fsExtra.ensureDir).toHaveBeenCalledWith(
resolve('/mock/dist', 'icons'),
);
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(fsExtra.ensureDir).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(fsExtra.ensureDir).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();
});
});
});
+1 -1
View File
@@ -45,7 +45,7 @@ export default defineWxtModule<AutoIconsOptions>({
wxt.hooks.hook('build:manifestGenerated', async (wxt, manifest) => {
if (manifest.icons)
return wxt.logger.warn(
wxt.logger.warn(
'`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons',
);
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
mockReset: true,
restoreMocks: true,
},
});
+26
View File
@@ -1,5 +1,31 @@
# 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)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/i18n",
"description": "Type-safe wrapper around browser.i18n.getMessage with additional features",
"version": "0.2.4",
"version": "0.2.5",
"type": "module",
"repository": {
"type": "git",
+12
View File
@@ -1,5 +1,17 @@
# 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)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/storage",
"description": "Web extension storage API provided by WXT, supports all browsers.",
"version": "1.2.7",
"version": "1.2.8",
"type": "module",
"repository": {
"type": "git",
+2 -2
View File
@@ -1,6 +1,6 @@
import { describe, it, expect, vi } from 'vitest';
import { TestProject } from '../utils';
import type { GenericEntrypoint, InlineConfig } from '../../src';
import type { InlineConfig, UnlistedScriptEntrypoint } from '../../src';
import { readFile } from 'fs-extra';
import { normalizePath } from '../../src';
@@ -49,7 +49,7 @@ describe('Module Helpers', () => {
'export default defineBackground(() => {})',
);
const entrypoint: GenericEntrypoint = {
const entrypoint: UnlistedScriptEntrypoint = {
type: 'unlisted-script',
inputPath: project.resolvePath('modules/test/injected.ts'),
name: 'injected',
@@ -459,4 +459,104 @@ 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);
});
});
});
+12 -2
View File
@@ -109,7 +109,7 @@ export async function createViteBuilder(
const plugins: NonNullable<vite.UserConfig['plugins']> = [
wxtPlugins.entrypointGroupGlobals(entrypoint),
];
const iifeReturnValueName = safeVarName(entrypoint.name);
let iifeReturnValueName = safeVarName(entrypoint.name);
if (
entrypoint.type === 'content-script-style' ||
@@ -122,7 +122,17 @@ export async function createViteBuilder(
entrypoint.type === 'content-script' ||
entrypoint.type === 'unlisted-script'
) {
plugins.push(wxtPlugins.iifeFooter(iifeReturnValueName));
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));
}
}
return {
@@ -0,0 +1,17 @@
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');
}
}
},
};
}
@@ -14,3 +14,4 @@ export * from './removeEntrypointMainFunction';
export * from './wxtPluginLoader';
export * from './resolveAppConfig';
export * from './iifeFooter';
export * from './iifeAnonymous';
@@ -10,6 +10,7 @@ import {
SidepanelEntrypoint,
MainWorldContentScriptEntrypointOptions,
IsolatedWorldContentScriptEntrypointOptions,
UnlistedScriptEntrypoint,
} from '../../../types';
import fs from 'fs-extra';
import { minimatch } from 'minimatch';
@@ -319,7 +320,7 @@ async function getUnlistedPageEntrypoint(
async function getUnlistedScriptEntrypoint(
{ inputPath, name }: EntrypointInfo,
options: Record<string, any>,
): Promise<GenericEntrypoint> {
): Promise<UnlistedScriptEntrypoint> {
return {
type: 'unlisted-script',
name,
@@ -21,6 +21,7 @@ import {
Wxt,
SidepanelEntrypoint,
BaseEntrypoint,
UnlistedScriptEntrypoint,
} from '../../../types';
import { mock } from 'vitest-mock-extended';
import { vi } from 'vitest';
@@ -99,7 +100,7 @@ export const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(
);
export const fakeUnlistedScriptEntrypoint =
fakeObjectCreator<GenericEntrypoint>(() => ({
fakeObjectCreator<UnlistedScriptEntrypoint>(() => ({
type: 'unlisted-script',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
@@ -186,7 +187,6 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
'newtab',
'devtools',
'unlisted-page',
'unlisted-script',
]),
inputPath: fakeFile('src'),
name: faker.string.alpha(),
+29 -3
View File
@@ -566,7 +566,28 @@ export interface BackgroundEntrypointOptions extends BaseEntrypointOptions {
type?: PerBrowserOption<'module'>;
}
export interface BaseContentScriptEntrypointOptions extends BaseEntrypointOptions {
export interface BaseScriptEntrypointOptions extends BaseEntrypointOptions {
/**
* The variable name for the IIFE in the output bundle.
*
* This option is relevant for scripts inserted into the page context where the default IIFE
* variable name may conflict with an existing variable on the target page. This applies to content
* scripts with world=MAIN, and others, such as unlisted scripts, that could be dynamically injected
* into the page with a <script> tag.
*
* Available options:
* - `true`: automatically generate a name for the IIFE based on the entrypoint name
* - `false`: Output the IIFE without a variable name, making it anonymous. This is the safest option
* to avoid conflicts with existing variables on the page. This will become the default in a future version of WXT.
* - `string`: Use the provided string as the global variable name.
* - `function`: A function that receives the entrypoint and returns a string to use as the variable name.
*
* @default true
*/
globalName?: string | boolean | ((entrypoint: Entrypoint) => string);
}
export interface BaseContentScriptEntrypointOptions extends BaseScriptEntrypointOptions {
matches?: PerBrowserOption<NonNullable<ManifestContentScript['matches']>>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
@@ -749,12 +770,16 @@ export interface GenericEntrypoint extends BaseEntrypoint {
| 'newtab'
| 'devtools'
| 'unlisted-page'
| 'unlisted-script'
| 'unlisted-style'
| 'content-script-style';
options: ResolvedPerBrowserOptions<BaseEntrypointOptions>;
}
export interface UnlistedScriptEntrypoint extends BaseEntrypoint {
type: 'unlisted-script';
options: ResolvedPerBrowserOptions<BaseScriptEntrypointOptions>;
}
export interface BackgroundEntrypoint extends BaseEntrypoint {
type: 'background';
options: ResolvedPerBrowserOptions<BackgroundEntrypointOptions>;
@@ -786,6 +811,7 @@ export interface SidepanelEntrypoint extends BaseEntrypoint {
export type Entrypoint =
| GenericEntrypoint
| BackgroundEntrypoint
| UnlistedScriptEntrypoint
| ContentScriptEntrypoint
| PopupEntrypoint
| OptionsEntrypoint
@@ -835,7 +861,7 @@ export interface BackgroundDefinition extends BackgroundEntrypointOptions {
main(): void;
}
export interface UnlistedScriptDefinition extends BaseEntrypointOptions {
export interface UnlistedScriptDefinition extends BaseScriptEntrypointOptions {
/**
* Main function executed when the unlisted script is ran.
*
+6
View File
@@ -121,6 +121,9 @@ importers:
'@wxt-dev/browser':
specifier: workspace:^
version: link:../browser
'@wxt-dev/is-background':
specifier: workspace:^
version: link:../is-background
ua-parser-js:
specifier: ^1.0.40
version: 1.0.41
@@ -159,6 +162,9 @@ importers:
typescript:
specifier: ^5.9.3
version: 5.9.3
vitest:
specifier: ^4.0.18
version: 4.0.18(@types/node@20.19.32)(happy-dom@20.5.0)(jiti@2.6.1)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2)
wxt:
specifier: workspace:*
version: link:../wxt
+3 -3
View File
@@ -20,6 +20,7 @@ for dir in "$PACKAGES_DIR"/*; do
# Check if the package name is in the ignored directories list
if [[ " ${IGNORED_DIRS[*]} " =~ " $pkg_name " ]]; then
echo "----------------------------------------"
echo "Skipping ignored package: $pkg_name"
continue # Skip to the next directory
fi
@@ -34,12 +35,11 @@ for dir in "$PACKAGES_DIR"/*; do
if [ -n "$last_tag" ]; then
# If a tag is found, show commits since that tag for the specific package directory
echo "Commits since last tag ($last_tag):"
git log "${last_tag}..HEAD" --oneline -- "$dir" | grep -v -E "^[a-f0-9]* (chore|docs)"
git log "${last_tag}..HEAD" --oneline -- "$dir" | grep -v -E "^[a-f0-9]* (chore|docs|refactor)" || true
else
# If no tag is found, show all commits for that package directory
echo "No tags found for this package. Listing all commits:"
git log --oneline -- "$dir" | grep -v -E "^[a-f0-9]* (chore|docs)"
git log --oneline -- "$dir" | grep -v -E "^[a-f0-9]* (chore|docs|refactor)" || true
fi
echo ""
fi
done