Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9491e6845e | |||
| e4433c4212 | |||
| c6fdc5a5d5 | |||
| 68dd3fc027 | |||
| cd7aec6752 | |||
| b80556498d | |||
| 38530cdb39 | |||
| 078fd5990e | |||
| b4be5feaa6 | |||
| d982f97bd8 | |||
| 653608c98c | |||
| d63be2d6f3 | |||
| 4500ca577f | |||
| cc935daec0 | |||
| 51ff46ec58 | |||
| 206b7731cf | |||
| d16f59d33d | |||
| a5ec046346 | |||
| 51dd803115 | |||
| 6e39424a30 | |||
| 6ca84782aa | |||
| ee1ba6369e | |||
| f35d70e6fe | |||
| 430a3acfeb | |||
| 395c5841f9 | |||
| 173161ca4d | |||
| a1a8e14368 | |||
| 2379806c61 | |||
| f604208a8b | |||
| cf31fbec5d | |||
| 279be8deb7 |
@@ -0,0 +1,111 @@
|
||||
name: ✨ Auto-label PR
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronized, reopened]
|
||||
|
||||
jobs:
|
||||
update-pr:
|
||||
name: Update PR
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Gather Info
|
||||
id: check
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number
|
||||
});
|
||||
|
||||
// Check if PR has assignees
|
||||
const hasAssignees = pr.assignees && pr.assignees.length > 0;
|
||||
core.setOutput('has_assignees', hasAssignees);
|
||||
core.setOutput('author', pr.user.login);
|
||||
|
||||
// Get list of changed files
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number
|
||||
});
|
||||
|
||||
// Find all packages that were modified
|
||||
const packagesRegex = /^packages\/([^\/]+)\//;
|
||||
const affectedPackages = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const match = file.filename.match(packagesRegex);
|
||||
if (match) {
|
||||
affectedPackages.add(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const labels = Array.from(affectedPackages).map(pkg => `pkg/${pkg}`);
|
||||
core.setOutput('labels', JSON.stringify(labels));
|
||||
console.log('Detected package labels:', labels);
|
||||
|
||||
// Get current labels on the PR that match pkg/* pattern
|
||||
const currentPkgLabels = pr.labels
|
||||
.map(label => label.name)
|
||||
.filter(name => name.startsWith('pkg/'));
|
||||
|
||||
core.setOutput('current_pkg_labels', JSON.stringify(currentPkgLabels));
|
||||
console.log('Current pkg labels:', currentPkgLabels);
|
||||
|
||||
- name: Sync Author
|
||||
if: steps.check.outputs.has_assignees == 'false'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
assignees: ['${{ steps.check.outputs.author }}']
|
||||
});
|
||||
|
||||
console.log('Assigned PR author: ${{ steps.check.outputs.author }}');
|
||||
|
||||
- name: Sync Labels
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const newLabels = ${{ steps.check.outputs.labels }};
|
||||
const currentLabels = ${{ steps.check.outputs.current_pkg_labels }};
|
||||
|
||||
// Find labels to add (in newLabels but not in currentLabels)
|
||||
const labelsToAdd = newLabels.filter(label => !currentLabels.includes(label));
|
||||
|
||||
// Find labels to remove (in currentLabels but not in newLabels)
|
||||
const labelsToRemove = currentLabels.filter(label => !newLabels.includes(label));
|
||||
|
||||
// Add new labels
|
||||
if (labelsToAdd.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: labelsToAdd
|
||||
});
|
||||
console.log('Added labels:', labelsToAdd);
|
||||
}
|
||||
|
||||
// Remove obsolete labels
|
||||
for (const label of labelsToRemove) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
name: label
|
||||
});
|
||||
console.log('Removed label:', label);
|
||||
}
|
||||
|
||||
if (labelsToAdd.length === 0 && labelsToRemove.length === 0) {
|
||||
console.log('No label changes needed');
|
||||
}
|
||||
@@ -10,6 +10,7 @@ on:
|
||||
- analytics
|
||||
- auto-icons
|
||||
- i18n
|
||||
- is-background
|
||||
- module-react
|
||||
- module-solid
|
||||
- module-svelte
|
||||
|
||||
@@ -10,6 +10,7 @@ on:
|
||||
- analytics
|
||||
- auto-icons
|
||||
- i18n
|
||||
- is-background
|
||||
- module-react
|
||||
- module-solid
|
||||
- module-svelte
|
||||
|
||||
@@ -163,13 +163,6 @@ cp -r templates/vanilla templates/<new-template-name>
|
||||
|
||||
That's it. Once your template is merged, it will be available inside `wxt init` immediately. You don't need to release a new version of WXT to release a new template.
|
||||
|
||||
## Releasing Updates
|
||||
|
||||
Releases are done with GitHub actions:
|
||||
|
||||
- Use the [Release workflow](https://github.com/wxt-dev/wxt/actions/workflows/release.yml) to release a single package in the monorepo. This automatically detects the version change with conventional commits, builds and uploads the package to NPM, and creates a GitHub release.
|
||||
- Use the [Sync Releases workflow](https://github.com/wxt-dev/wxt/actions/workflows/sync-releases.yml) to sync the GitHub releases with changes to the changelog. To change a release, update the `CHANGELOG.md` file and run the workflow. It will sync the releases of a single package in the monorepo.
|
||||
|
||||
## Upgrading Dependencies
|
||||
|
||||
WXT has custom rules around what dependencies can be upgraded. Use the `scripts/upgrade-deps.ts` script to upgrade dependencies and follow these rules.
|
||||
|
||||
@@ -47,3 +47,44 @@ Here's an example of how to ask for a reproduction: <https://github.com/wxt-dev/
|
||||
## Add yourself as a code owner
|
||||
|
||||
If you want to be responsible for a specific package or directory, add yourself to the [`.github/CODEOWNERS`](https://github.com/wxt-dev/wxt/blob/main/.github/CODEOWNERS) file to get added as a reviewer to PRs automatically. You can also add yourself to the default list to be added as a reviewer on all PRs.
|
||||
|
||||
## Releasing Package Updates
|
||||
|
||||
Releases are done with GitHub actions:
|
||||
|
||||
- Use the [Release workflow](https://github.com/wxt-dev/wxt/actions/workflows/release.yml) to release a single package in the monorepo. This automatically detects the version change with conventional commits, builds and uploads the package to NPM, and creates a GitHub release.
|
||||
- Use the [Sync Releases workflow](https://github.com/wxt-dev/wxt/actions/workflows/sync-releases.yml) to sync the GitHub releases with changes to the changelog. To change a release, update the `CHANGELOG.md` file and run the workflow. It will sync the releases of a single package in the monorepo.
|
||||
|
||||
## Creating New Packages
|
||||
|
||||
Example PR: <https://github.com/wxt-dev/wxt/pull/2152>
|
||||
|
||||
1. Create the package.
|
||||
|
||||
2. Update CI workflow inputs.
|
||||
|
||||
3. Add docs page and version for "Other Packages" dropdown.
|
||||
|
||||
4. Merge the PR.
|
||||
|
||||
5. Tag the commit (look at other tags for pattern):
|
||||
|
||||
```sh
|
||||
git tag <dir-name>-v<version>
|
||||
git push --tags
|
||||
```
|
||||
|
||||
6. Publish the package to NPM:
|
||||
|
||||
```sh
|
||||
cd packages/<dir-name>
|
||||
pnpm publish --access public
|
||||
```
|
||||
|
||||
7. Create a basic release on GitHub mentioning the new package is available.
|
||||
|
||||
A couple of things to note:
|
||||
|
||||
- pkg.pr.new will fail on the original PR. It's fine to ignore and merge your PR as long as it fails due to your new package not being published to NPM yet.
|
||||
- The regular release workflow DOES NOT WORK for new packages. You have to have at least one `<dir-name>-v<version>` tag created before you can run that workflow for your new package.
|
||||
- You don't need to create a CHANGELOG.md file for the package, it will be created automatically after future changes are released via the normal release workflow.
|
||||
|
||||
+2
-6
@@ -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.
|
||||
|
||||
@@ -117,6 +117,8 @@ const chromeExtensionIds = [
|
||||
'jdcppdokgfbnhiacbeplahgnciahnhck', // Lofi BGM Player - Free lofi focus music for work & study
|
||||
'cgpmbiiagnehkikhcbnhiagfomajncpa', // Margin - Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.
|
||||
'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
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { version as unocssVersion } from '../../packages/unocss/package.json';
|
||||
import { version as storageVersion } from '../../packages/storage/package.json';
|
||||
import { version as analyticsVersion } from '../../packages/analytics/package.json';
|
||||
import { version as runnerVersion } from '../../packages/runner/package.json';
|
||||
import { version as isBackgroundVersion } from '../../packages/is-background/package.json';
|
||||
import addKnowledge from 'vitepress-knowledge';
|
||||
import {
|
||||
groupIconMdPlugin,
|
||||
@@ -44,6 +45,7 @@ const otherPackages = {
|
||||
storage: storageVersion,
|
||||
unocss: unocssVersion,
|
||||
runner: runnerVersion,
|
||||
'is-background': isBackgroundVersion,
|
||||
};
|
||||
|
||||
const knowledge = addKnowledge<DefaultTheme.Config>({
|
||||
|
||||
@@ -23,12 +23,12 @@ export default defineAppConfig({
|
||||
This file is committed to the repo, so don't put any secrets here. Instead, use [Environment Variables](/guide/essentials/config/environment-variables)
|
||||
:::
|
||||
|
||||
To access runtime config, WXT provides the `useAppConfig` function:
|
||||
To access runtime config, WXT provides the `getAppConfig` function:
|
||||
|
||||
```ts
|
||||
import { useAppConfig } from '#imports';
|
||||
import { getAppConfig } from '#imports';
|
||||
|
||||
console.log(useAppConfig()); // { theme: "dark" }
|
||||
console.log(getAppConfig()); // { theme: "dark" }
|
||||
```
|
||||
|
||||
## Environment Variables in App Config
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# WXT Modules
|
||||
|
||||
WXT provides a "module system" that let's you run code at different steps in the build process to modify it.
|
||||
@@ -131,12 +135,64 @@ declare module 'wxt/utils/define-app-config' {
|
||||
Runtime options are returned when calling
|
||||
|
||||
```ts
|
||||
const config = useAppConfig();
|
||||
const config = getAppConfig();
|
||||
console.log(config.myModule);
|
||||
```
|
||||
|
||||
This is very useful when [generating runtime code](#generate-runtime-module).
|
||||
|
||||
#### Add custom entrypoint options
|
||||
|
||||
Modules can add custom options to entrypoints by augmenting the entrypoint options types. This allows you to add custom configuration that can be accessed during the build process.
|
||||
|
||||
```ts
|
||||
import { defineWxtModule } from 'wxt/modules';
|
||||
import 'wxt';
|
||||
|
||||
declare module 'wxt' {
|
||||
export interface BackgroundEntrypointOptions {
|
||||
// Add custom options to the background entrypoint
|
||||
myCustomOption?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineWxtModule({
|
||||
setup(wxt) {
|
||||
wxt.hook('entrypoints:resolved', (_, entrypoints) => {
|
||||
const background = entrypoints.find((e) => e.type === 'background');
|
||||
if (background) {
|
||||
console.log('Custom option:', background.options.myCustomOption);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Now users can set the custom option in their entrypoint:
|
||||
|
||||
```ts [entrypoints/background.ts]
|
||||
export default defineBackground({
|
||||
myCustomOption: 'custom value',
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This works for all other JS and HTML entrypoints, here's an example of how to pass a custom option from an HTML file.
|
||||
|
||||
```html [entrypoints/popup.html]
|
||||
<html>
|
||||
<head>
|
||||
<meta name="wxt.myHtmlOption" content="custom value" />
|
||||
<title>Popup</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
#### Generate output file
|
||||
|
||||
```ts
|
||||
@@ -220,7 +276,7 @@ export default defineWxtModule({
|
||||
const analyticsModuleCode = `
|
||||
import { createAnalytics } from 'some-module';
|
||||
|
||||
export const analytics = createAnalytics(useAppConfig().analytics);
|
||||
export const analytics = createAnalytics(getAppConfig().analytics);
|
||||
export const { reportEvent, reportPageView } = analytics;
|
||||
`;
|
||||
|
||||
|
||||
@@ -13,3 +13,4 @@ This page is dedicated to all the awesome people how have made something for WXT
|
||||
|
||||
- [`@webext-core/*`](https://webext-core.aklinker1.io/): Easy-to-use utilities for writing and testing web extensions that work on all browsers.
|
||||
- [`Comctx`](https://github.com/molvqingtai/comctx): Cross-context RPC solution with type safety and flexible adapters.
|
||||
- [`wxt-local-analytics`](https://github.com/HaNdTriX/wxt-local-analytics): Local analytics provider for [`@wxt-dev/analytics`](https://wxt.dev/analytics)
|
||||
|
||||
@@ -177,7 +177,7 @@ import { defineContentScript } from 'wxt/sandbox'; // [!code --]
|
||||
import { ContentScriptContext, useAppConfig } from 'wxt/client'; // [!code --]
|
||||
import { storage } from '#imports'; // [!code ++]
|
||||
import { defineContentScript } from '#imports'; // [!code ++]
|
||||
import { ContentScriptContext, useAppConfig } from '#imports'; // [!code ++]
|
||||
import { ContentScriptContext, getAppConfig } from '#imports'; // [!code ++]
|
||||
```
|
||||
|
||||
You can combine the imports into a single import statement, but it's easier to just find/replace each statement.
|
||||
@@ -191,7 +191,7 @@ import {
|
||||
storage, // [!code ++]
|
||||
defineContentScript, // [!code ++]
|
||||
ContentScriptContext, // [!code ++]
|
||||
useAppConfig, // [!code ++]
|
||||
getAppConfig, // [!code ++]
|
||||
} from '#imports'; // [!code ++]
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<!--@include: ../packages/is-background/README.md-->
|
||||
@@ -2,14 +2,30 @@ import { UAParser } from 'ua-parser-js';
|
||||
import type {
|
||||
Analytics,
|
||||
AnalyticsConfig,
|
||||
AnalyticsEventMetadata,
|
||||
AnalyticsPageViewEvent,
|
||||
AnalyticsProvider,
|
||||
AnalyticsStorageItem,
|
||||
AnalyticsTrackEvent,
|
||||
BaseAnalyticsEvent,
|
||||
AnalyticsEventMetadata,
|
||||
AnalyticsProvider,
|
||||
} from './types';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
import { isBackground } from '@wxt-dev/is-background';
|
||||
|
||||
type AnalyticsMessage = {
|
||||
[K in keyof Analytics]: {
|
||||
fn: K;
|
||||
args: Parameters<Analytics[K]>;
|
||||
};
|
||||
}[keyof Analytics];
|
||||
|
||||
type AnalyticsMethod =
|
||||
| ((...args: Parameters<Analytics[keyof Analytics]>) => void)
|
||||
| undefined;
|
||||
|
||||
type MethodForwarder = <K extends keyof Analytics>(
|
||||
fn: K,
|
||||
) => (...args: Parameters<Analytics[K]>) => void;
|
||||
|
||||
const ANALYTICS_PORT = '@wxt-dev/analytics';
|
||||
|
||||
@@ -40,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();
|
||||
}
|
||||
@@ -163,7 +176,7 @@ function createBackgroundAnalytics(
|
||||
},
|
||||
track: async (
|
||||
eventName: string,
|
||||
eventProperties?: Record<string, string>,
|
||||
eventProperties?: Record<string, string | undefined>,
|
||||
meta: AnalyticsEventMetadata = getBackgroundMeta(),
|
||||
) => {
|
||||
const baseEvent = await getBaseEvent(meta);
|
||||
@@ -197,9 +210,8 @@ function createBackgroundAnalytics(
|
||||
// Listen for messages from the rest of the extension
|
||||
browser.runtime.onConnect.addListener((port) => {
|
||||
if (port.name === ANALYTICS_PORT) {
|
||||
port.onMessage.addListener(({ fn, args }) => {
|
||||
// @ts-expect-error: Untyped fn key
|
||||
void analytics[fn]?.(...args);
|
||||
port.onMessage.addListener(({ fn, args }: AnalyticsMessage) => {
|
||||
void (analytics[fn] as AnalyticsMethod)?.(...args);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -217,17 +229,15 @@ function createFrontendAnalytics(): Analytics {
|
||||
sessionId,
|
||||
timestamp: Date.now(),
|
||||
language: navigator.language,
|
||||
referrer: globalThis.document?.referrer || undefined,
|
||||
screen: globalThis.window
|
||||
? `${globalThis.window.screen.width}x${globalThis.window.screen.height}`
|
||||
: undefined,
|
||||
referrer: document.referrer || undefined,
|
||||
screen: `${window.screen.width}x${window.screen.height}`,
|
||||
url: location.href,
|
||||
title: document.title || undefined,
|
||||
});
|
||||
|
||||
const methodForwarder =
|
||||
(fn: string) =>
|
||||
(...args: any[]) => {
|
||||
const methodForwarder: MethodForwarder =
|
||||
(fn) =>
|
||||
(...args) => {
|
||||
port.postMessage({ fn, args: [...args, getFrontendMetadata()] });
|
||||
};
|
||||
|
||||
@@ -238,11 +248,11 @@ function createFrontendAnalytics(): Analytics {
|
||||
setEnabled: methodForwarder('setEnabled'),
|
||||
autoTrack: (root) => {
|
||||
const onClick = (event: Event) => {
|
||||
const element = event.target as any;
|
||||
const element = event.target as HTMLElement | null;
|
||||
if (
|
||||
!element ||
|
||||
(!INTERACTIVE_TAGS.has(element.tagName) &&
|
||||
!INTERACTIVE_ROLES.has(element.getAttribute('role')))
|
||||
!INTERACTIVE_ROLES.has(element.getAttribute('role') ?? ''))
|
||||
)
|
||||
return;
|
||||
|
||||
@@ -251,7 +261,7 @@ function createFrontendAnalytics(): Analytics {
|
||||
id: element.id || undefined,
|
||||
className: element.className || undefined,
|
||||
textContent: element.textContent?.substring(0, 50) || undefined, // Limit text content length
|
||||
href: element.href,
|
||||
href: (element as HTMLAnchorElement).href,
|
||||
});
|
||||
};
|
||||
root.addEventListener('click', onClick, { capture: true, passive: true });
|
||||
@@ -263,13 +273,18 @@ function createFrontendAnalytics(): Analytics {
|
||||
return analytics;
|
||||
}
|
||||
|
||||
function defineStorageItem<T>(key: string): AnalyticsStorageItem<T | undefined>;
|
||||
function defineStorageItem<T>(
|
||||
key: string,
|
||||
defaultValue?: NonNullable<T>,
|
||||
): AnalyticsStorageItem<T> {
|
||||
defaultValue: T,
|
||||
): AnalyticsStorageItem<T>;
|
||||
function defineStorageItem(
|
||||
key: string,
|
||||
defaultValue?: unknown,
|
||||
): AnalyticsStorageItem<unknown> {
|
||||
return {
|
||||
getValue: async () =>
|
||||
(await browser.storage.local.get<Record<string, any>>(key))[key] ??
|
||||
(await browser.storage.local.get<Record<string, unknown>>(key))[key] ??
|
||||
defaultValue,
|
||||
setValue: (newValue) => browser.storage.local.set({ [key]: newValue }),
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -2,7 +2,10 @@ export interface Analytics {
|
||||
/** Report a page change. */
|
||||
page: (url: string) => void;
|
||||
/** Report a custom event. */
|
||||
track: (eventName: string, eventProperties?: Record<string, string>) => void;
|
||||
track: (
|
||||
eventName: string,
|
||||
eventProperties?: Record<string, string | undefined>,
|
||||
) => 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. */
|
||||
@@ -32,7 +35,7 @@ export interface AnalyticsConfig {
|
||||
/**
|
||||
* Configure how the user Id is persisted. Defaults to using `browser.storage.local`.
|
||||
*/
|
||||
userId?: AnalyticsStorageItem<string>;
|
||||
userId?: AnalyticsStorageItem<string | undefined>;
|
||||
/**
|
||||
* Configure how user properties are persisted. Defaults to using `browser.storage.local`.
|
||||
*/
|
||||
@@ -94,6 +97,6 @@ export interface AnalyticsPageViewEvent extends BaseAnalyticsEvent {
|
||||
export interface AnalyticsTrackEvent extends BaseAnalyticsEvent {
|
||||
event: {
|
||||
name: string;
|
||||
properties?: Record<string, string>;
|
||||
properties?: Record<string, string | undefined>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
mockReset: true,
|
||||
restoreMocks: true,
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@wxt-dev/browser",
|
||||
"description": "Provides a cross-browser API for using extension APIs and types based on @types/chrome",
|
||||
"version": "0.1.36",
|
||||
"version": "0.1.37",
|
||||
"type": "module",
|
||||
"main": "src/index.mjs",
|
||||
"types": "src/index.d.ts",
|
||||
@@ -23,7 +23,7 @@
|
||||
"src"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/chrome": "0.1.36",
|
||||
"@types/chrome": "0.1.37",
|
||||
"fs-extra": "^11.3.3",
|
||||
"nano-spawn": "^2.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
+47
@@ -694,9 +694,16 @@ export namespace Browser {
|
||||
|
||||
/** @deprecated Bookmark write operations are no longer limited by Chrome. */
|
||||
export const MAX_WRITE_OPERATIONS_PER_HOUR: 1000000;
|
||||
|
||||
/** @deprecated Bookmark write operations are no longer limited by Chrome. */
|
||||
export const MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE: 1000000;
|
||||
|
||||
/**
|
||||
* The `id` associated with the root level node.
|
||||
* @since Chrome 145
|
||||
*/
|
||||
export const ROOT_NODE_ID = "0";
|
||||
|
||||
/**
|
||||
* Creates a bookmark or folder under the specified parentId. If url is NULL or missing, it will be a folder.
|
||||
*
|
||||
@@ -11049,6 +11056,11 @@ export namespace Browser {
|
||||
height?: number | undefined;
|
||||
/** The session ID used to uniquely identify a tab obtained from the {@link sessions} API. */
|
||||
sessionId?: string | undefined;
|
||||
/**
|
||||
* The ID of the Split View that the tab belongs to.
|
||||
* @since Chrome 145
|
||||
*/
|
||||
splitViewId?: number | undefined;
|
||||
/**
|
||||
* The ID of the group that the tab belongs to.
|
||||
* @since Chrome 88
|
||||
@@ -11120,6 +11132,12 @@ export namespace Browser {
|
||||
*/
|
||||
export const MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND = 2;
|
||||
|
||||
/**
|
||||
* An ID that represents the absence of a split tab.
|
||||
* @since Chrome 145
|
||||
*/
|
||||
export const SPLIT_VIEW_ID_NONE: -1;
|
||||
|
||||
/**
|
||||
* An ID that represents the absence of a browser tab.
|
||||
* @since Chrome 46
|
||||
@@ -14085,6 +14103,30 @@ export namespace Browser {
|
||||
responseHeaders?: HeaderInfo[];
|
||||
}
|
||||
|
||||
/** @since Chrome 145 */
|
||||
export enum RuleConditionKeys {
|
||||
URL_FILTER = "urlFilter",
|
||||
REGEX_FILTER = "regexFilter",
|
||||
IS_URL_FILTER_CASE_SENSITIVE = "isUrlFilterCaseSensitive",
|
||||
INITIATOR_DOMAINS = "initiatorDomains",
|
||||
EXCLUDED_INITIATOR_DOMAINS = "excludedInitiatorDomains",
|
||||
REQUEST_DOMAINS = "requestDomains",
|
||||
EXCLUDED_REQUEST_DOMAINS = "excludedRequestDomains",
|
||||
TOP_DOMAINS = "topDomains",
|
||||
EXCLUDED_TOP_DOMAINS = "excludedTopDomains",
|
||||
DOMAINS = "domains",
|
||||
EXCLUDED_DOMAINS = "excludedDomains",
|
||||
RESOURCE_TYPES = "resourceTypes",
|
||||
EXCLUDED_RESOURCE_TYPES = "excludedResourceTypes",
|
||||
REQUEST_METHODS = "requestMethods",
|
||||
EXCLUDED_REQUEST_METHODS = "excludedRequestMethods",
|
||||
DOMAIN_TYPE = "domainType",
|
||||
TAB_IDS = "tabIds",
|
||||
EXCLUDED_TAB_IDS = "excludedTabIds",
|
||||
RESPONSE_HEADERS = "responseHeaders",
|
||||
EXCLUDED_RESPONSE_HEADERS = "excludedResponseHeaders",
|
||||
}
|
||||
|
||||
export interface MatchedRule {
|
||||
/** A matching rule's ID. */
|
||||
ruleId: number;
|
||||
@@ -14305,6 +14347,11 @@ export namespace Browser {
|
||||
responseHeaders?: { [name: string]: unknown };
|
||||
/** The ID of the tab in which the hypothetical request takes place. Does not need to correspond to a real tab ID. Default is -1, meaning that the request isn't related to a tab. */
|
||||
tabId?: number;
|
||||
/**
|
||||
* The associated top-level frame URL (if any) for the request.
|
||||
* @since Chrome 145
|
||||
*/
|
||||
topUrl?: string;
|
||||
/** The resource type of the hypothetical request. */
|
||||
type: `${ResourceType}`;
|
||||
/** The URL of the hypothetical request. */
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# `@wxt-dev/is-background`
|
||||
|
||||
Exports a getter to determine if the current JS context is the background or not.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @wxt-dev/is-background
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { isBackground } from '@wxt-dev/is-background';
|
||||
|
||||
isBackground(); // true | false
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@wxt-dev/is-background",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"description": "Check if the current context is the background or not.",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "buildc -- tsdown",
|
||||
"check": "pnpm build && check",
|
||||
"test": "buildc --deps-only -- vitest",
|
||||
"test:coverage": "pnpm test run --coverage",
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wxt-dev/browser": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"oxlint": "^1.43.0",
|
||||
"publint": "^0.3.17",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/is-background"
|
||||
},
|
||||
"keywords": [
|
||||
"wxt",
|
||||
"chrome",
|
||||
"web",
|
||||
"extension",
|
||||
"is",
|
||||
"background",
|
||||
"script",
|
||||
"page",
|
||||
"service",
|
||||
"worker"
|
||||
],
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"funding": "https://github.com/sponsors/wxt-dev",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"types": "./dist/index.d.mts",
|
||||
"module": "./dist/index.mjs",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { describe, it, vi, expect } from 'vitest';
|
||||
import { getIsBackground } from '../getter';
|
||||
|
||||
let mockBrowser: any;
|
||||
vi.mock('@wxt-dev/browser', () => ({
|
||||
get browser() {
|
||||
return mockBrowser;
|
||||
},
|
||||
}));
|
||||
|
||||
const backgroundWindow = Symbol('Background');
|
||||
const otherWindow = Symbol('Other');
|
||||
|
||||
function setupEnv(options: {
|
||||
window: symbol | undefined;
|
||||
hasExtensionApis: boolean;
|
||||
hasGetBackgroundPage: boolean;
|
||||
hasServiceWorkerGlobalScope: boolean;
|
||||
}): void {
|
||||
vi.unstubAllGlobals();
|
||||
mockBrowser = undefined;
|
||||
|
||||
if (options.window) {
|
||||
vi.stubGlobal('window', options.window);
|
||||
}
|
||||
|
||||
if (options.hasExtensionApis) {
|
||||
mockBrowser = {
|
||||
runtime: {
|
||||
id: 'test',
|
||||
},
|
||||
...(options.hasGetBackgroundPage && {
|
||||
extension: {
|
||||
getBackgroundPage: () => backgroundWindow,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (options.hasServiceWorkerGlobalScope) {
|
||||
class ServiceWorkerGlobalScope {}
|
||||
vi.stubGlobal('ServiceWorkerGlobalScope', ServiceWorkerGlobalScope);
|
||||
vi.stubGlobal('self', new ServiceWorkerGlobalScope());
|
||||
}
|
||||
}
|
||||
|
||||
describe('isBackground Getter', () => {
|
||||
describe('Non-extension contexts', () => {
|
||||
it('should return false', () => {
|
||||
setupEnv({
|
||||
hasServiceWorkerGlobalScope: false,
|
||||
hasExtensionApis: false,
|
||||
hasGetBackgroundPage: false,
|
||||
window: otherWindow,
|
||||
});
|
||||
|
||||
expect(getIsBackground()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Chromium & Safari', () => {
|
||||
const hasExtensionApis = true;
|
||||
|
||||
describe('MV2', () => {
|
||||
const hasServiceWorkerGlobalScope = false;
|
||||
const hasGetBackgroundPage = true;
|
||||
|
||||
it('should return true inside the background page', () => {
|
||||
setupEnv({
|
||||
hasServiceWorkerGlobalScope,
|
||||
hasExtensionApis,
|
||||
hasGetBackgroundPage,
|
||||
window: backgroundWindow,
|
||||
});
|
||||
|
||||
expect(getIsBackground()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false outside the background page', () => {
|
||||
setupEnv({
|
||||
hasServiceWorkerGlobalScope,
|
||||
hasExtensionApis,
|
||||
hasGetBackgroundPage,
|
||||
window: otherWindow,
|
||||
});
|
||||
|
||||
expect(getIsBackground()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MV3', () => {
|
||||
const hasGetBackgroundPage = false;
|
||||
|
||||
it('should return true inside the service worker', () => {
|
||||
setupEnv({
|
||||
hasServiceWorkerGlobalScope: true,
|
||||
hasExtensionApis,
|
||||
hasGetBackgroundPage,
|
||||
window: undefined,
|
||||
});
|
||||
|
||||
expect(getIsBackground()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false outside the service worker', () => {
|
||||
setupEnv({
|
||||
hasServiceWorkerGlobalScope: false,
|
||||
hasExtensionApis,
|
||||
hasGetBackgroundPage,
|
||||
window: otherWindow,
|
||||
});
|
||||
|
||||
expect(getIsBackground()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Firefox, MV2 & MV3', () => {
|
||||
const hasServiceWorkerGlobalScope = false;
|
||||
const hasExtensionApis = true;
|
||||
const hasGetBackgroundPage = true;
|
||||
|
||||
it('should return true inside the background page', () => {
|
||||
setupEnv({
|
||||
hasServiceWorkerGlobalScope,
|
||||
hasExtensionApis,
|
||||
hasGetBackgroundPage,
|
||||
window: backgroundWindow,
|
||||
});
|
||||
|
||||
expect(getIsBackground()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false outside the background page', () => {
|
||||
setupEnv({
|
||||
hasServiceWorkerGlobalScope,
|
||||
hasExtensionApis,
|
||||
hasGetBackgroundPage,
|
||||
window: otherWindow,
|
||||
});
|
||||
|
||||
expect(getIsBackground()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
|
||||
declare class ServiceWorkerGlobalScope {}
|
||||
|
||||
export function getIsBackground(): boolean {
|
||||
// Are we in an extension context?
|
||||
if (!browser?.runtime?.id) return false;
|
||||
|
||||
// Is this a true MV3 service worker?
|
||||
//
|
||||
// - ✅ Chromium MV3
|
||||
// - ❌ Firefox MV3 - Uses a non-persistent HTML page instead of a service worker.
|
||||
// - ✅ Safari MV3
|
||||
if (
|
||||
typeof ServiceWorkerGlobalScope !== 'undefined' &&
|
||||
self instanceof ServiceWorkerGlobalScope
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is this the background page?
|
||||
//
|
||||
// - ✅ Chromium MV2
|
||||
// - ✅ Firefox MV2
|
||||
// - ✅ Firefox MV3 - Works with the non-persistent HTML page
|
||||
// - ✅ Safari MV2
|
||||
return (
|
||||
typeof window !== 'undefined' &&
|
||||
typeof browser.extension?.getBackgroundPage === 'function' &&
|
||||
browser.extension.getBackgroundPage() === window
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @module @wxt-dev/is-background
|
||||
*/
|
||||
import { getIsBackground } from './getter';
|
||||
|
||||
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.
|
||||
*
|
||||
* @returns true when in a background page or service worker.
|
||||
*/
|
||||
export function isBackground(): boolean {
|
||||
if (cached == null) cached = getIsBackground();
|
||||
return cached;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## v1.2.7
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.6...storage-v1.2.7)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add another `defineItem` signature when `init` function is passed ([#1909](https://github.com/wxt-dev/wxt/pull/1909))
|
||||
- **storage:** Set version number on init ([#1996](https://github.com/wxt-dev/wxt/pull/1996))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Code cleanup in analytics package ([#2084](https://github.com/wxt-dev/wxt/pull/2084))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Rename keys name of getMetas() to be proper ([#2105](https://github.com/wxt-dev/wxt/pull/2105))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Fix type errors after `chrome` type upgrades ([6036c6e8](https://github.com/wxt-dev/wxt/commit/6036c6e8))
|
||||
- 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
|
||||
|
||||
- Willow ([@42willow](https://github.com/42willow))
|
||||
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Dan McGee <dpmcgee@gmail.com>
|
||||
|
||||
## v1.2.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.5...storage-v1.2.6)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@wxt-dev/storage",
|
||||
"description": "Web extension storage API provided by WXT, supports all browsers.",
|
||||
"version": "1.2.6",
|
||||
"version": "1.2.7",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fakeBrowser } from '@webext-core/fake-browser';
|
||||
import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
|
||||
import { MigrationError, type WxtStorageItem, storage } from '../index';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
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
|
||||
@@ -23,6 +23,9 @@ describe('Storage Utils', () => {
|
||||
beforeEach(() => {
|
||||
fakeBrowser.reset();
|
||||
storage.unwatch();
|
||||
|
||||
// Setup a spy to check for excessive calls
|
||||
fakeBrowser.storage.local.set = vi.spyOn(fakeBrowser.storage.local, 'set');
|
||||
});
|
||||
|
||||
describe.each(['local', 'sync', 'managed', 'session'] as const)(
|
||||
@@ -949,6 +952,7 @@ describe('Storage Utils', () => {
|
||||
{ migratedValue: expect.any(Number) },
|
||||
);
|
||||
});
|
||||
|
||||
it('should not print migration logs if debug option is undefined or false', async () => {
|
||||
await fakeBrowser.storage.local.set({
|
||||
count: 2,
|
||||
@@ -980,6 +984,62 @@ describe('Storage Utils', () => {
|
||||
await waitForMigrations();
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
describe('calling setValue', () => {
|
||||
const migrateToV2 = vi.fn((v1) => v1);
|
||||
const defineTestItem = () =>
|
||||
storage.defineItem<number>('local:count', {
|
||||
version: 2,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
},
|
||||
});
|
||||
|
||||
it('should set the version metadata when setting the value for the first time', async () => {
|
||||
const item = defineTestItem();
|
||||
|
||||
expect(await item.getValue()).toBeNull();
|
||||
expect(await item.getMeta()).toEqual({});
|
||||
expect(fakeBrowser.storage.local.set).toBeCalledTimes(0);
|
||||
|
||||
await item.setValue(1);
|
||||
|
||||
expect(await item.getValue()).toBe(1);
|
||||
expect(await item.getMeta()).toEqual({ v: 2 });
|
||||
// Called twice, once for setting the value, once for updating the metadata.
|
||||
expect(fakeBrowser.storage.local.set).toBeCalledTimes(2);
|
||||
expect(fakeBrowser.storage.local.set).toBeCalledWith({ count: 1 });
|
||||
expect(fakeBrowser.storage.local.set).toBeCalledWith({
|
||||
count$: { v: 2 },
|
||||
});
|
||||
|
||||
await item.setValue(2);
|
||||
expect(await item.getValue()).toBe(2);
|
||||
expect(await item.getMeta()).toEqual({ v: 2 });
|
||||
// Only called one more time, just for setting the value
|
||||
expect(fakeBrowser.storage.local.set).toBeCalledTimes(3);
|
||||
expect(fakeBrowser.storage.local.set).toBeCalledWith({ count: 2 });
|
||||
|
||||
// Migration function never called throughout the whole test
|
||||
expect(migrateToV2).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should not set the version metadata when a value is already in storage', async () => {
|
||||
await fakeBrowser.storage.local.set({ count: 1, count$: { v: 2 } });
|
||||
vi.mocked(fakeBrowser.storage.local.set).mockClear();
|
||||
|
||||
const item = defineTestItem();
|
||||
await item.setValue(2);
|
||||
|
||||
expect(await item.getValue()).toEqual(2);
|
||||
expect(await item.getMeta()).toEqual({ v: 2 });
|
||||
|
||||
expect(fakeBrowser.storage.local.set).toBeCalledTimes(1);
|
||||
expect(fakeBrowser.storage.local.set).toBeCalledWith({ count: 2 });
|
||||
|
||||
expect(migrateToV2).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValue', () => {
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* See [the guide](https://wxt.dev/storage.html) for more information.
|
||||
* @module @wxt-dev/storage
|
||||
*/
|
||||
import { dequal } from 'dequal/lite';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { browser, type Browser } from '@wxt-dev/browser';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { dequal } from 'dequal/lite';
|
||||
|
||||
export const storage = createStorage();
|
||||
|
||||
@@ -409,12 +409,18 @@ function createStorage(): WxtStorage {
|
||||
);
|
||||
}
|
||||
|
||||
let needsVersionSet = false;
|
||||
|
||||
const migrate: WxtStorageItem<any, any>['migrate'] = async () => {
|
||||
const driverMetaKey = getMetaKey(driverKey);
|
||||
const [{ value }, { value: meta }] = await driver.getItems([
|
||||
driverKey,
|
||||
driverMetaKey,
|
||||
]);
|
||||
|
||||
// Used in setValue to also set the version when needed
|
||||
needsVersionSet = value == null && meta?.v == null && !!targetVersion;
|
||||
|
||||
if (value == null) return;
|
||||
|
||||
const currentVersion = meta?.v ?? 1;
|
||||
@@ -529,7 +535,18 @@ function createStorage(): WxtStorage {
|
||||
setValue: async (value) => {
|
||||
await migrationsDone;
|
||||
|
||||
return await setItem(driver, driverKey, value);
|
||||
if (needsVersionSet) {
|
||||
needsVersionSet = false;
|
||||
await Promise.all([
|
||||
// Note: These calls cannot be done in a single `setItems` call;
|
||||
// metadata needs to be merged together with existing data and
|
||||
// setItems overwrites the whole value without merging.
|
||||
setItem(driver, driverKey, value),
|
||||
setMeta(driver, driverKey, { v: targetVersion }),
|
||||
]);
|
||||
} else {
|
||||
await setItem(driver, driverKey, value);
|
||||
}
|
||||
},
|
||||
|
||||
setMeta: async (properties) => {
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineBackground({
|
||||
manifestVersion: import.meta.env.MANIFEST_VERSION,
|
||||
});
|
||||
|
||||
console.log(useAppConfig());
|
||||
console.log(getAppConfig());
|
||||
|
||||
browser.runtime.getURL('/');
|
||||
browser.runtime.getURL('/background.js');
|
||||
@@ -43,3 +43,14 @@ export default defineBackground({
|
||||
storage.setItem('session:startTime', Date.now());
|
||||
},
|
||||
});
|
||||
|
||||
function _otherTypeChecksNotEvaluated() {
|
||||
browser.scripting.executeScript({
|
||||
target: { tabId: 1 },
|
||||
files: [
|
||||
'/background.js',
|
||||
// @ts-expect-error: Should error for non-existing paths
|
||||
'/other.js',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"getAppConfig": true,
|
||||
"injectScript": true,
|
||||
"storage": true,
|
||||
"useAppConfig": true
|
||||
@@ -101,6 +102,7 @@ const globals = {
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"getAppConfig": true,
|
||||
"injectScript": true,
|
||||
"storage": true,
|
||||
"useAppConfig": true
|
||||
@@ -164,6 +166,7 @@ exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config f
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"getAppConfig": true,
|
||||
"injectScript": true,
|
||||
"storage": true,
|
||||
"useAppConfig": true
|
||||
@@ -219,6 +222,7 @@ exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = `
|
||||
"defineUnlistedScript": "readonly",
|
||||
"defineWxtPlugin": "readonly",
|
||||
"fakeBrowser": "readonly",
|
||||
"getAppConfig": "readonly",
|
||||
"injectScript": "readonly",
|
||||
"storage": "readonly",
|
||||
"useAppConfig": "readonly"
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('Auto Imports', () => {
|
||||
const defineUnlistedScript: typeof import('wxt/utils/define-unlisted-script').defineUnlistedScript
|
||||
const defineWxtPlugin: typeof import('wxt/utils/define-wxt-plugin').defineWxtPlugin
|
||||
const fakeBrowser: typeof import('wxt/testing').fakeBrowser
|
||||
const getAppConfig: typeof import('wxt/utils/app-config').getAppConfig
|
||||
const injectScript: typeof import('wxt/utils/inject-script').injectScript
|
||||
const storage: typeof import('wxt/utils/storage').storage
|
||||
const useAppConfig: typeof import('wxt/utils/app-config').useAppConfig
|
||||
@@ -111,7 +112,7 @@ describe('Auto Imports', () => {
|
||||
declare module '#imports' {
|
||||
export { browser, Browser } from 'wxt/browser';
|
||||
export { storage, StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage';
|
||||
export { useAppConfig } from 'wxt/utils/app-config';
|
||||
export { getAppConfig, useAppConfig } from 'wxt/utils/app-config';
|
||||
export { ContentScriptContext, WxtWindowEventMap } from 'wxt/utils/content-script-context';
|
||||
export { createIframeUi, IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe';
|
||||
export { createIntegratedUi, IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated';
|
||||
@@ -196,7 +197,7 @@ describe('Auto Imports', () => {
|
||||
declare module '#imports' {
|
||||
export { browser, Browser } from 'wxt/browser';
|
||||
export { storage, StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage';
|
||||
export { useAppConfig } from 'wxt/utils/app-config';
|
||||
export { getAppConfig, useAppConfig } from 'wxt/utils/app-config';
|
||||
export { ContentScriptContext, WxtWindowEventMap } from 'wxt/utils/content-script-context';
|
||||
export { createIframeUi, IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe';
|
||||
export { createIntegratedUi, IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import { WxtHooks } from '../../src/types';
|
||||
import type { WxtHooks } from '../../src';
|
||||
|
||||
const hooks: WxtHooks = {
|
||||
ready: vi.fn(),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import type { GenericEntrypoint, InlineConfig } from '../../src/types';
|
||||
import type { InlineConfig, UnlistedScriptEntrypoint } from '../../src';
|
||||
import { readFile } from 'fs-extra';
|
||||
import { normalizePath } from '../../src/core/utils/paths';
|
||||
import { normalizePath } from '../../src';
|
||||
|
||||
describe('Module Helpers', () => {
|
||||
describe('options', () => {
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,6 +63,29 @@ describe('TypeScript Project', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('should include CSS entrypoints in browser.runtime.getURL paths', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html', '<html></html>');
|
||||
project.addFile(
|
||||
'entrypoints/plain.css',
|
||||
`body {
|
||||
color: red;
|
||||
}`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/overlay.content.css',
|
||||
`body {
|
||||
color: blue;
|
||||
}`,
|
||||
);
|
||||
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/paths.d.ts');
|
||||
expect(output).toContain('| "/plain.css"');
|
||||
expect(output).toContain('| "/content-scripts/overlay.css"');
|
||||
});
|
||||
|
||||
it('should augment the types for browser.i18n.getMessage', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html', '<html></html>');
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare namespace globalThis {
|
||||
var __ENTRYPOINT__: string;
|
||||
}
|
||||
Vendored
-17
@@ -1,22 +1,5 @@
|
||||
// Custom TS definitions for non-TS packages
|
||||
|
||||
declare module 'zip-dir' {
|
||||
// Represents the options object for zipdir function
|
||||
interface ZipDirOptions {
|
||||
saveTo?: string;
|
||||
filter?: (path: string, stat: import('fs').Stats) => boolean;
|
||||
each?: (path: string) => void;
|
||||
}
|
||||
|
||||
function zipdir(
|
||||
dirPath: string,
|
||||
options?: ZipDirOptions,
|
||||
callback?: (error: Error | null, buffer: Buffer) => void,
|
||||
): Promise<Buffer>;
|
||||
|
||||
export = zipdir;
|
||||
}
|
||||
|
||||
declare module 'web-ext-run' {
|
||||
export interface WebExtRunInstance {
|
||||
reloadAllExtensions(): Promise<void>;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* @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:
|
||||
@@ -23,9 +24,39 @@ export interface WxtRuntime {}
|
||||
*/
|
||||
export interface WxtI18n {}
|
||||
|
||||
export type WxtBrowser = Omit<typeof _browser, 'runtime' | 'i18n'> & {
|
||||
type ScriptInjection<Args extends any[], Result> =
|
||||
Browser.scripting.ScriptInjection<Args, Result> extends infer T
|
||||
? T extends { files: string[] }
|
||||
? Omit<T, 'files'> & { files: ScriptPublicPath[] }
|
||||
: T
|
||||
: never;
|
||||
type InjectionResult<Result> = Array<
|
||||
Browser.scripting.InjectionResult<Awaited<Result>>
|
||||
>;
|
||||
|
||||
export interface WxtScripting {
|
||||
executeScript: {
|
||||
/**
|
||||
* @see {@link Browser.scripting.executeScript}
|
||||
*/
|
||||
<Args extends any[], Result>(
|
||||
injection: ScriptInjection<Args, Result>,
|
||||
): Promise<InjectionResult<Result>>;
|
||||
<Args extends any[], Result>(
|
||||
injection: ScriptInjection<Args, Result>,
|
||||
callback: (results: InjectionResult<Result>) => void,
|
||||
): void;
|
||||
};
|
||||
}
|
||||
|
||||
export type WxtBrowser = Omit<
|
||||
typeof _browser,
|
||||
'runtime' | 'i18n' | 'scripting'
|
||||
> & {
|
||||
runtime: WxtRuntime & Omit<(typeof _browser)['runtime'], 'getURL'>;
|
||||
i18n: WxtI18n & Omit<(typeof _browser)['i18n'], 'getMessage'>;
|
||||
scripting: WxtScripting &
|
||||
Omit<(typeof _browser)['scripting'], 'executeScript'>;
|
||||
};
|
||||
|
||||
export const browser: WxtBrowser = _browser;
|
||||
|
||||
@@ -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 {
|
||||
@@ -241,10 +251,7 @@ export async function createViteBuilder(
|
||||
const config = vite.mergeConfig(baseConfig, envConfig);
|
||||
const server = await vite.createServer(config);
|
||||
await server.pluginContainer.buildStart({});
|
||||
const node = new ViteNodeServer(
|
||||
// @ts-ignore: Some weird type error...
|
||||
server,
|
||||
);
|
||||
const node = new ViteNodeServer(server);
|
||||
installSourcemapsSupport({
|
||||
getSourceMap: (source) => node.getSourceMap(source),
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -70,7 +70,7 @@ async function getPathsDeclarationEntry(
|
||||
getEntrypointBundlePath(
|
||||
entry,
|
||||
wxt.config.outDir,
|
||||
isHtmlEntrypoint(entry) ? '.html' : '.js',
|
||||
getEntrypointPublicExt(entry),
|
||||
),
|
||||
)
|
||||
.concat(await getPublicFiles());
|
||||
@@ -107,6 +107,18 @@ declare module "wxt/browser" {
|
||||
};
|
||||
}
|
||||
|
||||
function getEntrypointPublicExt(entry: Entrypoint): '.html' | '.js' | '.css' {
|
||||
if (isHtmlEntrypoint(entry)) return '.html';
|
||||
|
||||
switch (entry.type) {
|
||||
case 'content-script-style':
|
||||
case 'unlisted-style':
|
||||
return '.css';
|
||||
default:
|
||||
return '.js';
|
||||
}
|
||||
}
|
||||
|
||||
async function getI18nDeclarationEntry(): Promise<WxtDirFileEntry> {
|
||||
const defaultLocale = wxt.config.manifest.default_locale;
|
||||
const template = `// Generated by wxt
|
||||
|
||||
@@ -387,7 +387,7 @@ async function getUnimportOptions(
|
||||
},
|
||||
{
|
||||
from: 'wxt/utils/app-config',
|
||||
imports: defineImportsAndTypes(['useAppConfig'], []),
|
||||
imports: defineImportsAndTypes(['getAppConfig', 'useAppConfig'], []),
|
||||
},
|
||||
{
|
||||
from: 'wxt/utils/content-script-context',
|
||||
|
||||
@@ -17,15 +17,6 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
async openBrowser() {
|
||||
const startTime = Date.now();
|
||||
|
||||
if (
|
||||
wxt.config.browser === 'firefox' &&
|
||||
wxt.config.manifestVersion === 3
|
||||
) {
|
||||
throw Error(
|
||||
'Dev mode does not support Firefox MV3. For alternatives, see https://github.com/wxt-dev/wxt/issues/230#issuecomment-1806881653',
|
||||
);
|
||||
}
|
||||
|
||||
// Use WXT's logger instead of web-ext's built-in one.
|
||||
const webExtLogger = await import('web-ext-run/util/logger');
|
||||
webExtLogger.consoleStream.write = ({ level, msg, name }) => {
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('Manifest Utils', () => {
|
||||
defaultIcon: {
|
||||
'16': '/icon/16.png',
|
||||
},
|
||||
defaultTitle: 'Default Iitle',
|
||||
defaultTitle: 'Default Title',
|
||||
},
|
||||
outputDir: outDir,
|
||||
skipped: false,
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import * as dns from 'node:dns';
|
||||
import { isOnline, fetchCached } from '../network';
|
||||
import { ResolvedConfig } from '../../../types';
|
||||
|
||||
type DnsCallback = (err: NodeJS.ErrnoException | null) => void;
|
||||
type MockedFetch = ReturnType<typeof vi.fn>;
|
||||
|
||||
vi.mock('node:dns');
|
||||
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe('Network utils', () => {
|
||||
describe('isOnline', () => {
|
||||
it('should return true when online', async () => {
|
||||
vi.mocked(dns.resolve).mockImplementationOnce(((
|
||||
_: string,
|
||||
callback: DnsCallback,
|
||||
) => {
|
||||
callback(null);
|
||||
}) as typeof dns.resolve);
|
||||
|
||||
const result = await isOnline();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false on DNS error', async () => {
|
||||
vi.mocked(dns.resolve).mockImplementationOnce(((
|
||||
_: string,
|
||||
callback: DnsCallback,
|
||||
) => {
|
||||
callback(new Error('ENOTFOUND'));
|
||||
}) as typeof dns.resolve);
|
||||
|
||||
const result = await isOnline();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false on timeout', async () => {
|
||||
vi.mocked(dns.resolve).mockImplementationOnce(() => {
|
||||
// Simulate timeout by not calling callback
|
||||
});
|
||||
|
||||
const result = await isOnline();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle dns.resolve errors gracefully', async () => {
|
||||
vi.mocked(dns.resolve).mockImplementationOnce(() => {
|
||||
throw new Error('DNS resolution failed');
|
||||
});
|
||||
|
||||
const result = await isOnline();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchCached', () => {
|
||||
it('should fetch from network when online', async () => {
|
||||
vi.mocked(dns.resolve).mockImplementationOnce(((
|
||||
_: string,
|
||||
callback: DnsCallback,
|
||||
) => {
|
||||
callback(null);
|
||||
}) as typeof dns.resolve);
|
||||
|
||||
const mockConfig = {
|
||||
fsCache: {
|
||||
set: vi.fn(),
|
||||
get: vi.fn(),
|
||||
},
|
||||
logger: {
|
||||
debug: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const mockContent = 'cached content';
|
||||
(global.fetch as MockedFetch).mockReturnValueOnce(
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
text: async () => mockContent,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchCached(
|
||||
'https://example.com',
|
||||
mockConfig as unknown as ResolvedConfig,
|
||||
);
|
||||
|
||||
expect(result).toBe(mockContent);
|
||||
expect(mockConfig.fsCache.set).toHaveBeenCalledWith(
|
||||
'https://example.com',
|
||||
mockContent,
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to cache when network fails', async () => {
|
||||
vi.mocked(dns.resolve).mockImplementationOnce(((
|
||||
_: string,
|
||||
callback: DnsCallback,
|
||||
) => {
|
||||
callback(null);
|
||||
}) as typeof dns.resolve);
|
||||
|
||||
const mockConfig = {
|
||||
fsCache: {
|
||||
set: vi.fn(),
|
||||
get: vi.fn().mockResolvedValueOnce('from cache'),
|
||||
},
|
||||
logger: {
|
||||
debug: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
(global.fetch as MockedFetch).mockReturnValueOnce(
|
||||
Promise.resolve({
|
||||
status: 500,
|
||||
text: async () => '',
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchCached(
|
||||
'https://example.com',
|
||||
mockConfig as unknown as ResolvedConfig,
|
||||
);
|
||||
|
||||
expect(result).toBe('from cache');
|
||||
expect(mockConfig.logger.debug).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Failed to download'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use cache when offline', async () => {
|
||||
vi.mocked(dns.resolve).mockImplementationOnce(((
|
||||
_: string,
|
||||
callback: DnsCallback,
|
||||
) => {
|
||||
callback(new Error('ENOTFOUND'));
|
||||
}) as typeof dns.resolve);
|
||||
|
||||
const mockConfig = {
|
||||
fsCache: {
|
||||
set: vi.fn(),
|
||||
get: vi.fn().mockResolvedValueOnce('offline cache'),
|
||||
},
|
||||
logger: {
|
||||
debug: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await fetchCached(
|
||||
'https://example.com',
|
||||
mockConfig as any,
|
||||
);
|
||||
|
||||
expect(result).toBe('offline cache');
|
||||
expect(global.fetch as MockedFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when offline and no cache available', async () => {
|
||||
vi.mocked(dns.resolve).mockImplementationOnce(((
|
||||
_: string,
|
||||
callback: DnsCallback,
|
||||
) => {
|
||||
callback(new Error('ENOTFOUND'));
|
||||
}) as typeof dns.resolve);
|
||||
|
||||
const mockConfig = {
|
||||
fsCache: {
|
||||
set: vi.fn(),
|
||||
get: vi.fn().mockResolvedValueOnce(null),
|
||||
},
|
||||
logger: {
|
||||
debug: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
fetchCached(
|
||||
'https://example.com',
|
||||
mockConfig as unknown as ResolvedConfig,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'Offline and "https://example.com" has not been cached. Try again when online.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -112,7 +112,9 @@ describe('findEntrypoints', () => {
|
||||
name: 'options',
|
||||
inputPath: resolve(config.entrypointsDir, 'options.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
options: {
|
||||
title: 'Default Title',
|
||||
},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
@@ -133,6 +135,7 @@ describe('findEntrypoints', () => {
|
||||
outputDir: config.outDir,
|
||||
options: {
|
||||
openInTab: true,
|
||||
title: 'Title',
|
||||
},
|
||||
skipped: false,
|
||||
},
|
||||
@@ -150,6 +153,33 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it('should extract wxt.* meta tags from HTML entrypoints', async () => {
|
||||
const path = 'popup.html';
|
||||
const content = `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="manifest.default_icon" content="{ '16': '/icon/16.png' }" />
|
||||
<meta name="wxt.custom_option" content="custom_value" />
|
||||
<meta name="wxt.anotherOption" content="true" />
|
||||
<title>Test Title</title>
|
||||
</head>
|
||||
</html>
|
||||
`;
|
||||
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
readFileMock.mockResolvedValueOnce(content);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0].options).toMatchObject({
|
||||
defaultIcon: { '16': '/icon/16.png' },
|
||||
customOption: 'custom_value',
|
||||
anotherOption: true,
|
||||
defaultTitle: 'Test Title',
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, Omit<ContentScriptEntrypoint, 'options'>]>([
|
||||
[
|
||||
'content.ts',
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
SidepanelEntrypoint,
|
||||
MainWorldContentScriptEntrypointOptions,
|
||||
IsolatedWorldContentScriptEntrypointOptions,
|
||||
UnlistedScriptEntrypoint,
|
||||
} from '../../../types';
|
||||
import fs from 'fs-extra';
|
||||
import { minimatch } from 'minimatch';
|
||||
@@ -115,20 +116,14 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
|
||||
...info,
|
||||
type,
|
||||
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options: {
|
||||
include: options.include,
|
||||
exclude: options.exclude,
|
||||
},
|
||||
options,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...info,
|
||||
type,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: {
|
||||
include: options.include,
|
||||
exclude: options.exclude,
|
||||
},
|
||||
options,
|
||||
};
|
||||
}
|
||||
}),
|
||||
@@ -200,7 +195,7 @@ async function importEntrypoints(infos: EntrypointInfo[]) {
|
||||
return resMap;
|
||||
}
|
||||
|
||||
/** Extract `manifest.` 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>> {
|
||||
@@ -214,9 +209,16 @@ async function importHtmlEntrypoint(
|
||||
|
||||
metaTags.forEach((tag) => {
|
||||
const name = tag.name;
|
||||
if (!name.startsWith('manifest.')) return;
|
||||
let key: string;
|
||||
|
||||
if (name.startsWith('manifest.')) {
|
||||
key = camelCase(name.slice(9));
|
||||
} else if (name.startsWith('wxt.')) {
|
||||
key = camelCase(name.slice(4));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = camelCase(name.slice(9));
|
||||
try {
|
||||
res[key] = JSON5.parse(tag.content);
|
||||
} catch {
|
||||
@@ -266,24 +268,20 @@ async function getPopupEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
options: Record<string, any>,
|
||||
): Promise<PopupEntrypoint> {
|
||||
// Extract non-per-browser options
|
||||
const { themeIcons, title, type, ...perBrowserOptions } = options;
|
||||
|
||||
const strictOptions: PopupEntrypoint['options'] = resolvePerBrowserOptions(
|
||||
{
|
||||
browserStyle: options.browserStyle,
|
||||
exclude: options.exclude,
|
||||
include: options.include,
|
||||
defaultIcon: options.defaultIcon,
|
||||
defaultTitle: options.title,
|
||||
mv2Key: options.type,
|
||||
defaultArea: options.defaultArea,
|
||||
...perBrowserOptions,
|
||||
defaultTitle: title,
|
||||
mv2Key: type,
|
||||
},
|
||||
wxt.config.browser,
|
||||
);
|
||||
if (strictOptions.mv2Key && strictOptions.mv2Key !== 'page_action')
|
||||
strictOptions.mv2Key = 'browser_action';
|
||||
|
||||
// themeIcons is an array of objects, not a per-browser option
|
||||
const themeIcons = options.themeIcons;
|
||||
|
||||
return {
|
||||
type: 'popup',
|
||||
name: 'popup',
|
||||
@@ -300,16 +298,7 @@ async function getOptionsEntrypoint(
|
||||
return {
|
||||
type: 'options',
|
||||
name: 'options',
|
||||
options: resolvePerBrowserOptions(
|
||||
{
|
||||
browserStyle: options.browserStyle,
|
||||
chromeStyle: options.chromeStyle,
|
||||
exclude: options.exclude,
|
||||
include: options.include,
|
||||
openInTab: options.openInTab,
|
||||
},
|
||||
wxt.config.browser,
|
||||
),
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
};
|
||||
@@ -324,29 +313,20 @@ async function getUnlistedPageEntrypoint(
|
||||
name: info.name,
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: {
|
||||
include: options.include,
|
||||
exclude: options.exclude,
|
||||
},
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
async function getUnlistedScriptEntrypoint(
|
||||
{ inputPath, name }: EntrypointInfo,
|
||||
options: Record<string, any>,
|
||||
): Promise<GenericEntrypoint> {
|
||||
): Promise<UnlistedScriptEntrypoint> {
|
||||
return {
|
||||
type: 'unlisted-script',
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: resolvePerBrowserOptions(
|
||||
{
|
||||
include: options.include,
|
||||
exclude: options.exclude,
|
||||
},
|
||||
wxt.config.browser,
|
||||
),
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -355,15 +335,7 @@ async function getBackgroundEntrypoint(
|
||||
options: Record<string, any>,
|
||||
): Promise<BackgroundEntrypoint> {
|
||||
const strictOptions: BackgroundEntrypoint['options'] =
|
||||
resolvePerBrowserOptions(
|
||||
{
|
||||
include: options.include,
|
||||
exclude: options.exclude,
|
||||
persistent: options.persistent,
|
||||
type: options.type,
|
||||
},
|
||||
wxt.config.browser,
|
||||
);
|
||||
resolvePerBrowserOptions(options, wxt.config.browser);
|
||||
|
||||
if (wxt.config.manifestVersion !== 3) {
|
||||
delete strictOptions.type;
|
||||
@@ -400,17 +372,16 @@ async function getSidepanelEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
options: Record<string, any>,
|
||||
): Promise<SidepanelEntrypoint> {
|
||||
// Extract non-per-browser options and rename title to defaultTitle
|
||||
const { title, ...perBrowserOptions } = options;
|
||||
|
||||
return {
|
||||
type: 'sidepanel',
|
||||
name: info.name,
|
||||
options: resolvePerBrowserOptions(
|
||||
{
|
||||
browserStyle: options.browserStyle,
|
||||
exclude: options.exclude,
|
||||
include: options.include,
|
||||
defaultIcon: options.defaultIcon,
|
||||
defaultTitle: options.title,
|
||||
openAtInstall: options.openAtInstall,
|
||||
...perBrowserOptions,
|
||||
defaultTitle: title,
|
||||
},
|
||||
wxt.config.browser,
|
||||
),
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import dns from 'node:dns';
|
||||
import { ResolvedConfig } from '../../types';
|
||||
import { withTimeout } from './time';
|
||||
import consola from 'consola';
|
||||
|
||||
function isOffline(): Promise<boolean> {
|
||||
const isOffline = new Promise<boolean>((res) => {
|
||||
dns.resolve('google.com', (err) => res(err != null));
|
||||
});
|
||||
return withTimeout(isOffline, 1e3).catch(() => true);
|
||||
async function isOffline(): Promise<boolean> {
|
||||
try {
|
||||
const isOffline = new Promise<boolean>((res) => {
|
||||
dns.resolve('google.com', (err) => res(err != null));
|
||||
});
|
||||
return await withTimeout(isOffline, 1e3);
|
||||
} catch (error) {
|
||||
consola.error('Error checking offline status:', error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function isOnline(): Promise<boolean> {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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/
|
||||
@@ -684,6 +705,7 @@ export interface PopupEntrypointOptions extends BaseEntrypointOptions {
|
||||
}
|
||||
|
||||
export interface OptionsEntrypointOptions extends BaseEntrypointOptions {
|
||||
title?: string;
|
||||
openInTab?: PerBrowserOption<boolean>;
|
||||
browserStyle?: PerBrowserOption<boolean>;
|
||||
chromeStyle?: PerBrowserOption<boolean>;
|
||||
@@ -748,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>;
|
||||
@@ -785,6 +811,7 @@ export interface SidepanelEntrypoint extends BaseEntrypoint {
|
||||
export type Entrypoint =
|
||||
| GenericEntrypoint
|
||||
| BackgroundEntrypoint
|
||||
| UnlistedScriptEntrypoint
|
||||
| ContentScriptEntrypoint
|
||||
| PopupEntrypoint
|
||||
| OptionsEntrypoint
|
||||
@@ -834,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.
|
||||
*
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
import appConfig from 'virtual:app-config';
|
||||
import type { WxtAppConfig } from './define-app-config';
|
||||
|
||||
export function useAppConfig(): WxtAppConfig {
|
||||
/**
|
||||
* Get runtime config defined in `<srcDir>/app.config.ts`
|
||||
*
|
||||
* @see https://wxt.dev/guide/essentials/config/runtime.html
|
||||
*/
|
||||
export function getAppConfig(): WxtAppConfig {
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for {@link getAppConfig}.
|
||||
*
|
||||
* @see https://wxt.dev/guide/essentials/config/runtime.html
|
||||
*/
|
||||
export function useAppConfig(): WxtAppConfig {
|
||||
return getAppConfig();
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { ContentScriptContext } from '../content-script-context';
|
||||
import { WxtLocationChangeEvent } from './custom-events';
|
||||
|
||||
const supportsNavigationApi =
|
||||
typeof (globalThis as any).navigation?.addEventListener === 'function';
|
||||
|
||||
/**
|
||||
* Create a util that watches for URL changes, dispatching the custom event when detected. Stops
|
||||
* watching when content script is invalidated.
|
||||
* watching when content script is invalidated. Uses Navigation API when available, otherwise
|
||||
* falls back to polling.
|
||||
*/
|
||||
export function createLocationWatcher(ctx: ContentScriptContext) {
|
||||
let interval: number | undefined;
|
||||
let oldUrl: URL;
|
||||
let lastUrl: URL;
|
||||
let watching = false;
|
||||
|
||||
return {
|
||||
/**
|
||||
@@ -15,16 +19,30 @@ export function createLocationWatcher(ctx: ContentScriptContext) {
|
||||
* this is a noop.
|
||||
*/
|
||||
run() {
|
||||
if (interval != null) return;
|
||||
if (watching) return;
|
||||
watching = true;
|
||||
lastUrl = new URL(location.href);
|
||||
|
||||
oldUrl = new URL(location.href);
|
||||
interval = ctx.setInterval(() => {
|
||||
let newUrl = new URL(location.href);
|
||||
if (newUrl.href !== oldUrl.href) {
|
||||
window.dispatchEvent(new WxtLocationChangeEvent(newUrl, oldUrl));
|
||||
oldUrl = newUrl;
|
||||
}
|
||||
}, 1e3);
|
||||
if (supportsNavigationApi) {
|
||||
(globalThis as any).navigation.addEventListener(
|
||||
'navigate',
|
||||
(event: any) => {
|
||||
const newUrl = new URL(event.destination.url);
|
||||
if (newUrl.href === lastUrl.href) return;
|
||||
window.dispatchEvent(new WxtLocationChangeEvent(newUrl, lastUrl));
|
||||
lastUrl = newUrl;
|
||||
},
|
||||
{ signal: ctx.signal },
|
||||
);
|
||||
} else {
|
||||
ctx.setInterval(() => {
|
||||
const newUrl = new URL(location.href);
|
||||
if (newUrl.href !== lastUrl.href) {
|
||||
window.dispatchEvent(new WxtLocationChangeEvent(newUrl, lastUrl));
|
||||
lastUrl = newUrl;
|
||||
}
|
||||
}, 1e3);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ export async function setup() {
|
||||
|
||||
setupHappened = true;
|
||||
|
||||
// @ts-expect-error
|
||||
globalThis.__ENTRYPOINT__ = 'test';
|
||||
|
||||
const e2eDistPath = './e2e/dist/';
|
||||
|
||||
Generated
+30
-5
@@ -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
|
||||
@@ -173,8 +179,8 @@ importers:
|
||||
version: 1.2.16
|
||||
devDependencies:
|
||||
'@types/chrome':
|
||||
specifier: 0.1.36
|
||||
version: 0.1.36
|
||||
specifier: 0.1.37
|
||||
version: 0.1.37
|
||||
fs-extra:
|
||||
specifier: ^11.3.3
|
||||
version: 11.3.3
|
||||
@@ -225,6 +231,25 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../wxt
|
||||
|
||||
packages/is-background:
|
||||
dependencies:
|
||||
'@wxt-dev/browser':
|
||||
specifier: workspace:^
|
||||
version: link:../browser
|
||||
devDependencies:
|
||||
oxlint:
|
||||
specifier: ^1.43.0
|
||||
version: 1.43.0
|
||||
publint:
|
||||
specifier: ^0.3.17
|
||||
version: 0.3.17
|
||||
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)
|
||||
|
||||
packages/module-react:
|
||||
dependencies:
|
||||
'@vitejs/plugin-react':
|
||||
@@ -1904,8 +1929,8 @@ packages:
|
||||
'@types/chai@5.2.3':
|
||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||
|
||||
'@types/chrome@0.1.36':
|
||||
resolution: {integrity: sha512-BvHbuyGttYXnGt5Gpwa4769KIinKHY1iLjlAPrrMBS2GI9m/XNMPtdsq0NgQalyuUdxvlMN/0OyGw0shFVIoUQ==}
|
||||
'@types/chrome@0.1.37':
|
||||
resolution: {integrity: sha512-IJE4ceuDO7lrEuua7Pow47zwNcI8E6qqkowRP7aFPaZ0lrjxh6y836OPqqkIZeTX64FTogbw+4RNH0+QrweCTQ==}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
@@ -5910,7 +5935,7 @@ snapshots:
|
||||
'@types/deep-eql': 4.0.2
|
||||
assertion-error: 2.0.1
|
||||
|
||||
'@types/chrome@0.1.36':
|
||||
'@types/chrome@0.1.37':
|
||||
dependencies:
|
||||
'@types/filesystem': 0.0.36
|
||||
'@types/har-format': 1.2.16
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user