Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9205bb08f | |||
| 05e23e70b4 | |||
| 68fa36ef2b | |||
| 182d575205 | |||
| 09eadf6796 | |||
| a6904b6319 | |||
| 42d65cb10b | |||
| 636cf1f841 | |||
| 3b154508bd | |||
| d34eac9a4c | |||
| 54c6fcca10 | |||
| 0114ee6a7a | |||
| 51dd803115 | |||
| 6e39424a30 | |||
| 6ca84782aa | |||
| ee1ba6369e | |||
| f35d70e6fe | |||
| 430a3acfeb | |||
| 395c5841f9 | |||
| 173161ca4d | |||
| a1a8e14368 | |||
| 2379806c61 | |||
| f604208a8b | |||
| cf31fbec5d | |||
| 279be8deb7 | |||
| 7366d8aab7 | |||
| a7ab409d48 | |||
| 9dea0cf3ae | |||
| 50d7c1ec80 | |||
| f32cdafdf6 | |||
| 2e2272d07a | |||
| 27ffff7e0a | |||
| 305cebe0d0 | |||
| 4602a068f8 | |||
| 3a947d72c6 | |||
| 1940e3530f | |||
| dd549dc686 | |||
| 3417bd3ed2 | |||
| 9470d2b696 | |||
| f523a7d31d | |||
| ea4c2b035b | |||
| c5bfba837f | |||
| c5ad957b25 | |||
| cc849e7cea | |||
| 22428da54d | |||
| f6071f7e2d | |||
| 08e8560865 | |||
| 75f01db543 | |||
| 5a028d2551 | |||
| 07a4ba8bc5 | |||
| f84921f735 | |||
| 872494e490 | |||
| d0848b0be5 | |||
| c848956777 | |||
| 296997d10e | |||
| ffdb22238c | |||
| a63c320403 | |||
| aa86ed78a3 | |||
| 19456ec855 | |||
| 2c57840c44 | |||
| 26b7927f62 | |||
| d4267af725 | |||
| 16a2aedcd0 | |||
| 755f04f777 | |||
| cf9539ddab | |||
| 99d558f33c | |||
| 1fe073004b | |||
| 6ae5fb5edb | |||
| 3a3953edf6 | |||
| c8e788eaf8 | |||
| 088a44496d | |||
| 60a57c5abc | |||
| 482466bbbf | |||
| 65deb7317b | |||
| 00ae061822 | |||
| 9bd7fc1bd5 | |||
| 98ddb83f1d | |||
| 6dd8b9d072 |
@@ -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,7 +10,7 @@ jobs:
|
||||
if: github.repository_owner == 'wxt-dev'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
if: ${{ github.repository == 'wxt-dev/wxt' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Only fetch the config file from the repository
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
@@ -10,6 +10,7 @@ on:
|
||||
- analytics
|
||||
- auto-icons
|
||||
- i18n
|
||||
- is-background
|
||||
- module-react
|
||||
- module-solid
|
||||
- module-svelte
|
||||
@@ -39,7 +40,7 @@ jobs:
|
||||
- validate
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ssh-key: ${{ secrets.DEPLOY_KEY }} # https://github.com/sbellone/release-workflow-example
|
||||
|
||||
@@ -10,6 +10,7 @@ on:
|
||||
- analytics
|
||||
- auto-icons
|
||||
- i18n
|
||||
- is-background
|
||||
- module-react
|
||||
- module-solid
|
||||
- module-svelte
|
||||
@@ -30,7 +31,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
id-token: write # OIDC for NPM publishing
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ssh-key: ${{ secrets.DEPLOY_KEY }}
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Commit Changes
|
||||
id: commit
|
||||
uses: stefanzweifel/git-auto-commit-action@01d77ca6cb089da1360e540865f7d035c95aa199 # v6.0.1
|
||||
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -68,13 +68,13 @@ jobs:
|
||||
coverage: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@22457c87c1b161cf7dde222c3e82b2b5f8d2bed2 # v2.0.2
|
||||
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
|
||||
|
||||
- name: Run Tests
|
||||
if: ${{ ! matrix.coverage }}
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
- name: Upload Coverage
|
||||
if: matrix.coverage
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
- vue
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
path: 'docs/tapes/init-demo.tape'
|
||||
|
||||
- name: Save recorded GIF
|
||||
uses: stefanzweifel/git-auto-commit-action@01d77ca6cb089da1360e540865f7d035c95aa199 # v6.0.1
|
||||
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
Vendored
+2
-1
@@ -10,5 +10,6 @@
|
||||
// Additional guidelines for Copilot
|
||||
"github.copilot.chat.codeGeneration.instructions": [
|
||||
{ "file": "CONTRIBUTING.md" }
|
||||
]
|
||||
],
|
||||
"oxc.enable": true
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# Maintainers
|
||||
|
||||
A couple of things for you to consider before merging a PR or giving the go-ahead on a feature proposal.
|
||||
|
||||
## Be picky about new features and packages
|
||||
|
||||
We are responsible for maintaining them and fixing related bugs after the PR is merged. The community can always release their own WXT modules or packages, not everything needs to be built into WXT.
|
||||
|
||||
## Prefer standards over customization
|
||||
|
||||
Don't merge PRs that just add another way to do something, like [this one](https://github.com/wxt-dev/wxt/pull/2053#issuecomment-3857010196).
|
||||
|
||||
WXT is opinionated, if you have questions about what is WXT's opinion or we need to create a new one, create an issue and @ me to discuss.
|
||||
|
||||
## PRs should be small and targeted
|
||||
|
||||
A PR should make one change. They should not make any unrelated changes outside of accomplishing the one thing. This makes PRs easier to review and they get merged more quickly - a win-win for everyone.
|
||||
|
||||
## `@wxt-dev/*` packages are separate
|
||||
|
||||
We can't make changes to these packages assuming people are using them only with WXT. For example, I almost missed [this PR](https://github.com/wxt-dev/wxt/pull/2049#issuecomment-3861251599).
|
||||
|
||||
I want these packages to be usable on their own so if people don't like WXT's build tool, they can still use our other awesome packages.
|
||||
|
||||
## Minimize dependencies
|
||||
|
||||
I don't like how heavy lots of frameworks are. It's unavoidable to a certain extent, but if you can do something without another dependency, try to.
|
||||
|
||||
If you need a dependency, look for one with zero dependencies and that's respectable.
|
||||
|
||||
## Look at milestones
|
||||
|
||||
I've organized WXT's long term plans into [milestones](https://github.com/wxt-dev/wxt/milestones), check those out to get an idea of my priorities for some of the larger features.
|
||||
|
||||
## Require tests
|
||||
|
||||
If someone opens a PR to fix a bug but doesn't include tests, don't merge the PR. Tests are required to prevent regressions and maintain a codebase long term.
|
||||
|
||||
## Ask for reproductions for bugs
|
||||
|
||||
You don't need to triage bugs if someone doesn't give you enough information. You can always ask for a repo with a reproduction or wait for more details.
|
||||
|
||||
If there's not an easy way to reproduce a bug, you're wasting your time triaging it. Just be mindful of your own time!
|
||||
|
||||
Here's an example of how to ask for a reproduction: <https://github.com/wxt-dev/wxt/issues/2064#issuecomment-3862579110>. You could be more blunt than this.
|
||||
|
||||
## 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.
|
||||
@@ -116,6 +116,9 @@ const chromeExtensionIds = [
|
||||
'jmghclbfbbapimhbgnpffbimphlpolnm', // Teams Chat Exporter
|
||||
'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,
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
import { Feed } from 'feed';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import llmstxt from 'vitepress-plugin-llms';
|
||||
|
||||
const origin = 'https://wxt.dev';
|
||||
|
||||
@@ -43,6 +45,7 @@ const otherPackages = {
|
||||
storage: storageVersion,
|
||||
unocss: unocssVersion,
|
||||
runner: runnerVersion,
|
||||
'is-background': isBackgroundVersion,
|
||||
};
|
||||
|
||||
const knowledge = addKnowledge<DefaultTheme.Config>({
|
||||
@@ -71,6 +74,7 @@ export default defineConfig({
|
||||
vite: {
|
||||
clearScreen: false,
|
||||
plugins: [
|
||||
llmstxt(),
|
||||
groupIconVitePlugin({
|
||||
customIcon: {
|
||||
'wxt.config.ts': localIconLoader(
|
||||
|
||||
@@ -105,3 +105,24 @@ export default defineWebExtConfig({
|
||||
disabled: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Enabling Chrome Features
|
||||
|
||||
Some APIs are disabled in Chrome during development because of the default flags `web-ext` uses to launch Chrome, like the [Prompt API](https://developer.chrome.com/docs/ai/prompt-api).
|
||||
|
||||
If your extension depends on new features or services, you can enable them via `chromiumArgs`:
|
||||
|
||||
```ts
|
||||
import { defineWebExtConfig } from 'wxt';
|
||||
|
||||
export default defineWebExtConfig({
|
||||
chromiumArgs: [
|
||||
// For example, this flag enables the Prompt API
|
||||
'--disable-features=DisableLoadExtensionCommandLineSwitch',
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
There is no comprehensive list of what feature flags enable what APIs and services.
|
||||
|
||||
Alternatively, if you can't find a flag that enables a feature you're looking for, [disable the opening the browser during development](#disable-opening-browser) and use your regular chrome profile for development.
|
||||
|
||||
@@ -20,6 +20,8 @@ export default defineConfig({
|
||||
|
||||
Most hooks provide the `wxt` object as the first argument. It contains the resolved config and other info about the current build. The other arguments can be modified by reference to change different parts of the build system.
|
||||
|
||||
You can find the [list of all available hooks](/api/reference/wxt/interfaces/WxtHooks) in the API reference.
|
||||
|
||||
Putting one-off hooks like this in your config file is simple, but if you find yourself writing lots of hooks, you should extract them into [WXT Modules](/guide/essentials/wxt-modules) instead.
|
||||
|
||||
## Execution Order
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -470,6 +470,19 @@ When you define a Newtab entrypoint, WXT will automatically update the manifest
|
||||
/>
|
||||
<meta name="manifest.type" content="page_action|browser_action" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
<!-- Firefox only: where to place the action button -->
|
||||
<meta
|
||||
name="manifest.default_area"
|
||||
content="navbar|menupanel|tabstrip|personaltoolbar"
|
||||
/>
|
||||
<!-- Firefox only: icons for light/dark themes -->
|
||||
<meta
|
||||
name="manifest.theme_icons"
|
||||
content="[
|
||||
{ light: '/icon-light-16.png', dark: '/icon-dark-16.png', size: 16 },
|
||||
{ light: '/icon-light-32.png', dark: '/icon-dark-32.png', size: 32 }
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
|
||||
@@ -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.
|
||||
@@ -66,7 +70,7 @@ Each module's setup function is executed after the `wxt.config.ts` file is loade
|
||||
|
||||
Refer to the [API reference](/api/reference/wxt/interfaces/Wxt) for a complete list of properties and functions available.
|
||||
|
||||
Also to make sure and read about all the [hooks that are available](https://wxt.dev/api/reference/wxt/interfaces/WxtHooks) - they are essential to writing modules.
|
||||
Also make sure to read about [all the hooks that are available](/api/reference/wxt/interfaces/WxtHooks) - they are essential to writing modules.
|
||||
|
||||
### Recipes
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ This page is dedicated to all the awesome people how have made something for WXT
|
||||
## Blog Posts
|
||||
|
||||
- [Building Modern Cross Browser Web Extensions](https://aabidk.dev/tags/wxt/) by Aabid ([@aabidk20](https://github.com/aabidk20))
|
||||
- [Building Browser Extensions with WXT](https://rxliuli.com/blog/browser-extension-dev-01-introduction-to-basic-concepts/) by rxliuli ([@rxliuli](https://github.com/rxliuli))
|
||||
|
||||
## NPM Packages
|
||||
|
||||
- [`@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)
|
||||
|
||||
@@ -164,6 +164,10 @@ Both issues have the same cause: the library puts something outside the `ShadowR
|
||||
|
||||
Both issues have the same fix: tell the library to put elements inside the `ShadowRoot`, not outside it. See the details above for more information and example fixes for each problem.
|
||||
|
||||
## Does WXT provide docs for LLMs?
|
||||
|
||||
Yes, WXT's documentation provides markdown files based on the [the /llms.txt proposal](https://llmstxt.org/).
|
||||
|
||||
## Is there an LLM trained on WXT's docs that I chat with?
|
||||
|
||||
Yes! There's a "Ask AI" button in the bottom right of the page, try it out! Or visit <https://knowledge.wxt.dev/> for a fullscreen experience.
|
||||
@@ -186,3 +190,33 @@ To run the WXT dev server in a devcontainer, but load the dev build of your exte
|
||||
|
||||
3. **Tell WXT to listen on all network interfaces**
|
||||
To enable hot-reloading, your extension has to connect to the WXT dev server running inside your container. WXT will only listen on `localhost` by default, which prevents connections from outside the devcontainer. To fix this you can instruct WXT to listen on all interfaces with `wxt --host 0.0.0.0`.
|
||||
|
||||
## How do I use the new Prompt API in Chrome?
|
||||
|
||||
The service responsible for the [Prompt API](https://developer.chrome.com/docs/ai/prompt-api) is not enabled by default if you let WXT open the browser during dev mode. When checking `LanguageModel.availability`, you will always receive "unavailable".
|
||||
|
||||
You have two options:
|
||||
|
||||
1. Pass the `--disable-features=DisableLoadExtensionCommandLineSwitch` feature flag to enable the service in the browser WXT opens:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
webExt: {
|
||||
chromiumArgs: [
|
||||
'--disable-features=DisableLoadExtensionCommandLineSwitch',
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
2. Disable the runner and install your extension in your regular chrome profile manually:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
webExt: {
|
||||
disabled: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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
-2
@@ -4,13 +4,12 @@
|
||||
"engines": {
|
||||
"node": ">=18.20.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.15.1",
|
||||
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8",
|
||||
"scripts": {
|
||||
"check": "check && pnpm -r --sequential run check",
|
||||
"test": "pnpm -r --sequential run test run",
|
||||
"test:coverage": "pnpm -r --sequential run test:coverage",
|
||||
"prepare": "simple-git-hooks",
|
||||
"prepublish": "pnpm -s build",
|
||||
"docs:gen": "typedoc --options docs/typedoc.json",
|
||||
"docs:dev": "pnpm -s docs:gen && vitepress dev docs",
|
||||
"docs:build": "pnpm -s docs:gen && vitepress build docs",
|
||||
@@ -47,6 +46,7 @@
|
||||
"vitepress": "^1.6.4",
|
||||
"vitepress-knowledge": "^0.4.1",
|
||||
"vitepress-plugin-group-icons": "^1.7.1",
|
||||
"vitepress-plugin-llms": "^1.11.0",
|
||||
"vitest-mock-extended": "^3.1.0",
|
||||
"vue": "^3.5.27",
|
||||
"wxt": "workspace:*"
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# Changelog
|
||||
|
||||
## v0.5.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.1...analytics-v0.5.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Normalize path for createAnalytics of analytics/index.ts ([#2013](https://github.com/wxt-dev/wxt/pull/2013))
|
||||
- Allow custom API URL in Google Analytics 4 provider options ([#1653](https://github.com/wxt-dev/wxt/pull/1653))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Code cleanup in analytics package ([#2084](https://github.com/wxt-dev/wxt/pull/2084))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Fix other type error after `chrome` types update ([31ebf966](https://github.com/wxt-dev/wxt/commit/31ebf966))
|
||||
- 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
|
||||
|
||||
- Honwhy Wang <honwhy.wang@gmail.com>
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
|
||||
|
||||
## v0.5.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.0...analytics-v0.5.1)
|
||||
|
||||
@@ -2,17 +2,48 @@ 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';
|
||||
|
||||
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';
|
||||
|
||||
const INTERACTIVE_TAGS = new Set([
|
||||
'A',
|
||||
'BUTTON',
|
||||
'INPUT',
|
||||
'SELECT',
|
||||
'TEXTAREA',
|
||||
]);
|
||||
const INTERACTIVE_ROLES = new Set([
|
||||
'button',
|
||||
'link',
|
||||
'checkbox',
|
||||
'menuitem',
|
||||
'tab',
|
||||
'radio',
|
||||
]);
|
||||
|
||||
export function createAnalytics(config?: AnalyticsConfig): Analytics {
|
||||
if (!browser?.runtime?.id)
|
||||
throw Error(
|
||||
@@ -62,8 +93,8 @@ function createBackgroundAnalytics(
|
||||
|
||||
const getBackgroundMeta = () => ({
|
||||
timestamp: Date.now(),
|
||||
// Don't track sessions for the background, it can be running
|
||||
// indefinitely, and will inflate session duration stats.
|
||||
// Don't track sessions for the background, it can be running indefinitely
|
||||
// and will inflate session duration stats.
|
||||
sessionId: undefined,
|
||||
language: navigator.language,
|
||||
referrer: undefined,
|
||||
@@ -75,7 +106,7 @@ function createBackgroundAnalytics(
|
||||
const getBaseEvent = async (
|
||||
meta: AnalyticsEventMetadata,
|
||||
): Promise<BaseAnalyticsEvent> => {
|
||||
const platform = await platformInfo;
|
||||
const { arch, os } = await platformInfo;
|
||||
return {
|
||||
meta,
|
||||
user: {
|
||||
@@ -84,8 +115,8 @@ function createBackgroundAnalytics(
|
||||
version: config?.version ?? manifest.version_name ?? manifest.version,
|
||||
wxtMode: import.meta.env.MODE,
|
||||
wxtBrowser: import.meta.env.BROWSER,
|
||||
arch: platform.arch,
|
||||
os: platform.os,
|
||||
arch,
|
||||
os,
|
||||
browser: userAgent.browser.name,
|
||||
browserVersion: userAgent.browser.version,
|
||||
...(await userProperties),
|
||||
@@ -147,7 +178,7 @@ function createBackgroundAnalytics(
|
||||
},
|
||||
track: async (
|
||||
eventName: string,
|
||||
eventProperties?: Record<string, string>,
|
||||
eventProperties?: Record<string, string | undefined>,
|
||||
meta: AnalyticsEventMetadata = getBackgroundMeta(),
|
||||
) => {
|
||||
const baseEvent = await getBaseEvent(meta);
|
||||
@@ -181,9 +212,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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -201,17 +231,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()] });
|
||||
};
|
||||
|
||||
@@ -222,11 +250,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;
|
||||
|
||||
@@ -235,7 +263,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 });
|
||||
@@ -247,34 +275,23 @@ 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 }),
|
||||
};
|
||||
}
|
||||
|
||||
const INTERACTIVE_TAGS = new Set([
|
||||
'A',
|
||||
'BUTTON',
|
||||
'INPUT',
|
||||
'SELECT',
|
||||
'TEXTAREA',
|
||||
]);
|
||||
const INTERACTIVE_ROLES = new Set([
|
||||
'button',
|
||||
'link',
|
||||
'checkbox',
|
||||
'menuitem',
|
||||
'tab',
|
||||
'radio',
|
||||
]);
|
||||
|
||||
export function defineAnalyticsProvider<T = never>(
|
||||
definition: (
|
||||
/** The analytics object. */
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'wxt/modules';
|
||||
import { relative, resolve } from 'node:path';
|
||||
import type { AnalyticsConfig } from './types';
|
||||
import { normalizePath } from 'wxt';
|
||||
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
@@ -38,17 +39,15 @@ export default defineWxtModule({
|
||||
});
|
||||
|
||||
// Generate #analytics module
|
||||
const wxtAnalyticsCode = [
|
||||
`import { createAnalytics } from '${
|
||||
process.env.NPM
|
||||
? clientModuleId
|
||||
: relative(wxtAnalyticsFolder, clientModuleId)
|
||||
}';`,
|
||||
`import { useAppConfig } from '#imports';`,
|
||||
``,
|
||||
`export const analytics = createAnalytics(useAppConfig().analytics);`,
|
||||
``,
|
||||
].join('\n');
|
||||
const wxtAnalyticsCode = `import { createAnalytics } from '${
|
||||
process.env.NPM
|
||||
? clientModuleId
|
||||
: normalizePath(relative(wxtAnalyticsFolder, clientModuleId))
|
||||
}';
|
||||
import { getAppConfig } from '#imports';
|
||||
|
||||
export const analytics = createAnalytics(getAppConfig().analytics);
|
||||
`;
|
||||
addAlias(wxt, '#analytics', wxtAnalyticsIndex);
|
||||
wxt.hook('prepare:types', async (_, entries) => {
|
||||
entries.push({
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { BaseAnalyticsEvent } from '../types';
|
||||
const DEFAULT_ENGAGEMENT_TIME_IN_MSEC = 100;
|
||||
|
||||
export interface GoogleAnalytics4ProviderOptions {
|
||||
apiUrl?: string;
|
||||
apiSecret: string;
|
||||
measurementId: string;
|
||||
}
|
||||
@@ -18,7 +19,7 @@ export const googleAnalytics4 =
|
||||
): Promise<void> => {
|
||||
const url = new URL(
|
||||
config?.debug ? '/debug/mp/collect' : '/mp/collect',
|
||||
'https://www.google-analytics.com',
|
||||
options.apiUrl ?? 'https://www.google-analytics.com',
|
||||
);
|
||||
if (options.apiSecret)
|
||||
url.searchParams.set('api_secret', options.apiSecret);
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wxt-dev/analytics",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"description": "Add analytics to your web extension",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { defineWxtModule } from 'wxt/modules';
|
||||
import { resolve, relative } from 'node:path';
|
||||
import defu from 'defu';
|
||||
import sharp from 'sharp';
|
||||
import { ensureDir, exists } from 'fs-extra';
|
||||
import { ensureDir, pathExists } from 'fs-extra';
|
||||
|
||||
export default defineWxtModule<AutoIconsOptions>({
|
||||
name: '@wxt-dev/auto-icons',
|
||||
@@ -37,7 +37,7 @@ export default defineWxtModule<AutoIconsOptions>({
|
||||
if (!parsedOptions.enabled)
|
||||
return wxt.logger.warn(`\`[auto-icons]\` ${this.name} disabled`);
|
||||
|
||||
if (!(await exists(resolvedPath))) {
|
||||
if (!(await pathExists(resolvedPath))) {
|
||||
return wxt.logger.warn(
|
||||
`\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`,
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -5,19 +5,19 @@ import { browser, type Browser } from '../index';
|
||||
describe('browser', () => {
|
||||
describe('types', () => {
|
||||
it('should provide types via the Browser import', () => {
|
||||
expectTypeOf<Browser.runtime.MessageSender>().toMatchTypeOf<chrome.runtime.MessageSender>();
|
||||
expectTypeOf<Browser.storage.AreaName>().toMatchTypeOf<chrome.storage.AreaName>();
|
||||
expectTypeOf<Browser.i18n.LanguageDetectionResult>().toMatchTypeOf<chrome.i18n.LanguageDetectionResult>();
|
||||
expectTypeOf<Browser.runtime.MessageSender>().toEqualTypeOf<chrome.runtime.MessageSender>();
|
||||
expectTypeOf<Browser.storage.AreaName>().toEqualTypeOf<chrome.storage.AreaName>();
|
||||
expectTypeOf<Browser.i18n.LanguageDetectionResult>().toEqualTypeOf<chrome.i18n.LanguageDetectionResult>();
|
||||
});
|
||||
|
||||
it('should provide values via the browser import', () => {
|
||||
expectTypeOf(browser.runtime.id).toMatchTypeOf<string>();
|
||||
expectTypeOf(browser.runtime.id).toEqualTypeOf<string>();
|
||||
expectTypeOf(
|
||||
browser.storage.local,
|
||||
).toMatchTypeOf<Browser.storage.StorageArea>();
|
||||
).toEqualTypeOf<Browser.storage.LocalStorageArea>();
|
||||
expectTypeOf(
|
||||
browser.i18n.detectLanguage('Hello, world!'),
|
||||
).resolves.toMatchTypeOf<chrome.i18n.LanguageDetectionResult>();
|
||||
browser.i18n.detectLanguage,
|
||||
).returns.resolves.toEqualTypeOf<chrome.i18n.LanguageDetectionResult>();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+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. */
|
||||
|
||||
@@ -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/**"]
|
||||
}
|
||||
@@ -114,7 +114,7 @@ async function findBrowserBinary(target: string): Promise<string | undefined> {
|
||||
for (const target of targets) {
|
||||
const potentialPaths = KNOWN_BROWSER_PATHS[target]?.[platform] ?? [];
|
||||
for (const path of potentialPaths) {
|
||||
if (await exists(path)) return path;
|
||||
if (await pathExists(path)) return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,7 +207,7 @@ function deduplicateArgs(
|
||||
return args;
|
||||
}
|
||||
|
||||
async function exists(path: string): Promise<boolean> {
|
||||
async function pathExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await open(path, 'r');
|
||||
return true;
|
||||
|
||||
@@ -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)(
|
||||
@@ -706,6 +709,47 @@ describe('Storage Utils', () => {
|
||||
expect(migrateToV3).toBeCalledWith(4);
|
||||
});
|
||||
|
||||
it('should set the version without running migrations for empty storage items', async () => {
|
||||
const migrate = vi.fn((n: number) => n * 2);
|
||||
|
||||
const item = storage.defineItem<number>('local:key', {
|
||||
init: () => 1,
|
||||
version: 2,
|
||||
migrations: {
|
||||
2: migrate,
|
||||
},
|
||||
});
|
||||
const value = await item.getValue();
|
||||
const meta = await item.getMeta();
|
||||
|
||||
expect(value).toEqual(1);
|
||||
expect(meta).toEqual({ v: 2 });
|
||||
expect(migrate).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should set the version and run migrations if a storage value already exists and v2 is added', async () => {
|
||||
const key = 'local:key';
|
||||
|
||||
// Mimic as if an initial value was set by a previous version. We expect
|
||||
// this value to be migrated and doubled to V2.
|
||||
await storage.setItem(key, 1);
|
||||
|
||||
const migrate = vi.fn((n: number) => n * 2);
|
||||
const item = storage.defineItem<number>(key, {
|
||||
init: () => 1,
|
||||
version: 2,
|
||||
migrations: {
|
||||
2: migrate,
|
||||
},
|
||||
});
|
||||
const value = await item.getValue();
|
||||
const meta = await item.getMeta();
|
||||
|
||||
expect(value).toEqual(2);
|
||||
expect(meta).toEqual({ v: 2 });
|
||||
expect(migrate).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onMigrationComplete callback function if defined', async () => {
|
||||
await fakeBrowser.storage.local.set({
|
||||
count: 2,
|
||||
@@ -908,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,
|
||||
@@ -939,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', () => {
|
||||
|
||||
+155
-24
@@ -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();
|
||||
|
||||
@@ -17,6 +17,7 @@ function createStorage(): WxtStorage {
|
||||
sync: createDriver('sync'),
|
||||
managed: createDriver('managed'),
|
||||
};
|
||||
|
||||
const getDriver = (area: StorageArea) => {
|
||||
const driver = drivers[area];
|
||||
if (driver == null) {
|
||||
@@ -25,14 +26,17 @@ function createStorage(): WxtStorage {
|
||||
}
|
||||
return driver;
|
||||
};
|
||||
|
||||
const resolveKey = (key: StorageItemKey) => {
|
||||
const deliminatorIndex = key.indexOf(':');
|
||||
const driverArea = key.substring(0, deliminatorIndex) as StorageArea;
|
||||
|
||||
const driverKey = key.substring(deliminatorIndex + 1);
|
||||
if (driverKey == null)
|
||||
if (driverKey == null) {
|
||||
throw Error(
|
||||
`Storage key should be in the form of "area:key", but received "${key}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
driverArea,
|
||||
@@ -40,17 +44,23 @@ function createStorage(): WxtStorage {
|
||||
driver: getDriver(driverArea),
|
||||
};
|
||||
};
|
||||
|
||||
const getMetaKey = (key: string) => key + '$';
|
||||
|
||||
const mergeMeta = (oldMeta: any, newMeta: any): any => {
|
||||
const newFields = { ...oldMeta };
|
||||
|
||||
Object.entries(newMeta).forEach(([key, value]) => {
|
||||
if (value == null) delete newFields[key];
|
||||
else newFields[key] = value;
|
||||
});
|
||||
|
||||
return newFields;
|
||||
};
|
||||
|
||||
const getValueOrFallback = (value: any, fallback: any) =>
|
||||
value ?? fallback ?? null;
|
||||
|
||||
const getMetaValue = (properties: any) =>
|
||||
typeof properties === 'object' && !Array.isArray(properties)
|
||||
? properties
|
||||
@@ -64,11 +74,13 @@ function createStorage(): WxtStorage {
|
||||
const res = await driver.getItem<any>(driverKey);
|
||||
return getValueOrFallback(res, opts?.fallback ?? opts?.defaultValue);
|
||||
};
|
||||
|
||||
const getMeta = async (driver: WxtStorageDriver, driverKey: string) => {
|
||||
const metaKey = getMetaKey(driverKey);
|
||||
const res = await driver.getItem<any>(metaKey);
|
||||
return getMetaValue(res);
|
||||
};
|
||||
|
||||
const setItem = async (
|
||||
driver: WxtStorageDriver,
|
||||
driverKey: string,
|
||||
@@ -76,6 +88,7 @@ function createStorage(): WxtStorage {
|
||||
) => {
|
||||
await driver.setItem(driverKey, value ?? null);
|
||||
};
|
||||
|
||||
const setMeta = async (
|
||||
driver: WxtStorageDriver,
|
||||
driverKey: string,
|
||||
@@ -85,23 +98,27 @@ function createStorage(): WxtStorage {
|
||||
const existingFields = getMetaValue(await driver.getItem(metaKey));
|
||||
await driver.setItem(metaKey, mergeMeta(existingFields, properties));
|
||||
};
|
||||
|
||||
const removeItem = async (
|
||||
driver: WxtStorageDriver,
|
||||
driverKey: string,
|
||||
opts: RemoveItemOptions | undefined,
|
||||
) => {
|
||||
await driver.removeItem(driverKey);
|
||||
|
||||
if (opts?.removeMeta) {
|
||||
const metaKey = getMetaKey(driverKey);
|
||||
await driver.removeItem(metaKey);
|
||||
}
|
||||
};
|
||||
|
||||
const removeMeta = async (
|
||||
driver: WxtStorageDriver,
|
||||
driverKey: string,
|
||||
properties: string | string[] | undefined,
|
||||
) => {
|
||||
const metaKey = getMetaKey(driverKey);
|
||||
|
||||
if (properties == null) {
|
||||
await driver.removeItem(metaKey);
|
||||
} else {
|
||||
@@ -110,19 +127,19 @@ function createStorage(): WxtStorage {
|
||||
await driver.setItem(metaKey, newFields);
|
||||
}
|
||||
};
|
||||
|
||||
const watch = (
|
||||
driver: WxtStorageDriver,
|
||||
driverKey: string,
|
||||
cb: WatchCallback<any>,
|
||||
) => {
|
||||
return driver.watch(driverKey, cb);
|
||||
};
|
||||
) => driver.watch(driverKey, cb);
|
||||
|
||||
const storage: WxtStorage = {
|
||||
return {
|
||||
getItem: async (key, opts) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
return await getItem(driver, driverKey, opts);
|
||||
},
|
||||
|
||||
getItems: async (keys) => {
|
||||
const areaToKeyMap = new Map<StorageArea, string[]>();
|
||||
const keyToOptsMap = new Map<string, GetItemOptions<any> | undefined>();
|
||||
@@ -131,6 +148,7 @@ function createStorage(): WxtStorage {
|
||||
keys.forEach((key) => {
|
||||
let keyStr: StorageItemKey;
|
||||
let opts: GetItemOptions<any> | undefined;
|
||||
|
||||
if (typeof key === 'string') {
|
||||
// key: string
|
||||
keyStr = key;
|
||||
@@ -143,9 +161,11 @@ function createStorage(): WxtStorage {
|
||||
keyStr = key.key;
|
||||
opts = key.options;
|
||||
}
|
||||
|
||||
orderedKeys.push(keyStr);
|
||||
const { driverArea, driverKey } = resolveKey(keyStr);
|
||||
const areaKeys = areaToKeyMap.get(driverArea) ?? [];
|
||||
|
||||
areaToKeyMap.set(driverArea, areaKeys.concat(driverKey));
|
||||
keyToOptsMap.set(keyStr, opts);
|
||||
});
|
||||
@@ -154,6 +174,7 @@ function createStorage(): WxtStorage {
|
||||
await Promise.all(
|
||||
Array.from(areaToKeyMap.entries()).map(async ([driverArea, keys]) => {
|
||||
const driverResults = await drivers[driverArea].getItems(keys);
|
||||
|
||||
driverResults.forEach((driverResult) => {
|
||||
const key = `${driverArea}:${driverResult.key}` as StorageItemKey;
|
||||
const opts = keyToOptsMap.get(key);
|
||||
@@ -161,6 +182,7 @@ function createStorage(): WxtStorage {
|
||||
driverResult.value,
|
||||
opts?.fallback ?? opts?.defaultValue,
|
||||
);
|
||||
|
||||
resultsMap.set(key, value);
|
||||
});
|
||||
}),
|
||||
@@ -171,14 +193,17 @@ function createStorage(): WxtStorage {
|
||||
value: resultsMap.get(key),
|
||||
}));
|
||||
},
|
||||
|
||||
getMeta: async (key) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
return await getMeta(driver, driverKey);
|
||||
},
|
||||
|
||||
getMetas: async (args) => {
|
||||
const keys = args.map((arg) => {
|
||||
const key = typeof arg === 'string' ? arg : arg.key;
|
||||
const { driverArea, driverKey } = resolveKey(key);
|
||||
|
||||
return {
|
||||
key,
|
||||
driverArea,
|
||||
@@ -186,6 +211,7 @@ function createStorage(): WxtStorage {
|
||||
driverMetaKey: getMetaKey(driverKey),
|
||||
};
|
||||
});
|
||||
|
||||
const areaToDriverMetaKeysMap = keys.reduce<
|
||||
Partial<Record<StorageArea, (typeof keys)[number][]>>
|
||||
>((map, key) => {
|
||||
@@ -211,10 +237,12 @@ function createStorage(): WxtStorage {
|
||||
meta: resultsMap[key.key],
|
||||
}));
|
||||
},
|
||||
|
||||
setItem: async (key, value) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
await setItem(driver, driverKey, value);
|
||||
},
|
||||
|
||||
setItems: async (items) => {
|
||||
const areaToKeyValueMap: Partial<
|
||||
Record<StorageArea, Array<{ key: string; value: any }>>
|
||||
@@ -229,6 +257,7 @@ function createStorage(): WxtStorage {
|
||||
value: item.value,
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(areaToKeyValueMap).map(async ([driverArea, values]) => {
|
||||
const driver = getDriver(driverArea as StorageArea);
|
||||
@@ -236,10 +265,12 @@ function createStorage(): WxtStorage {
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
setMeta: async (key, properties) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
await setMeta(driver, driverKey, properties);
|
||||
},
|
||||
|
||||
setMetas: async (items) => {
|
||||
const areaToMetaUpdatesMap: Partial<
|
||||
Record<StorageArea, { key: string; properties: any }[]>
|
||||
@@ -278,16 +309,19 @@ function createStorage(): WxtStorage {
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
removeItem: async (key, opts) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
await removeItem(driver, driverKey, opts);
|
||||
},
|
||||
|
||||
removeItems: async (keys) => {
|
||||
const areaToKeysMap: Partial<Record<StorageArea, string[]>> = {};
|
||||
|
||||
keys.forEach((key) => {
|
||||
let keyStr: StorageItemKey;
|
||||
let opts: RemoveItemOptions | undefined;
|
||||
|
||||
if (typeof key === 'string') {
|
||||
// key: string
|
||||
keyStr = key;
|
||||
@@ -303,9 +337,11 @@ function createStorage(): WxtStorage {
|
||||
keyStr = key.key;
|
||||
opts = key.options;
|
||||
}
|
||||
|
||||
const { driverArea, driverKey } = resolveKey(keyStr);
|
||||
areaToKeysMap[driverArea] ??= [];
|
||||
areaToKeysMap[driverArea].push(driverKey);
|
||||
|
||||
if (opts?.removeMeta) {
|
||||
areaToKeysMap[driverArea].push(getMetaKey(driverKey));
|
||||
}
|
||||
@@ -318,36 +354,45 @@ function createStorage(): WxtStorage {
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
clear: async (base) => {
|
||||
const driver = getDriver(base);
|
||||
await driver.clear();
|
||||
},
|
||||
|
||||
removeMeta: async (key, properties) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
await removeMeta(driver, driverKey, properties);
|
||||
},
|
||||
|
||||
snapshot: async (base, opts) => {
|
||||
const driver = getDriver(base);
|
||||
const data = await driver.snapshot();
|
||||
|
||||
opts?.excludeKeys?.forEach((key) => {
|
||||
delete data[key];
|
||||
delete data[getMetaKey(key)];
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
restoreSnapshot: async (base, data) => {
|
||||
const driver = getDriver(base);
|
||||
await driver.restoreSnapshot(data);
|
||||
},
|
||||
|
||||
watch: (key, cb) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
return watch(driver, driverKey, cb);
|
||||
},
|
||||
|
||||
unwatch() {
|
||||
Object.values(drivers).forEach((driver) => {
|
||||
driver.unwatch();
|
||||
});
|
||||
},
|
||||
|
||||
defineItem: (key, opts?: WxtStorageItemOptions<any>) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
|
||||
@@ -357,17 +402,25 @@ function createStorage(): WxtStorage {
|
||||
onMigrationComplete,
|
||||
debug = false,
|
||||
} = opts ?? {};
|
||||
|
||||
if (targetVersion < 1) {
|
||||
throw Error(
|
||||
'Storage item version cannot be less than 1. Initial versions should be set to 1, not 0.',
|
||||
);
|
||||
}
|
||||
const migrate = async () => {
|
||||
|
||||
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;
|
||||
@@ -376,11 +429,12 @@ function createStorage(): WxtStorage {
|
||||
`Version downgrade detected (v${currentVersion} -> v${targetVersion}) for "${key}"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (currentVersion === targetVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debug === true) {
|
||||
if (debug) {
|
||||
console.debug(
|
||||
`[@wxt-dev/storage] Running storage migration for ${key}: v${currentVersion} -> v${targetVersion}`,
|
||||
);
|
||||
@@ -395,7 +449,7 @@ function createStorage(): WxtStorage {
|
||||
migratedValue =
|
||||
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
|
||||
migratedValue;
|
||||
if (debug === true) {
|
||||
if (debug) {
|
||||
console.debug(
|
||||
`[@wxt-dev/storage] Storage migration processed for version: v${migrateToVersion}`,
|
||||
);
|
||||
@@ -411,14 +465,16 @@ function createStorage(): WxtStorage {
|
||||
{ key: driverMetaKey, value: { ...meta, v: targetVersion } },
|
||||
]);
|
||||
|
||||
if (debug === true) {
|
||||
if (debug) {
|
||||
console.debug(
|
||||
`[@wxt-dev/storage] Storage migration completed for ${key} v${targetVersion}`,
|
||||
{ migratedValue },
|
||||
);
|
||||
}
|
||||
|
||||
onMigrationComplete?.(migratedValue, targetVersion);
|
||||
};
|
||||
|
||||
const migrationsDone =
|
||||
opts?.migrations == null
|
||||
? Promise.resolve()
|
||||
@@ -441,6 +497,9 @@ function createStorage(): WxtStorage {
|
||||
|
||||
const newValue = await opts.init();
|
||||
await driver.setItem<any>(driverKey, newValue);
|
||||
if (value == null && targetVersion > 1) {
|
||||
await setMeta(driver, driverKey, { v: targetVersion });
|
||||
}
|
||||
return newValue;
|
||||
});
|
||||
|
||||
@@ -449,62 +508,86 @@ function createStorage(): WxtStorage {
|
||||
|
||||
return {
|
||||
key,
|
||||
|
||||
get defaultValue() {
|
||||
return getFallback();
|
||||
},
|
||||
get fallback() {
|
||||
return getFallback();
|
||||
},
|
||||
|
||||
getValue: async () => {
|
||||
await migrationsDone;
|
||||
|
||||
if (opts?.init) {
|
||||
return await getOrInitValue();
|
||||
} else {
|
||||
return await getItem(driver, driverKey, opts);
|
||||
}
|
||||
},
|
||||
|
||||
getMeta: async () => {
|
||||
await migrationsDone;
|
||||
|
||||
return await getMeta(driver, driverKey);
|
||||
},
|
||||
|
||||
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) => {
|
||||
await migrationsDone;
|
||||
|
||||
return await setMeta(driver, driverKey, properties);
|
||||
},
|
||||
|
||||
removeValue: async (opts) => {
|
||||
await migrationsDone;
|
||||
|
||||
return await removeItem(driver, driverKey, opts);
|
||||
},
|
||||
|
||||
removeMeta: async (properties) => {
|
||||
await migrationsDone;
|
||||
|
||||
return await removeMeta(driver, driverKey, properties);
|
||||
},
|
||||
|
||||
watch: (cb) =>
|
||||
watch(driver, driverKey, (newValue, oldValue) =>
|
||||
cb(newValue ?? getFallback(), oldValue ?? getFallback()),
|
||||
),
|
||||
|
||||
migrate,
|
||||
};
|
||||
},
|
||||
};
|
||||
return storage;
|
||||
}
|
||||
|
||||
function createDriver(storageArea: StorageArea): WxtStorageDriver {
|
||||
const getStorageArea = () => {
|
||||
if (browser.runtime == null) {
|
||||
throw Error(
|
||||
[
|
||||
"'wxt/storage' must be loaded in a web extension environment",
|
||||
'\n - If thrown during a build, see https://github.com/wxt-dev/wxt/issues/371',
|
||||
" - If thrown during tests, mock 'wxt/browser' correctly. See https://wxt.dev/guide/go-further/testing.html\n",
|
||||
].join('\n'),
|
||||
);
|
||||
throw Error(`'wxt/storage' must be loaded in a web extension environment
|
||||
|
||||
- If thrown during a build, see https://github.com/wxt-dev/wxt/issues/371
|
||||
- If thrown during tests, mock 'wxt/browser' correctly. See https://wxt.dev/guide/go-further/testing.html
|
||||
`);
|
||||
}
|
||||
|
||||
if (browser.storage == null) {
|
||||
throw Error(
|
||||
"You must add the 'storage' permission to your manifest to use 'wxt/storage'",
|
||||
@@ -514,18 +597,23 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
|
||||
const area = browser.storage[storageArea];
|
||||
if (area == null)
|
||||
throw Error(`"browser.storage.${storageArea}" is undefined`);
|
||||
|
||||
return area;
|
||||
};
|
||||
|
||||
const watchListeners = new Set<(changes: StorageAreaChanges) => void>();
|
||||
|
||||
return {
|
||||
getItem: async (key) => {
|
||||
const res = await getStorageArea().get<Record<string, any>>(key);
|
||||
return res[key];
|
||||
},
|
||||
|
||||
getItems: async (keys) => {
|
||||
const result = await getStorageArea().get(keys);
|
||||
return keys.map((key) => ({ key, value: result[key] ?? null }));
|
||||
},
|
||||
|
||||
setItem: async (key, value) => {
|
||||
if (value == null) {
|
||||
await getStorageArea().remove(key);
|
||||
@@ -533,6 +621,7 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
|
||||
await getStorageArea().set({ [key]: value });
|
||||
}
|
||||
},
|
||||
|
||||
setItems: async (values) => {
|
||||
const map = values.reduce<Record<string, unknown>>(
|
||||
(map, { key, value }) => {
|
||||
@@ -541,40 +630,51 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await getStorageArea().set(map);
|
||||
},
|
||||
|
||||
removeItem: async (key) => {
|
||||
await getStorageArea().remove(key);
|
||||
},
|
||||
|
||||
removeItems: async (keys) => {
|
||||
await getStorageArea().remove(keys);
|
||||
},
|
||||
|
||||
clear: async () => {
|
||||
await getStorageArea().clear();
|
||||
},
|
||||
|
||||
snapshot: async () => {
|
||||
return await getStorageArea().get();
|
||||
},
|
||||
|
||||
restoreSnapshot: async (data) => {
|
||||
await getStorageArea().set(data);
|
||||
},
|
||||
|
||||
watch(key, cb) {
|
||||
const listener = (changes: StorageAreaChanges) => {
|
||||
const change = changes[key] as {
|
||||
newValue?: any;
|
||||
oldValue?: any | null;
|
||||
} | null;
|
||||
if (change == null) return;
|
||||
if (dequal(change.newValue, change.oldValue)) return;
|
||||
|
||||
if (change == null || dequal(change.newValue, change.oldValue)) return;
|
||||
|
||||
cb(change.newValue ?? null, change.oldValue ?? null);
|
||||
};
|
||||
|
||||
getStorageArea().onChanged.addListener(listener);
|
||||
watchListeners.add(listener);
|
||||
|
||||
return () => {
|
||||
getStorageArea().onChanged.removeListener(listener);
|
||||
watchListeners.delete(listener);
|
||||
};
|
||||
},
|
||||
|
||||
unwatch() {
|
||||
watchListeners.forEach((listener) => {
|
||||
getStorageArea().onChanged.removeListener(listener);
|
||||
@@ -615,6 +715,7 @@ export interface WxtStorage {
|
||||
| { key: StorageItemKey; options?: GetItemOptions<any> }
|
||||
>,
|
||||
): Promise<Array<{ key: StorageItemKey; value: any }>>;
|
||||
|
||||
/**
|
||||
* Return an object containing metadata about the key. Object is stored at `key + "$"`. If value
|
||||
* is not an object, it returns an empty object.
|
||||
@@ -623,15 +724,17 @@ export interface WxtStorage {
|
||||
* await storage.getMeta("local:installDate");
|
||||
*/
|
||||
getMeta<T extends Record<string, unknown>>(key: StorageItemKey): Promise<T>;
|
||||
|
||||
/**
|
||||
* Get the metadata of multiple storage items.
|
||||
*
|
||||
* @param items List of keys or items to get the metadata of.
|
||||
* @param keys List of keys or items to get the metadata of.
|
||||
* @returns An array containing storage keys and their metadata.
|
||||
*/
|
||||
getMetas(
|
||||
keys: Array<StorageItemKey | WxtStorageItem<any, any>>,
|
||||
): Promise<Array<{ key: StorageItemKey; meta: any }>>;
|
||||
|
||||
/**
|
||||
* Set a value in storage. Setting a value to `null` or `undefined` is equivalent to calling
|
||||
* `removeItem`.
|
||||
@@ -640,6 +743,7 @@ export interface WxtStorage {
|
||||
* await storage.setItem<number>("local:installDate", Date.now());
|
||||
*/
|
||||
setItem<T>(key: StorageItemKey, value: T | null): Promise<void>;
|
||||
|
||||
/**
|
||||
* Set multiple values in storage. If a value is set to `null` or `undefined`, the key is removed.
|
||||
*
|
||||
@@ -655,6 +759,7 @@ export interface WxtStorage {
|
||||
| { item: WxtStorageItem<any, any>; value: any }
|
||||
>,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Sets metadata properties. If some properties are already set, but are not included in the
|
||||
* `properties` parameter, they will not be removed.
|
||||
@@ -666,10 +771,11 @@ export interface WxtStorage {
|
||||
key: StorageItemKey,
|
||||
properties: T | null,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Set the metadata of multiple storage items.
|
||||
*
|
||||
* @param items List of storage keys or items and metadata to set for each.
|
||||
* @param metas List of storage keys or items and metadata to set for each.
|
||||
*/
|
||||
setMetas(
|
||||
metas: Array<
|
||||
@@ -677,6 +783,7 @@ export interface WxtStorage {
|
||||
| { item: WxtStorageItem<any, any>; meta: Record<string, any> }
|
||||
>,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Removes an item from storage.
|
||||
*
|
||||
@@ -684,6 +791,7 @@ export interface WxtStorage {
|
||||
* await storage.removeItem("local:installDate");
|
||||
*/
|
||||
removeItem(key: StorageItemKey, opts?: RemoveItemOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Remove a list of keys from storage.
|
||||
*/
|
||||
@@ -715,6 +823,7 @@ export interface WxtStorage {
|
||||
key: StorageItemKey,
|
||||
properties?: string | string[],
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Return all the items in storage.
|
||||
*/
|
||||
@@ -722,15 +831,18 @@ export interface WxtStorage {
|
||||
base: StorageArea,
|
||||
opts?: SnapshotOptions,
|
||||
): Promise<Record<string, unknown>>;
|
||||
|
||||
/**
|
||||
* Restores the results of `snapshot`. If new properties have been saved since the snapshot, they are
|
||||
* not overridden. Only values existing in the snapshot are overridden.
|
||||
*/
|
||||
restoreSnapshot(base: StorageArea, data: any): Promise<void>;
|
||||
|
||||
/**
|
||||
* Watch for changes to a specific key in storage.
|
||||
*/
|
||||
watch<T>(key: StorageItemKey, cb: WatchCallback<T | null>): Unwatch;
|
||||
|
||||
/**
|
||||
* Remove all watch listeners.
|
||||
*/
|
||||
@@ -786,42 +898,52 @@ export interface WxtStorageItem<
|
||||
* The storage key passed when creating the storage item.
|
||||
*/
|
||||
key: StorageItemKey;
|
||||
|
||||
/**
|
||||
* @deprecated Renamed to fallback, use it instead.
|
||||
*/
|
||||
defaultValue: TValue;
|
||||
|
||||
/**
|
||||
* The value provided by the `fallback` option.
|
||||
*/
|
||||
fallback: TValue;
|
||||
|
||||
/**
|
||||
* Get the latest value from storage.
|
||||
*/
|
||||
getValue(): Promise<TValue>;
|
||||
|
||||
/**
|
||||
* Get metadata.
|
||||
*/
|
||||
getMeta(): Promise<NullablePartial<TMetadata>>;
|
||||
|
||||
/**
|
||||
* Set the value in storage.
|
||||
*/
|
||||
setValue(value: TValue): Promise<void>;
|
||||
|
||||
/**
|
||||
* Set metadata properties.
|
||||
*/
|
||||
setMeta(properties: NullablePartial<TMetadata>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Remove the value from storage.
|
||||
*/
|
||||
removeValue(opts?: RemoveItemOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Remove all metadata or certain properties from metadata.
|
||||
*/
|
||||
removeMeta(properties?: string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Listen for changes to the value in storage.
|
||||
*/
|
||||
watch(cb: WatchCallback<TValue>): Unwatch;
|
||||
|
||||
/**
|
||||
* If there are migrations defined on the storage item, migrate to the latest version.
|
||||
*
|
||||
@@ -839,6 +961,7 @@ export interface GetItemOptions<T> {
|
||||
* @deprecated Renamed to `fallback`, use it instead.
|
||||
*/
|
||||
defaultValue?: T;
|
||||
|
||||
/**
|
||||
* Default value returned when `getItem` would otherwise return `null`.
|
||||
*/
|
||||
@@ -867,10 +990,12 @@ export interface WxtStorageItemOptions<T> {
|
||||
* @deprecated Renamed to `fallback`, use it instead.
|
||||
*/
|
||||
defaultValue?: T;
|
||||
|
||||
/**
|
||||
* Default value returned when `getValue` would otherwise return `null`.
|
||||
*/
|
||||
fallback?: T;
|
||||
|
||||
/**
|
||||
* If passed, a value in storage will be initialized immediately after
|
||||
* defining the storage item. This function returns the value that will be
|
||||
@@ -878,20 +1003,24 @@ export interface WxtStorageItemOptions<T> {
|
||||
* already exist.
|
||||
*/
|
||||
init?: () => T | Promise<T>;
|
||||
|
||||
/**
|
||||
* Provide a version number for the storage item to enable migrations. When changing the version
|
||||
* in the future, migration functions will be ran on application startup.
|
||||
*/
|
||||
version?: number;
|
||||
|
||||
/**
|
||||
* A map of version numbers to the functions used to migrate the data to that version.
|
||||
*/
|
||||
migrations?: Record<number, (oldValue: any) => any>;
|
||||
|
||||
/**
|
||||
* Print debug logs, such as migration process.
|
||||
* @default false
|
||||
*/
|
||||
debug?: boolean;
|
||||
|
||||
/**
|
||||
* A callback function that runs on migration complete.
|
||||
*/
|
||||
@@ -909,10 +1038,12 @@ export type StorageAreaChanges = {
|
||||
type NullablePartial<T> = {
|
||||
[key in keyof T]+?: T[key] | undefined | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback called when a value in storage is changed.
|
||||
*/
|
||||
export type WatchCallback<T> = (newValue: T, oldValue: T) => void;
|
||||
|
||||
/**
|
||||
* Call to remove a watch listener
|
||||
*/
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig } from 'wxt';
|
||||
import { presetUno } from 'unocss';
|
||||
import { presetWind3 } from 'unocss';
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
@@ -53,7 +53,7 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
},
|
||||
presets: [presetUno()],
|
||||
presets: [presetWind3()],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,80 @@
|
||||
# Changelog
|
||||
|
||||
## v0.20.17
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.16...wxt-v0.20.17)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Update left-over CJS require of `publish-browser-extension` to ESM ([#2120](https://github.com/wxt-dev/wxt/pull/2120))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.20.16
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.15...wxt-v0.20.16)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Revert "rename `name` to `names` prop of `rollup` to fix deprecated warning " ([#2106](https://github.com/wxt-dev/wxt/pull/2106))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Speed up E2E tests by skipping `pnpm install` when possible ([#2113](https://github.com/wxt-dev/wxt/pull/2113))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.20.15
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.14...wxt-v0.20.15)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Export `normalizePath` from `wxt` module ([#2080](https://github.com/wxt-dev/wxt/pull/2080))
|
||||
- **popup:** Add Firefox `default_area` and `theme_icons` support ([#2097](https://github.com/wxt-dev/wxt/pull/2097))
|
||||
- Add log `--level` cli option ([#1973](https://github.com/wxt-dev/wxt/pull/1973))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Content script is incorrectly invalidated when injected multiple times ([#2035](https://github.com/wxt-dev/wxt/pull/2035))
|
||||
- Use `uiContainer` to apply position when creating shadow root ui ([#2036](https://github.com/wxt-dev/wxt/pull/2036))
|
||||
- Make `ExtensionRunner#closeBrowser` optional ([#2092](https://github.com/wxt-dev/wxt/pull/2092))
|
||||
- Rename `name` to `names` prop of `rollup` to fix deprecated warning ([#2106](https://github.com/wxt-dev/wxt/pull/2106))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Standardize file existence checks to `pathExists` ([#2083](https://github.com/wxt-dev/wxt/pull/2083))
|
||||
- Cleanup esm ([#1950](https://github.com/wxt-dev/wxt/pull/1950))
|
||||
- `async`/`await` cleanup ([#2088](https://github.com/wxt-dev/wxt/pull/2088))
|
||||
- Cleanup unnecssary checks ([#2089](https://github.com/wxt-dev/wxt/pull/2089))
|
||||
- Simplify import paths ([#2093](https://github.com/wxt-dev/wxt/pull/2093))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix JSDoc ([6ae5fb5e](https://github.com/wxt-dev/wxt/commit/6ae5fb5e))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Cleanup comment line lengths ([#2090](https://github.com/wxt-dev/wxt/pull/2090))
|
||||
- Cleanup `@ts-*` comments ([#2094](https://github.com/wxt-dev/wxt/pull/2094))
|
||||
- Update JSDoc to use `@internal` ([#2091](https://github.com/wxt-dev/wxt/pull/2091))
|
||||
- Fix typos ([#2086](https://github.com/wxt-dev/wxt/pull/2086))
|
||||
- Remove unused code ([#2087](https://github.com/wxt-dev/wxt/pull/2087))
|
||||
- Simplify imports of `cli/__tests__/index.ts` ([#2104](https://github.com/wxt-dev/wxt/pull/2104))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Nick Doan ([@nickbar01234](https://github.com/nickbar01234))
|
||||
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Wotan-allfather <wotan-ai@proton.me>
|
||||
- Namu ([@namuorg](https://github.com/namuorg))
|
||||
- Eli ([@lishaduck](https://github.com/lishaduck))
|
||||
|
||||
## v0.20.14
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.13...wxt-v0.20.14)
|
||||
@@ -31,7 +106,7 @@
|
||||
|
||||
- Dareka ([@darekadareka](https://github.com/darekadareka))
|
||||
- Mark ([@mjfaga](https://github.com/mjfaga))
|
||||
- Ariasuni <aria@ariasuni.eu>
|
||||
- ariasuni ([@ariasuni](https://github.com/ariasuni))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
|
||||
- Bohuslavsemenov ([@bohuslavsemenov](https://github.com/bohuslavsemenov))
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
* `publish-browser-extension` as a direct dependency (like for PNPM, which doesn't link
|
||||
* sub-dependency binaries to "node_modules/.bin")
|
||||
*/
|
||||
require('publish-browser-extension/cli');
|
||||
import 'publish-browser-extension/cli';
|
||||
@@ -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,8 +30,8 @@ describe('Analysis', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('stats.html')).toBe(true);
|
||||
expect(await project.fileExists('.output/chrome-mv3/stats-0.json')).toBe(
|
||||
expect(await project.pathExists('stats.html')).toBe(true);
|
||||
expect(await project.pathExists('.output/chrome-mv3/stats-0.json')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -52,9 +52,9 @@ describe('Analysis', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('stats.html')).toBe(true);
|
||||
expect(await project.fileExists('stats-0.json')).toBe(true);
|
||||
expect(await project.fileExists('stats-1.json')).toBe(true);
|
||||
expect(await project.pathExists('stats.html')).toBe(true);
|
||||
expect(await project.pathExists('stats-0.json')).toBe(true);
|
||||
expect(await project.pathExists('stats-1.json')).toBe(true);
|
||||
});
|
||||
|
||||
it('should support customizing the stats output directory', async () => {
|
||||
@@ -73,7 +73,7 @@ describe('Analysis', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('stats/bundle.html')).toBe(true);
|
||||
expect(await project.pathExists('stats/bundle.html')).toBe(true);
|
||||
});
|
||||
|
||||
it('should place artifacts next to the custom output file', async () => {
|
||||
@@ -93,9 +93,9 @@ describe('Analysis', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('stats/bundle.html')).toBe(true);
|
||||
expect(await project.fileExists('stats/bundle-0.json')).toBe(true);
|
||||
expect(await project.fileExists('stats/bundle-1.json')).toBe(true);
|
||||
expect(await project.pathExists('stats/bundle.html')).toBe(true);
|
||||
expect(await project.pathExists('stats/bundle-0.json')).toBe(true);
|
||||
expect(await project.pathExists('stats/bundle-1.json')).toBe(true);
|
||||
});
|
||||
|
||||
it('should open the stats in the browser when requested', async () => {
|
||||
|
||||
@@ -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';
|
||||
@@ -142,7 +143,7 @@ describe('Auto Imports', () => {
|
||||
|
||||
await project.prepare();
|
||||
|
||||
expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
|
||||
expect(await project.pathExists('.wxt/types/imports.d.ts')).toBe(false);
|
||||
});
|
||||
|
||||
it('should only include imports-module.d.ts in the the project', async () => {
|
||||
@@ -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';
|
||||
@@ -280,10 +281,10 @@ describe('Auto Imports', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('.wxt/eslint-auto-imports.mjs')).toBe(
|
||||
expect(await project.pathExists('.wxt/eslint-auto-imports.mjs')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(await project.fileExists('.wxt/eslintrc-auto-import.json')).toBe(
|
||||
expect(await project.pathExists('.wxt/eslintrc-auto-import.json')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 { GenericEntrypoint, InlineConfig } from '../../src';
|
||||
import { readFile } from 'fs-extra';
|
||||
import { normalizePath } from '../../src/core/utils/paths';
|
||||
import { normalizePath } from '../../src';
|
||||
|
||||
describe('Module Helpers', () => {
|
||||
describe('options', () => {
|
||||
@@ -77,7 +77,7 @@ describe('Module Helpers', () => {
|
||||
|
||||
await project.build(config);
|
||||
|
||||
expect(await project.fileExists('.output/chrome-mv3/injected.js')).toBe(
|
||||
expect(await project.pathExists('.output/chrome-mv3/injected.js')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -118,10 +118,10 @@ describe('Module Helpers', () => {
|
||||
fileName: 'module.txt',
|
||||
});
|
||||
await expect(
|
||||
project.fileExists('.output/chrome-mv3/module.txt'),
|
||||
project.pathExists('.output/chrome-mv3/module.txt'),
|
||||
).resolves.toBe(true);
|
||||
await expect(
|
||||
project.fileExists('.output/chrome-mv3/example/generated.txt'),
|
||||
project.pathExists('.output/chrome-mv3/example/generated.txt'),
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
await project.build({ browser: 'firefox' });
|
||||
|
||||
expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
|
||||
expect(await project.pathExists('.output/firefox-mv2/background.js')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -146,7 +146,7 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
await project.build({ browser: 'chrome' });
|
||||
|
||||
expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
|
||||
expect(await project.pathExists('.output/firefox-mv2/background.js')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -174,7 +174,7 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.fileExists('stats.html')).toBe(true);
|
||||
expect(await project.pathExists('stats.html')).toBe(true);
|
||||
});
|
||||
|
||||
it('should support JavaScript entrypoints', async () => {
|
||||
@@ -210,14 +210,14 @@ describe('Output Directory Structure', () => {
|
||||
----------------------------------------
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"},"content_scripts":[{"matches":["*://*.google.com/*"],"js":["content-scripts/content.js"]},{"matches":["*://*.duckduckgo.com/*"],"js":["content-scripts/named.js"]}]}"
|
||||
`);
|
||||
expect(await project.fileExists('.output/chrome-mv3/background.js'));
|
||||
expect(await project.pathExists('.output/chrome-mv3/background.js'));
|
||||
expect(
|
||||
await project.fileExists('.output/chrome-mv3/content-scripts/content.js'),
|
||||
await project.pathExists('.output/chrome-mv3/content-scripts/content.js'),
|
||||
);
|
||||
expect(
|
||||
await project.fileExists('.output/chrome-mv3/content-scripts/named.js'),
|
||||
await project.pathExists('.output/chrome-mv3/content-scripts/named.js'),
|
||||
);
|
||||
expect(await project.fileExists('.output/chrome-mv3/unlisted.js'));
|
||||
expect(await project.pathExists('.output/chrome-mv3/unlisted.js'));
|
||||
});
|
||||
|
||||
it('should support CSS entrypoints', async () => {
|
||||
@@ -300,7 +300,7 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.fileExists('dist/chrome-mv3/manifest.json')).toBe(
|
||||
expect(await project.pathExists('dist/chrome-mv3/manifest.json')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -335,7 +335,7 @@ describe('Output Directory Structure', () => {
|
||||
await project.build({
|
||||
vite: () => ({
|
||||
build: {
|
||||
// Make output for snapshot readible
|
||||
// Make output for the snapshot readable
|
||||
minify: false,
|
||||
},
|
||||
}),
|
||||
@@ -408,7 +408,7 @@ describe('Output Directory Structure', () => {
|
||||
await project.build({
|
||||
vite: () => ({
|
||||
build: {
|
||||
// Make output for snapshot readible
|
||||
// Make output for the snapshot readable
|
||||
minify: false,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('React', () => {
|
||||
await project.build();
|
||||
|
||||
expect(
|
||||
await project.fileExists('.output/chrome-mv3/content-scripts/demo.js'),
|
||||
await project.pathExists('.output/chrome-mv3/content-scripts/demo.js'),
|
||||
).toBe(true);
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('Remote Code', () => {
|
||||
);
|
||||
expect(output).not.toContain(url);
|
||||
expect(
|
||||
await project.fileExists(`.wxt/cache/${encodeURIComponent(url)}`),
|
||||
await project.pathExists(`.wxt/cache/${encodeURIComponent(url)}`),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>');
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('User Config', () => {
|
||||
await project.build({ configFile: 'test.config.ts' });
|
||||
|
||||
expect(
|
||||
await project.fileExists('.custom-output/chrome-mv3/background.js'),
|
||||
await project.pathExists('.custom-output/chrome-mv3/background.js'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -129,13 +129,13 @@ describe('User Config', () => {
|
||||
await project.build();
|
||||
|
||||
expect(
|
||||
await project.fileExists('.output/test-chrome-mv3-production-build'),
|
||||
await project.pathExists('.output/test-chrome-mv3-production-build'),
|
||||
).toBe(true);
|
||||
|
||||
await project.build({ mode: 'development' });
|
||||
|
||||
expect(
|
||||
await project.fileExists('.output/test-chrome-mv3-development-dev-build'),
|
||||
await project.pathExists('.output/test-chrome-mv3-development-dev-build'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('Zipping', () => {
|
||||
browser: 'firefox',
|
||||
zip: { downloadPackages: ['flatten'] },
|
||||
});
|
||||
expect(await project.fileExists('.output/')).toBe(true);
|
||||
expect(await project.pathExists('.output/')).toBe(true);
|
||||
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
// Update package json wxt path
|
||||
@@ -51,7 +51,7 @@ describe('Zipping', () => {
|
||||
}),
|
||||
).resolves.not.toHaveProperty('exitCode');
|
||||
|
||||
await expect(project.fileExists(unzipDir, '.output')).resolves.toBe(true);
|
||||
await expect(project.pathExists(unzipDir, '.output')).resolves.toBe(true);
|
||||
expect(
|
||||
await project.serializeFile(
|
||||
project.resolvePath(unzipDir, 'package.json'),
|
||||
@@ -95,8 +95,8 @@ describe('Zipping', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists(artifactZip)).toBe(true);
|
||||
expect(await project.fileExists(sourcesZip)).toBe(true);
|
||||
expect(await project.pathExists(artifactZip)).toBe(true);
|
||||
expect(await project.pathExists(sourcesZip)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not zip hidden files into sources by default', async () => {
|
||||
@@ -117,8 +117,8 @@ describe('Zipping', () => {
|
||||
browser: 'firefox',
|
||||
});
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
expect(await project.fileExists(unzipDir, '.env')).toBe(false);
|
||||
expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(false);
|
||||
expect(await project.pathExists(unzipDir, '.env')).toBe(false);
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not zip files inside hidden directories if only the directory is specified', async () => {
|
||||
@@ -142,8 +142,8 @@ describe('Zipping', () => {
|
||||
},
|
||||
});
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(false);
|
||||
expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file')).toBe(
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(false);
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -171,12 +171,12 @@ describe('Zipping', () => {
|
||||
},
|
||||
});
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
expect(await project.fileExists(unzipDir, '.env')).toBe(true);
|
||||
expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(true);
|
||||
expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file1')).toBe(
|
||||
expect(await project.pathExists(unzipDir, '.env')).toBe(true);
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(true);
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file1')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file2')).toBe(
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file2')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -210,10 +210,10 @@ describe('Zipping', () => {
|
||||
});
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
expect(
|
||||
await project.fileExists(unzipDir, 'entrypoints/not-firefox.content.ts'),
|
||||
await project.pathExists(unzipDir, 'entrypoints/not-firefox.content.ts'),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await project.fileExists(unzipDir, 'entrypoints/all.content.ts'),
|
||||
await project.pathExists(unzipDir, 'entrypoints/all.content.ts'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -234,7 +234,7 @@ describe('Zipping', () => {
|
||||
browser,
|
||||
});
|
||||
|
||||
expect(await project.fileExists(sourcesZip)).toBe(true);
|
||||
expect(await project.pathExists(sourcesZip)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -258,7 +258,7 @@ describe('Zipping', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists(sourcesZip)).toBe(true);
|
||||
expect(await project.pathExists(sourcesZip)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -282,7 +282,7 @@ describe('Zipping', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists(sourcesZip)).toBe(false);
|
||||
expect(await project.pathExists(sourcesZip)).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -305,6 +305,6 @@ describe('Zipping', () => {
|
||||
});
|
||||
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
expect(await project.fileExists(unzipDir, 'manifest.json')).toBe(true);
|
||||
expect(await project.pathExists(unzipDir, 'manifest.json')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+24
-19
@@ -1,7 +1,8 @@
|
||||
import { dirname, relative, resolve } from 'path';
|
||||
import fs, { mkdir } from 'fs-extra';
|
||||
import glob from 'fast-glob';
|
||||
import fs, { mkdir } from 'fs-extra';
|
||||
import merge from 'lodash.merge';
|
||||
import spawn from 'nano-spawn';
|
||||
import { dirname, relative, resolve } from 'path';
|
||||
import {
|
||||
InlineConfig,
|
||||
UserConfig,
|
||||
@@ -10,8 +11,7 @@ import {
|
||||
prepare,
|
||||
zip,
|
||||
} from '../src';
|
||||
import { normalizePath } from '../src/core/utils/paths';
|
||||
import merge from 'lodash.merge';
|
||||
import { normalizePath } from '../src/core/utils';
|
||||
|
||||
// Run "pnpm wxt" to use the "wxt" dev script, not the "wxt" binary from the
|
||||
// wxt package. This uses the TS files instead of the compiled JS package
|
||||
@@ -24,6 +24,7 @@ export class TestProject {
|
||||
files: Array<[string, string]> = [];
|
||||
config: UserConfig | undefined;
|
||||
readonly root: string;
|
||||
readonly hasCustomDependencies: boolean;
|
||||
|
||||
constructor(packageJson: any = {}) {
|
||||
// We can't put each test's project inside e2e/dist directly, otherwise the wxt.config.ts
|
||||
@@ -31,6 +32,13 @@ export class TestProject {
|
||||
// end to make each test's path unique.
|
||||
const id = Math.random().toString(32).substring(3);
|
||||
this.root = resolve(E2E_DIR, 'dist', id);
|
||||
|
||||
this.hasCustomDependencies =
|
||||
Object.keys({
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies,
|
||||
}).length > 0;
|
||||
|
||||
this.files.push([
|
||||
'package.json',
|
||||
JSON.stringify(
|
||||
@@ -119,9 +127,14 @@ export class TestProject {
|
||||
await fs.writeFile(filePath, content ?? '', 'utf-8');
|
||||
}
|
||||
|
||||
await spawn('pnpm', ['--ignore-workspace', 'i', '--ignore-scripts'], {
|
||||
cwd: this.root,
|
||||
});
|
||||
// Only install dependencies if the project has custom ones - otherwise the
|
||||
// project will reuse the ones in `packages/wxt/node_modules`!
|
||||
if (this.hasCustomDependencies) {
|
||||
await spawn('pnpm', ['--ignore-workspace', 'i', '--ignore-scripts'], {
|
||||
cwd: this.root,
|
||||
});
|
||||
}
|
||||
|
||||
await mkdir(resolve(this.root, 'public'), { recursive: true }).catch(
|
||||
() => {},
|
||||
);
|
||||
@@ -138,14 +151,6 @@ export class TestProject {
|
||||
return this.serializeDir('.output', ignoreContentsOfFilenames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all the files from the test project's `.wxt` directory and combine them into a string
|
||||
* that can be used in a snapshot.
|
||||
*/
|
||||
serializeWxtDir(): Promise<string> {
|
||||
return this.serializeDir(resolve(this.root, '.wxt/types'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deeply print the filename and contents of all files in a directory.
|
||||
*
|
||||
@@ -185,13 +190,13 @@ export class TestProject {
|
||||
].join(`\n${''.padEnd(40, '-')}\n`);
|
||||
}
|
||||
|
||||
fileExists(...path: string[]): Promise<boolean> {
|
||||
return fs.exists(this.resolvePath(...path));
|
||||
pathExists(...path: string[]): Promise<boolean> {
|
||||
return fs.pathExists(this.resolvePath(...path));
|
||||
}
|
||||
|
||||
async getOutputManifest(
|
||||
getOutputManifest(
|
||||
path: string = '.output/chrome-mv3/manifest.json',
|
||||
): Promise<any> {
|
||||
return await fs.readJson(this.resolvePath(path));
|
||||
return fs.readJson(this.resolvePath(path));
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare namespace globalThis {
|
||||
var __ENTRYPOINT__: string;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.20.14",
|
||||
"version": "0.20.17",
|
||||
"description": "⚡ Next-gen Web Extension Framework",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@@ -62,6 +62,14 @@
|
||||
"vite-node": "^3.2.4 || ^5.0.0",
|
||||
"web-ext-run": "^0.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^10.2.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
@@ -69,6 +77,7 @@
|
||||
"@types/node": "^20.17.6",
|
||||
"@types/normalize-path": "^3.0.2",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"eslint": "^9.38.0",
|
||||
"extract-zip": "^2.0.1",
|
||||
"happy-dom": "^20.5.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
@@ -78,7 +87,6 @@
|
||||
"vitest": "^4.0.18",
|
||||
"vitest-plugin-random-seed": "^1.1.2"
|
||||
},
|
||||
"peerDependenciesMeta": {},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git"
|
||||
@@ -104,7 +112,7 @@
|
||||
],
|
||||
"bin": {
|
||||
"wxt": "./bin/wxt.mjs",
|
||||
"wxt-publish-extension": "./bin/wxt-publish-extension.cjs"
|
||||
"wxt-publish-extension": "./bin/wxt-publish-extension.mjs"
|
||||
},
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
|
||||
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;
|
||||
|
||||
@@ -49,7 +49,7 @@ export default defineWxtModule({
|
||||
// Only create global types when user has enabled auto-imports
|
||||
entries.push(await getImportsDeclarationEntry(unimport));
|
||||
|
||||
if (wxt.config.imports.eslintrc.enabled === false) return;
|
||||
if (!wxt.config.imports.eslintrc.enabled) return;
|
||||
|
||||
// Only generate ESLint config if that feature is enabled
|
||||
entries.push(
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { describe, it, vi, beforeEach, expect } from 'vitest';
|
||||
import { build } from '../../core/build';
|
||||
import { createServer } from '../../core/create-server';
|
||||
import { zip } from '../../core/zip';
|
||||
import { prepare } from '../../core/prepare';
|
||||
import { clean } from '../../core/clean';
|
||||
import { initialize } from '../../core/initialize';
|
||||
import {
|
||||
build,
|
||||
createServer,
|
||||
zip,
|
||||
prepare,
|
||||
clean,
|
||||
initialize,
|
||||
} from '../../core';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import consola from 'consola';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
|
||||
vi.mock('../../core/build');
|
||||
const buildMock = vi.mocked(build);
|
||||
@@ -137,6 +139,21 @@ describe('CLI', () => {
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
debug: true,
|
||||
});
|
||||
expect(consola.level).toBe(LogLevels.debug);
|
||||
});
|
||||
|
||||
it('should set log --level', async () => {
|
||||
mockArgv('--level', 'warn');
|
||||
await importCli();
|
||||
|
||||
expect(consola.level).toBe(LogLevels.warn);
|
||||
});
|
||||
|
||||
it('--debug should override --level', async () => {
|
||||
mockArgv('--debug', '--level', 'silent');
|
||||
await importCli();
|
||||
|
||||
expect(consola.level).toBe(LogLevels.debug);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -229,6 +246,21 @@ describe('CLI', () => {
|
||||
expect(buildMock).toBeCalledWith({
|
||||
debug: true,
|
||||
});
|
||||
expect(consola.level).toBe(LogLevels.debug);
|
||||
});
|
||||
|
||||
it('should set log --level', async () => {
|
||||
mockArgv('build', '--level', 'warn');
|
||||
await importCli();
|
||||
|
||||
expect(consola.level).toBe(LogLevels.warn);
|
||||
});
|
||||
|
||||
it('--debug should override --level', async () => {
|
||||
mockArgv('build', '--debug', '--level', 'silent');
|
||||
await importCli();
|
||||
|
||||
expect(consola.level).toBe(LogLevels.debug);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -310,6 +342,21 @@ describe('CLI', () => {
|
||||
debug: true,
|
||||
zip: {},
|
||||
});
|
||||
expect(consola.level).toBe(LogLevels.debug);
|
||||
});
|
||||
|
||||
it('should set log --level', async () => {
|
||||
mockArgv('zip', '--level', 'warn');
|
||||
await importCli();
|
||||
|
||||
expect(consola.level).toBe(LogLevels.warn);
|
||||
});
|
||||
|
||||
it('--debug should override --level', async () => {
|
||||
mockArgv('zip', '--debug', '--level', 'silent');
|
||||
await importCli();
|
||||
|
||||
expect(consola.level).toBe(LogLevels.debug);
|
||||
});
|
||||
|
||||
it('should pass undefined for zipSources when --sources is not passed', async () => {
|
||||
@@ -379,6 +426,21 @@ describe('CLI', () => {
|
||||
expect(prepareMock).toBeCalledWith({
|
||||
debug: true,
|
||||
});
|
||||
expect(consola.level).toBe(LogLevels.debug);
|
||||
});
|
||||
|
||||
it('should set log --level', async () => {
|
||||
mockArgv('prepare', '--level', 'warn');
|
||||
await importCli();
|
||||
|
||||
expect(consola.level).toBe(LogLevels.warn);
|
||||
});
|
||||
|
||||
it('--debug should override --level', async () => {
|
||||
mockArgv('prepare', '--debug', '--level', 'silent');
|
||||
await importCli();
|
||||
|
||||
expect(consola.level).toBe(LogLevels.debug);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -413,6 +475,21 @@ describe('CLI', () => {
|
||||
expect(cleanMock).toBeCalledWith({
|
||||
debug: true,
|
||||
});
|
||||
expect(consola.level).toBe(LogLevels.debug);
|
||||
});
|
||||
|
||||
it('should set log --level', async () => {
|
||||
mockArgv('clean', '--level', 'warn');
|
||||
await importCli();
|
||||
|
||||
expect(consola.level).toBe(LogLevels.warn);
|
||||
});
|
||||
|
||||
it('--debug should override --level', async () => {
|
||||
mockArgv('clean', '--debug', '--level', 'silent');
|
||||
await importCli();
|
||||
|
||||
expect(consola.level).toBe(LogLevels.debug);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CAC, Command } from 'cac';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
import consola, { LogLevels, LogType } from 'consola';
|
||||
import { filterTruthy, toArray } from '../core/utils/arrays';
|
||||
import { printHeader } from '../core/utils/log';
|
||||
import { formatDuration } from '../core/utils/time';
|
||||
@@ -19,8 +19,13 @@ export function wrapAction(
|
||||
},
|
||||
) {
|
||||
return async (...args: any[]) => {
|
||||
// Enable consola's debug mode globally at the start of all commands when the `--debug` flag is
|
||||
// passed
|
||||
const level: LogType | undefined = args.find((arg) => arg?.level)?.level;
|
||||
if (level && Object.keys(LogLevels).includes(level)) {
|
||||
consola.level = LogLevels[level];
|
||||
}
|
||||
|
||||
// Enable consola's debug mode globally at the start of all commands when
|
||||
// the `--debug` flag is passed
|
||||
const isDebug = !!args.find((arg) => arg?.debug);
|
||||
if (isDebug) {
|
||||
consola.level = LogLevels.debug;
|
||||
@@ -40,9 +45,7 @@ export function wrapAction(
|
||||
consola.fail(
|
||||
`Command failed after ${formatDuration(Date.now() - startTime)}`,
|
||||
);
|
||||
if (err instanceof ValidationError) {
|
||||
// Don't log these errors, they've already been logged
|
||||
} else {
|
||||
if (!(err instanceof ValidationError)) {
|
||||
consola.error(err);
|
||||
}
|
||||
process.exit(1);
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
const cli = cac('wxt');
|
||||
|
||||
cli.option('--debug', 'enable debug mode');
|
||||
cli.option(
|
||||
'--level <level>',
|
||||
'specify log level ("silent" | "fatal" | "error" | "warn" | "log" | "info" | "success" | "fail" | "ready" | "start" | "box" | "debug" | "trace" | "verbose")',
|
||||
);
|
||||
|
||||
// DEV
|
||||
cli
|
||||
|
||||
@@ -27,7 +27,7 @@ import { installSourcemapsSupport } from 'vite-node/source-map';
|
||||
import { createExtensionEnvironment } from '../../utils/environments';
|
||||
import { dirname, extname, join, relative } from 'node:path';
|
||||
import fs from 'fs-extra';
|
||||
import { normalizePath } from '../../utils/paths';
|
||||
import { normalizePath } from '../../utils';
|
||||
|
||||
export async function createViteBuilder(
|
||||
wxtConfig: ResolvedConfig,
|
||||
@@ -241,10 +241,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),
|
||||
});
|
||||
@@ -453,7 +450,7 @@ async function moveHtmlFiles(
|
||||
);
|
||||
|
||||
// TODO: Optimize and only delete old path directories
|
||||
removeEmptyDirs(config.outDir);
|
||||
await removeEmptyDirs(config.outDir);
|
||||
|
||||
return movedChunks;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
fakeDevServer,
|
||||
fakeResolvedConfig,
|
||||
} from '../../../../utils/testing/fake-objects';
|
||||
import { normalizePath } from '../../../../utils/paths';
|
||||
import { normalizePath } from '../../../../utils';
|
||||
import { resolve } from 'node:path';
|
||||
import { parseHTML } from 'linkedom';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export function bundleAnalysis(config: ResolvedConfig): vite.Plugin {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated FOR TESTING ONLY.
|
||||
* @internal FOR TESTING ONLY.
|
||||
*/
|
||||
export function resetBundleIncrement() {
|
||||
increment = 0;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ResolvedConfig, WxtDevServer } from '../../../../types';
|
||||
import { getEntrypointName } from '../../../utils/entrypoints';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import { dirname, relative, resolve } from 'node:path';
|
||||
import { normalizePath } from '../../../utils/paths';
|
||||
import { normalizePath } from '../../../utils';
|
||||
import { hash } from 'ohash';
|
||||
|
||||
// Stored outside the plugin to effect all instances of the devHtmlPrerender plugin.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Plugin } from 'vite';
|
||||
import { ResolvedConfig, WxtDevServer } from '../../../../types';
|
||||
import type { Plugin } from 'vite';
|
||||
import type { ResolvedConfig, WxtDevServer } from '../../../../types';
|
||||
|
||||
/**
|
||||
* Defines global constants about the dev server. Helps scripts connect to the server's web socket.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Plugin } from 'vite';
|
||||
import { ResolvedConfig } from '../../../../types';
|
||||
import type { Plugin } from 'vite';
|
||||
import type { ResolvedConfig } from '../../../../types';
|
||||
import { fetchCached } from '../../../utils/network';
|
||||
|
||||
/**
|
||||
@@ -15,12 +15,12 @@ export function download(config: ResolvedConfig): Plugin {
|
||||
resolveId(id) {
|
||||
if (id.startsWith('url:')) return '\0' + id;
|
||||
},
|
||||
async load(id) {
|
||||
load(id) {
|
||||
if (!id.startsWith('\0url:')) return;
|
||||
|
||||
// Load file from network or cache
|
||||
const url = id.replace('\0url:', '');
|
||||
return await fetchCached(url, config);
|
||||
return fetchCached(url, config);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
|
||||
],
|
||||
},
|
||||
ssr: {
|
||||
// Inline all WXT modules subdependencies can be mocked
|
||||
// Inline all WXT modules sub-dependencies can be mocked
|
||||
noExternal: ['wxt'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Plugin } from 'vite';
|
||||
import type { Plugin } from 'vite';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../../../utils/constants';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ResolvedConfig } from '../../../../types';
|
||||
import * as vite from 'vite';
|
||||
import { normalizePath } from '../../../utils/paths';
|
||||
import type * as vite from 'vite';
|
||||
import { normalizePath } from '../../../utils';
|
||||
import { removeMainFunctionCode } from '../../../utils/transform';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { exists } from 'fs-extra';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import { resolve } from 'node:path';
|
||||
import type * as vite from 'vite';
|
||||
import { ResolvedConfig } from '../../../../types';
|
||||
@@ -25,7 +25,7 @@ export function resolveAppConfig(config: ResolvedConfig): vite.Plugin {
|
||||
async resolveId(id) {
|
||||
if (id !== virtualModuleId) return;
|
||||
|
||||
return (await exists(appConfigFile))
|
||||
return (await pathExists(appConfigFile))
|
||||
? appConfigFile
|
||||
: resolvedVirtualModuleId;
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Plugin } from 'vite';
|
||||
import type { Plugin } from 'vite';
|
||||
import { ResolvedConfig } from '../../../../types';
|
||||
import { normalizePath } from '../../../utils/paths';
|
||||
import { normalizePath } from '../../../utils';
|
||||
import {
|
||||
VirtualModuleId,
|
||||
virtualModuleNames,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { parseHTML } from 'linkedom';
|
||||
import type * as vite from 'vite';
|
||||
import { normalizePath } from '../../../utils/paths';
|
||||
import { normalizePath } from '../../../utils';
|
||||
import { ResolvedConfig } from '../../../../types';
|
||||
|
||||
/**
|
||||
|
||||
@@ -126,7 +126,7 @@ async function createServerInternal(): Promise<WxtDevServer> {
|
||||
async stop() {
|
||||
wasStopped = true;
|
||||
keyboardShortcuts.stop();
|
||||
await runner.closeBrowser();
|
||||
await runner.closeBrowser?.();
|
||||
await builderServer.close();
|
||||
await wxt.hooks.callHook('server:closed', wxt, server);
|
||||
|
||||
@@ -150,7 +150,7 @@ async function createServerInternal(): Promise<WxtDevServer> {
|
||||
server.ws.send('wxt:reload-extension');
|
||||
},
|
||||
async restartBrowser() {
|
||||
await runner.closeBrowser();
|
||||
await runner.closeBrowser?.();
|
||||
keyboardShortcuts.stop();
|
||||
await wxt.reloadConfig();
|
||||
runner = await createExtensionRunner();
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
|
||||
import { dirname, relative, resolve } from 'node:path';
|
||||
import { getEntrypointBundlePath, isHtmlEntrypoint } from './utils/entrypoints';
|
||||
import { getEntrypointGlobals, getGlobals } from './utils/globals';
|
||||
import { normalizePath } from './utils/paths';
|
||||
import { normalizePath } from './utils';
|
||||
import path from 'node:path';
|
||||
import { Message, parseI18nMessages } from './utils/i18n';
|
||||
import { writeFileIfDifferent, getPublicFiles } from './utils/fs';
|
||||
@@ -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
|
||||
@@ -136,7 +148,7 @@ declare module "wxt/browser" {
|
||||
'messages.json',
|
||||
);
|
||||
let messages: Message[];
|
||||
if (await fs.exists(defaultLocalePath)) {
|
||||
if (await fs.pathExists(defaultLocalePath)) {
|
||||
const content = JSON.parse(await fs.readFile(defaultLocalePath, 'utf-8'));
|
||||
messages = parseI18nMessages(content);
|
||||
} else {
|
||||
@@ -166,7 +178,8 @@ declare module "wxt/browser" {
|
||||
};
|
||||
|
||||
const overrides = [
|
||||
// Generate individual overloads for each message so JSDoc contains description and base translation.
|
||||
// Generate individual overloads for each message so JSDoc contains
|
||||
// description and base translation.
|
||||
...messages.map((message) =>
|
||||
renderGetMessageOverload(
|
||||
`"${message.name}"`,
|
||||
@@ -174,7 +187,8 @@ declare module "wxt/browser" {
|
||||
message.message,
|
||||
),
|
||||
),
|
||||
// Include a final union-based override so TS accepts valid string templates or concatenations
|
||||
// Include a final union-based override so TS accepts valid string
|
||||
// templates or concatenations.
|
||||
// ie: browser.i18n.getMessage(`some_enum_${enumValue}`)
|
||||
renderGetMessageOverload(
|
||||
messages.map((message) => `"${message.name}"`).join(' | '),
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './create-server';
|
||||
export * from './initialize';
|
||||
export * from './prepare';
|
||||
export * from './zip';
|
||||
export * from './utils';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { npm } from '../npm';
|
||||
import spawn from 'nano-spawn';
|
||||
import { exists } from 'fs-extra';
|
||||
import { pathExists } from 'fs-extra';
|
||||
|
||||
describe('NPM Package Management Utils', () => {
|
||||
describe('listDependencies', () => {
|
||||
@@ -41,7 +41,7 @@ describe('NPM Package Management Utils', () => {
|
||||
const actual = await npm.downloadDependency(id, downloadDir);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
expect(await exists(actual)).toBe(true);
|
||||
expect(await pathExists(actual)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ import consola, { LogLevels } from 'consola';
|
||||
import defu from 'defu';
|
||||
import { NullablyRequired } from './utils/types';
|
||||
import fs from 'fs-extra';
|
||||
import { normalizePath } from './utils/paths';
|
||||
import { normalizePath } from './utils';
|
||||
import glob from 'fast-glob';
|
||||
import { builtinModules } from '../builtin-modules';
|
||||
import { getEslintVersion } from './utils/eslint';
|
||||
@@ -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',
|
||||
@@ -532,17 +532,10 @@ async function getUnimportEslintOptions(
|
||||
* Returns the path to `node_modules/wxt`.
|
||||
*/
|
||||
function resolveWxtModuleDir() {
|
||||
// TODO: Drop the __filename expression once we're fully running in ESM
|
||||
// (see https://github.com/wxt-dev/wxt/issues/277)
|
||||
const importer =
|
||||
typeof __filename === 'string'
|
||||
? pathToFileURL(__filename).href
|
||||
: import.meta.url;
|
||||
|
||||
// TODO: Switch to import.meta.resolve() once the parent argument is unflagged
|
||||
// (e.g. --experimental-import-meta-resolve) and all Node.js versions we support
|
||||
// have it.
|
||||
const url = esmResolve('wxt', importer);
|
||||
const url = esmResolve('wxt', import.meta.url);
|
||||
|
||||
// esmResolve() returns the "wxt/dist/index.mjs" file, not the package's root
|
||||
// directory, which we want to return from this function.
|
||||
@@ -550,7 +543,7 @@ function resolveWxtModuleDir() {
|
||||
}
|
||||
|
||||
async function isDirMissing(dir: string) {
|
||||
return !(await fs.exists(dir));
|
||||
return !(await fs.pathExists(dir));
|
||||
}
|
||||
|
||||
function logMissingDir(logger: Logger, name: string, expected: string) {
|
||||
@@ -580,8 +573,8 @@ export async function mergeBuilderConfig(
|
||||
if (vite) {
|
||||
return {
|
||||
vite: async (env) => {
|
||||
const resolvedInlineConfig = (await inlineConfig.vite?.(env)) ?? {};
|
||||
const resolvedUserConfig = (await userConfig.vite?.(env)) ?? {};
|
||||
const [resolvedInlineConfig = {}, resolvedUserConfig = {}] =
|
||||
await Promise.all([inlineConfig.vite?.(env), userConfig.vite?.(env)]);
|
||||
return vite.mergeConfig(resolvedUserConfig, resolvedInlineConfig);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('createExtensionRunner', () => {
|
||||
});
|
||||
|
||||
it('should return a WSL runner when `is-wsl` is true', async () => {
|
||||
isWslMock.mockResolvedValueOnce(true);
|
||||
isWslMock.mockReturnValueOnce(true);
|
||||
setFakeWxt({
|
||||
config: {
|
||||
browser: 'chrome',
|
||||
@@ -51,7 +51,7 @@ describe('createExtensionRunner', () => {
|
||||
});
|
||||
|
||||
it('should return a manual runner when `runner.disabled` is true', async () => {
|
||||
isWslMock.mockResolvedValueOnce(false);
|
||||
isWslMock.mockReturnValueOnce(false);
|
||||
setFakeWxt({
|
||||
config: {
|
||||
browser: 'chrome',
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ExtensionRunner } from '../../types';
|
||||
import { createWslRunner } from './wsl';
|
||||
import { createWebExtRunner } from './web-ext';
|
||||
import { createSafariRunner } from './safari';
|
||||
import { createManualRunner } from './manual';
|
||||
import type { ExtensionRunner } from '../../types';
|
||||
import { isWsl } from '../utils/wsl';
|
||||
import { wxt } from '../wxt';
|
||||
import { createManualRunner } from './manual';
|
||||
import { createSafariRunner } from './safari';
|
||||
import { createWebExtRunner } from './web-ext';
|
||||
import { createWslRunner } from './wsl';
|
||||
|
||||
export async function createExtensionRunner(): Promise<ExtensionRunner> {
|
||||
if (wxt.config.browser === 'safari') return createSafariRunner();
|
||||
|
||||
if (await isWsl()) return createWslRunner();
|
||||
if (isWsl()) return createWslRunner();
|
||||
if (wxt.config.runnerConfig.config?.disabled) return createManualRunner();
|
||||
|
||||
return createWebExtRunner();
|
||||
|
||||
@@ -15,8 +15,5 @@ export function createManualRunner(): ExtensionRunner {
|
||||
)}" as an unpacked extension manually`,
|
||||
);
|
||||
},
|
||||
async closeBrowser() {
|
||||
// noop
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,8 +15,5 @@ export function createSafariRunner(): ExtensionRunner {
|
||||
)}" as an unpacked extension manually`,
|
||||
);
|
||||
},
|
||||
async closeBrowser() {
|
||||
// noop
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
@@ -88,7 +79,7 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
},
|
||||
|
||||
async closeBrowser() {
|
||||
return await runner?.exit();
|
||||
await runner?.exit();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,8 +15,5 @@ export function createWslRunner(): ExtensionRunner {
|
||||
)}" as an unpacked extension manually`,
|
||||
);
|
||||
},
|
||||
async closeBrowser() {
|
||||
// noop
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('Manifest Utils', () => {
|
||||
defaultIcon: {
|
||||
'16': '/icon/16.png',
|
||||
},
|
||||
defaultTitle: 'Default Iitle',
|
||||
defaultTitle: 'Default Title',
|
||||
},
|
||||
outputDir: outDir,
|
||||
skipped: false,
|
||||
@@ -106,6 +106,88 @@ describe('Manifest Utils', () => {
|
||||
expect(actual[expectedType]).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it('should include default_area for Firefox in mv3', async () => {
|
||||
const popup = fakePopupEntrypoint({
|
||||
options: {
|
||||
// @ts-expect-error: Force this to be undefined
|
||||
mv2Key: null,
|
||||
defaultArea: 'navbar',
|
||||
},
|
||||
outputDir: outDir,
|
||||
skipped: false,
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
outDir,
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[popup],
|
||||
buildOutput,
|
||||
);
|
||||
|
||||
expect((actual.action as any).default_area).toBe('navbar');
|
||||
});
|
||||
|
||||
it('should include theme_icons for Firefox in mv3', async () => {
|
||||
const themeIcons = [
|
||||
{ light: '/icon-light-16.png', dark: '/icon-dark-16.png', size: 16 },
|
||||
{ light: '/icon-light-32.png', dark: '/icon-dark-32.png', size: 32 },
|
||||
];
|
||||
const popup = fakePopupEntrypoint({
|
||||
options: {
|
||||
// @ts-expect-error: Force this to be undefined
|
||||
mv2Key: null,
|
||||
themeIcons,
|
||||
},
|
||||
outputDir: outDir,
|
||||
skipped: false,
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
outDir,
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[popup],
|
||||
buildOutput,
|
||||
);
|
||||
|
||||
expect((actual.action as any).theme_icons).toEqual(themeIcons);
|
||||
});
|
||||
|
||||
it('should include default_area for Firefox in mv2', async () => {
|
||||
const popup = fakePopupEntrypoint({
|
||||
options: {
|
||||
// @ts-expect-error: Force this to be undefined
|
||||
mv2Key: null,
|
||||
defaultArea: 'menupanel',
|
||||
},
|
||||
outputDir: outDir,
|
||||
skipped: false,
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 2,
|
||||
outDir,
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[popup],
|
||||
buildOutput,
|
||||
);
|
||||
|
||||
expect((actual.browser_action as any).default_area).toBe('menupanel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('action without popup', () => {
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { DevModeChange, detectDevChanges } from '../../../utils/building';
|
||||
import { DevModeChange, detectDevChanges } from '../detect-dev-changes';
|
||||
import {
|
||||
fakeBackgroundEntrypoint,
|
||||
fakeContentScriptEntrypoint,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
fakeOutputChunk,
|
||||
fakeWxt,
|
||||
setFakeWxt,
|
||||
} from '../../../utils/testing/fake-objects';
|
||||
} from '../../testing/fake-objects';
|
||||
import { BuildOutput, BuildStepOutput } from '../../../../types';
|
||||
import { setWxtForTesting } from '../../../wxt';
|
||||
|
||||
|
||||
@@ -13,11 +13,8 @@ import { resolve } from 'path';
|
||||
import { findEntrypoints } from '../find-entrypoints';
|
||||
import fs from 'fs-extra';
|
||||
import glob from 'fast-glob';
|
||||
import {
|
||||
fakeResolvedConfig,
|
||||
setFakeWxt,
|
||||
} from '../../../utils/testing/fake-objects';
|
||||
import { unnormalizePath } from '../../../utils/paths';
|
||||
import { fakeResolvedConfig, setFakeWxt } from '../../testing/fake-objects';
|
||||
import { unnormalizePath } from '../../paths';
|
||||
import { wxt } from '../../../wxt';
|
||||
|
||||
vi.mock('fast-glob');
|
||||
@@ -115,7 +112,9 @@ describe('findEntrypoints', () => {
|
||||
name: 'options',
|
||||
inputPath: resolve(config.entrypointsDir, 'options.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
options: {
|
||||
title: 'Default Title',
|
||||
},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
@@ -136,6 +135,7 @@ describe('findEntrypoints', () => {
|
||||
outputDir: config.outDir,
|
||||
options: {
|
||||
openInTab: true,
|
||||
title: 'Title',
|
||||
},
|
||||
skipped: false,
|
||||
},
|
||||
@@ -153,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',
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
EntrypointGroup,
|
||||
OutputFile,
|
||||
} from '../../../types';
|
||||
import { every, some } from '../../utils/arrays';
|
||||
import { normalizePath } from '../../utils/paths';
|
||||
import { every, some } from '../arrays';
|
||||
import { normalizePath } from '../paths';
|
||||
import { wxt } from '../../wxt';
|
||||
|
||||
/**
|
||||
@@ -207,10 +207,6 @@ interface ExtensionReload extends RebuildChange {
|
||||
type: 'extension-reload';
|
||||
}
|
||||
|
||||
// interface BrowserRestart extends RebuildChange {
|
||||
// type: 'browser-restart';
|
||||
// }
|
||||
|
||||
interface ContentScriptReload extends RebuildChange {
|
||||
type: 'content-script-reload';
|
||||
changedSteps: BuildStepOutput[];
|
||||
|
||||
@@ -21,9 +21,9 @@ import {
|
||||
isHtmlEntrypoint,
|
||||
isJsEntrypoint,
|
||||
resolvePerBrowserOptions,
|
||||
} from '../../utils/entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../../utils/constants';
|
||||
import { CSS_EXTENSIONS_PATTERN } from '../../utils/paths';
|
||||
} from '../entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../constants';
|
||||
import { CSS_EXTENSIONS_PATTERN } from '../paths';
|
||||
import pc from 'picocolors';
|
||||
import { wxt } from '../../wxt';
|
||||
import { camelCase } from 'scule';
|
||||
@@ -77,9 +77,8 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
|
||||
(entry) =>
|
||||
entry.name === name && entry.inputPath.endsWith('index.html'),
|
||||
);
|
||||
if (hasIndexHtml) return false;
|
||||
|
||||
return true;
|
||||
return !hasIndexHtml;
|
||||
});
|
||||
|
||||
await wxt.hooks.callHook('entrypoints:found', wxt, entrypointInfos);
|
||||
@@ -116,20 +115,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,
|
||||
};
|
||||
}
|
||||
}),
|
||||
@@ -184,8 +177,7 @@ async function importEntrypoints(infos: EntrypointInfo[]) {
|
||||
await Promise.all([
|
||||
// HTML
|
||||
...htmlInfos.map(async (info) => {
|
||||
const res = await importHtmlEntrypoint(info);
|
||||
resMap[info.inputPath] = res;
|
||||
resMap[info.inputPath] = await importHtmlEntrypoint(info);
|
||||
}),
|
||||
// JS
|
||||
(async () => {
|
||||
@@ -202,7 +194,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>> {
|
||||
@@ -216,9 +208,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 {
|
||||
@@ -268,24 +267,24 @@ async function getPopupEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
options: Record<string, any>,
|
||||
): Promise<PopupEntrypoint> {
|
||||
const stictOptions: PopupEntrypoint['options'] = resolvePerBrowserOptions(
|
||||
// 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,
|
||||
...perBrowserOptions,
|
||||
defaultTitle: title,
|
||||
mv2Key: type,
|
||||
},
|
||||
wxt.config.browser,
|
||||
);
|
||||
if (stictOptions.mv2Key && stictOptions.mv2Key !== 'page_action')
|
||||
stictOptions.mv2Key = 'browser_action';
|
||||
if (strictOptions.mv2Key && strictOptions.mv2Key !== 'page_action')
|
||||
strictOptions.mv2Key = 'browser_action';
|
||||
|
||||
return {
|
||||
type: 'popup',
|
||||
name: 'popup',
|
||||
options: stictOptions,
|
||||
options: { ...strictOptions, themeIcons },
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
};
|
||||
@@ -298,16 +297,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,
|
||||
};
|
||||
@@ -322,10 +312,7 @@ async function getUnlistedPageEntrypoint(
|
||||
name: info.name,
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: {
|
||||
include: options.include,
|
||||
exclude: options.exclude,
|
||||
},
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -338,13 +325,7 @@ async function getUnlistedScriptEntrypoint(
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: resolvePerBrowserOptions(
|
||||
{
|
||||
include: options.include,
|
||||
exclude: options.exclude,
|
||||
},
|
||||
wxt.config.browser,
|
||||
),
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -353,15 +334,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;
|
||||
@@ -398,17 +371,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,
|
||||
),
|
||||
|
||||
@@ -3,10 +3,10 @@ import { BuildOutput, Entrypoint } from '../../../types';
|
||||
import pc from 'picocolors';
|
||||
import fs from 'fs-extra';
|
||||
import { groupEntrypoints } from './group-entrypoints';
|
||||
import { formatDuration } from '../../utils/time';
|
||||
import { printBuildSummary } from '../../utils/log';
|
||||
import { formatDuration } from '../time';
|
||||
import { printBuildSummary } from '../log';
|
||||
import glob from 'fast-glob';
|
||||
import { unnormalizePath } from '../../utils/paths';
|
||||
import { unnormalizePath } from '../paths';
|
||||
import { rebuild } from './rebuild';
|
||||
import { relative } from 'node:path';
|
||||
import {
|
||||
@@ -87,7 +87,7 @@ export async function internalBuild(): Promise<BuildOutput> {
|
||||
} else {
|
||||
wxt.logger.info(`Opening ${pc.yellow(statsPath)} in browser...`);
|
||||
const { default: open } = await import('open');
|
||||
open(wxt.config.analysis.outputFile);
|
||||
await open(wxt.config.analysis.outputFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,12 +133,12 @@ function printValidationResults({
|
||||
}, new Map<Entrypoint, ValidationResult[]>());
|
||||
|
||||
Array.from(entrypointErrors.entries()).forEach(([entrypoint, errors]) => {
|
||||
wxt.logger.log(relative(cwd, entrypoint.inputPath));
|
||||
console.log();
|
||||
wxt.logger.log(relative(cwd, entrypoint.inputPath) + '\n');
|
||||
|
||||
errors.forEach((err) => {
|
||||
const type = err.type === 'error' ? pc.red('ERROR') : pc.yellow('WARN');
|
||||
const recieved = pc.dim(`(recieved: ${JSON.stringify(err.value)})`);
|
||||
wxt.logger.log(` - ${type} ${err.message} ${recieved}`);
|
||||
const received = pc.dim(`(received: ${JSON.stringify(err.value)})`);
|
||||
wxt.logger.log(` - ${type} ${err.message} ${received}`);
|
||||
});
|
||||
console.log();
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user