Compare commits
150 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2dbcde124 | |||
| 60f00db2c5 | |||
| a9332453fc | |||
| b99da14799 | |||
| f02400a1fb | |||
| bbcb7f7cdd | |||
| 30cf8e6d54 | |||
| 4f772fc0f7 | |||
| 2cf2582138 | |||
| ad4de4b92c | |||
| 27ed73a6ae | |||
| a1570c8917 | |||
| 7e419ddd43 | |||
| b045d3616d | |||
| f6b50e2e77 | |||
| 9d1381cbb3 | |||
| 99b0242d86 | |||
| db27628211 | |||
| 9b9d86229a | |||
| 46f830dacf | |||
| 3dd9bc7772 | |||
| c7335add19 | |||
| c133498208 | |||
| 6a52bb22a3 | |||
| 6f970efda5 | |||
| c69350a0eb | |||
| 4add8820eb | |||
| 033dd4811e | |||
| da5cd32502 | |||
| edf33fdec9 | |||
| 448cbf16c9 | |||
| e45b77b528 | |||
| b61fd1716e | |||
| d584b5cc7e | |||
| faf9cedfbd | |||
| 3a38ac9339 | |||
| 7ea36a3cfc | |||
| 3577c0b47a | |||
| d0bef42186 | |||
| d9f10c62ad | |||
| 35d3c03e4b | |||
| fdd38a1580 | |||
| 3018801f06 | |||
| 8e96bfef06 | |||
| 31071bd11e | |||
| f5619f6be1 | |||
| adad1b5a2c | |||
| a6c4e19a5d | |||
| af6f74c344 | |||
| ce45cb8a89 | |||
| 14d4aa202d | |||
| 4c35798dba | |||
| 656a9b365f | |||
| 67fa3db921 | |||
| f2d3061e97 | |||
| 760c34e416 | |||
| 48398b315c | |||
| 45d0d9d7f1 | |||
| b0f4ac8221 | |||
| 719192b1aa | |||
| 7eb32bdac8 | |||
| 862756fd5b | |||
| 5ba16e72e0 | |||
| e43ae0504d | |||
| acb6cd180c | |||
| a3301d0413 | |||
| b9e72358ac | |||
| 61b42ef326 | |||
| da9e27c28d | |||
| 6f80fbabaa | |||
| d2308bd45c | |||
| d35972d24f | |||
| 298a264934 | |||
| ad63b595b4 | |||
| 986a9ceebf | |||
| 682b65e723 | |||
| cb661506c0 | |||
| 505aa06443 | |||
| f534a978f1 | |||
| 1293815934 | |||
| 0c0fd18bf4 | |||
| c49c31813f | |||
| 877ab11bb4 | |||
| 0d18d8d7e3 | |||
| 5f977857a1 | |||
| a63b21d417 | |||
| e1b7d6090e | |||
| 1bcc5cdd81 | |||
| 873fdbc4b3 | |||
| 79128effea | |||
| 14ef94fc09 | |||
| 759e3e3a4d | |||
| 5b7b16efba | |||
| 7fd171acb5 | |||
| 02ed3f1d44 | |||
| 4f7f3f36a3 | |||
| 73a8cf12a9 | |||
| 5f5eb16705 | |||
| f0f4437870 | |||
| 868c042511 | |||
| bdb49c0b3d | |||
| cc3c43d1bd | |||
| e47131efc1 | |||
| 82d8024fb4 | |||
| c662a59d85 | |||
| 4e50fb1f92 | |||
| fbae370d1e | |||
| 9c64c9dd61 | |||
| 8f6dba20d2 | |||
| f531fcd5c3 | |||
| ff1720a4c6 | |||
| d62203dff8 | |||
| 99b5076d69 | |||
| 298e7101f7 | |||
| b4ce36b708 | |||
| 5d096a4a92 | |||
| e54df0aca8 | |||
| 2e8baf0161 | |||
| c9dca0222c | |||
| 6044ab73bd | |||
| 660945c792 | |||
| bcb20874a8 | |||
| 83ad0e3ff0 | |||
| aad17c8d26 | |||
| b0ef178c9c | |||
| 0175c430a3 | |||
| 2776587392 | |||
| b978465d7a | |||
| f7989ea1e0 | |||
| 0cf34d3170 | |||
| 4fe04c6f8a | |||
| 1773762d2f | |||
| 9057000e45 | |||
| d540233673 | |||
| 9907290a04 | |||
| d9fb919580 | |||
| 17723d5828 | |||
| c7db2d26b3 | |||
| 259cec9ea8 | |||
| 2ced9c40d3 | |||
| 756efc9311 | |||
| 17fd2ff0ce | |||
| 35833c00f0 | |||
| 300c045187 | |||
| 4c05cc19e5 | |||
| 72dab65a1d | |||
| 4e5fb7745a | |||
| 2bc92043f8 | |||
| 3024bd825d | |||
| 0b39774690 |
@@ -11,3 +11,6 @@ yarn.lock linguist-generated
|
||||
|
||||
# Exclude templates from language statistics
|
||||
templates/**/* linguist-vendored
|
||||
|
||||
# Other generated files
|
||||
packages/browser/src/gen/** linguist-generated
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: "\U0001F41E Bug report"
|
||||
description: Report an issue with WXT
|
||||
labels: [pending-triage]
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for WXT
|
||||
title: ''
|
||||
labels: feature
|
||||
type: Feature
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@ on:
|
||||
- module-solid
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- runner
|
||||
- storage
|
||||
- unocss
|
||||
- webextension-polyfill
|
||||
- wxt
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -14,7 +14,9 @@ on:
|
||||
- module-solid
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- runner
|
||||
- storage
|
||||
- webextension-polyfill
|
||||
- wxt
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
name: 🔄 Update @wxt-dev/browser
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Every day at midnight
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: 'Sync with @types/chrome'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
installArgs: --ignore-scripts
|
||||
|
||||
- name: Generate Latest Code
|
||||
working-directory: packages/browser
|
||||
run: pnpm gen
|
||||
|
||||
- name: Run Checks
|
||||
working-directory: packages/browser
|
||||
run: pnpm check
|
||||
|
||||
- name: Commit Changes
|
||||
id: commit
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
commit_message: 'fix: Upgrade `@wxt-dev/browser` to latest `@types/chrome` version'
|
||||
|
||||
- name: Publish Package
|
||||
if: steps.commit.outputs.changes_detected == 'true'
|
||||
working-directory: packages/browser
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_AUTH_TOKEN }}" > ~/.npmrc
|
||||
pnpm publish
|
||||
@@ -5,8 +5,10 @@
|
||||
.output
|
||||
.webextrc
|
||||
.wxt
|
||||
.wxt-runner
|
||||
*.log
|
||||
/docs/.vitepress/cache
|
||||
docs/.vitepress/.temp
|
||||
coverage
|
||||
dist
|
||||
node_modules
|
||||
|
||||
@@ -5,3 +5,4 @@ dist
|
||||
docs/.vitepress/cache
|
||||
pnpm-lock.yaml
|
||||
CHANGELOG.md
|
||||
packages/browser/src/gen
|
||||
|
||||
@@ -174,3 +174,13 @@ npm i https://pkg.pr.new/@wxt-dev/module-react@main
|
||||
# Install `@wxt-dev/storage` from a specific commit:
|
||||
npm i https://pkg.pr.new/@wxt-dev/module-react@426f907
|
||||
```
|
||||
|
||||
## Blog Posts
|
||||
|
||||
Anyone is welcome to submit a blog post on https://wxt.dev/blog!
|
||||
|
||||
> [!NOTE]
|
||||
> Before starting on a blog post, please message Aaron on Discord or start a discussion on GitHub to get permission to write about a topic, but most topics are welcome: Major version updates, tutorials, etc.
|
||||
|
||||
- **English only**: Blog posts should be written in English. Unfortunately, our maintainers doesn't have the bandwidth right now to translate our docs, let alone blog posts. Sorry 😓
|
||||
- **AI**: Please only use AI to translate or proof-read your blog post. Don't generate the whole thing... We don't want to publish that.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
// @ts-expect-error: Vitepress data-loader magic, this import is correct
|
||||
import { data } from '../loaders/blog.data';
|
||||
import BlogPostPreview from './BlogPostPreview.vue';
|
||||
|
||||
const posts = computed(() =>
|
||||
data
|
||||
.map((post) => ({
|
||||
...post,
|
||||
...post.frontmatter,
|
||||
date: new Date(post.frontmatter.date),
|
||||
}))
|
||||
.sort((a, b) => b.date.getTime() - a.date.getTime()),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div>
|
||||
<div class="vp-doc">
|
||||
<h1>Blog</h1>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<BlogPostPreview v-for="post of posts" :key="post.url" :post />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.container > div {
|
||||
padding: 32px;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul li {
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid var(--vp-c-default);
|
||||
}
|
||||
ul li:last-child {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--vp-c-default);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import useBlogDate from '../composables/useBlogDate';
|
||||
import { useData } from 'vitepress';
|
||||
|
||||
const { frontmatter } = useData();
|
||||
const date = useBlogDate(() => frontmatter.value.date);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-doc">
|
||||
<main class="container-content">
|
||||
<h1 v-html="$frontmatter.title" />
|
||||
<p class="meta-row">
|
||||
<a
|
||||
class="author"
|
||||
v-for="author of $frontmatter.authors"
|
||||
:key="author.github"
|
||||
:href="`https://github.com/${author.github}`"
|
||||
>
|
||||
<img :src="`https://github.com/${author.github}.png?size=96`" />
|
||||
<span>{{ author.name }}</span>
|
||||
</a>
|
||||
<span>•</span>
|
||||
<span>{{ date }}</span>
|
||||
</p>
|
||||
<Content />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
vp-doc {
|
||||
display: flex;
|
||||
}
|
||||
main {
|
||||
max-width: 1080px;
|
||||
padding: 32px;
|
||||
margin: auto;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
main {
|
||||
padding: 64px;
|
||||
}
|
||||
}
|
||||
.meta-row {
|
||||
display: flex;
|
||||
color: var(--vp-c-text-2);
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
.meta-row > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.author {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
.author img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
.author span {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.author:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts" setup>
|
||||
import useBlogDate from '../composables/useBlogDate';
|
||||
|
||||
const props = defineProps<{
|
||||
post: {
|
||||
title: string;
|
||||
description?: string;
|
||||
date: Date;
|
||||
url: string;
|
||||
authors: Array<{ name: string; github: string }>;
|
||||
};
|
||||
}>();
|
||||
|
||||
const date = useBlogDate(() => props.post.date);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="blog-list-item">
|
||||
<a :href="post.url">
|
||||
<div class="vp-doc">
|
||||
<h3 class="title" v-html="post.title" />
|
||||
<p class="description" v-html="post.description" />
|
||||
<p class="meta">
|
||||
{{ post.authors.map((author) => author.name).join(', ') }}
|
||||
•
|
||||
{{ date }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
li > a > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 -16px;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
li > a > div:hover {
|
||||
background: var(--vp-c-default);
|
||||
}
|
||||
li .title {
|
||||
color: var(--vp-c-text);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
li .description {
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
li .meta {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
@@ -68,6 +68,23 @@ const chromeExtensionIds = [
|
||||
'dlnjcbkmomenmieechnmgglgcljhoepd', // Youtube Live Chat Fullscreen
|
||||
'keiealdacakpnbbljlmhfgcebmaadieg', // Python Code Runner
|
||||
'hafcajcllbjnoolpfngclfmmgpikdhlm', // Monochromate
|
||||
'bmoggiinmnodjphdjnmpcnlleamkfedj', // AliasVault - Open-Source Password & (Email) Alias Manager
|
||||
'hlnhhamckimoaiekbglafiebkfimhapb', // SnapThePrice: AI-Powered Real-time Lowest Price Finder
|
||||
'gdjampjdgjmbifnhldgcnccdjkcoicmg', // radiofrance - news & broadcasts (French), music (international)
|
||||
'jlnhphlghikichhgbnkepenehbmloenb', // Blens - Time Tracker and AI Insight
|
||||
'njnammmpdodmfkodnfpammnpdcbhnlcm', // Always Light Mode - Setting website always in light mode
|
||||
'lblmfclcfniabobmamfkdogcgdagbhhb', // DesignPicker - Color Picker & Font Detector
|
||||
'pamnlaoeobcmhkliljfaofekeddpmfoh', // Web to PDF
|
||||
'jmbcbeepjfenihlocplnbmbhimcoooka', // Online CSV Viewer
|
||||
'nkjcoophmpcmmgadnljnlpbpfdfacgbo', // YouTube Video Transcript
|
||||
'lcaieahkjgeggeiihblhcjbbjlppgieh', // NetSuite Record Scripts
|
||||
'gmocfknjllodfiomnljmaehcplnekhlo', // VueTracker
|
||||
'ggcfemmoabhhelfkhknhbnkmeahloiod', // CanCopy - A web extension that allow you to copy any content from website
|
||||
'modkelfkcfjpgbfmnbnllalkiogfofhb', // Language Learning with AI
|
||||
'npfopljnjbamegincfjelhjhnonnjloo', // Bilibili Feed History Helper
|
||||
'edkhpdceeinkcacjdgebjehipmnbomce', // NZBDonkey - The ultimate NZB file download tool
|
||||
'cckggnbnimdbbpmdinkkgbbncopbloob', // WeChat Markdown Editor(微信 Markdown 编辑器)
|
||||
'jcblcjolcojmfopefcighfmkkefbaofg', // Tab Grab
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { computed, toValue, MaybeRefOrGetter } from 'vue';
|
||||
|
||||
const MONTH_FORMATTER = new Intl.DateTimeFormat(
|
||||
globalThis?.navigator?.language,
|
||||
{
|
||||
month: 'long',
|
||||
},
|
||||
);
|
||||
|
||||
export default function (date: MaybeRefOrGetter<Date | string>) {
|
||||
return computed(() => {
|
||||
const d = new Date(toValue(date));
|
||||
return `${MONTH_FORMATTER.format(d)} ${d.getDate()}, ${d.getFullYear()}`;
|
||||
});
|
||||
}
|
||||
+75
-15
@@ -1,4 +1,4 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { DefaultTheme, defineConfig } from 'vitepress';
|
||||
import typedocSidebar from '../api/reference/typedoc-sidebar.json';
|
||||
import {
|
||||
menuGroup,
|
||||
@@ -15,15 +15,26 @@ import { version as autoIconsVersion } from '../../packages/auto-icons/package.j
|
||||
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 knowledge from 'vitepress-knowledge';
|
||||
import { version as runnerVersion } from '../../packages/runner/package.json';
|
||||
import addKnowledge from 'vitepress-knowledge';
|
||||
import {
|
||||
groupIconMdPlugin,
|
||||
groupIconVitePlugin,
|
||||
localIconLoader,
|
||||
} from 'vitepress-plugin-group-icons';
|
||||
import { Feed } from 'feed';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const origin = 'https://wxt.dev';
|
||||
|
||||
const title = 'Next-gen Web Extension Framework';
|
||||
const titleSuffix = ' – WXT';
|
||||
const description =
|
||||
"WXT provides the best developer experience, making it quick, easy, and fun to develop web extensions. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started.";
|
||||
const ogTitle = `${title}${titleSuffix}`;
|
||||
const ogUrl = 'https://wxt.dev';
|
||||
const ogImage = 'https://wxt.dev/social-preview.png';
|
||||
const ogUrl = origin;
|
||||
const ogImage = `${origin}/social-preview.png`;
|
||||
|
||||
const otherPackages = {
|
||||
analytics: analyticsVersion,
|
||||
@@ -31,30 +42,74 @@ const otherPackages = {
|
||||
i18n: i18nVersion,
|
||||
storage: storageVersion,
|
||||
unocss: unocssVersion,
|
||||
runner: runnerVersion,
|
||||
};
|
||||
|
||||
const knowledge = addKnowledge<DefaultTheme.Config>({
|
||||
serverUrl: 'https://knowledge.wxt.dev',
|
||||
paths: {
|
||||
'/': 'docs',
|
||||
'/api/': 'api-reference',
|
||||
'/blog/': 'blog',
|
||||
},
|
||||
layoutSelectors: {
|
||||
blog: '.container-content',
|
||||
},
|
||||
pageSelectors: {
|
||||
'examples.md': '#VPContent > .VPPage',
|
||||
'blog.md': '#VPContent > .VPPage',
|
||||
},
|
||||
});
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
extends: knowledge({
|
||||
serverUrl: 'https://knowledge.wxt.dev',
|
||||
paths: {
|
||||
'/': 'docs',
|
||||
'/api/': 'api-reference',
|
||||
},
|
||||
pageSelectors: {
|
||||
'examples.md': '#VPContent > .VPPage',
|
||||
},
|
||||
}),
|
||||
extends: knowledge,
|
||||
|
||||
titleTemplate: `:title${titleSuffix}`,
|
||||
title: 'WXT',
|
||||
description,
|
||||
vite: {
|
||||
clearScreen: false,
|
||||
plugins: [
|
||||
groupIconVitePlugin({
|
||||
customIcon: {
|
||||
'wxt.config.ts': localIconLoader(
|
||||
import.meta.url,
|
||||
'../public/logo.svg',
|
||||
),
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
lastUpdated: true,
|
||||
sitemap: {
|
||||
hostname: 'https://wxt.dev',
|
||||
hostname: origin,
|
||||
},
|
||||
|
||||
async buildEnd(site) {
|
||||
// @ts-expect-error: knowledge.buildEnd is not typed, but it exists.
|
||||
await knowledge.buildEnd(site);
|
||||
|
||||
// Only construct the RSS document for production builds
|
||||
const { default: blogDataLoader } = await import('./loaders/blog.data');
|
||||
const posts = await blogDataLoader.load();
|
||||
const feed = new Feed({
|
||||
copyright: 'MIT',
|
||||
id: 'wxt',
|
||||
title: 'WXT Blog',
|
||||
link: `${origin}/blog`,
|
||||
});
|
||||
posts.forEach((post) => {
|
||||
feed.addItem({
|
||||
date: post.frontmatter.date,
|
||||
link: new URL(post.url, origin).href,
|
||||
title: post.frontmatter.title,
|
||||
description: post.frontmatter.description,
|
||||
});
|
||||
});
|
||||
// console.log('rss.xml:');
|
||||
// console.log(feed.rss2());
|
||||
await writeFile(join(site.outDir, 'rss.xml'), feed.rss2(), 'utf8');
|
||||
},
|
||||
|
||||
head: [
|
||||
@@ -73,6 +128,10 @@ export default defineConfig({
|
||||
markdown: {
|
||||
config: (md) => {
|
||||
md.use(footnote);
|
||||
md.use(groupIconMdPlugin);
|
||||
},
|
||||
languageAlias: {
|
||||
mjs: 'js',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -110,6 +169,7 @@ export default defineConfig({
|
||||
navItem('Guide', '/guide/installation'),
|
||||
navItem('Examples', '/examples'),
|
||||
navItem('API', '/api/reference/wxt'),
|
||||
navItem('Blog', '/blog'),
|
||||
navItem(`v${wxtVersion}`, [
|
||||
navItem('wxt', [
|
||||
navItem(`v${wxtVersion}`, '/'),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createContentLoader } from 'vitepress';
|
||||
|
||||
export default createContentLoader('blog/*.md');
|
||||
@@ -3,14 +3,18 @@ import Icon from '../components/Icon.vue';
|
||||
import EntrypointPatterns from '../components/EntrypointPatterns.vue';
|
||||
import UsingWxtSection from '../components/UsingWxtSection.vue';
|
||||
import ExampleSearch from '../components/ExampleSearch.vue';
|
||||
import BlogLayout from '../components/BlogLayout.vue';
|
||||
import './custom.css';
|
||||
import 'virtual:group-icons.css';
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp(ctx) {
|
||||
ctx.app.component('Icon', Icon);
|
||||
ctx.app.component('EntrypointPatterns', EntrypointPatterns);
|
||||
ctx.app.component('UsingWxtSection', UsingWxtSection);
|
||||
ctx.app.component('ExampleSearch', ExampleSearch);
|
||||
ctx.app
|
||||
.component('Icon', Icon)
|
||||
.component('EntrypointPatterns', EntrypointPatterns)
|
||||
.component('UsingWxtSection', UsingWxtSection)
|
||||
.component('ExampleSearch', ExampleSearch)
|
||||
.component('blog', BlogLayout);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
layout: page
|
||||
---
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BlogHome from './.vitepress/components/BlogHome.vue';
|
||||
</script>
|
||||
|
||||
<BlogHome />
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
layout: blog
|
||||
title: Real World Messaging
|
||||
description: |
|
||||
The extension messaging APIs are difficult to learn. Let's go beyond the simple examples from Chrome and Firefox's documentation to build our own simple messaging system from scratch.
|
||||
authors:
|
||||
- name: Aaron Klinker
|
||||
github: aklinker1
|
||||
date: 2024-10-20T04:54:23.601Z
|
||||
---
|
||||
|
||||
Test content **bold** _italic_
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
layout: blog
|
||||
title: Introducing <code>#imports</code>
|
||||
description: Learn how WXT's new <code>#imports</code> module works and how to use it.
|
||||
authors:
|
||||
- name: Aaron Klinker
|
||||
github: aklinker1
|
||||
date: 2024-12-06T14:39:00.000Z
|
||||
---
|
||||
|
||||
WXT v0.20 introduced a new way of manually importing its APIs: **the `#imports` module**. This module was introduced to simplify import statements and provide more visibility into all the APIs WXT provides.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
import { browser } from 'wxt/browser'; // [!code --]
|
||||
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root'; // [!code --]
|
||||
import { defineContentScript } from 'wxt/utils/define-content-script'; // [!code --]
|
||||
import { injectScript } from 'wxt/utils/inject-script'; // [!code --]
|
||||
import { // [!code ++]
|
||||
browser, createShadowRootUi, defineContentScript, injectScript // [!code ++]
|
||||
} from '#imports'; // [!code ++]
|
||||
```
|
||||
|
||||
The `#imports` module is considered a "virtual module", because the file doesn't actually exist. At build-time, imports are split into individual statements for each API:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [What you write]
|
||||
import { defineContentScript, injectScript } from '#imports';
|
||||
```
|
||||
|
||||
```ts [What the bundler sees]
|
||||
import { defineContentScript } from 'wxt/utils/define-content-script';
|
||||
import { injectScript } from 'wxt/utils/inject-script';
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Think of `#imports` as a convenient way to access all of WXT's APIs from one place, without impacting performance or bundle size.
|
||||
|
||||
This enables better tree-shaking compared to v0.19 and below.
|
||||
|
||||
:::tip Need to lookup the full import path of an API?
|
||||
Open up your project's `.wxt/types/imports-module.d.ts` file.
|
||||
:::
|
||||
|
||||
## Mocking
|
||||
|
||||
When writing tests, you might need to mock APIs from the `#imports` module. While mocking these APIs is very easy, it may not be immediately clear how to accomplish it.
|
||||
|
||||
Let's look at an example using Vitest. When [configured with `wxt/testing`](/guide/essentials/unit-testing#vitest), Vitest sees the same transformed code as the bundler. That means to mock an API from `#imports`, you need to call `vi.mock` with the real import path, not `#imports`:
|
||||
|
||||
```ts
|
||||
import { injectScript } from '#imports';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
vi.mock('wxt/utils/inject-script')
|
||||
const injectScriptMock = vi.mocked(injectScript);
|
||||
|
||||
injectScriptMock.mockReturnValueOnce(...);
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
You don't have to use `#imports` if you don't like - you can continue importing APIs from their submodules. However, using `#imports` is the recommended approach moving forwards.
|
||||
|
||||
- As more APIs are added, you won't have to memorize additional import paths.
|
||||
- If breaking changes are made to import paths in future major versions, `#imports` won't break.
|
||||
|
||||
Happy Coding 😄
|
||||
|
||||
> P.S. Yes, this is exactly how [Nuxt's `#imports`](https://nuxt.com/docs/guide/concepts/auto-imports#explicit-imports) works! We use the exact same library, [`unimport`](https://github.com/unjs/unimport).
|
||||
|
||||
---
|
||||
|
||||
[Discuss this blog post on Github](https://github.com/wxt-dev/wxt/discussions/1543).
|
||||
@@ -46,7 +46,7 @@ import image from '~/assets/image.png';
|
||||
|
||||
## `/public` Directory
|
||||
|
||||
Files inside `<srcDir>/public/` are copied into the output folder as-is, without being processed by WXT's bundler.
|
||||
Files inside `<rootDir>/public/` are copied into the output folder as-is, without being processed by WXT's bundler.
|
||||
|
||||
Here's how you access them:
|
||||
|
||||
@@ -81,6 +81,10 @@ img.src = imageUrl;
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Assets in the `public/` directory are **_not_** accessible in content scripts by default. To use a public asset in a content script, you must add it to your manifest's [`web_accessible_resources` array](/api/reference/wxt/type-aliases/UserManifest#web-accessible-resources).
|
||||
:::
|
||||
|
||||
## Inside Content Scripts
|
||||
|
||||
Assets inside content scripts are a little different. By default, when you import an asset, it returns just the path to the asset. This is because Vite assumes you're loading assets from the same hostname.
|
||||
@@ -89,8 +93,7 @@ But, inside content scripts, the hostname is whatever the tab is set to. So if y
|
||||
|
||||
To fix this, you need to convert the image to a full URL using `browser.runtime.getURL`:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
```ts [entrypoints/content.ts]
|
||||
import iconUrl from '/icon/128.png';
|
||||
|
||||
export default defineContentScript({
|
||||
@@ -135,8 +138,7 @@ Run `wxt build`, and you should see the WASM file copied into your `.output/chro
|
||||
|
||||
Next, since this is in a content script and we'll be fetching the WASM file over the network to load it, we need to add the file to the `web_accessible_resources`:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
web_accessible_resources: [
|
||||
@@ -153,8 +155,7 @@ export default defineConfig({
|
||||
|
||||
And finally, we need to load and initialize the `.wasm` file inside the content script to use it:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
```ts [entrypoints/content.ts]
|
||||
import initWasm, { parseSync } from '@oxc-parser/wasm';
|
||||
|
||||
export default defineContentScript({
|
||||
|
||||
@@ -11,19 +11,7 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
By default, WXT sets up auto-imports for all of it's own APIs:
|
||||
|
||||
- [`browser`](/api/reference/wxt/browser/variables/browser) from `wxt/browser`
|
||||
- [`defineContentScript`](/api/reference/wxt/sandbox/functions/defineContentScript) from `wxt/sandbox`
|
||||
- [`defineBackground`](/api/reference/wxt/sandbox/functions/defineBackground) from `wxt/sandbox`
|
||||
- [`defineUnlistedScript`](/api/reference/wxt/sandbox/functions/defineUnlistedScript) from `wxt/sandbox`
|
||||
- [`createIntegratedUi`](/api/reference/wxt/client/functions/createIntegratedUi) from `wxt/client`
|
||||
- [`createShadowRootUi`](/api/reference/wxt/client/functions/createShadowRootUi) from `wxt/client`
|
||||
- [`createIframeUi`](/api/reference/wxt/client/functions/createIframeUi) from `wxt/client`
|
||||
- [`fakeBrowser`](/api/reference/wxt/testing/variables/fakeBrowser) from `wxt/testing`
|
||||
- And more!
|
||||
|
||||
WXT also adds some project directories as auto-import sources automatically:
|
||||
By default, WXT automatically sets up auto-imports for all of it's own APIs and some of your project directories:
|
||||
|
||||
- `<srcDir>/components/*`
|
||||
- `<srcDir>/composables/*`
|
||||
@@ -32,6 +20,8 @@ WXT also adds some project directories as auto-import sources automatically:
|
||||
|
||||
All named and default exports from files in these directories are available everywhere else in your project without having to import them.
|
||||
|
||||
To see the complete list of auto-imported APIs, run [`wxt prepare`](/api/cli/wxt-prepare) and look at your project's `.wxt/types/imports-module.d.ts` file.
|
||||
|
||||
## TypeScript
|
||||
|
||||
For TypeScript and your editor to recognize auto-imported variables, you need to run the [`wxt prepare` command](/api/cli/wxt-prepare).
|
||||
@@ -110,3 +100,19 @@ export default defineConfig({
|
||||
imports: false, // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
## Explicit Imports (`#imports`)
|
||||
|
||||
You can manually import all of WXT's APIs via the `#imports` module:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createShadowRootUi,
|
||||
ContentScriptContext,
|
||||
MatchPattern,
|
||||
} from '#imports';
|
||||
```
|
||||
|
||||
To learn more about how the `#imports` module works, read the [related blog post](/blog/2024-12-06-using-imports-module).
|
||||
|
||||
If you've disabled auto-imports, you should still use `#imports` to import all of WXT's APIs from a single place.
|
||||
|
||||
@@ -4,7 +4,7 @@ outline: deep
|
||||
|
||||
# Browser Startup
|
||||
|
||||
> See the [API Reference](/api/reference/wxt/interfaces/ExtensionRunnerConfig) for a full list of config.
|
||||
> See the [API Reference](/api/reference/wxt/interfaces/WebExtConfig) for a full list of config.
|
||||
|
||||
During development, WXT uses [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext) to automatically open a browser window with your extension installed.
|
||||
|
||||
@@ -14,15 +14,15 @@ You can configure browser startup in 3 places:
|
||||
|
||||
1. `<rootDir>/web-ext.config.ts`: Ignored from version control, this file lets you configure your own options for a specific project without affecting other developers
|
||||
|
||||
```ts
|
||||
import { defineRunnerConfig } from 'wxt';
|
||||
```ts [web-ext.config.ts]
|
||||
import { defineWebExtConfig } from 'wxt';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
2. `<rootDir>/wxt.config.ts`: Via the [`runner` config](/api/reference/wxt/interfaces/InlineConfig#runner), included in version control
|
||||
2. `<rootDir>/wxt.config.ts`: Via the [`webExt` config](/api/reference/wxt/interfaces/InlineConfig#webext), included in version control
|
||||
3. `$HOME/web-ext.config.ts`: Provide default values for all WXT projects on your computer
|
||||
|
||||
## Recipes
|
||||
@@ -31,8 +31,8 @@ You can configure browser startup in 3 places:
|
||||
|
||||
To set or customize the browser opened during development:
|
||||
|
||||
```ts
|
||||
export default defineRunnerConfig({
|
||||
```ts [web-ext.config.ts]
|
||||
export default defineWebExtConfig({
|
||||
binaries: {
|
||||
chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
|
||||
firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox
|
||||
@@ -54,7 +54,7 @@ To persist data, set the `--user-data-dir` flag:
|
||||
:::code-group
|
||||
|
||||
```ts [Mac/Linux]
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'],
|
||||
});
|
||||
```
|
||||
@@ -62,7 +62,7 @@ export default defineRunnerConfig({
|
||||
```ts [Windows]
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
// On Windows, the path must be absolute
|
||||
chromiumProfile: resolve('.wxt/chrome-data'),
|
||||
keepProfileChanges: true,
|
||||
@@ -81,8 +81,8 @@ You can use any directory you'd like for `--user-data-dir`, the examples above c
|
||||
|
||||
If you prefer to load the extension into your browser manually, you can disable the auto-open behavior:
|
||||
|
||||
```ts
|
||||
export default defineRunnerConfig({
|
||||
```ts [web-ext.config.ts]
|
||||
export default defineWebExtConfig({
|
||||
disabled: true,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -17,63 +17,3 @@ If you're running into errors while importing entrypoints, run `wxt prepare --de
|
||||
:::
|
||||
|
||||
Once the environment has been polyfilled and your code pre-processed, it's up the entrypoint loader to import your code, extracting the options from the default export.
|
||||
|
||||
There are two options for loading your entrypoints:
|
||||
|
||||
1. `vite-node` - default as of `v0.19.0`
|
||||
2. `jiti` (**DEPRECATED, will be removed in `v0.20.0`**) - Default before `v0.19.0`
|
||||
|
||||
## vite-node
|
||||
|
||||
Since 0.19.0, WXT uses `vite-node`, the same tool that powers Vitest and Nuxt, to import your entrypoint files. It re-uses the same vite config used when building your extension, making it the most stable entrypoint loader.
|
||||
|
||||
## jiti
|
||||
|
||||
To enable `jiti`:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
entrypointLoader: 'jiti',
|
||||
});
|
||||
```
|
||||
|
||||
This is the original method WXT used to import TS files. However, because it doesn't support vite plugins like `vite-node`, it does one additional pre-processing step: It removes **_ALL_** imports from your code.
|
||||
|
||||
That means you cannot use imported variables outside the `main` function in JS entrypoints, like for content script `matches` or other options:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: GOOGLE_MATCHES,
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```
|
||||
$ wxt build
|
||||
wxt build
|
||||
|
||||
WXT 0.14.1
|
||||
ℹ Building chrome-mv3 for production with Vite 5.0.5
|
||||
✖ Command failed after 360 ms
|
||||
|
||||
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function.
|
||||
```
|
||||
|
||||
Usually, this error occurs when you try to extract options into a shared file or when running code outside the `main` function. To fix the example from above, use literal values when defining an entrypoint instead of importing them:
|
||||
|
||||
```ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --]
|
||||
|
||||
export default defineContentScript({
|
||||
matches: GOOGLE_MATCHES, // [!code --]
|
||||
matches: ['*//*.google.com/*'], // [!code ++]
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -42,6 +42,8 @@ WXT provides some custom environment variables based on the current command:
|
||||
| `import.meta.env.EDGE` | `boolean` | Equivalent to `import.meta.env.BROWSER === "edge"` |
|
||||
| `import.meta.env.OPERA` | `boolean` | Equivalent to `import.meta.env.BROWSER === "opera"` |
|
||||
|
||||
You can set the [`targetBrowsers`](/api/reference/wxt/interfaces/InlineConfig#targetbrowsers) option to make the `BROWSER` variable a more specific type, like `"chrome" | "firefox"`.
|
||||
|
||||
You can also access all of [Vite's environment variables](https://vite.dev/guide/env-and-mode.html#env-variables):
|
||||
|
||||
| Usage | Type | Description |
|
||||
|
||||
@@ -6,8 +6,7 @@ WXT includes a system that lets you hook into the build process and make changes
|
||||
|
||||
The easiest way to add a hook is via the `wxt.config.ts`. Here's an example hook that modifies the `manifest.json` file before it is written to the output directory:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
hooks: {
|
||||
'build:manifestGenerated': (wxt, manifest) => {
|
||||
|
||||
@@ -183,7 +183,7 @@ export default defineConfig({
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['storage', 'tabs'],
|
||||
host_permissions: ['https://www.google.com/*'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
Define runtime configuration in a single place, `<srcDir>/app.config.ts`:
|
||||
|
||||
```ts
|
||||
import { defineAppConfig } from 'wxt/sandbox';
|
||||
import { defineAppConfig } from '#imports';
|
||||
|
||||
// Define types for your config
|
||||
declare module 'wxt/sandbox' {
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
theme?: 'light' | 'dark';
|
||||
}
|
||||
@@ -26,7 +26,7 @@ This file is committed to the repo, so don't put any secrets here. Instead, use
|
||||
To access runtime config, WXT provides the `useAppConfig` function:
|
||||
|
||||
```ts
|
||||
import { useAppConfig } from 'wxt/sandbox';
|
||||
import { useAppConfig } from '#imports';
|
||||
|
||||
console.log(useAppConfig()); // { theme: "dark" }
|
||||
```
|
||||
@@ -36,7 +36,7 @@ console.log(useAppConfig()); // { theme: "dark" }
|
||||
You can use environment variables in the `app.config.ts` file.
|
||||
|
||||
```ts
|
||||
declare module 'wxt/sandbox' {
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
apiKey?: string;
|
||||
skipWelcome: boolean;
|
||||
|
||||
@@ -12,8 +12,7 @@ In most cases, you shouldn't change Vite's build settings. WXT provides sensible
|
||||
|
||||
You can change Vite's config via the `wxt.config.ts` file:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -28,8 +27,7 @@ export default defineConfig({
|
||||
|
||||
To add a plugin, install the NPM package and add it to the Vite config:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
import { defineConfig } from 'wxt';
|
||||
import VueRouter from 'unplugin-vue-router/vite';
|
||||
|
||||
@@ -47,8 +45,7 @@ export default defineConfig({
|
||||
:::warning
|
||||
Due to the way WXT orchestrates Vite builds, some plugins may not work as expected. For example, `vite-plugin-remove-console` normally only runs when you build for production (`vite build`). However, WXT uses a combination of dev server and builds during development, so you need to manually tell it when to run:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
import { defineConfig } from 'wxt';
|
||||
import removeConsole from 'vite-plugin-remove-console';
|
||||
|
||||
|
||||
@@ -76,8 +76,7 @@ To create a standalone content script that only includes a CSS file:
|
||||
|
||||
1. Create the CSS file: `entrypoints/example.content.css`
|
||||
2. Use the `build:manifestGenerated` hook to add the content script to the manifest:
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
hooks: {
|
||||
'build:manifestGenerated': (wxt, manifest) => {
|
||||
@@ -257,13 +256,13 @@ export default defineContentScript({
|
||||
|
||||
:::
|
||||
|
||||
See the [API Reference](/api/reference/wxt/client/functions/createIntegratedUi) for the complete list of options.
|
||||
See the [API Reference](/api/reference/wxt/utils/content-script-ui/integrated/functions/createIntegratedUi) for the complete list of options.
|
||||
|
||||
### Shadow Root
|
||||
|
||||
Often in web extensions, you don't want your content script's CSS affecting the page, or vise-versa. The [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) API is ideal for this.
|
||||
|
||||
WXT's [`createShadowRootUi`](/api/reference/wxt/client/functions/createShadowRootUi) abstracts all the `ShadowRoot` setup away, making it easy to create UIs whose styles are isolated from the page. It also supports an optional `isolateEvents` parameter to further isolate user interactions.
|
||||
WXT's [`createShadowRootUi`](/api/reference/wxt/utils/content-script-ui/shadow-root/functions/createShadowRootUi) abstracts all the `ShadowRoot` setup away, making it easy to create UIs whose styles are isolated from the page. It also supports an optional `isolateEvents` parameter to further isolate user interactions.
|
||||
|
||||
To use `createShadowRootUi`, follow these steps:
|
||||
|
||||
@@ -446,7 +445,7 @@ export default defineContentScript({
|
||||
|
||||
:::
|
||||
|
||||
See the [API Reference](/api/reference/wxt/client/functions/createShadowRootUi) for the complete list of options.
|
||||
See the [API Reference](/api/reference/wxt/utils/content-script-ui/shadow-root/functions/createShadowRootUi) for the complete list of options.
|
||||
|
||||
Full examples:
|
||||
|
||||
@@ -457,7 +456,7 @@ Full examples:
|
||||
|
||||
If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, **_HMR is supported_**.
|
||||
|
||||
WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/client/functions/createIframeUi), which simplifies setting up the IFrame.
|
||||
WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/utils/content-script-ui/iframe/functions/createIframeUi), which simplifies setting up the IFrame.
|
||||
|
||||
1. Create an HTML page that will be loaded into your IFrame:
|
||||
```html
|
||||
@@ -475,8 +474,7 @@ WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/client/fun
|
||||
</html>
|
||||
```
|
||||
1. Add the page to the manifest's `web_accessible_resources`:
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
web_accessible_resources: [
|
||||
@@ -512,7 +510,7 @@ WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/client/fun
|
||||
});
|
||||
```
|
||||
|
||||
See the [API Reference](/api/reference/wxt/client/functions/createIframeUi) for the complete list of options.
|
||||
See the [API Reference](/api/reference/wxt/utils/content-script-ui/iframe/functions/createIframeUi) for the complete list of options.
|
||||
|
||||
## Isolated World vs Main World
|
||||
|
||||
@@ -623,7 +621,7 @@ export default defineContentScript({
|
||||
When the `ui.remove` is called, `autoMount` also stops.
|
||||
:::
|
||||
|
||||
See the [API Reference](/api/reference/wxt/client/interfaces/ContentScriptUi.html#automount) for the complete list of options.
|
||||
See the [API Reference](/api/reference/wxt/utils/content-script-ui/types/interfaces/ContentScriptUi#automount) for the complete list of options.
|
||||
|
||||
## Dealing with SPAs
|
||||
|
||||
|
||||
@@ -4,50 +4,30 @@ outline: deep
|
||||
|
||||
# Entrypoints
|
||||
|
||||
WXT uses the files inside the `entrypoints/` directory as inputs when bundling your extension. They can be HTML, JS, CSS, or any variant of those file types supported by Vite (Pug, TS, JSX, SCSS, etc).
|
||||
WXT uses the files inside the `entrypoints/` directory as inputs when bundling your extension. They can be HTML, JS, CSS, or any variant of those file types supported by Vite (TS, JSX, SCSS, etc).
|
||||
|
||||
Here's an example set of entrypoints:
|
||||
## Folder Structure
|
||||
|
||||
Inside the `entrypoints/` directory, an entrypoint is defined as a single file or directory (with an `index` file) inside it.
|
||||
|
||||
:::code-group
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
```html [Single File]
|
||||
📂 entrypoints/
|
||||
📂 popup/
|
||||
📄 index.html
|
||||
📄 main.ts
|
||||
📄 style.css
|
||||
📄 background.ts
|
||||
📄 content.ts
|
||||
📄 {name}.{ext}
|
||||
```
|
||||
|
||||
[[toc]]
|
||||
<!-- prettier-ignore -->
|
||||
```html [Directory]
|
||||
📂 entrypoints/
|
||||
📂 {name}/
|
||||
📄 index.{ext}
|
||||
```
|
||||
|
||||
## Listed vs Unlisted
|
||||
|
||||
For web extensions, there are two types of entrypoints:
|
||||
|
||||
- **Listed**: Referenced in the `manifest.json`
|
||||
- **Unlisted**: Not referenced in the `manifest.json`
|
||||
|
||||
Throughout the rest of WXT's documentation, listed entrypoints are referred to by name. For example:
|
||||
|
||||
- Popup
|
||||
- Options
|
||||
- Background
|
||||
- Content Scripts
|
||||
- Etc.
|
||||
|
||||
Some examples of "unlisted" entrypoints:
|
||||
|
||||
- A welcome page shown when the extension is installed
|
||||
- JS files injected by content scripts into the page's main world
|
||||
|
||||
:::tip
|
||||
Regardless of whether an entrypoint is listed or unlisted, it will still be bundled into your extension and be available at runtime.
|
||||
:::
|
||||
|
||||
## Adding Entrypoints
|
||||
|
||||
An entrypoint can be defined as a single file or directory with an `index` file inside it.
|
||||
The entrypoint's `name` dictates the type of entrypoint. For example, to add a ["Background" entrypoint](#background), either of these files would work:
|
||||
|
||||
:::code-group
|
||||
|
||||
@@ -66,18 +46,103 @@ An entrypoint can be defined as a single file or directory with an `index` file
|
||||
|
||||
:::
|
||||
|
||||
The entrypoint's name dictates the type of entrypoint, listed vs unlisted. In this example, "background" is the name of the ["Background" entrypoint](#background).
|
||||
|
||||
Refer to the [Entrypoint Types](#entrypoint-types) section for the full list of listed entrypoints and their filename patterns.
|
||||
|
||||
### Including Other Files
|
||||
|
||||
When using an entrypoint directory, `entrypoints/{name}/index.{ext}`, you can add related files next to the `index` file.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 entrypoints/
|
||||
📂 popup/
|
||||
📄 index.html ← This file is the entrypoint
|
||||
📄 main.ts
|
||||
📄 style.css
|
||||
📂 background/
|
||||
📄 index.ts ← This file is the entrypoint
|
||||
📄 alarms.ts
|
||||
📄 messaging.ts
|
||||
📂 youtube.content/
|
||||
📄 index.ts ← This file is the entrypoint
|
||||
📄 style.css
|
||||
```
|
||||
|
||||
:::danger
|
||||
**DO NOT** put files related to an entrypoint directly inside the `entrypoints/` directory. WXT will treat them as entrypoints and try to build them, usually resulting in an error.
|
||||
|
||||
Instead, use a directory for that entrypoint:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 entrypoints/
|
||||
📄 popup.html <!-- [!code --] -->
|
||||
📄 popup.ts <!-- [!code --] -->
|
||||
📄 popup.css <!-- [!code --] -->
|
||||
📂 popup/ <!-- [!code ++] -->
|
||||
📄 index.html <!-- [!code ++] -->
|
||||
📄 main.ts <!-- [!code ++] -->
|
||||
📄 style.css <!-- [!code ++] -->
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Deeply Nested Entrypoints
|
||||
|
||||
While the `entrypoints/` directory might resemble the `pages/` directory of other web frameworks, like Nuxt or Next.js, **it does not support deeply nesting entrypoints** in the same way.
|
||||
|
||||
Entrypoints must be zero or one levels deep for WXT to discover and build them:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 entrypoints/
|
||||
📂 youtube/ <!-- [!code --] -->
|
||||
📂 content/ <!-- [!code --] -->
|
||||
📄 index.ts <!-- [!code --] -->
|
||||
📄 ... <!-- [!code --] -->
|
||||
📂 injected/ <!-- [!code --] -->
|
||||
📄 index.ts <!-- [!code --] -->
|
||||
📄 ... <!-- [!code --] -->
|
||||
📂 youtube.content/ <!-- [!code ++] -->
|
||||
📄 index.ts <!-- [!code ++] -->
|
||||
📄 ... <!-- [!code ++] -->
|
||||
📂 youtube-injected/ <!-- [!code ++] -->
|
||||
📄 index.ts <!-- [!code ++] -->
|
||||
📄 ... <!-- [!code ++] -->
|
||||
```
|
||||
|
||||
## Unlisted Entrypoints
|
||||
|
||||
In web extensions, there are two types of entrypoints:
|
||||
|
||||
1. **Listed**: Referenced in the `manifest.json`
|
||||
2. **Unlisted**: Not referenced in the `manifest.json`
|
||||
|
||||
Throughout the rest of WXT's documentation, listed entrypoints are referred to by name. For example:
|
||||
|
||||
- Popup
|
||||
- Options
|
||||
- Background
|
||||
- Content Script
|
||||
|
||||
However, not all entrypoints in web extensions are listed in the manifest. Some are not listed in the manifest, but are still used by extensions. For example:
|
||||
|
||||
- A welcome page shown in a new tab when the extension is installed
|
||||
- JS files injected by content scripts into the main world
|
||||
|
||||
For more details on how to add unlisted entrypoints, see:
|
||||
|
||||
- [Unlisted Pages](#unlisted-pages)
|
||||
- [Unlisted Scripts](#unlisted-scripts)
|
||||
- [Unlisted CSS](#unlisted-css)
|
||||
|
||||
## Defining Manifest Options
|
||||
|
||||
Most listed entrypoints have options that need to be added to the `manifest.json`. However with WXT, instead of defining the options in a separate file, _you define these options inside the entrypoint file itself_.
|
||||
|
||||
For example, here's how to define `matches` for content scripts:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
```ts [entrypoints/content.ts]
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.wxt.dev/*'],
|
||||
main() {
|
||||
@@ -107,8 +172,6 @@ When building your extension, WXT will look at the options defined in your entry
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/background/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background)
|
||||
|
||||
For MV2, the background is added as a script to the background page. For MV3, the background becomes a service worker.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['background.[jt]s', 'background.js'],
|
||||
@@ -142,6 +205,25 @@ export default defineBackground({
|
||||
|
||||
:::
|
||||
|
||||
For MV2, the background is added as a script to the background page. For MV3, the background becomes a service worker.
|
||||
|
||||
When defining your background entrypoint, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
browser.action.onClicked.addListener(() => { // [!code --]
|
||||
// ... // [!code --]
|
||||
}); // [!code --]
|
||||
|
||||
export default defineBackground(() => {
|
||||
browser.action.onClicked.addListener(() => { // [!code ++]
|
||||
// ... // [!code ++]
|
||||
}); // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
Refer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) documentation for more details.
|
||||
|
||||
### Bookmarks
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
@@ -170,18 +252,18 @@ export default defineBackground({
|
||||
</html>
|
||||
```
|
||||
|
||||
When you define a Bookmarks entrypoint, WXT will automatically update the manifest to override the browser's bookmarks page with your own HTML page.
|
||||
|
||||
### Content Scripts
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/content_scripts/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts)
|
||||
|
||||
See [Content Script UI](/guide/essentials/content-scripts) for more info on creating UIs and including CSS in content scripts.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['content.[jt]sx?', 'content-scripts/content.js'],
|
||||
['content/index.[jt]sx?', 'content-scripts/content.js'],
|
||||
['<name>.content.[jt]sx?', 'content-scripts/<name>.js'],
|
||||
['<name>.content/index.[jt]sx?', 'content-scripts/<name>.js'],
|
||||
['{name}.content.[jt]sx?', 'content-scripts/{name}.js'],
|
||||
['{name}.content/index.[jt]sx?', 'content-scripts/{name}.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -214,12 +296,29 @@ export default defineContentScript({
|
||||
});
|
||||
```
|
||||
|
||||
When defining content script entrypoints, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
browser.runtime.onMessage.addListener((message) => { // [!code --]
|
||||
// ... // [!code --]
|
||||
}); // [!code --]
|
||||
|
||||
export default defineBackground(() => {
|
||||
browser.runtime.onMessage.addListener((message) => { // [!code ++]
|
||||
// ... // [!code ++]
|
||||
}); // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
Refer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) documentation for more details.
|
||||
|
||||
See [Content Script UI](/guide/essentials/content-scripts) for more info on creating UIs and including CSS in content scripts.
|
||||
|
||||
### Devtools
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/devtools/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/devtools_page)
|
||||
|
||||
Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/examples/devtools-extension#readme) to add different panels and panes.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['devtools.html', 'devtools.html'],
|
||||
@@ -243,6 +342,8 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
</html>
|
||||
```
|
||||
|
||||
Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/examples/devtools-extension#readme) to add different panels and panes.
|
||||
|
||||
### History
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
@@ -271,6 +372,8 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
</html>
|
||||
```
|
||||
|
||||
When you define a History entrypoint, WXT will automatically update the manifest to override the browser's history page with your own HTML page.
|
||||
|
||||
### Newtab
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
@@ -299,6 +402,8 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
</html>
|
||||
```
|
||||
|
||||
When you define a Newtab entrypoint, WXT will automatically update the manifest to override the browser's new tab page with your own HTML page.
|
||||
|
||||
### Options
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/options/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options_ui)
|
||||
@@ -388,8 +493,8 @@ Firefox does not support sandboxed pages.
|
||||
:patterns="[
|
||||
['sandbox.html', 'sandbox.html'],
|
||||
['sandbox/index.html', 'sandbox.html'],
|
||||
['<name>.sandbox.html', '<name>.html'],
|
||||
['<name>.sandbox/index.html', '<name>.html'],
|
||||
['{name}.sandbox.html', '{name}.html'],
|
||||
['{name}.sandbox/index.html', '{name}.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -415,14 +520,12 @@ Firefox does not support sandboxed pages.
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Sidebars)
|
||||
|
||||
In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar_action` API.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['sidepanel.html', 'sidepanel.html'],
|
||||
['sidepanel/index.html', 'sidepanel.html'],
|
||||
['<name>.sidepanel.html', '<name>.html` '],
|
||||
['<name>.sidepanel/index.html', '<name>.html` '],
|
||||
['{name}.sidepanel.html', '{name}.html` '],
|
||||
['{name}.sidepanel/index.html', '{name}.html` '],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -456,20 +559,18 @@ In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar
|
||||
</html>
|
||||
```
|
||||
|
||||
In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar_action` API.
|
||||
|
||||
### Unlisted CSS
|
||||
|
||||
Follow Vite's guide to setup your preprocessor of choice: https://vitejs.dev/guide/features.html#css-pre-processors
|
||||
|
||||
CSS entrypoints are always unlisted. To add CSS to a content script, see the [Content Script](/guide/essentials/content-scripts#css) docs.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.(css|scss|sass|less|styl|stylus)', '<name>.css'],
|
||||
['<name>/index.(css|scss|sass|less|styl|stylus)', '<name>.css'],
|
||||
['{name}.(css|scss|sass|less|styl|stylus)', '{name}.css'],
|
||||
['{name}/index.(css|scss|sass|less|styl|stylus)', '{name}.css'],
|
||||
['content.(css|scss|sass|less|styl|stylus)', 'content-scripts/content.css'],
|
||||
['content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/content.css'],
|
||||
['<name>.content.(css|scss|sass|less|styl|stylus)', 'content-scripts/<name>.css'],
|
||||
['<name>.content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/<name>.css'],
|
||||
['{name}.content.(css|scss|sass|less|styl|stylus)', 'content-scripts/{name}.css'],
|
||||
['{name}.content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/{name}.css'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -479,12 +580,16 @@ body {
|
||||
}
|
||||
```
|
||||
|
||||
Follow Vite's guide to setup your preprocessor of choice: https://vitejs.dev/guide/features.html#css-pre-processors
|
||||
|
||||
CSS entrypoints are always unlisted. To add CSS to a content script, see the [Content Script](/guide/essentials/content-scripts#css) docs.
|
||||
|
||||
### Unlisted Pages
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.html', '<name>.html'],
|
||||
['<name>/index.html', '<name>.html'],
|
||||
['{name}.html', '{name}.html'],
|
||||
['{name}/index.html', '{name}.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -506,20 +611,21 @@ body {
|
||||
</html>
|
||||
```
|
||||
|
||||
Pages are accessible at `/<name>.html`:
|
||||
At runtime, unlisted pages are accessible at `/{name}.html`:
|
||||
|
||||
```ts
|
||||
const url = browser.runtime.getURL('/<name>.html');
|
||||
const url = browser.runtime.getURL('/{name}.html');
|
||||
|
||||
console.log(url); // "chrome-extension://<id>/<name>.html"
|
||||
console.log(url); // "chrome-extension://{id}/{name}.html"
|
||||
window.open(url); // Open the page in a new tab
|
||||
```
|
||||
|
||||
### Unlisted Scripts
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.[jt]sx?', '<name>.js'],
|
||||
['<name>/index.[jt]sx?', '<name>.js'],
|
||||
['{name}.[jt]sx?', '{name}.js'],
|
||||
['{name}/index.[jt]sx?', '{name}.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -545,12 +651,29 @@ export default defineUnlistedScript({
|
||||
|
||||
:::
|
||||
|
||||
Scripts are accessible from `/<name>.js`:
|
||||
At runtime, unlisted scripts are accessible from `/{name}.js`:
|
||||
|
||||
```ts
|
||||
const url = browser.runtime.getURL('/<name>.js');
|
||||
const url = browser.runtime.getURL('/{name}.js');
|
||||
|
||||
console.log(url); // "chrome-extension://<id>/<name>.js"
|
||||
console.log(url); // "chrome-extension://{id}/{name}.js"
|
||||
```
|
||||
|
||||
You are responsible for loading/running these scripts where needed. If necessary, don't forget to add the script and/or any related assets to [`web_accessible_resources`](https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources).
|
||||
|
||||
When defining an unlisted script, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
document.querySelectorAll('a').forEach((anchor) => { // [!code --]
|
||||
// ... // [!code --]
|
||||
}); // [!code --]
|
||||
|
||||
export default defineUnlistedScript(() => {
|
||||
document.querySelectorAll('a').forEach((anchor) => { // [!code ++]
|
||||
// ... // [!code ++]
|
||||
}); // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
Refer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) documentation for more details.
|
||||
|
||||
@@ -4,62 +4,47 @@
|
||||
|
||||
Different browsers provide different global variables for accessing the extension APIs (chrome provides `chrome`, firefox provides `browser`, etc).
|
||||
|
||||
WXT simplifies this - always use `browser`:
|
||||
WXT merges these two into a unified API accessed through the `browser` variable.
|
||||
|
||||
```ts
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
browser.action.onClicked.addListener(() => {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
Other than that, refer to Chrome and Mozilla's documentation for how to use specific APIs. Everything a normal extension can do, WXT can do as well, just via `browser` instead of `chrome`.
|
||||
:::tip
|
||||
With auto-imports enabled, you don't even need to import this variable from `wxt/browser`!
|
||||
:::
|
||||
|
||||
## Webextension Polyfill
|
||||
The `browser` variable WXT provides is a simple export of the `browser` or `chrome` globals provided by the browser at runtime:
|
||||
|
||||
> Since `v0.1.0`
|
||||
<<< @/../packages/browser/src/index.mjs#snippet
|
||||
|
||||
By default, WXT uses the [`webextension-polyfill` by Mozilla](https://www.npmjs.com/package/webextension-polyfill) to make the extension API consistent between browsers.
|
||||
This means you can use the promise-style API for both MV2 and MV3, and it will work across all browsers (Chromium, Firefox, Safari, etc).
|
||||
|
||||
To access types, you should import the relevant namespace from `wxt/browser`:
|
||||
## Accessing Types
|
||||
|
||||
All types can be accessed via WXT's `Browser` namespace:
|
||||
|
||||
```ts
|
||||
import { Runtime } from 'wxt/browser';
|
||||
import { type Browser } from 'wxt/browser';
|
||||
|
||||
function handleMessage(message: any, sender: Runtime.Sender) {
|
||||
function handleMessage(message: any, sender: Browser.runtime.MessageSender) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Disabling the polyfill
|
||||
## Using `webextension-polyfill`
|
||||
|
||||
> Since `v0.19.0`
|
||||
If you want to use the `webextension-polyfill` when importing `browser`, you can do so by installing the `@wxt-dev/webextension-polyfill` package.
|
||||
|
||||
After the release of MV3 and Chrome's official deprecation of MV2 in June 2024, the polyfill isn't really doing anything useful anymore.
|
||||
|
||||
You can disable it with a single line:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
extensionApi: 'chrome',
|
||||
});
|
||||
```
|
||||
|
||||
This will change `wxt/browser` to simply export the `browser` or `chrome` globals based on browser at runtime:
|
||||
|
||||
<<< @/../packages/wxt/src/browser/chrome.ts#snippet
|
||||
|
||||
Accessing types is a little different with the polyfill disabled. They do not need to be imported; they're available on the `browser` object itself:
|
||||
|
||||
```ts
|
||||
function handleMessage(message: any, sender: browser.runtime.Sender) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
See it's [Installation Guide](https://github.com/wxt-dev/wxt/blob/main/packages/webextension-polyfill/README.md) to get started.
|
||||
|
||||
## Feature Detection
|
||||
|
||||
Depending on the manifest version and browser, some APIs are not available at runtime. If an API is not available, it will be `undefined`.
|
||||
Depending on the manifest version, browser, and permissions, some APIs are not available at runtime. If an API is not available, it will be `undefined`.
|
||||
|
||||
:::warning
|
||||
Types will not help you here. The types WXT provides for `browser` assume all APIs exist. You are responsible for knowing whether an API is available or not.
|
||||
|
||||
@@ -30,9 +30,10 @@ Here's a brief summary of each of these files and directories:
|
||||
- `.wxt/`: Generated by WXT, it contains TS config
|
||||
- `assets/`: Contains all CSS, images, and other assets that should be processed by WXT
|
||||
- `components/`: Auto-imported by default, contains UI components
|
||||
- `composables/`: Auto-imported by default, contains composable functions for Vue
|
||||
- `composables/`: Auto-imported by default, contains source code for your project's composable functions for Vue
|
||||
- `entrypoints/`: Contains all the entrypoints that get bundled into your extension
|
||||
- `hooks/`: Auto-imported by default, contains hooks for React and Solid
|
||||
- `hooks/`: Auto-imported by default, contains source code for your project's hooks for React and Solid
|
||||
- `modules/`: Contains [local WXT Modules](/guide/essentials/wxt-modules) for your project
|
||||
- `public/`: Contains any files you want to copy into the output folder as-is, without being processed by WXT
|
||||
- `utils/`: Auto-imported by default, contains generic utilities used throughout your project
|
||||
- `.env`: Contains [Environment Variables](/guide/essentials/config/environment-variables)
|
||||
@@ -47,8 +48,7 @@ Here's a brief summary of each of these files and directories:
|
||||
|
||||
Many developers like having a `src/` directory to separate source code from configuration files. You can enable it inside the `wxt.config.ts` file:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
});
|
||||
@@ -61,14 +61,14 @@ After enabling it, your project structure should look like this:
|
||||
📂 {rootDir}/
|
||||
📁 .output/
|
||||
📁 .wxt/
|
||||
📁 modules/
|
||||
📁 public/
|
||||
📂 src/
|
||||
📁 assets/
|
||||
📁 components/
|
||||
📁 composables/
|
||||
📁 entrypoints/
|
||||
📁 hooks/
|
||||
📁 modules/
|
||||
📁 public/
|
||||
📁 utils/
|
||||
📄 app.config.ts
|
||||
📄 .env
|
||||
@@ -84,17 +84,16 @@ After enabling it, your project structure should look like this:
|
||||
You can configure the following directories:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
// Relative to project root
|
||||
srcDir: "src", // default: "."
|
||||
modulesDir: "wxt-modules", // default: "modules"
|
||||
outDir: "dist", // default: ".output"
|
||||
publicDir: "static", // default: "public"
|
||||
|
||||
// Relative to srcDir
|
||||
entrypointsDir: "entries", // default: "entrypoints"
|
||||
modulesDir: "wxt-modules", // default: "modules"
|
||||
publicDir: "static", // default: "public"
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ WXT provides two commands to help automate submitting a new version for review a
|
||||
- `wxt submit init`: Setup all the required secrets and options for the `wxt submit` command
|
||||
- `wxt submit`: Submit new versions of your extension for review (and publish them automatically once approved)
|
||||
|
||||
To get started, run `wxt submit init` and follow the prompts. Once finished, you should have a `.env.submit` file! WXT will use this file to submit your updates.
|
||||
To get started, run `wxt submit init` and follow the prompts, or run `wxt submit --help` to view all available options. Once finished, you should have a `.env.submit` file! WXT will use this file to submit your updates.
|
||||
|
||||
> In CI, make sure you add all the environment variables to the submit step.
|
||||
|
||||
@@ -133,8 +133,7 @@ When running `wxt zip -b firefox`, WXT will zip both your extension and sources.
|
||||
|
||||
To customize which files are zipped, add the `zip` option to your config file.
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -184,8 +183,7 @@ See Issue [#377](https://github.com/wxt-dev/wxt/issues/377) for more details.
|
||||
|
||||
If you use private packages and you don't want to provide your auth token to the Firefox team during the review process, you can use `zip.downloadPackages` to download any private packages and include them in the zip.
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
zip: {
|
||||
downloadPackages: [
|
||||
|
||||
@@ -6,7 +6,7 @@ You can use the vanilla APIs (see docs above), use [WXT's built-in storage API](
|
||||
|
||||
## Alternatives
|
||||
|
||||
1. [`wxt/storage`](/storage) (recommended): WXT ships with its own wrapper around the vanilla storage APIs that simplifies common use cases
|
||||
1. [`wxt/utils/storage`](/storage) (recommended): WXT ships with its own wrapper around the vanilla storage APIs that simplifies common use cases
|
||||
|
||||
2. DIY: If you're migrating to WXT and already have a storage wrapper, keep using it. In the future, if you want to delete that code, you can use one of these alternatives, but there's no reason to replace working code during a migration.
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Here are real projects with unit testing setup. Look at the code and tests to se
|
||||
|
||||
### Example Tests
|
||||
|
||||
This example demonstrates that you don't have to mock `browser.storage` (used by `wxt/storage`) in tests - [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation) implements storage in-memory so it behaves like it would in a real extension!
|
||||
This example demonstrates that you don't have to mock `browser.storage` (used by `wxt/utils/storage`) in tests - [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation) implements storage in-memory so it behaves like it would in a real extension!
|
||||
|
||||
```ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
@@ -71,6 +71,34 @@ describe('isLoggedIn', () => {
|
||||
});
|
||||
```
|
||||
|
||||
### Mocking WXT APIs
|
||||
|
||||
First, you need to understand how the `#imports` module works. When WXT (and vitest) sees this import during a preprocessing step, the import is replaced with multiple imports pointing to their "real" import path.
|
||||
|
||||
For example, this is what your write in your source code:
|
||||
|
||||
```ts
|
||||
// What you write
|
||||
import { injectScript, createShadowRootUi } from '#imports';
|
||||
```
|
||||
|
||||
But Vitest sees this:
|
||||
|
||||
```ts
|
||||
import { injectScript } from 'wxt/utils/inject-script';
|
||||
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
|
||||
```
|
||||
|
||||
So in this case, if you wanted to mock `injectScript`, you need to pass in `"wxt/utils/inject-script"`, not `"#imports"`.
|
||||
|
||||
```ts
|
||||
vi.mock("wxt/utils/inject-script", () => ({
|
||||
injectScript: ...
|
||||
}))
|
||||
```
|
||||
|
||||
Refer to your project's `.wxt/types/imports-module.d.ts` file to lookup real import paths for `#imports`. If the file doesn't exist, run [`wxt prepare`](/guide/essentials/config/typescript).
|
||||
|
||||
## Other Testing Frameworks
|
||||
|
||||
To use a different framework, you will likely have to disable auto-imports, setup import aliases, manually mock the extension APIs, and setup the test environment to support all of WXT's features that you use.
|
||||
|
||||
@@ -9,8 +9,7 @@ WXT provides a "module system" that let's you run code at different steps in the
|
||||
There are two ways to add a module to your project:
|
||||
|
||||
1. **NPM**: install an NPM package, like [`@wxt-dev/auto-icons`](https://www.npmjs.com/package/@wxt-dev/auto-icons) and add it to your config:
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/auto-icons'],
|
||||
});
|
||||
@@ -18,7 +17,7 @@ There are two ways to add a module to your project:
|
||||
> Searching for ["wxt module"](https://www.npmjs.com/search?q=wxt%20module) on NPM is a good way to find published WXT modules.
|
||||
2. **Local**: add a file to your project's `modules/` directory:
|
||||
```
|
||||
<srcDir>/
|
||||
<rootDir>/
|
||||
modules/
|
||||
my-module.ts
|
||||
```
|
||||
@@ -112,12 +111,12 @@ export default defineWxtModule<AnalyticModuleOptions>({
|
||||
|
||||
```ts
|
||||
import { defineWxtModule } from 'wxt/modules';
|
||||
import 'wxt/sandbox';
|
||||
import 'wxt/utils/define-app-config';
|
||||
|
||||
export interface MyModuleRuntimeOptions {
|
||||
// Add your runtime options here...
|
||||
}
|
||||
declare module 'wxt/sandbox' {
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
myModule: MyModuleOptions;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ Once you've run the `dev` command, continue to [Next Steps](#next-steps)!
|
||||
```
|
||||
:::
|
||||
4. Add scripts to your `package.json`:
|
||||
```json
|
||||
```json [package.json]
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "wxt", // [!code ++]
|
||||
|
||||
@@ -167,8 +167,23 @@ Both issues have the same fix: tell the library to put elements inside the `Shad
|
||||
|
||||
## Is there an LLM trained on WXT's docs that I chat with?
|
||||
|
||||
Not yet, but we're working on it. For now, https://wxt.dev hosts pre-aggregated and pre-formatted knowledge files containing all the docs from this website:
|
||||
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.
|
||||
|
||||
- Index listing available knowledge files: https://wxt.dev/knowledge/index.json
|
||||
Additionally, if you want to train your own model or provide context to your editor, you can use the LLM knowledge files hosted by the site:
|
||||
|
||||
https://wxt.dev/knowledge/index.json
|
||||
|
||||
You don't need to crawl the entire website, these files already contain all the relevant docs for training a LLM on WXT. But feel free to crawl it and generate your own files if you want!
|
||||
|
||||
## How do I run my WXT project with docker / [devcontainers](https://containers.dev)?
|
||||
|
||||
To run the WXT dev server in a devcontainer, but load the dev build of your extension in your browser:
|
||||
|
||||
1. **Bind-mount your project directory to your host**
|
||||
If you're using VS Code, you can open your project folder with the `Dev Containers: Open Folder in Container...` command. This keeps the folder synchronized between your host and the devcontainer, ensuring that the extension `dist` directory remains accessible from the host.
|
||||
|
||||
2. **Disable auto-opening the browser**
|
||||
WXT automatically opens your browser during development, but since you're running inside a container, it won't be able to access it. Follow the instructions [here](https://wxt.dev/guide/essentials/config/browser-startup.html#disable-opening-browser) to disable browser auto-opening in your `wxt.config.ts`.
|
||||
|
||||
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`.
|
||||
|
||||
@@ -1,17 +1,275 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Upgrading WXT
|
||||
|
||||
## Overview
|
||||
|
||||
To upgrade WXT to the latest version... just install it!
|
||||
To upgrade WXT to the latest major version:
|
||||
|
||||
1. Install it, skipping scripts so `wxt prepare` doesn't run - it will probably throw an error after a major version change (we'll run it later).
|
||||
```sh
|
||||
pnpm i wxt@latest --ignore-scripts
|
||||
```
|
||||
2. Follow the upgrade steps below to fix any breaking changes.
|
||||
3. Run `wxt prepare`. It should succeed and type errors will go away afterwords.
|
||||
```sh
|
||||
pnpm wxt prepare
|
||||
```
|
||||
4. Manually test to make sure both dev mode and production builds work.
|
||||
|
||||
For minor or patch version updates, there are no special steps. Just update it with your package manager:
|
||||
|
||||
```sh
|
||||
pnpm i wxt@latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Listed below are all the breaking changes you should address when upgrading to a new version of WXT.
|
||||
|
||||
Currently, WXT is in pre-release. This means changes to the second digit, `v0.X`, are considered major and have breaking changes. Once v1 is released, only major version bumps will have breaking changes.
|
||||
|
||||
## v0.19.0 → v0.20.0
|
||||
|
||||
v0.20 is a big release! There are lots of breaking changes because this version is intended to be a release candidate for v1.0. If all goes well, v1.0 will be released with no additional breaking changes.
|
||||
|
||||
:::tip
|
||||
Read through all the changes once before updating your code.
|
||||
:::
|
||||
|
||||
### `webextension-polyfill` Removed
|
||||
|
||||
WXT's `browser` no longer uses the `webextension-polyfill`!
|
||||
|
||||
:::details Why?
|
||||
See https://github.com/wxt-dev/wxt/issues/784
|
||||
:::
|
||||
|
||||
To upgrade, you have two options:
|
||||
|
||||
1. **Stop using the polyfill** - No changes necessary, though you may want to do some manual testing to make sure everything continues to work. None of the early testers of this feature reported any runtime issues once they stopped using the polyfill.
|
||||
- If you're already using `extensionApi: "chrome"`, then you don't need to test anything! You're already using the same `browser` object v0.20 provides by default.
|
||||
2. **Continue using the polyfill** - If you want to keep using the polyfill, you can! One less thing to worry about during this upgrade.
|
||||
- Install `webextension-polyfill` and WXT's [new polyfill module](https://www.npmjs.com/package/@wxt-dev/webextension-polyfill):
|
||||
```sh
|
||||
pnpm i webextension-polyfill @wxt-dev/webextension-polyfill
|
||||
```
|
||||
- Add the WXT module to your config:
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/webextension-polyfill'],
|
||||
});
|
||||
```
|
||||
|
||||
The new `browser` object (and types) is backed by WXT's new package: [`@wxt-dev/browser`](https://www.npmjs.com/package/@wxt-dev/browser). This package continues WXT's mission of providing useful packages for the whole community. Just like [`@wxt-dev/storage`](https://www.npmjs.com/package/@wxt-dev/storage), [`@wxt-dev/i18n`](https://www.npmjs.com/package/@wxt-dev/i18n), [`@wxt-dev/analytics`](https://www.npmjs.com/package/@wxt-dev/analytics), it is designed to be easy to use in any web extension project, not just those using WXT, and provides a consistent API across all browsers and manifest versions.
|
||||
|
||||
### `extensionApi` Config Removed
|
||||
|
||||
The `extensionApi` config has been removed. Before, this config provided a way to opt into using the new `browser` object prior to v0.20.0.
|
||||
|
||||
Remove it from your `wxt.config.ts` file if present:
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
extensionApi: 'chrome', // [!code --]
|
||||
});
|
||||
```
|
||||
|
||||
### Extension API Type Changes
|
||||
|
||||
With the new `browser` introduced in v0.20, how you access types has changed. WXT now provides types based on `@types/chrome` instead of `@types/webextension-polyfill`.
|
||||
|
||||
These types are more up-to-date with MV3 APIs, contain less bugs, are better organized, and don't have any auto-generated names.
|
||||
|
||||
To access types, use the new `Browser` namespace from `wxt/browser`:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
import type { Runtime } from 'wxt/browser'; // [!code --]
|
||||
import type { Browser } from 'wxt/browser'; // [!code ++]
|
||||
|
||||
function getMessageSenderUrl(sender: Runtime.MessageSender): string { // [!code --]
|
||||
function getMessageSenderUrl(sender: Browser.runtime.MessageSender): string { // [!code ++]
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
> If you use auto-imports, `Browser` will be available without manually importing it.
|
||||
|
||||
Not all type names will be the same as what `@types/webextension-polyfill` provides. You'll have to find the new type names by looking at the types of the `browser.*` API's you use.
|
||||
|
||||
### `public/` and `modules/` Directories Moved
|
||||
|
||||
The default location for the `public/` and `modules/` directories have changed to better align with standards set by other frameworks (Nuxt, Next, Astro, etc). Now, each path is relative to the project's **root directory**, not the src directory.
|
||||
|
||||
- If you follow the default folder structure, you don't need to make any changes.
|
||||
- If you set a custom `srcDir`, you have two options:
|
||||
1. Move the your `public/` and `modules/` directories to the project root:
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 {rootDir}/
|
||||
📁 modules/ <!-- [!code ++] -->
|
||||
📁 public/ <!-- [!code ++] -->
|
||||
📂 src/
|
||||
📁 components/
|
||||
📁 entrypoints/
|
||||
📁 modules/ <!-- [!code --] -->
|
||||
📁 public/ <!-- [!code --] -->
|
||||
📁 utils/
|
||||
📄 app.config.ts
|
||||
📄 wxt.config.ts
|
||||
```
|
||||
2. Keep the folders in the same place and update your project config:
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
publicDir: 'src/public', // [!code ++]
|
||||
modulesDir: 'src/modules', // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
### Import Path Changes and `#imports`
|
||||
|
||||
The APIs exported by `wxt/sandbox`, `wxt/client`, or `wxt/storage` have moved to individual exports under the `wxt/utils/*` path.
|
||||
|
||||
:::details Why?
|
||||
As WXT grows and more utilities are added, any helper with side-effects will not be tree-shaken out of your final bundle.
|
||||
|
||||
This can cause problems because not every API used by these side-effects is available in every type of entrypoint. Some APIs can only be used in the background, sandboxed pages can't use any extension API, etc. This was leading to JS throwing errors in the top-level scope, preventing your code from running.
|
||||
|
||||
Splitting each util into it's own module solves this problem, making sure you're only importing APIs and side-effects into entrypoints they can run in.
|
||||
:::
|
||||
|
||||
Refer to the updated [API Reference](/api/reference/) to see the list of new import paths.
|
||||
|
||||
However, you don't need to memorize or learn the new import paths! v0.20 introduces a new virtual module, `#imports`, that abstracts all this away from developers. See the [blog post](/blog/2024-12-06-using-imports-module) for more details about how this module works.
|
||||
|
||||
So to upgrade, just replace any imports from `wxt/storage`, `wxt/client`, and `wxt/sandbox` with an import to the new `#imports` module:
|
||||
|
||||
```ts
|
||||
import { storage } from 'wxt/storage'; // [!code --]
|
||||
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 ++]
|
||||
```
|
||||
|
||||
You can combine the imports into a single import statement, but it's easier to just find/replace each statement.
|
||||
|
||||
```ts
|
||||
import { storage } from 'wxt/storage'; // [!code --]
|
||||
import { defineContentScript } from 'wxt/sandbox'; // [!code --]
|
||||
import { ContentScriptContext, useAppConfig } from 'wxt/client'; // [!code --]
|
||||
import {
|
||||
// [!code ++]
|
||||
storage, // [!code ++]
|
||||
defineContentScript, // [!code ++]
|
||||
ContentScriptContext, // [!code ++]
|
||||
useAppConfig, // [!code ++]
|
||||
} from '#imports'; // [!code ++]
|
||||
```
|
||||
|
||||
:::tip
|
||||
Before types will work, you'll need to run `wxt prepare` after installing v0.20 to generate the new TypeScript declarations.
|
||||
:::
|
||||
|
||||
### `createShadowRootUi` CSS Changes
|
||||
|
||||
WXT now resets styles inherited from the webpage (`visibility`, `color`, `font-size`, etc.) by setting `all: initial` inside the shadow root.
|
||||
|
||||
:::warning
|
||||
This doesn't effect `rem` units. You should continue using `postcss-rem-to-px` or an equivalent library if the webpage sets the HTML element's `font-size`.
|
||||
:::
|
||||
|
||||
If you use `createShadowRootUi`:
|
||||
|
||||
1. Remove any manual CSS overrides that reset the style of specific websites. For example:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```css [entrypoints/reddit.content/style.css]
|
||||
body { /* [!code --] */
|
||||
/* Override Reddit's default "hidden" visibility on elements */ /* [!code --] */
|
||||
visibility: visible !important; /* [!code --] */
|
||||
} /* [!code --] */
|
||||
```
|
||||
|
||||
2. Double check that your UI looks the same as before.
|
||||
|
||||
If you run into problems with the new behavior, you can disable it and continue using your current CSS:
|
||||
|
||||
```ts
|
||||
const ui = await createShadowRootUi({
|
||||
inheritStyles: true, // [!code ++]
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### Default Output Directories Changed
|
||||
|
||||
The default value for the [`outDirTemplate`](/api/reference/wxt/interfaces/InlineConfig#outdirtemplate) config has changed. Now, different build modes are output to different directories:
|
||||
|
||||
- `--mode production` → `.output/chrome-mv3`: Production builds are unchanged
|
||||
- `--mode development` → `.output/chrome-mv3-dev`: Dev mode now has a `-dev` suffix so it doesn't overwrite production builds
|
||||
- `--mode custom` → `.output/chrome-mv3-custom`: Other custom modes end with a `-[mode]` suffix
|
||||
|
||||
To use the old behavior, writing all output to the same directory, set the `outDirTemplate` option:
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
outDirTemplate: '{{browser}}-mv{{manifestVersion}}', // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
:::warning
|
||||
If you've previously loaded the extension into your browser manually for development, you'll need to uninstall and re-install it from the new dev output directory.
|
||||
:::
|
||||
|
||||
### Deprecated APIs Removed
|
||||
|
||||
- `entrypointLoader` option: WXT now uses `vite-node` for importing entrypoints during the build process.
|
||||
> This was deprecated in v0.19.0, see the [v0.19 section](#v0-18-5-rarr-v0-19-0) for migration steps.
|
||||
- `transformManifest` option: Use the `build:manifestGenerated` hook to transform the manifest instead:
|
||||
<!-- prettier-ignore -->
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
transformManifest(manifest) { // [!code --]
|
||||
hooks: { // [!code ++]
|
||||
'build:manifestGenerated': (_, manifest) => { // [!code ++]
|
||||
// ...
|
||||
}, // [!code ++]
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### New Deprecations
|
||||
|
||||
#### `runner` APIs Renamed
|
||||
|
||||
To improve consistency with the `web-ext.config.ts` filename, the "runner" API and config options have been renamed. You can continue using the old names, but they have been deprecated and will be removed in a future version:
|
||||
|
||||
1. The `runner` option has been renamed to `webExt`:
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
runner: { // [!code --]
|
||||
webExt: { // [!code ++]
|
||||
startUrls: ["https://wxt.dev"],
|
||||
},
|
||||
});
|
||||
```
|
||||
2. `defineRunnerConfig` has been renamed to `defineWebExtConfig`:
|
||||
```ts [web-ext.config.ts]
|
||||
import { defineRunnerConfig } from 'wxt'; // [!code --]
|
||||
import { defineWebExtConfig } from 'wxt'; // [!code ++]
|
||||
```
|
||||
3. The `ExtensionRunnerConfig` type has been renamed to `WebExtConfig`
|
||||
```ts
|
||||
import type { ExtensionRunnerConfig } from 'wxt'; // [!code --]
|
||||
import type { WebExtConfig } from 'wxt'; // [!code ++]
|
||||
```
|
||||
|
||||
## v0.18.5 → v0.19.0
|
||||
|
||||
### `vite-node` Entrypoint Loader
|
||||
@@ -19,7 +277,7 @@ Currently, WXT is in pre-release. This means changes to the second digit, `v0.X`
|
||||
The default entrypoint loader has changed to `vite-node`. If you use any NPM packages that depend on the `webextension-polyfill`, you need to add them to Vite's `ssr.noExternal` option:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
vite: () => ({ // [!code ++]
|
||||
ssr: { // [!code ++]
|
||||
@@ -35,8 +293,7 @@ export default defineConfig({
|
||||
|
||||
Importing variables and using them in the entrypoint options:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
```ts [entrypoints/content.ts]
|
||||
import { GOOGLE_MATCHES } from '~/utils/constants'
|
||||
|
||||
export default defineContentScript({
|
||||
@@ -47,8 +304,7 @@ export default defineContentScript({
|
||||
|
||||
Using Vite-specific APIs like `import.meta.glob` to define entrypoint options:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
```ts [entrypoints/content.ts]
|
||||
const providers: Record<string, any> = import.meta.glob('../providers/*', {
|
||||
eager: true,
|
||||
});
|
||||
@@ -69,7 +325,7 @@ Basically, you can now import and do things outside the `main` function of the e
|
||||
|
||||
To continue using the old approach, add the following to your `wxt.config.ts` file:
|
||||
|
||||
```ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
entrypointLoader: 'jiti', // [!code ++]
|
||||
});
|
||||
@@ -101,8 +357,7 @@ If you already have `<srcDir>/modules` or `<srcDir>/Modules` directory, `wxt pre
|
||||
You have two options:
|
||||
|
||||
1. [Recommended] Keep your files where they are and tell WXT to look in a different folder:
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
modulesDir: 'wxt-modules', // defaults to "modules"
|
||||
});
|
||||
@@ -175,8 +430,7 @@ JS entrypoints in the output directory have been moved. Unless you're doing some
|
||||
|
||||
### Renamed `zip.ignoredSources` to `zip.excludeSources`
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
zip: {
|
||||
ignoredSources: [
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
/guide/build-targets.html /guide/multiple-browsers.html
|
||||
/guide/installation.html /get-started/installation.html
|
||||
/guide/introduction.html /get-started/introduction.html
|
||||
/guide/upgrade-guide/wxt /guide/resources/upgrading.html
|
||||
/guide/upgrade-guide/wxt.html /guide/resources/upgrading.html
|
||||
|
||||
# 0.19.0
|
||||
/guide/go-further/entrypoint-side-effects.html /guide/go-further/entrypoint-loaders.html
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<!--@include: ../packages/runner/README.md-->
|
||||
+7
-8
@@ -15,7 +15,7 @@ A simplified wrapper around the extension storage APIs.
|
||||
This module is built-in to WXT, so you don't need to install anything.
|
||||
|
||||
```ts
|
||||
import { storage } from 'wxt/storage';
|
||||
import { storage } from '#imports';
|
||||
```
|
||||
|
||||
If you use auto-imports, `storage` is auto-imported for you, so you don't even need to import it!
|
||||
@@ -37,10 +37,9 @@ import { storage } from '@wxt-dev/storage';
|
||||
|
||||
## Storage Permission
|
||||
|
||||
To use the `wxt/storage` API, the `"storage"` permission must be added to the manifest:
|
||||
To use the `@wxt-dev/storage` API, the `"storage"` permission must be added to the manifest:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['storage'],
|
||||
@@ -75,7 +74,7 @@ await storage.watch<number>(
|
||||
await storage.getMeta<{ v: number }>('local:installDate');
|
||||
```
|
||||
|
||||
For a full list of methods available, see the [API reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorage).
|
||||
For a full list of methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage).
|
||||
|
||||
## Watchers
|
||||
|
||||
@@ -98,7 +97,7 @@ unwatch();
|
||||
|
||||
## Metadata
|
||||
|
||||
`wxt/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc.
|
||||
`@wxt-dev/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc.
|
||||
|
||||
[Other than versioning](#versioning), you are responsible for managing a field's metadata:
|
||||
|
||||
@@ -158,7 +157,7 @@ const unwatch = showChangelogOnUpdate.watch((newValue) => {
|
||||
});
|
||||
```
|
||||
|
||||
For a full list of properties and methods available, see the [API reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorageItem).
|
||||
For a full list of properties and methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorageItem).
|
||||
|
||||
### Versioning
|
||||
|
||||
@@ -354,4 +353,4 @@ await storage.setItems([
|
||||
]);
|
||||
```
|
||||
|
||||
Refer to the [API Reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs.
|
||||
Refer to the [API Reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs.
|
||||
|
||||
+28
-26
@@ -4,7 +4,7 @@
|
||||
"engines": {
|
||||
"node": ">=18.20.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.5.2",
|
||||
"packageManager": "pnpm@10.10.0",
|
||||
"scripts": {
|
||||
"check": "check && pnpm -r --sequential run check",
|
||||
"test": "pnpm -r --sequential run test run",
|
||||
@@ -17,31 +17,33 @@
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/buildc": "catalog:",
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@commitlint/config-conventional": "catalog:",
|
||||
"@commitlint/types": "catalog:",
|
||||
"@types/fs-extra": "catalog:",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"changelogen": "catalog:",
|
||||
"consola": "catalog:",
|
||||
"fast-glob": "catalog:",
|
||||
"fs-extra": "catalog:",
|
||||
"lint-staged": "catalog:",
|
||||
"markdown-it-footnote": "catalog:",
|
||||
"nano-spawn": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"simple-git-hooks": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"typedoc": "catalog:",
|
||||
"typedoc-plugin-frontmatter": "catalog:",
|
||||
"typedoc-plugin-markdown": "catalog:",
|
||||
"typedoc-vitepress-theme": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vitepress": "catalog:",
|
||||
"vitepress-knowledge": "catalog:",
|
||||
"vitest-mock-extended": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"@aklinker1/buildc": "^1.1.4",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"@commitlint/config-conventional": "^19.8.0",
|
||||
"@commitlint/types": "^19.8.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@vitest/coverage-v8": "^3.1.2",
|
||||
"changelogen": "^0.6.1",
|
||||
"consola": "^3.4.2",
|
||||
"fast-glob": "^3.3.3",
|
||||
"feed": "^4.2.2",
|
||||
"fs-extra": "^11.3.0",
|
||||
"lint-staged": "^15.5.1",
|
||||
"markdown-it-footnote": "^4.0.0",
|
||||
"nano-spawn": "^0.2.0",
|
||||
"prettier": "^3.5.3",
|
||||
"simple-git-hooks": "^2.13.0",
|
||||
"tsx": "4.19.4",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-frontmatter": "^1.3.0",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vitepress": "^1.6.3",
|
||||
"vitepress-knowledge": "^0.4.1",
|
||||
"vitepress-plugin-group-icons": "^1.5.2",
|
||||
"vitest-mock-extended": "^3.1.0",
|
||||
"vue": "^3.5.13",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## v0.5.0
|
||||
[⚠️ breaking changes](https://wxt.dev/guide/resources/upgrading.html) • [compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.4.1...analytics-v0.5.0)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- ⚠️ Update min WXT version to 0.20 ([2e8baf0](https://github.com/wxt-dev/wxt/commit/2e8baf0))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineAppConfig } from 'wxt/sandbox';
|
||||
import { defineAppConfig } from 'wxt/utils/define-app-config';
|
||||
import { googleAnalytics4 } from './modules/analytics/providers/google-analytics-4';
|
||||
import { umami } from './modules/analytics/providers/umami';
|
||||
|
||||
|
||||
@@ -9,11 +9,12 @@ import type {
|
||||
AnalyticsEventMetadata,
|
||||
AnalyticsProvider,
|
||||
} from './types';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
|
||||
const ANALYTICS_PORT = '@wxt-dev/analytics';
|
||||
|
||||
export function createAnalytics(config?: AnalyticsConfig): Analytics {
|
||||
if (typeof chrome === 'undefined' || !chrome?.runtime?.id)
|
||||
if (!browser?.runtime?.id)
|
||||
throw Error(
|
||||
'Cannot use WXT analytics in contexts without access to the browser.runtime APIs',
|
||||
);
|
||||
@@ -51,13 +52,13 @@ function createBackgroundAnalytics(
|
||||
defineStorageItem<boolean>('local:wxt-analytics:enabled', false);
|
||||
|
||||
// Cached values
|
||||
const platformInfo = chrome.runtime.getPlatformInfo();
|
||||
const platformInfo = browser.runtime.getPlatformInfo();
|
||||
const userAgent = UAParser();
|
||||
let userId = Promise.resolve(userIdStorage.getValue()).then(
|
||||
(id) => id ?? globalThis.crypto.randomUUID(),
|
||||
);
|
||||
let userProperties = userPropertiesStorage.getValue();
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
const manifest = browser.runtime.getManifest();
|
||||
|
||||
const getBackgroundMeta = () => ({
|
||||
timestamp: Date.now(),
|
||||
@@ -178,7 +179,7 @@ function createBackgroundAnalytics(
|
||||
config?.providers?.map((provider) => provider(analytics, config)) ?? [];
|
||||
|
||||
// Listen for messages from the rest of the extension
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
browser.runtime.onConnect.addListener((port) => {
|
||||
if (port.name === ANALYTICS_PORT) {
|
||||
port.onMessage.addListener(({ fn, args }) => {
|
||||
// @ts-expect-error: Untyped fn key
|
||||
@@ -194,7 +195,7 @@ function createBackgroundAnalytics(
|
||||
* Creates an analytics client for non-background contexts.
|
||||
*/
|
||||
function createFrontendAnalytics(): Analytics {
|
||||
const port = chrome.runtime.connect({ name: ANALYTICS_PORT });
|
||||
const port = browser.runtime.connect({ name: ANALYTICS_PORT });
|
||||
const sessionId = Date.now();
|
||||
const getFrontendMetadata = (): AnalyticsEventMetadata => ({
|
||||
sessionId,
|
||||
@@ -252,8 +253,8 @@ function defineStorageItem<T>(
|
||||
): AnalyticsStorageItem<T> {
|
||||
return {
|
||||
getValue: async () =>
|
||||
(await chrome.storage.local.get(key))[key] ?? defaultValue,
|
||||
setValue: (newValue) => chrome.storage.local.set({ [key]: newValue }),
|
||||
(await browser.storage.local.get(key))[key] ?? defaultValue,
|
||||
setValue: (newValue) => browser.storage.local.set({ [key]: newValue }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'wxt';
|
||||
import 'wxt/sandbox';
|
||||
import 'wxt/utils/define-app-config';
|
||||
import {
|
||||
addAlias,
|
||||
addViteConfig,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { relative, resolve } from 'node:path';
|
||||
import type { AnalyticsConfig } from './types';
|
||||
|
||||
declare module 'wxt/sandbox' {
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
analytics: AnalyticsConfig;
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export default defineWxtModule({
|
||||
? clientModuleId
|
||||
: relative(wxtAnalyticsFolder, clientModuleId)
|
||||
}';`,
|
||||
`import { useAppConfig } from 'wxt/client';`,
|
||||
`import { useAppConfig } from '#imports';`,
|
||||
``,
|
||||
`export const analytics = createAnalytics(useAppConfig().analytics);`,
|
||||
``,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wxt-dev/analytics",
|
||||
"version": "0.4.1",
|
||||
"version": "0.5.0",
|
||||
"description": "Add analytics to your web extension",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -48,18 +48,18 @@
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.19.23"
|
||||
"wxt": ">=0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@types/chrome": "catalog:",
|
||||
"@types/ua-parser-js": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"ua-parser-js": "catalog:"
|
||||
"@wxt-dev/browser": "workspace:^",
|
||||
"ua-parser-js": "^1.0.40"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"#analytics": ["./.wxt/analytics/index.ts"]
|
||||
},
|
||||
"types": ["chrome"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -45,16 +45,16 @@
|
||||
"wxt": ">=0.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"defu": "catalog:",
|
||||
"fs-extra": "catalog:",
|
||||
"sharp": "catalog:"
|
||||
"defu": "^6.1.4",
|
||||
"fs-extra": "^11.3.0",
|
||||
"sharp": "^0.34.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"oxlint": "^0.16.8",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# `@wxt-dev/browser`
|
||||
|
||||
Provides access to the `browser` or `chrome` extension APIs and related types.
|
||||
|
||||
```ts
|
||||
import { browser, Browser } from '@wxt-dev/browser';
|
||||
// Or if you're using WXT:
|
||||
// import { browser, Browser } from 'wxt/browser';
|
||||
|
||||
console.log(browser.runtime.id);
|
||||
|
||||
const onMessage = (message: any, sender: Browser.runtime.MessageSender) => {
|
||||
console.log(message);
|
||||
};
|
||||
browser.runtime.onMessage.addListener(onMessage);
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
If you're using WXT, this package is already installed, you don't need to install it manually.
|
||||
|
||||
Otherwise, you can install the package from NPM:
|
||||
|
||||
```sh
|
||||
pnpm install @wxt-dev/browser
|
||||
```
|
||||
|
||||
## Upgrading to Latest Types
|
||||
|
||||
Just run:
|
||||
|
||||
```sh
|
||||
pnpm upgrade @wxt-dev/browser
|
||||
```
|
||||
|
||||
This should update both the manually installed version and the subdependency inside WXT.
|
||||
|
||||
## Contributing
|
||||
|
||||
### Code Generation
|
||||
|
||||
Types are generated based on the `@types/chrome` package, and with modifications specifically for use with WXT.
|
||||
|
||||
### Updating `@types/chrome` Version
|
||||
|
||||
You don't need to do anything! [A github action](https://github.com/wxt-dev/wxt/actions/workflows/update-browser-package.yml) is ran every day to generate and publish this package using the latest `@types/chrome` version.
|
||||
|
||||
You can manually generate types via:
|
||||
|
||||
```sh
|
||||
pnpm gen
|
||||
```
|
||||
|
||||
### Why not just use `@types/chrome`?
|
||||
|
||||
With WXT, you must import the `browser` variable to use the extension APIs. The way `@types/chrome` is implemented forces you to define a global `chrome` variable. With WXT, this isn't acceptable, we don't want to pollute the global (type) scope or introduce conflicts with auto-imports.
|
||||
|
||||
Additionally, WXT overrides types to provide additional type safety for some APIs, like `browser.runtime.getURL` and `browser.i18n.getMessage`. With `@types/chrome`'s nested namespace approach, it's not possible to override the types for those functions.
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@wxt-dev/browser",
|
||||
"description": "Provides a cross-browser API for using extension APIs and types based on @types/chrome",
|
||||
"version": "0.0.326",
|
||||
"type": "module",
|
||||
"main": "src/index.mjs",
|
||||
"types": "src/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/browser"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "check",
|
||||
"gen": "tsx scripts/generate.ts"
|
||||
},
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/chrome": "0.0.326",
|
||||
"fs-extra": "^11.3.0",
|
||||
"nano-spawn": "^0.2.0",
|
||||
"tsx": "4.19.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/filesystem": "*",
|
||||
"@types/har-format": "*"
|
||||
},
|
||||
"peerDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import spawn from 'nano-spawn';
|
||||
import fs from 'fs-extra';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join, resolve, sep } from 'node:path';
|
||||
import { sep as posixSep } from 'node:path/posix';
|
||||
|
||||
// Fetch latest version
|
||||
|
||||
console.log('Getting latest version of \x1b[36m@types/chrome\x1b[0m');
|
||||
await spawn('pnpm', ['i', '--ignore-scripts', '-D', '@types/chrome@latest']);
|
||||
|
||||
// Generate new package.json
|
||||
|
||||
console.log('Generating new \x1b[36mpackage.json\x1b[0m');
|
||||
|
||||
const pkgJsonPath = fileURLToPath(
|
||||
import.meta.resolve('@types/chrome/package.json'),
|
||||
);
|
||||
const pkgDir = dirname(pkgJsonPath);
|
||||
const pkgJson = await fs.readJson(pkgJsonPath);
|
||||
const pkgJsonTemplate = await fs.readFile('templates/package.json', 'utf8');
|
||||
const newPkgJson = JSON.parse(
|
||||
pkgJsonTemplate.replaceAll('{{chromeTypesVersion}}', pkgJson.version),
|
||||
);
|
||||
newPkgJson.dependencies = pkgJson.dependencies;
|
||||
newPkgJson.peerDependencies = pkgJson.peerDependencies;
|
||||
newPkgJson.peerDependenciesMeta = pkgJson.peerDependenciesMeta;
|
||||
|
||||
const outPkgJsonPath = resolve('package.json');
|
||||
await fs.writeJson(outPkgJsonPath, newPkgJson);
|
||||
await spawn('pnpm', ['-w', 'prettier', '--write', outPkgJsonPath]);
|
||||
|
||||
// Generate declaration files
|
||||
|
||||
console.log('Generating declaration files');
|
||||
const outDir = resolve('src/gen');
|
||||
const declarationFileMapping = (
|
||||
await fs.readdir(pkgDir, {
|
||||
recursive: true,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
)
|
||||
// Filter to .d.ts files
|
||||
.filter((file) => file.endsWith('.d.ts'))
|
||||
// Map to usable paths
|
||||
.map((file) => ({
|
||||
file: file.replaceAll(sep, posixSep),
|
||||
srcPath: join(pkgDir, file),
|
||||
destPath: join(outDir, file),
|
||||
}));
|
||||
|
||||
for (const { file, srcPath, destPath } of declarationFileMapping) {
|
||||
const content = await fs.readFile(srcPath, 'utf8');
|
||||
const transformedContent = transformFile(file, content);
|
||||
const destDir = dirname(destPath);
|
||||
await fs.mkdir(destDir, { recursive: true });
|
||||
await fs.writeFile(destPath, transformedContent);
|
||||
console.log(` \x1b[2m-\x1b[0m \x1b[36m${file}\x1b[0m`);
|
||||
}
|
||||
|
||||
// Done!
|
||||
|
||||
console.log(
|
||||
'\x1b[32m✔\x1b[0m Done in ' + performance.now().toFixed(0) + ' ms',
|
||||
);
|
||||
|
||||
// Transformations
|
||||
|
||||
function transformFile(file: string, content: string): string {
|
||||
return (
|
||||
// Add prefix
|
||||
`/* DO NOT EDIT - generated by scripts/generate.ts */\n\n${content}\n`
|
||||
// Remove global type declaration
|
||||
.replaceAll('chrome: typeof chrome;', '// chrome: typeof chrome;')
|
||||
// Rename `chrome` namespace to `Browser` and export it
|
||||
.replaceAll('declare namespace chrome', 'export namespace Browser')
|
||||
// Update references to `chrome` namespace to `Browser`
|
||||
.replaceAll('chrome.', 'Browser.')
|
||||
// Fix links to developer.chrome.com
|
||||
.replaceAll('developer.Browser.com', 'developer.chrome.com')
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/// <reference types="chrome" />
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
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>();
|
||||
});
|
||||
|
||||
it('should provide values via the browser import', () => {
|
||||
expectTypeOf(browser.runtime.id).toMatchTypeOf<string>();
|
||||
expectTypeOf(
|
||||
browser.storage.local,
|
||||
).toMatchTypeOf<Browser.storage.StorageArea>();
|
||||
expectTypeOf(
|
||||
browser.i18n.detectLanguage('Hello, world!'),
|
||||
).resolves.toMatchTypeOf<chrome.i18n.LanguageDetectionResult>();
|
||||
});
|
||||
});
|
||||
});
|
||||
+1160
File diff suppressed because it is too large
Load Diff
+9
@@ -0,0 +1,9 @@
|
||||
/* DO NOT EDIT - generated by scripts/generate.ts */
|
||||
|
||||
import { Entry, Log } from "har-format";
|
||||
|
||||
declare global {
|
||||
export type HARFormatEntry = Entry;
|
||||
export type HARFormatLog = Log;
|
||||
}
|
||||
|
||||
+15478
File diff suppressed because it is too large
Load Diff
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import { Browser } from './gen';
|
||||
|
||||
export const browser: typeof Browser;
|
||||
export { Browser };
|
||||
@@ -0,0 +1,5 @@
|
||||
// #region snippet
|
||||
export const browser = globalThis.browser?.runtime?.id
|
||||
? globalThis.browser
|
||||
: globalThis.chrome;
|
||||
// #endregion snippet
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@wxt-dev/browser",
|
||||
"description": "Provides a cross-browser API for using extension APIs and types based on @types/chrome",
|
||||
"version": "{{chromeTypesVersion}}",
|
||||
"type": "module",
|
||||
"main": "src/index.mjs",
|
||||
"types": "src/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/browser"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "check",
|
||||
"gen": "tsx scripts/generate.ts"
|
||||
},
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/chrome": "{{chromeTypesVersion}}",
|
||||
"fs-extra": "^11.3.0",
|
||||
"nano-spawn": "^0.2.0",
|
||||
"tsx": "4.19.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -1,5 +1,36 @@
|
||||
# Changelog
|
||||
|
||||
## v0.2.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.3...i18n-v0.2.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Standardize locale codes and warn about unsupported ones ([#1617](https://github.com/wxt-dev/wxt/pull/1617))
|
||||
- Use `@wxt-dev/browser` instead of `@types/chrome` ([#1645](https://github.com/wxt-dev/wxt/pull/1645))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add react language ID to README ([#1347](https://github.com/wxt-dev/wxt/pull/1347))
|
||||
- Fix public path reference ([bcb20874](https://github.com/wxt-dev/wxt/commit/bcb20874))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))
|
||||
- **deps:** Bump dev and non-breaking major dependencies ([#1167](https://github.com/wxt-dev/wxt/pull/1167))
|
||||
- Add funding links to `package.json` files ([#1446](https://github.com/wxt-dev/wxt/pull/1446))
|
||||
- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))
|
||||
- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))
|
||||
- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))
|
||||
- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))
|
||||
- Change browser workspace dependency to `^` ([c7335add](https://github.com/wxt-dev/wxt/commit/c7335add))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Okinea Dev ([@okineadev](https://github.com/okineadev))
|
||||
- Redwoodlid ([@redwoodlid](https://github.com/redwoodlid))
|
||||
|
||||
## v0.2.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.2...i18n-v0.2.3)
|
||||
|
||||
@@ -49,7 +49,7 @@ However, it does have one major downside:
|
||||
helloWorld: Hello world!
|
||||
```
|
||||
|
||||
> `@wxt-dev/i18n` supports the standard messages format, so if you already have localization files at `<srcDir>/public/_locale/<lang>/messages.json`, you don't need to convert them to YAML or refactor them - just move them to `<srcDir>/locales/<lang>.json` and they'll just work out of the box!
|
||||
> `@wxt-dev/i18n` supports the standard messages format, so if you already have localization files at `<rootDir>/public/_locale/<lang>/messages.json`, you don't need to convert them to YAML or refactor them - just move them to `<srcDir>/locales/<lang>.json` and they'll just work out of the box!
|
||||
|
||||
4. To get a translation, use the auto-imported `i18n` object or import it manually:
|
||||
|
||||
|
||||
+13
-13
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@wxt-dev/i18n",
|
||||
"description": "Type-safe wrapper around browser.i18n.getMessage with additional features",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,9 +26,10 @@
|
||||
"test": "buildc --deps-only -- vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "catalog:",
|
||||
"confbox": "catalog:",
|
||||
"fast-glob": "catalog:"
|
||||
"@wxt-dev/browser": "workspace:^",
|
||||
"chokidar": "^4.0.3",
|
||||
"confbox": "^0.1.8 || ^0.2.2",
|
||||
"fast-glob": "^3.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.19.7"
|
||||
@@ -39,15 +40,14 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@types/chrome": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"vitest-plugin-random-seed": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"@types/node": "^20.17.6",
|
||||
"oxlint": "^0.16.8",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"vitest": "^3.1.2",
|
||||
"vitest-plugin-random-seed": "^1.1.1",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"main": "./dist/index.cjs",
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createI18n } from '../index';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
|
||||
const getMessageMock = vi.fn();
|
||||
|
||||
vi.stubGlobal('chrome', {
|
||||
i18n: {
|
||||
getMessage: getMessageMock,
|
||||
},
|
||||
vi.mock('@wxt-dev/browser', async () => {
|
||||
const { vi } = await import('vitest');
|
||||
return {
|
||||
browser: {
|
||||
i18n: {
|
||||
getMessage: vi.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
const getMessageMock = vi.mocked(browser.i18n.getMessage);
|
||||
|
||||
describe('createI18n', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { beforeEach, describe, it, vi } from 'vitest';
|
||||
import { createI18n } from '..';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
|
||||
const getMessageMock = vi.fn();
|
||||
|
||||
vi.stubGlobal('chrome', {
|
||||
i18n: {
|
||||
getMessage: getMessageMock,
|
||||
},
|
||||
vi.mock('@wxt-dev/browser', async () => {
|
||||
const { vi } = await import('vitest');
|
||||
return {
|
||||
browser: {
|
||||
i18n: {
|
||||
getMessage: vi.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
const getMessageMock = vi.mocked(browser.i18n.getMessage);
|
||||
|
||||
const n: number = 1;
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ChromeMessage } from '../build';
|
||||
import { applyChromeMessagePlaceholders, getSubstitutionCount } from '../utils';
|
||||
import {
|
||||
applyChromeMessagePlaceholders,
|
||||
getSubstitutionCount,
|
||||
standardizeLocale,
|
||||
} from '../utils';
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('applyChromeMessagePlaceholders', () => {
|
||||
it('should return the combined stirng', () => {
|
||||
it('should return the combined string', () => {
|
||||
const input = {
|
||||
message: 'Hello $username$, welcome to $appName$',
|
||||
placeholders: {
|
||||
@@ -60,4 +64,30 @@ describe('Utils', () => {
|
||||
expect(getSubstitutionCount('Hello $10')).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('standardizeLocale', () => {
|
||||
it('should convert two-letter locale codes to lowercase', () => {
|
||||
expect(standardizeLocale('en')).toEqual('en');
|
||||
expect(standardizeLocale('EN')).toEqual('en');
|
||||
});
|
||||
|
||||
it('should convert locale code extensions to uppercase', () => {
|
||||
expect(standardizeLocale('en_US')).toEqual('en_US');
|
||||
expect(standardizeLocale('en_us')).toEqual('en_US');
|
||||
expect(standardizeLocale('es_419')).toEqual('es_419');
|
||||
});
|
||||
|
||||
it('should convert dashes to underscores', () => {
|
||||
expect(standardizeLocale('en_US')).toEqual('en_US');
|
||||
expect(standardizeLocale('en-US')).toEqual('en_US');
|
||||
});
|
||||
|
||||
it('should return the input string as-is for unknown formats', () => {
|
||||
expect(standardizeLocale('en_USSS')).toEqual('en_USSS');
|
||||
expect(standardizeLocale('en-')).toEqual('en-');
|
||||
expect(standardizeLocale('------')).toEqual('------');
|
||||
expect(standardizeLocale('test')).toEqual('test');
|
||||
expect(standardizeLocale('hello-world')).toEqual('hello-world');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ import { parseYAML, parseJSON5, parseTOML } from 'confbox';
|
||||
import { dirname, extname } from 'node:path';
|
||||
import { applyChromeMessagePlaceholders, getSubstitutionCount } from './utils';
|
||||
|
||||
export { SUPPORTED_LOCALES } from './supported-locales';
|
||||
|
||||
//
|
||||
// TYPES
|
||||
//
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
I18n,
|
||||
Substitution,
|
||||
} from './types';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
|
||||
export function createI18n<
|
||||
T extends I18nStructure = DefaultI18nStructure,
|
||||
@@ -39,9 +40,9 @@ export function createI18n<
|
||||
if (sub?.length) {
|
||||
// Convert all substitutions to strings
|
||||
const stringSubs = sub?.map((sub) => String(sub));
|
||||
message = chrome.i18n.getMessage(key.replaceAll('.', '_'), stringSubs);
|
||||
message = browser.i18n.getMessage(key.replaceAll('.', '_'), stringSubs);
|
||||
} else {
|
||||
message = chrome.i18n.getMessage(key.replaceAll('.', '_'));
|
||||
message = browser.i18n.getMessage(key.replaceAll('.', '_'));
|
||||
}
|
||||
if (!message) {
|
||||
console.warn(`[i18n] Message not found: "${key}"`);
|
||||
|
||||
@@ -16,12 +16,14 @@ import {
|
||||
generateChromeMessagesText,
|
||||
parseMessagesFile,
|
||||
generateTypeText,
|
||||
SUPPORTED_LOCALES,
|
||||
} from './build';
|
||||
import glob from 'fast-glob';
|
||||
import { basename, extname, join, resolve } from 'node:path';
|
||||
import { watch } from 'chokidar';
|
||||
import { GeneratedPublicFile, WxtDirFileEntry } from 'wxt';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { standardizeLocale } from './utils';
|
||||
|
||||
export default defineWxtModule<I18nOptions>({
|
||||
name: '@wxt-dev/i18n',
|
||||
@@ -46,10 +48,22 @@ export default defineWxtModule<I18nOptions>({
|
||||
cwd: localesDir,
|
||||
absolute: true,
|
||||
});
|
||||
return files.map((file) => ({
|
||||
file,
|
||||
locale: basename(file).replace(extname(file), ''),
|
||||
}));
|
||||
|
||||
const unsupportedLocales: string[] = [];
|
||||
|
||||
const res = files.map((file) => {
|
||||
const rawLocale = basename(file).replace(extname(file), '');
|
||||
const locale = standardizeLocale(rawLocale);
|
||||
if (!SUPPORTED_LOCALES.has(locale)) unsupportedLocales.push(locale);
|
||||
return { file, locale };
|
||||
});
|
||||
|
||||
if (unsupportedLocales.length > 0)
|
||||
wxt.logger.warn(
|
||||
`Unsupported locales: [${unsupportedLocales.join(', ')}].\n\nWeb extensions only support a limited set of locales as described here: https://developer.chrome.com/docs/extensions/reference/api/i18n#locales`,
|
||||
);
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
const generateOutputJsonFiles = async (): Promise<
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/** From https://developer.chrome.com/docs/extensions/reference/api/i18n#locales */
|
||||
export const SUPPORTED_LOCALES = new Set([
|
||||
'ar',
|
||||
'am',
|
||||
'bg',
|
||||
'bn',
|
||||
'ca',
|
||||
'cs',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'en',
|
||||
'en_AU',
|
||||
'en_GB',
|
||||
'en_US',
|
||||
'es',
|
||||
'es_419',
|
||||
'et',
|
||||
'fa',
|
||||
'fi',
|
||||
'fil',
|
||||
'fr',
|
||||
'gu',
|
||||
'he',
|
||||
'hi',
|
||||
'hr',
|
||||
'hu',
|
||||
'id',
|
||||
'it',
|
||||
'ja',
|
||||
'kn',
|
||||
'ko',
|
||||
'lt',
|
||||
'lv',
|
||||
'ml',
|
||||
'mr',
|
||||
'ms',
|
||||
'nl',
|
||||
'no',
|
||||
'pl',
|
||||
'pt_BR',
|
||||
'pt_PT',
|
||||
'ro',
|
||||
'ru',
|
||||
'sk',
|
||||
'sl',
|
||||
'sr',
|
||||
'sv',
|
||||
'sw',
|
||||
'ta',
|
||||
'te',
|
||||
'th',
|
||||
'tr',
|
||||
'uk',
|
||||
'vi',
|
||||
'zh_CN',
|
||||
'zh_TW',
|
||||
]);
|
||||
@@ -21,3 +21,16 @@ export function getSubstitutionCount(message: string): number {
|
||||
}
|
||||
|
||||
const MAX_SUBSTITUTIONS = 9;
|
||||
|
||||
/** Given a string, standardize it to the format `xx_YY`. */
|
||||
export function standardizeLocale(locale: string): string {
|
||||
if (locale.length === 2) return locale.toLowerCase();
|
||||
|
||||
const [is_match, prefix, suffix] =
|
||||
locale.match(/^([a-z]{2})[-_]([a-z]{2,3})$/i) ?? [];
|
||||
if (is_match) {
|
||||
return `${prefix.toLowerCase()}_${suffix.toUpperCase()}`;
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["chrome", "node"]
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
|
||||
import {
|
||||
defineContentScript,
|
||||
ContentScriptContext,
|
||||
createShadowRootUi,
|
||||
} from '#imports';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ export default defineWxtModule<ReactModuleOptions>({
|
||||
|
||||
// Enable auto-imports for JSX files
|
||||
wxt.hook('config:resolved', (wxt) => {
|
||||
if (wxt.config.imports === false) return;
|
||||
// In older versions of WXT, `wxt.config.imports` could be false
|
||||
if (!wxt.config.imports) return;
|
||||
|
||||
wxt.config.imports.dirsScanOptions ??= {};
|
||||
wxt.config.imports.dirsScanOptions.filePatterns = [
|
||||
|
||||
@@ -48,17 +48,17 @@
|
||||
"wxt": ">=0.19.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "catalog:"
|
||||
"@vitejs/plugin-react": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"publint": "^0.3.12",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
|
||||
import {
|
||||
defineContentScript,
|
||||
ContentScriptContext,
|
||||
createShadowRootUi,
|
||||
} from '#imports';
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
export default defineContentScript({
|
||||
|
||||
@@ -19,7 +19,8 @@ export default defineWxtModule<SolidModuleOptions>({
|
||||
|
||||
// Enable auto-imports for JSX files
|
||||
wxt.hook('config:resolved', (wxt) => {
|
||||
if (wxt.config.imports === false) return;
|
||||
// In older versions of WXT, `wxt.config.imports` could be false
|
||||
if (!wxt.config.imports) return;
|
||||
|
||||
wxt.config.imports.dirsScanOptions ??= {};
|
||||
wxt.config.imports.dirsScanOptions.filePatterns = [
|
||||
|
||||
@@ -48,14 +48,14 @@
|
||||
"wxt": ">=0.19.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"vite-plugin-solid": "catalog:"
|
||||
"vite-plugin-solid": "^2.11.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"publint": "^0.3.12",
|
||||
"solid-js": "^1.9.6",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,13 @@
|
||||
"svelte": ">=5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "catalog:"
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0 || ^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,13 +46,13 @@
|
||||
"wxt": ">=0.19.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "catalog:"
|
||||
"@vitejs/plugin-vue": "^5.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## v0.1.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/runner-v0.1.0...runner-v0.1.1)
|
||||
@@ -0,0 +1,225 @@
|
||||
# `@wxt-dev/runner`
|
||||
|
||||
Programmatically open a browser and install a web extension from a local directory.
|
||||
|
||||
###### With WXT
|
||||
|
||||
> [!WARNING]
|
||||
> This package is intended to replace [`web-ext`](https://github.com/mozilla/web-ext) in the future, but it is not ready at the moment. Once it's ready for testing in WXT, more details will be added here.
|
||||
|
||||
```ts
|
||||
// ~/wxt.runner.config.ts OR <project>/wxt.runner.config.ts
|
||||
import { defineRunnerConfig } from 'wxt';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
// Options go here
|
||||
});
|
||||
```
|
||||
|
||||
###### JS API
|
||||
|
||||
```ts
|
||||
import { run } from '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
extensionDir: '/path/to/extension',
|
||||
// Other options...
|
||||
});
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Supports all Chromium and Firefox based browsers
|
||||
- Zero dependencies
|
||||
- One-line config for persisting data between launches
|
||||
|
||||
## Requirements
|
||||
|
||||
`@wxt-dev/runner` requires a JS runtime that implements the `WebSocket` standard:
|
||||
|
||||
| JS Runtime | Version |
|
||||
| ---------- | ----------- |
|
||||
| NodeJS | ≥ 22.4.0 |
|
||||
| Bun | ≥ 1.2.0 |
|
||||
|
||||
You also need to have a specific version of the browser installed that supports the latest features so extensions can be loaded:
|
||||
|
||||
| Browser | Version |
|
||||
| -------- | -------- |
|
||||
| Chromium | Unknown |
|
||||
| Firefox | ≥ 139 |
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] Provide install functions to allow hooking into already running instances of Chrome/Firefox
|
||||
- [ ] Try to setup E2E tests on Firefox with Puppeteer using this approach
|
||||
- [ ] Try to setup E2E tests on Chrome with Puppeteer using this approach
|
||||
|
||||
## Options
|
||||
|
||||
### Target
|
||||
|
||||
To open a specific browser, use the `target` option:
|
||||
|
||||
```ts
|
||||
import { run } from '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
extensionDir: 'path/to/extension',
|
||||
target: 'firefox',
|
||||
});
|
||||
```
|
||||
|
||||
Defaults to opening `chrome`. You may see type-hints for a list of popular browsers, but you can enter any string you want here.
|
||||
|
||||
### Data Persistence
|
||||
|
||||
Browsers block you from using your normal browser profiles when using the [BiDi and CDP protocols](#implementation-details) for security reasons.
|
||||
|
||||
To change how the new profile's data is saved between sessions, use the `dataPersistence` option:
|
||||
|
||||
```ts
|
||||
import { run } from '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
dataPersistence: 'user',
|
||||
});
|
||||
```
|
||||
|
||||
- `"none"` (default): Use a brand new browser profile every time the browser is opened (stored in the system's tmp directory)
|
||||
- `"project"`: Create a new profile that is re-used for your current directory (by default stored in `.wxt-runner` or `.wxt/runner` for WXT projects)
|
||||
- `"user"`: Create a new profile that is re-used for all projects using `@wxt-dev/runner` (by default stored in `$HOME/.wxt-runner`)
|
||||
|
||||
These presets configure different flags for different operating systems when spawning the browser process.
|
||||
|
||||
If you want to customize your data persistence beyond what these presets define, [you can override the browser flags yourself](#Arguments) to configure persistence.
|
||||
|
||||
### Browser Binaries
|
||||
|
||||
`@wxt-dev/runner` will look for browser binaries/executables in [a hard-coded list of paths](https://github.com/wxt-dev/wxt/blob/main/packages/runner/src/browser-paths.ts). It does not and will not explore your filesystem/`$PATH` to find where the browser is installed. That means there are times you will need to specify the path to a browser's binary on your system:
|
||||
|
||||
- Your browser's path is non-standard or missing from the hard-coded list.
|
||||
- You want to use a specific version/release of the browser.
|
||||
- You're using a less popular browser and `@wxt-dev/runner` doesn't have hard-coded paths for it.
|
||||
|
||||
To do this, use the `browserBinaries` option and set the path to the browser's binary:
|
||||
|
||||
```ts
|
||||
import { run } from '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
extensionDir: 'path/to/extension',
|
||||
browserBinaries: {
|
||||
chrome: '/path/to/chrome',
|
||||
firefox: '/path/to/firefox',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
To pass custom arguments to the browser on startup, use the `chromiumArgs` or `firefoxArgs` options:
|
||||
|
||||
```ts
|
||||
import { run } from '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
extensionDir: 'path/to/extension',
|
||||
chromiumArgs: ['--window-size=1920,1080'],
|
||||
firefoxArgs: ['--window-size', '1920,1080'],
|
||||
});
|
||||
```
|
||||
|
||||
### Start URLs
|
||||
|
||||
To open specific URLs in tabs by default, you also use the `chromiumArgs` or `firefoxArgs` options.
|
||||
|
||||
Any URLs passed as a CLI argument will be opened in the browser when it starts.
|
||||
|
||||
```ts
|
||||
import { run } from '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
extensionDir: 'path/to/extension',
|
||||
chromiumArgs: ['https://example.com'],
|
||||
firefoxArgs: ['https://example.com'],
|
||||
});
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
To see debug logs, set the `DEBUG` env var to `"@wxt-dev/runner"`. This will print the resolved config, commands used to spawn the browser, any messages sent on the browser's communication protocol, and more for you to debug.
|
||||
|
||||
<details>
|
||||
<summary>Example debug output</summary>
|
||||
|
||||
```
|
||||
@wxt-dev/runner:options User options: { extensionDir: 'demo-extension', target: undefined }
|
||||
@wxt-dev/runner:options Resolved options: {
|
||||
browserBinary: '/usr/bin/chromium',
|
||||
chromiumArgs: [
|
||||
'--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculateNativeWinOcclusion,InterestFeedContentSuggestions,CertificateTransparencyComponentUpdater,AutofillServerCommunication,PrivacySandboxSettings4',
|
||||
'--disable-component-extensions-with-background-pages',
|
||||
'--disable-background-networking',
|
||||
'--disable-component-update',
|
||||
'--disable-client-side-phishing-detection',
|
||||
'--disable-sync',
|
||||
'--metrics-recording-only',
|
||||
'--disable-default-apps',
|
||||
'--no-default-browser-check',
|
||||
'--no-first-run',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-ipc-flooding-protection',
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
'--force-fieldtrials=*BackgroundTracing/default/',
|
||||
'--disable-hang-monitor',
|
||||
'--disable-prompt-on-repost',
|
||||
'--disable-domain-reliability',
|
||||
'--propagate-iph-for-testing',
|
||||
'--remote-debugging-port=0',
|
||||
'--remote-debugging-pipe',
|
||||
'--user-data-dir=/tmp/wxt-runner-pWXLO1',
|
||||
'--enable-unsafe-extension-debugging'
|
||||
],
|
||||
dataDir: '/tmp/wxt-runner-pWXLO1',
|
||||
dataPersistence: 'none',
|
||||
chromiumRemoteDebuggingPort: 0,
|
||||
extensionDir: '/home/aklinker1/Development/github.com/wxt-dev/wxt/packages/runner/demo-extension',
|
||||
firefoxArgs: [
|
||||
'--new-instance',
|
||||
'--no-remote',
|
||||
'--profile',
|
||||
'/tmp/wxt-runner-pWXLO1',
|
||||
'--remote-debugging-port=0',
|
||||
'about:debugging#/runtime/this-firefox'
|
||||
],
|
||||
firefoxRemoteDebuggingPort: 0,
|
||||
target: 'chrome'
|
||||
}
|
||||
@wxt-dev/runner:chrome:stderr DevTools listening on ws://127.0.0.1:38397/devtools/browser/93dc4de5-64cb-4e0b-a9d3-7549527015f0
|
||||
@wxt-dev/runner:cdp Sending command: {
|
||||
id: 1,
|
||||
method: 'Extensions.loadUnpacked',
|
||||
params: {
|
||||
path: '/home/aklinker1/Development/github.com/wxt-dev/wxt/packages/runner/demo-extension'
|
||||
}
|
||||
}
|
||||
@wxt-dev/runner:cdp Received response: { id: 1, result: { id: 'hckhakegfgenefhikdcfkaaonnclljmf' } }
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Implementation Details
|
||||
|
||||
All this package does is spawn a child process to open the browser with some default flags before using remote protocols to install the extension.
|
||||
|
||||
### Firefox
|
||||
|
||||
We use the new [WebDriver BiDi protocol](https://www.w3.org/TR/webdriver-bidi) to install the extension. This just involves connecting to a web socket and sending a few messages.
|
||||
|
||||
### Chrome
|
||||
|
||||
We use the [CDP](https://chromedevtools.github.io/devtools-protocol/) with `--remote-debugging-pipe` and `--enable-unsafe-extension-debugging` to install the extension by sending a message via IO pipes 3 and 4.
|
||||
|
||||
We don't use Webdriver Bidi because it's not built into Chrome yet. It requires us instantiating a separate child process for `chromedriver`. This is slower and more difficult than just using the CDP built into Chrome.
|
||||
@@ -0,0 +1 @@
|
||||
console.log('Hello background!');
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "Test",
|
||||
"version": "1.0.0",
|
||||
"manifest_version": 3,
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"scripts": ["background.js"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// USAGE:
|
||||
// pnpm dev
|
||||
// pnpm dev firefox-nightly
|
||||
// pnpm dev <target>
|
||||
//
|
||||
|
||||
import { run } from './src';
|
||||
|
||||
// Uncomment to enable debug logs
|
||||
process.env.DEBUG = '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
extensionDir: 'demo-extension',
|
||||
target: process.argv[2],
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@wxt-dev/runner",
|
||||
"description": "Launch Chrome and Firefox with a web extension installed",
|
||||
"version": "0.1.1",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/runner"
|
||||
},
|
||||
"homepage": "https://github.com/wxt-dev/wxt/tree/main/packages/runner#readme",
|
||||
"keywords": [
|
||||
"web-extension",
|
||||
"chrome-extension",
|
||||
"wxt"
|
||||
],
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"funding": "https://github.com/sponsors/wxt-dev",
|
||||
"scripts": {
|
||||
"check": "pnpm build && check",
|
||||
"test": "buildc --deps-only -- vitest",
|
||||
"dev": "tsx --trace-warnings dev.ts",
|
||||
"build": "buildc -- unbuild",
|
||||
"prepublishOnly": "pnpm build"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"oxlint": "^0.16.8",
|
||||
"publint": "^0.3.12",
|
||||
"tsx": "4.19.4",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"vitest": "^3.1.2"
|
||||
},
|
||||
"main": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { createCdpConnection, type CDPConnection } from '../cdp';
|
||||
import { createBidiConnection, type BidiConnection } from '../bidi';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { installChromium, installFirefox } from '../install';
|
||||
|
||||
vi.mock('../cdp');
|
||||
const createCdpConnectionMock = vi.mocked(createCdpConnection);
|
||||
|
||||
vi.mock('../bidi');
|
||||
const createBidiConnectionMock = vi.mocked(createBidiConnection);
|
||||
|
||||
describe('Install', () => {
|
||||
describe('Chromium', () => {
|
||||
it('Should send the install command to the process', async () => {
|
||||
const browserProcess = mock<ChildProcess>();
|
||||
const connection = mock<CDPConnection>({
|
||||
[Symbol.dispose]: vi.fn(),
|
||||
});
|
||||
const extensionDir = '/path/to/extension';
|
||||
const expectedExtensionId = 'chromium-extension-id';
|
||||
|
||||
createCdpConnectionMock.mockReturnValue(connection);
|
||||
connection.send.mockImplementation(async (method) => {
|
||||
if (method === 'Extensions.loadUnpacked')
|
||||
return { id: expectedExtensionId };
|
||||
throw Error('Unknown method');
|
||||
});
|
||||
|
||||
const res = await installChromium(browserProcess, extensionDir);
|
||||
|
||||
expect(createCdpConnectionMock).toBeCalledTimes(1);
|
||||
expect(createCdpConnectionMock).toBeCalledWith(browserProcess);
|
||||
|
||||
expect(connection.send).toBeCalledTimes(1);
|
||||
expect(connection.send).toBeCalledWith('Extensions.loadUnpacked', {
|
||||
path: extensionDir,
|
||||
});
|
||||
|
||||
expect(res).toEqual({ id: expectedExtensionId });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Firefox', () => {
|
||||
it('Should connect to the server, start a session, then install the extension', async () => {
|
||||
const debuggerUrl = 'http://127.0.0.1:9222';
|
||||
const extensionDir = '/path/to/extension';
|
||||
const expectedExtensionId = 'firefox-extension-id';
|
||||
const connection = mock<BidiConnection>({
|
||||
[Symbol.dispose]: vi.fn(),
|
||||
});
|
||||
|
||||
createBidiConnectionMock.mockResolvedValue(connection);
|
||||
connection.send.mockImplementation(async (method) => {
|
||||
if (method === 'session.new') return { sessionId: 'session-id' };
|
||||
if (method === 'webExtension.install')
|
||||
return { extension: expectedExtensionId };
|
||||
});
|
||||
|
||||
const res = await installFirefox(debuggerUrl, extensionDir);
|
||||
|
||||
expect(createBidiConnectionMock).toBeCalledTimes(1);
|
||||
expect(createBidiConnectionMock).toBeCalledWith(debuggerUrl);
|
||||
|
||||
expect(connection.send).toBeCalledTimes(2);
|
||||
expect(connection.send).toBeCalledWith('session.new', {
|
||||
capabilities: {},
|
||||
});
|
||||
expect(connection.send).toBeCalledWith('webExtension.install', {
|
||||
extensionData: {
|
||||
type: 'path',
|
||||
path: extensionDir,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res).toEqual({ extension: expectedExtensionId });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,306 @@
|
||||
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
||||
import { ResolvedRunOptions, resolveRunOptions } from '../options';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { tmpdir, homedir } from 'node:os';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
|
||||
vi.mock('node:os', async () => {
|
||||
const { vi } = await import('vitest');
|
||||
const os: any = await vi.importActual('node:os');
|
||||
const { join } = await import('node:path');
|
||||
return {
|
||||
...os,
|
||||
tmpdir: () => join(os.tmpdir(), 'tmpdir-mock'),
|
||||
homedir: () => join(os.tmpdir(), 'homedir-mock'),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Options', () => {
|
||||
beforeAll(async () => {
|
||||
// Make sure mock test directories exist
|
||||
await mkdir(tmpdir(), { recursive: true });
|
||||
await mkdir(homedir(), { recursive: true });
|
||||
});
|
||||
|
||||
describe('extensionDir', () => {
|
||||
it('should default to the current working directory', async () => {
|
||||
const actual = await resolveRunOptions({});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
extensionDir: process.cwd(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve relative to the current working directory', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
extensionDir: './path/to/extension',
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
extensionDir: resolve(process.cwd(), './path/to/extension'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('target', () => {
|
||||
it('should be "chrome" by default', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
extensionDir: 'path/to/extension',
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
target: 'chrome',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be what is passed in', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
extensionDir: 'path/to/extension',
|
||||
target: 'custom',
|
||||
browserBinaries: {
|
||||
custom: '/path/to/custom/browser',
|
||||
},
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
target: 'custom',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if the target binary could not be found', async () => {
|
||||
const actual = resolveRunOptions({
|
||||
extensionDir: 'path/to/extension',
|
||||
target: 'custom',
|
||||
});
|
||||
await expect(actual).rejects.toThrow('Could not find "custom" binary.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('browserBinary', () => {
|
||||
it('should denormalize the browserBinary', async () => {
|
||||
const path =
|
||||
process.platform === 'win32'
|
||||
? 'C:/path/to/custom/browser.exe'
|
||||
: '/path/to/custom/browser';
|
||||
const expectedPath =
|
||||
process.platform === 'win32'
|
||||
? 'C:\\path\\to\\custom\\browser.exe'
|
||||
: path;
|
||||
const actual = await resolveRunOptions({
|
||||
extensionDir: 'path/to/extension',
|
||||
target: 'custom',
|
||||
browserBinaries: {
|
||||
custom: path,
|
||||
},
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
browserBinary: expectedPath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('chromiumArgs', () => {
|
||||
it('should log a warning when --user-data-dir is passed in', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn');
|
||||
await resolveRunOptions({
|
||||
chromiumArgs: ['--user-data-dir=some/custom/path'],
|
||||
});
|
||||
expect(warnSpy).toBeCalledTimes(1);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Custom Chromium --user-data-dir argument ignored',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should log a warning when --remote-debugging-port is passed in', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn');
|
||||
await resolveRunOptions({
|
||||
chromiumArgs: ['--remote-debugging-port=9222'],
|
||||
});
|
||||
expect(warnSpy).toBeCalledTimes(1);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Custom Chromium --remote-debugging-port argument ignored',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should combine default args with user provided ones', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
chromiumArgs: ['--window-size=1920,1080'],
|
||||
});
|
||||
expect(actual.chromiumArgs).toEqual([
|
||||
// Defaults
|
||||
'--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculateNativeWinOcclusion,InterestFeedContentSuggestions,CertificateTransparencyComponentUpdater,AutofillServerCommunication,PrivacySandboxSettings4',
|
||||
'--disable-component-extensions-with-background-pages',
|
||||
'--disable-background-networking',
|
||||
'--disable-component-update',
|
||||
'--disable-client-side-phishing-detection',
|
||||
'--disable-sync',
|
||||
'--metrics-recording-only',
|
||||
'--disable-default-apps',
|
||||
'--no-default-browser-check',
|
||||
'--no-first-run',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-ipc-flooding-protection',
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
'--force-fieldtrials=*BackgroundTracing/default/',
|
||||
'--disable-hang-monitor',
|
||||
'--disable-prompt-on-repost',
|
||||
'--disable-domain-reliability',
|
||||
'--propagate-iph-for-testing',
|
||||
// Debugging
|
||||
expect.stringContaining('--remote-debugging-port='),
|
||||
'--remote-debugging-pipe',
|
||||
expect.stringContaining('--user-data-dir='), // See dataPersistence tests
|
||||
'--enable-unsafe-extension-debugging',
|
||||
// User provided
|
||||
'--window-size=1920,1080',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('firefoxArgs', () => {
|
||||
it('should log a warning when --profile is passed in', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn');
|
||||
await resolveRunOptions({
|
||||
firefoxArgs: ['--profile=some/custom/path'],
|
||||
});
|
||||
expect(warnSpy).toBeCalledTimes(1);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Custom Firefox --profile argument ignored'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should log a warning when --remote-debugging-port is passed in', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn');
|
||||
await resolveRunOptions({
|
||||
firefoxArgs: ['--remote-debugging-port=9222'],
|
||||
});
|
||||
expect(warnSpy).toBeCalledTimes(1);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Custom Firefox --remote-debugging-port argument ignored',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should combine default args with user provided ones', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
firefoxArgs: ['--window-size=1920,1080'],
|
||||
});
|
||||
expect(actual.firefoxArgs).toEqual([
|
||||
// Defaults
|
||||
'--new-instance',
|
||||
'--no-remote',
|
||||
'--profile',
|
||||
expect.any(String), // See dataPersistence tests
|
||||
expect.stringContaining('--remote-debugging-port='),
|
||||
'about:debugging#/runtime/this-firefox',
|
||||
// User provided
|
||||
'--window-size=1920,1080',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chromiumRemoteDebuggingPort', () => {
|
||||
it('should default to 0', async () => {
|
||||
const actual = await resolveRunOptions({});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
chromiumRemoteDebuggingPort: 0,
|
||||
chromiumArgs: expect.arrayContaining([`--remote-debugging-port=0`]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect user provided port', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
chromiumRemoteDebuggingPort: 9222,
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
chromiumRemoteDebuggingPort: 9222,
|
||||
chromiumArgs: expect.arrayContaining([`--remote-debugging-port=9222`]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('firefoxRemoteDebuggingPort', () => {
|
||||
it('should default to 0', async () => {
|
||||
const actual = await resolveRunOptions({});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
firefoxRemoteDebuggingPort: 0,
|
||||
firefoxArgs: expect.arrayContaining([`--remote-debugging-port=0`]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect user provided port', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
firefoxRemoteDebuggingPort: 9222,
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
firefoxRemoteDebuggingPort: 9222,
|
||||
firefoxArgs: expect.arrayContaining([`--remote-debugging-port=9222`]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dataPersistence', () => {
|
||||
it('should default to "none"', async () => {
|
||||
const actual = await resolveRunOptions({});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
dataPersistence: 'none',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use a temporary directory when set to "none"', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
dataPersistence: 'none',
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
dataPersistence: 'none',
|
||||
dataDir: expect.stringContaining(join(tmpdir(), 'wxt-runner-')),
|
||||
chromiumArgs: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
`--user-data-dir=${join(tmpdir(), 'wxt-runner-')}`,
|
||||
),
|
||||
]),
|
||||
firefoxArgs: expect.arrayContaining([
|
||||
expect.stringContaining(join(tmpdir(), 'wxt-runner-')),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should use a directory in the current working directory when set to "project"', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
dataPersistence: 'project',
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
dataPersistence: 'project',
|
||||
dataDir: expect.stringContaining(join(process.cwd(), '.wxt-runner')),
|
||||
chromiumArgs: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
`--user-data-dir=${join(process.cwd(), '.wxt-runner')}`,
|
||||
),
|
||||
]),
|
||||
firefoxArgs: expect.arrayContaining([
|
||||
expect.stringContaining(join(process.cwd(), '.wxt-runner')),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should use a directory in the user\'s home directory when set to "user"', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
dataPersistence: 'user',
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
dataPersistence: 'user',
|
||||
dataDir: expect.stringContaining(join(homedir(), '.wxt-runner')),
|
||||
chromiumArgs: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
`--user-data-dir=${join(homedir(), '.wxt-runner')}`,
|
||||
),
|
||||
]),
|
||||
firefoxArgs: expect.arrayContaining([
|
||||
expect.stringContaining(join(homedir(), '.wxt-runner')),
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { openWebSocket } from './web-socket';
|
||||
import { debug } from './debug';
|
||||
|
||||
const debugBidi = debug.scoped('bidi');
|
||||
|
||||
export interface BidiConnection extends Disposable {
|
||||
send<T>(method: string, params: any, timeout?: number): Promise<T>;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export async function createBidiConnection(
|
||||
baseUrl: string,
|
||||
): Promise<BidiConnection> {
|
||||
const url = new URL('/session', baseUrl);
|
||||
debugBidi('Connecting to BiDi server @', url.href);
|
||||
|
||||
const webSocket = await openWebSocket(url.href);
|
||||
debugBidi('Connected');
|
||||
|
||||
let requestId = 0;
|
||||
|
||||
return {
|
||||
send(method, params, timeout = 10e3) {
|
||||
const id = ++requestId;
|
||||
const command = { id, method, params };
|
||||
debugBidi('Sending command:', command);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
webSocket.removeEventListener('message', onMessage);
|
||||
webSocket.removeEventListener('error', onError);
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
cleanup();
|
||||
reject(
|
||||
new Error(
|
||||
`Timed out after ${timeout}ms waiting for ${method} response`,
|
||||
),
|
||||
);
|
||||
}, timeout);
|
||||
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.id === id) {
|
||||
debugBidi('Received response:', data);
|
||||
cleanup();
|
||||
if (data.type === 'success') resolve(data.result);
|
||||
else reject(Error(data.message, { cause: data }));
|
||||
}
|
||||
};
|
||||
const onError = (error: any) => {
|
||||
cleanup();
|
||||
reject(new Error('Error sending request', { cause: error }));
|
||||
};
|
||||
|
||||
webSocket.addEventListener('message', onMessage);
|
||||
webSocket.addEventListener('error', onError);
|
||||
|
||||
webSocket.send(JSON.stringify(command));
|
||||
});
|
||||
},
|
||||
|
||||
close() {
|
||||
debugBidi('Closing connection...');
|
||||
webSocket.close();
|
||||
debugBidi('Closed connection');
|
||||
},
|
||||
[Symbol.dispose]() {
|
||||
debugBidi('Disposing connection...');
|
||||
webSocket.close();
|
||||
debugBidi('Disposed connection');
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
export type BrowserPlatform = 'windows' | 'mac' | 'linux';
|
||||
|
||||
export type KnownTarget =
|
||||
| 'chromium'
|
||||
| 'chrome'
|
||||
| 'chrome-beta'
|
||||
| 'chrome-dev'
|
||||
| 'chrome-canary'
|
||||
| 'edge'
|
||||
| 'edge-beta'
|
||||
| 'edge-dev'
|
||||
| 'edge-canary'
|
||||
| 'firefox'
|
||||
| 'firefox-nightly'
|
||||
| 'firefox-developer-edition'
|
||||
| 'zen';
|
||||
|
||||
export const KNOWN_BROWSER_PATHS: Record<
|
||||
KnownTarget,
|
||||
Record<BrowserPlatform, string[]>
|
||||
> = {
|
||||
// Chromium based targets
|
||||
|
||||
chromium: {
|
||||
mac: [],
|
||||
linux: [
|
||||
// Arch
|
||||
'/usr/bin/chromium',
|
||||
],
|
||||
windows: [],
|
||||
},
|
||||
chrome: {
|
||||
mac: ['/Applications/Chrome.app/Contents/MacOS/Google Chrome'],
|
||||
linux: [],
|
||||
windows: ['C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'],
|
||||
},
|
||||
'chrome-beta': {
|
||||
mac: [],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
'chrome-canary': {
|
||||
mac: [
|
||||
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
||||
],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
'chrome-dev': {
|
||||
mac: [],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
edge: {
|
||||
mac: [],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
'edge-beta': {
|
||||
mac: [],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
'edge-canary': {
|
||||
mac: [],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
'edge-dev': {
|
||||
mac: [],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
|
||||
// Firefox based targets
|
||||
|
||||
firefox: {
|
||||
mac: ['/Applications/Firefox.app/Contents/MacOS/firefox'],
|
||||
linux: [
|
||||
// Arch
|
||||
'/usr/bin/firefox',
|
||||
],
|
||||
windows: [],
|
||||
},
|
||||
'firefox-nightly': {
|
||||
mac: ['/Applications/Firefox Nightly.app/Contents/MacOS/firefox'],
|
||||
linux: [],
|
||||
windows: ['C:\\Program Files\\Firefox Nightly\\firefox.exe'],
|
||||
},
|
||||
'firefox-developer-edition': {
|
||||
mac: ['/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox'],
|
||||
linux: [
|
||||
// Arch
|
||||
'/usr/bin/firefox-developer-edition',
|
||||
],
|
||||
windows: [],
|
||||
},
|
||||
zen: {
|
||||
mac: ['/Applications/Zen Browser.app/Contents/MacOS/zen'],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* When targeting a browser, this map contains the other targets to fall back on when a binary could not be found for the primary target.
|
||||
*/
|
||||
export const FALLBACK_TARGETS: Partial<Record<KnownTarget, KnownTarget[]>> = {
|
||||
chrome: [
|
||||
'chromium',
|
||||
'chrome-canary',
|
||||
'chrome-beta',
|
||||
'chrome-dev',
|
||||
'edge',
|
||||
'edge-canary',
|
||||
'edge-beta',
|
||||
'edge-dev',
|
||||
],
|
||||
firefox: ['firefox-developer-edition', 'firefox-nightly', 'zen'],
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import type { Readable, Writable } from 'node:stream';
|
||||
import { debug } from './debug';
|
||||
|
||||
const debugCdp = debug.scoped('cdp');
|
||||
|
||||
export interface CDPConnection extends Disposable {
|
||||
send<T>(method: string, params: any, timeout?: number): Promise<T>;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export function createCdpConnection(
|
||||
browserProcess: ChildProcess,
|
||||
): CDPConnection {
|
||||
const inputStream = browserProcess.stdio[3] as Writable;
|
||||
const outputStream = browserProcess.stdio[4] as Readable;
|
||||
|
||||
let requestId = 0;
|
||||
|
||||
return {
|
||||
send(method, params, timeout = 10e3) {
|
||||
const id = ++requestId;
|
||||
const command = { id, method, params };
|
||||
debugCdp('Sending command:', command);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`CDP command timed out: ${method}`));
|
||||
}, timeout);
|
||||
|
||||
const onData = (data: Buffer) => {
|
||||
// Trim the trailing null character
|
||||
const text = data.toString().slice(0, -1);
|
||||
const res = JSON.parse(text);
|
||||
|
||||
if (res.id !== id) return;
|
||||
|
||||
debugCdp('Received response:', res);
|
||||
clearTimeout(timer);
|
||||
outputStream.removeListener('data', onData);
|
||||
|
||||
if ('error' in res) {
|
||||
reject(new Error(res.error.message, { cause: res.error }));
|
||||
} else {
|
||||
resolve(res.result);
|
||||
}
|
||||
};
|
||||
|
||||
outputStream.addListener('data', onData);
|
||||
|
||||
inputStream.write(JSON.stringify(command) + '\0');
|
||||
});
|
||||
},
|
||||
close() {},
|
||||
[Symbol.dispose]() {},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface Debug {
|
||||
(...args: any[]): void;
|
||||
scoped: (scope: string) => Debug;
|
||||
}
|
||||
|
||||
function createDebug(scopes: string[]): Debug {
|
||||
const debug = (...args: any[]) => {
|
||||
const scope = scopes.join(':');
|
||||
if (
|
||||
process.env.DEBUG === '1' ||
|
||||
process.env.DEBUG === 'true' ||
|
||||
scope.startsWith(process.env.DEBUG ?? '@NOT')
|
||||
) {
|
||||
const params = scope ? [`\x1b[31m${scope}\x1b[0m`, ...args] : args;
|
||||
console.log(...params);
|
||||
}
|
||||
};
|
||||
|
||||
debug.scoped = (scope: string) => createDebug([...scopes, scope]);
|
||||
|
||||
return debug;
|
||||
}
|
||||
|
||||
export const debug = createDebug(['@wxt-dev/runner']);
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './run';
|
||||
export * from './options';
|
||||
export * from './install';
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { createBidiConnection } from './bidi';
|
||||
import { createCdpConnection } from './cdp';
|
||||
|
||||
/**
|
||||
* Install an extension to an already running instance of Firefox.
|
||||
* @param debuggerUrl The URL of the Firefox BiDi server (ex: `ws://127.0.0.1:45912`).
|
||||
* @param extensionDir Absolute path to the directory containing the extension to be installed.
|
||||
*/
|
||||
export async function installFirefox(
|
||||
debuggerUrl: string,
|
||||
extensionDir: string,
|
||||
): Promise<BidiWebExtensionInstallResponse> {
|
||||
using bidi = await createBidiConnection(debuggerUrl);
|
||||
|
||||
// Start a session
|
||||
await bidi.send<unknown>('session.new', { capabilities: {} });
|
||||
|
||||
// Install the extension
|
||||
return await bidi.send<BidiWebExtensionInstallResponse>(
|
||||
'webExtension.install',
|
||||
{
|
||||
extensionData: {
|
||||
type: 'path',
|
||||
path: extensionDir,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export type BidiWebExtensionInstallResponse = {
|
||||
extension: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a child process of Chrome, install an extension. The process must be started with the following flags:
|
||||
*
|
||||
* - `--remote-debugging-pipe`
|
||||
* - `--user-data-dir=...`
|
||||
* - `--enable-unsafe-extension-debugging`
|
||||
*
|
||||
* Otherwise it the CDP doesn't have permission to install extensions.
|
||||
*/
|
||||
export async function installChromium(
|
||||
browserProcess: ChildProcess,
|
||||
extensionDir: string,
|
||||
): Promise<CdpExtensionsLoadUnpackedResponse> {
|
||||
using cdp = createCdpConnection(browserProcess);
|
||||
return await cdp.send<CdpExtensionsLoadUnpackedResponse>(
|
||||
'Extensions.loadUnpacked',
|
||||
{
|
||||
path: extensionDir,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export type CdpExtensionsLoadUnpackedResponse = {
|
||||
id: string;
|
||||
};
|
||||
@@ -0,0 +1,286 @@
|
||||
import {
|
||||
FALLBACK_TARGETS,
|
||||
KNOWN_BROWSER_PATHS,
|
||||
KnownTarget,
|
||||
type BrowserPlatform,
|
||||
} from './browser-paths';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { homedir, tmpdir } from 'node:os';
|
||||
import { debug } from './debug';
|
||||
import { mkdtemp, open } from 'node:fs/promises';
|
||||
|
||||
const debugOptions = debug.scoped('options');
|
||||
|
||||
export type UnknownTarget = string & {};
|
||||
export type Target = KnownTarget | UnknownTarget;
|
||||
|
||||
export type RunOptions = {
|
||||
/** Paths to binaries to use for each target. */
|
||||
browserBinaries?: Record<string, string>;
|
||||
/** Customize the arguments passed to the chromium binary. Conflicting arguments with required ones to install extensions are ignored. */
|
||||
chromiumArgs?: string[];
|
||||
/** Control how data is persisted between launches. Either save data at a user level, project level, or don't persist data at all. Defaults to `project`. */
|
||||
dataPersistence?: 'user' | 'project' | 'none';
|
||||
/** Customize where your profile's data is stored when using `dataPersistence: 'project'`. Can be absolute or relative to the current working directory. */
|
||||
projectDataDir?: string;
|
||||
/** Customize the port Chrome's debugger is listening on. Defaults to a random open port. */
|
||||
chromiumRemoteDebuggingPort?: number;
|
||||
/** Directory where the extension will be installed from. Should contain a `manifest.json` file. Can be relative to the current working directory. Defaults to the current working directory. */
|
||||
extensionDir?: string;
|
||||
/** Customize the arguments passed to the firefox binary. Conflicting arguments with required ones to install extensions are ignored. */
|
||||
firefoxArgs?: string[];
|
||||
/** Customize the port Firefox's debugger is listening on. Defaults to a random open port. */
|
||||
firefoxRemoteDebuggingPort?: number;
|
||||
/** Specify the browser to open. Defaults to `"chrome"`, but you can pass any string. */
|
||||
target?: Target;
|
||||
};
|
||||
|
||||
export type ResolvedRunOptions = {
|
||||
/** Absolute path to the browser binary. */
|
||||
browserBinary: string;
|
||||
chromiumArgs: string[];
|
||||
chromiumRemoteDebuggingPort: number;
|
||||
/** Absolute path to the directory where browser data will be stored. */
|
||||
dataDir: string;
|
||||
dataPersistence: 'user' | 'project' | 'none';
|
||||
/** Absolute path to the extension directory. */
|
||||
extensionDir: string;
|
||||
firefoxArgs: string[];
|
||||
firefoxRemoteDebuggingPort: number;
|
||||
target: string;
|
||||
};
|
||||
|
||||
export async function resolveRunOptions(
|
||||
options: RunOptions | undefined,
|
||||
): Promise<ResolvedRunOptions> {
|
||||
debugOptions('User options:', options);
|
||||
|
||||
const target = options?.target || 'chrome';
|
||||
|
||||
const _browserBinary =
|
||||
options?.browserBinaries?.[target] ?? (await findBrowserBinary(target));
|
||||
if (!_browserBinary)
|
||||
throw Error(
|
||||
`Could not find "${target}" binary.\n\nIf it is installed in a custom location, you can specify the path with the browserPaths option.`,
|
||||
);
|
||||
|
||||
// Denormalize the path so it uses the correct path separator for the OS
|
||||
const browserBinary = resolve(_browserBinary);
|
||||
|
||||
const chromiumRemoteDebuggingPort = options?.chromiumRemoteDebuggingPort ?? 0;
|
||||
const firefoxRemoteDebuggingPort = options?.firefoxRemoteDebuggingPort ?? 0;
|
||||
const dataPersistence = options?.dataPersistence ?? 'none';
|
||||
const dataDir =
|
||||
dataPersistence === 'user'
|
||||
? join(homedir(), '.wxt-runner', target)
|
||||
: dataPersistence === 'project'
|
||||
? options?.projectDataDir
|
||||
? resolve(options.projectDataDir)
|
||||
: resolve('.wxt-runner', target)
|
||||
: dataPersistence === 'none'
|
||||
? await mkdtemp(join(tmpdir(), 'wxt-runner-'))
|
||||
: resolve(dataPersistence);
|
||||
|
||||
const resolved: ResolvedRunOptions = {
|
||||
browserBinary,
|
||||
chromiumArgs: resolveChromiumArgs(
|
||||
options?.chromiumArgs,
|
||||
chromiumRemoteDebuggingPort,
|
||||
dataDir,
|
||||
),
|
||||
dataDir,
|
||||
dataPersistence,
|
||||
chromiumRemoteDebuggingPort,
|
||||
extensionDir: resolve(options?.extensionDir ?? '.'),
|
||||
firefoxArgs: resolveFirefoxArgs(
|
||||
options?.firefoxArgs,
|
||||
firefoxRemoteDebuggingPort,
|
||||
dataDir,
|
||||
),
|
||||
firefoxRemoteDebuggingPort,
|
||||
target,
|
||||
};
|
||||
debugOptions('Resolved options:', resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
async function findBrowserBinary(target: string): Promise<string | undefined> {
|
||||
const targets = new Set<KnownTarget>([target as KnownTarget]);
|
||||
FALLBACK_TARGETS[target as KnownTarget]?.forEach((fallback) =>
|
||||
targets.add(fallback),
|
||||
);
|
||||
const platform = getPlatform();
|
||||
|
||||
for (const target of targets) {
|
||||
const potentialPaths = KNOWN_BROWSER_PATHS[target]?.[platform] ?? [];
|
||||
for (const path of potentialPaths) {
|
||||
if (await exists(path)) return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPlatform(): BrowserPlatform {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return 'windows';
|
||||
case 'darwin':
|
||||
return 'mac';
|
||||
default:
|
||||
return 'linux';
|
||||
}
|
||||
}
|
||||
|
||||
function resolveChromiumArgs(
|
||||
userArgs: string[] | undefined,
|
||||
chromiumRemoteDebuggingPort: ResolvedRunOptions['chromiumRemoteDebuggingPort'],
|
||||
dataDir: string,
|
||||
): string[] {
|
||||
return deduplicateArgs(
|
||||
[
|
||||
// Limit features to improve performance
|
||||
...CHROME_LAUNCHER_DEFAULT_FLAGS,
|
||||
// Enable debugging
|
||||
`--remote-debugging-port=${chromiumRemoteDebuggingPort}`,
|
||||
// Required for installing extensions
|
||||
`--remote-debugging-pipe`,
|
||||
`--user-data-dir=${dataDir}`,
|
||||
`--enable-unsafe-extension-debugging`,
|
||||
],
|
||||
userArgs,
|
||||
{
|
||||
'--remote-debugging-port':
|
||||
'\x1b[1m\x1b[33mCustom Chromium --remote-debugging-port argument ignored.\x1b[0m Use \x1b[36mchromiumRemoteDebuggingPort\x1b[0m option instead.',
|
||||
'--user-data-dir':
|
||||
'\x1b[1m\x1b[33mCustom Chromium --user-data-dir argument ignored.\x1b[0m Use \x1b[36mdataPersistence\x1b[0m option instead.',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function resolveFirefoxArgs(
|
||||
userArgs: string[] | undefined,
|
||||
firefoxRemoteDebuggingPort: ResolvedRunOptions['firefoxRemoteDebuggingPort'],
|
||||
dataDir: string,
|
||||
): string[] {
|
||||
return deduplicateArgs(
|
||||
[
|
||||
// Allows opening multiple instances of Firefox at the same time
|
||||
`--new-instance`,
|
||||
`--no-remote`,
|
||||
`--profile`,
|
||||
dataDir,
|
||||
// Required for installing extensions
|
||||
`--remote-debugging-port=${firefoxRemoteDebuggingPort}`,
|
||||
// Default URL to start with
|
||||
`about:debugging#/runtime/this-firefox`,
|
||||
],
|
||||
userArgs,
|
||||
{
|
||||
'--remote-debugging-port':
|
||||
'\x1b[1m\x1b[33mCustom Firefox --remote-debugging-port argument ignored.\x1b[0m Use \x1b[36mfirefoxDebuggerPort\x1b[0m option instead.',
|
||||
'--profile':
|
||||
'\x1b[1m\x1b[33mCustom Firefox --profile argument ignored.\x1b[0m Use \x1b[36mdataPersistence\x1b[0m option instead.',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function deduplicateArgs(
|
||||
requiredArgs: string[],
|
||||
userArgs: string[] | undefined,
|
||||
warnings: Record<string, string>,
|
||||
): string[] {
|
||||
const getKey = (arg: string) => {
|
||||
return arg.startsWith('--') ? arg.split('=')[0] : arg;
|
||||
};
|
||||
const alreadyAdded = new Set<string>(requiredArgs.map(getKey));
|
||||
|
||||
const args = [...requiredArgs];
|
||||
userArgs?.forEach((arg) => {
|
||||
const key = getKey(arg);
|
||||
if (alreadyAdded.has(key)) {
|
||||
if (warnings[key]) console.warn(`[@wxt-dev/runner] ${warnings[key]}`);
|
||||
} else {
|
||||
alreadyAdded.add(key);
|
||||
args.push(arg);
|
||||
}
|
||||
});
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
async function exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await open(path, 'r');
|
||||
return true;
|
||||
} catch (err) {
|
||||
// @ts-expect-error: Unknown error type
|
||||
if (err?.code === 'ENOENT') return false;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from https://github.com/GoogleChrome/chrome-launcher/blob/main/src/flags.ts
|
||||
* with some flags commented out. Run tests after updating to compare.
|
||||
*/
|
||||
const CHROME_LAUNCHER_DEFAULT_FLAGS = [
|
||||
'--disable-features=' +
|
||||
[
|
||||
// Disable built-in Google Translate service
|
||||
'Translate',
|
||||
// Disable the Chrome Optimization Guide background networking
|
||||
'OptimizationHints',
|
||||
// Disable the Chrome Media Router (cast target discovery) background networking
|
||||
'MediaRouter',
|
||||
/// Avoid the startup dialog for _Do you want the application “Chromium.app” to accept incoming network connections?_. This is a sub-component of the MediaRouter.
|
||||
'DialMediaRouteProvider',
|
||||
// Disable the feature of: Calculate window occlusion on Windows will be used in the future to throttle and potentially unload foreground tabs in occluded windows.
|
||||
'CalculateNativeWinOcclusion',
|
||||
// Disables the Discover feed on NTP
|
||||
'InterestFeedContentSuggestions',
|
||||
// Don't update the CT lists
|
||||
'CertificateTransparencyComponentUpdater',
|
||||
// Disables autofill server communication. This feature isn't disabled via other 'parent' flags.
|
||||
'AutofillServerCommunication',
|
||||
// Disables "Enhanced ad privacy in Chrome" dialog (though as of 2024-03-20 it shouldn't show up if the profile has no stored country).
|
||||
'PrivacySandboxSettings4',
|
||||
].join(','),
|
||||
|
||||
// Disable some extensions that aren't affected by --disable-extensions
|
||||
'--disable-component-extensions-with-background-pages',
|
||||
// Disable various background network services, including extension updating,
|
||||
// safe browsing service, upgrade detector, translate, UMA
|
||||
'--disable-background-networking',
|
||||
// Don't update the browser 'components' listed at chrome://components/
|
||||
'--disable-component-update',
|
||||
// Disables client-side phishing detection.
|
||||
'--disable-client-side-phishing-detection',
|
||||
// Disable syncing to a Google account
|
||||
'--disable-sync',
|
||||
// Disable reporting to UMA, but allows for collection
|
||||
'--metrics-recording-only',
|
||||
// Disable installation of default apps on first run
|
||||
'--disable-default-apps',
|
||||
// Disable the default browser check, do not prompt to set it as such
|
||||
'--no-default-browser-check',
|
||||
// Skip first run wizards
|
||||
'--no-first-run',
|
||||
// Disable task throttling of timer tasks from background pages.
|
||||
'--disable-background-timer-throttling',
|
||||
// Disable the default throttling of IPC between renderer & browser processes.
|
||||
'--disable-ipc-flooding-protection',
|
||||
// Avoid potential instability of using Gnome Keyring or KDE wallet. crbug.com/571003 crbug.com/991424
|
||||
'--password-store=basic',
|
||||
// Use mock keychain on Mac to prevent blocking permissions dialogs
|
||||
'--use-mock-keychain',
|
||||
// Disable background tracing (aka slow reports & deep reports) to avoid 'Tracing already started'
|
||||
'--force-fieldtrials=*BackgroundTracing/default/',
|
||||
|
||||
// Suppresses hang monitor dialogs in renderer processes. This flag may allow slow unload handlers on a page to prevent the tab from closing.
|
||||
'--disable-hang-monitor',
|
||||
// Reloading a page that came from a POST normally prompts the user.
|
||||
'--disable-prompt-on-repost',
|
||||
// Disables Domain Reliability Monitoring, which tracks whether the browser has difficulty contacting Google-owned sites and uploads reports to Google.
|
||||
'--disable-domain-reliability',
|
||||
// Disable the in-product Help (IPH) system.
|
||||
'--propagate-iph-for-testing',
|
||||
];
|
||||
@@ -0,0 +1,19 @@
|
||||
export function promiseWithResolvers<T>(): {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T) => void;
|
||||
reject: (error: unknown) => void;
|
||||
} {
|
||||
let resolve: (value: T) => void;
|
||||
let reject: (error: unknown) => void;
|
||||
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
return {
|
||||
promise,
|
||||
resolve: resolve!,
|
||||
reject: reject!,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { debug } from './debug';
|
||||
import {
|
||||
resolveRunOptions,
|
||||
type ResolvedRunOptions,
|
||||
type RunOptions,
|
||||
} from './options';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { installChromium, installFirefox } from './install';
|
||||
import { promiseWithResolvers } from './promises';
|
||||
|
||||
const debugFirefox = debug.scoped('firefox');
|
||||
const debugChrome = debug.scoped('chrome');
|
||||
|
||||
export interface Runner {
|
||||
stop(): void;
|
||||
}
|
||||
|
||||
export async function run(options: RunOptions): Promise<Runner> {
|
||||
const resolvedOptions = await resolveRunOptions(options);
|
||||
|
||||
if (
|
||||
resolvedOptions.target.includes('firefox') ||
|
||||
resolvedOptions.target.includes('zen')
|
||||
) {
|
||||
return runFirefox(resolvedOptions);
|
||||
} else {
|
||||
return runChromium(resolvedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
async function runFirefox(options: ResolvedRunOptions): Promise<Runner> {
|
||||
const urlRes = promiseWithResolvers<string>();
|
||||
const urlTimeout = setTimeout(() => {
|
||||
urlRes.reject(Error('Timed out after 10s waiting for the browser to open'));
|
||||
}, 10e3);
|
||||
|
||||
// Firefox notifies the user if an instance is already running, so we don't add any logs for it.
|
||||
|
||||
const browserProcess = spawn(
|
||||
`"${options.browserBinary}"`,
|
||||
options.firefoxArgs,
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
},
|
||||
);
|
||||
const debugFirefoxStderr = debugFirefox.scoped('stderr');
|
||||
browserProcess.stderr.on('data', (data: string) => {
|
||||
const message = data.toString().trim();
|
||||
debugFirefoxStderr(message);
|
||||
|
||||
if (message.startsWith('WebDriver BiDi listening on ws://')) {
|
||||
clearTimeout(urlTimeout);
|
||||
urlRes.resolve(message.slice(28));
|
||||
}
|
||||
});
|
||||
const debugFirefoxStdout = debugFirefox.scoped('stdout');
|
||||
browserProcess.stdout.on('data', (data: string) => {
|
||||
const message = data.toString().trim();
|
||||
debugFirefoxStdout(message);
|
||||
});
|
||||
|
||||
const baseUrl = await urlRes.promise;
|
||||
await installFirefox(baseUrl, options.extensionDir);
|
||||
|
||||
return {
|
||||
stop() {
|
||||
browserProcess.kill('SIGINT');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function runChromium(options: ResolvedRunOptions): Promise<Runner> {
|
||||
const browserProcess = spawn(
|
||||
`"${options.browserBinary}"`,
|
||||
options.chromiumArgs,
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
},
|
||||
);
|
||||
|
||||
const opened = promiseWithResolvers<void>();
|
||||
const openedTimeout = setTimeout(() => {
|
||||
opened.reject(Error('Timed out after 10s waiting for browser to open.'));
|
||||
}, 10e3);
|
||||
|
||||
const debugChromeStderr = debugChrome.scoped('stderr');
|
||||
browserProcess.stderr!.on('data', (data: string) => {
|
||||
const message = data.toString().trim();
|
||||
debugChromeStderr(message);
|
||||
|
||||
// This message signifies Chrome started up correctly.
|
||||
if (message.startsWith('DevTools listening on')) {
|
||||
clearTimeout(openedTimeout);
|
||||
opened.resolve();
|
||||
}
|
||||
});
|
||||
const debugChromeStdout = debugChrome.scoped('stdout');
|
||||
browserProcess.stdout!.on('data', (data: string) => {
|
||||
const message = data.toString().trim();
|
||||
debugChromeStdout(message);
|
||||
|
||||
// This message signifies Chrome was already open, and thus we couldn't open the required new instance.
|
||||
if (message === 'Opening in existing browser session.') {
|
||||
clearTimeout(openedTimeout);
|
||||
opened.reject(
|
||||
Error(
|
||||
'An instance of the browser is already running. Close it and try again.',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the browser to open before proceeding.
|
||||
await opened.promise;
|
||||
|
||||
await installChromium(browserProcess, options.extensionDir);
|
||||
|
||||
return {
|
||||
stop() {
|
||||
browserProcess.kill('SIGINT');
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
export function openWebSocket(url: string): Promise<WebSocket> {
|
||||
if (typeof WebSocket === 'undefined') {
|
||||
throw new Error(
|
||||
'To open Firefox, your JS runtime must support the standard WebSocket API (NodeJS >=22.4.0, Bun, etc).',
|
||||
);
|
||||
}
|
||||
|
||||
return new Promise<WebSocket>((resolve, reject) => {
|
||||
const webSocket = new WebSocket(url);
|
||||
|
||||
const cleanup = () => {
|
||||
webSocket.removeEventListener('open', onOpen);
|
||||
webSocket.removeEventListener('error', onError);
|
||||
webSocket.removeEventListener('close', onClose);
|
||||
};
|
||||
const onOpen = async () => {
|
||||
cleanup();
|
||||
resolve(webSocket);
|
||||
};
|
||||
const onClose = (event: CloseEvent) => {
|
||||
cleanup();
|
||||
reject(
|
||||
new Error(
|
||||
`Connection closed: code=${event.code}, reason=${event.reason}`,
|
||||
),
|
||||
);
|
||||
};
|
||||
const onError = (error: any) => {
|
||||
cleanup();
|
||||
reject(new Error('Error connecting to WebSocket', { cause: error }));
|
||||
};
|
||||
|
||||
webSocket.addEventListener('open', onOpen);
|
||||
webSocket.addEventListener('error', onError);
|
||||
webSocket.addEventListener('close', onClose);
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user