Compare commits

...

28 Commits

Author SHA1 Message Date
GitHub Actions 330e39159a chore(release): @wxt-dev/module-svelte v2.0.0
vhs / vhs (push) Waiting to run
2024-11-13 14:04:41 +00:00
kongmoumou aa14676f91 fix: Throw when config file does not exist (#1156)
Co-authored-by: kongmoumou <kongmoumou@users.noreply.github.com>
2024-11-13 07:59:49 -06:00
Goran Nikic 199f330324 docs: Add "propbar.co.uk" to homepage (#1149)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-11-13 07:59:14 -06:00
jasonbla 63dee97ac3 docs: fix incorrect syntax for MatchPattern (#1148) 2024-11-13 07:56:58 -06:00
nirtamir2 b121ed2fe2 docs: Fix solid-js example (#1145) 2024-11-13 07:56:34 -06:00
Johnson Towoju 607e1162e5 docs: Add "YouTube Simple View" to homepage (#1142)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-11-13 07:55:41 -06:00
dsrrlldr b4d569a915 docs: Add "Keyword Rank Checker" to homepage (#1141) 2024-11-13 07:52:30 -06:00
Aaron 4fac364542 feat!: Svelte 5 support (#1104) 2024-11-13 07:48:57 -06:00
Aaron 51c7ee5fa8 chore: Update FUNDING.yml 2024-11-11 17:19:20 -06:00
Aaron 9b780f2d00 docs: Add FAQ about component library styles in content scripts 2024-11-11 16:36:04 -06:00
Aaron f5aec7ec9b chore: Create FUNDING.yml 2024-11-11 15:29:36 -06:00
Florian Metz 5d991eda31 feat(storage): Support storage items in batch functions (#990)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-10-30 23:36:24 -05:00
Bread Grocery 0318a34046 docs: Fix typo in popup and options EntrypointOptions (#1121) 2024-10-29 08:05:13 -05:00
1natsu e7c66304d3 chore: add "@types/chrome" to templates (#1119) 2024-10-28 14:33:21 -05:00
Ayub Kokabi 4603ebb511 docs: Add missing closing bracket (#1112)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-10-28 14:33:10 -05:00
GitHub Actions ea570c12b7 chore(release): @wxt-dev/i18n v0.2.3
vhs / vhs (push) Waiting to run
2024-10-28 13:48:20 +00:00
Bread Grocery c17ce34a27 fix: Prevent app crashes from parse errors due to incomplete file saves (#1114) 2024-10-28 08:43:21 -05:00
Berkin Anik 0e4d3ad8ab docs: fix typo in introduction page (#1117) 2024-10-28 08:30:23 -05:00
Aaron 4304f71df2 docs: Add section about env vars in the manifest 2024-10-23 09:05:07 -05:00
Aaron bb5ea34396 docs: Cleanup typos and broken links 2024-10-23 09:04:43 -05:00
GitHub Actions c30adb409c chore(release): @wxt-dev/i18n v0.2.2
vhs / vhs (push) Waiting to run
2024-10-23 13:44:57 +00:00
Bread Grocery ab83031462 feat: Add support for configuring the locales directory (#1109) 2024-10-23 08:39:30 -05:00
imnaK cd7285c3ef docs: fix wrong anchor to "Environment Variables" (#1107) 2024-10-22 22:58:22 -05:00
Aaron bdb775c88d Update changelog 2024-10-22 09:58:51 -05:00
Aaron d20793d5e6 docs: Use full URLs in README so they work on the docs site 2024-10-22 09:50:57 -05:00
GitHub Actions ae1e276577 chore(release): wxt v0.19.13
vhs / vhs (push) Waiting to run
2024-10-22 13:45:03 +00:00
Craig Slusher c031c6e82e feat(env): Load env from .env.[browser] variants (#1078)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-10-22 08:36:24 -05:00
Malix d06f8128f5 enhance(docs): comparison to real footnotes (#1096)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-10-22 08:35:23 -05:00
40 changed files with 1239 additions and 244 deletions
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: [wxt-dev] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
@@ -50,6 +50,9 @@ const chromeExtensionIds = [
'fcphghnknhkimeagdglkljinmpbagone', // YouTube Auto HD + FPS
'lpomjgbicdemjkgmbnkjncgdebogkhlb', // MultiViewer Companion
'ggiafipgeeaaahnjamgpjcgkdpanhddg', // Sync Watch - Watch videos together on any site
'nmldnjcblcihmegipecakhmnieiofmgl', // Keyword Rank Checker
'gppllamhaciichleihemgilcpledblpn', // YouTube Simple View - Hide distractions & more
'pccbghdfdnnkkbcdcibchpbffdgednkf', // Propbar - Property Data Enhancer
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
+7
View File
@@ -8,6 +8,7 @@ import {
prepareTypedocSidebar,
} from './utils/menus';
import { meta, script } from './utils/head';
import footnote from 'markdown-it-footnote';
import { version as wxtVersion } from '../../packages/wxt/package.json';
import { version as i18nVersion } from '../../packages/i18n/package.json';
import { version as autoIconsVersion } from '../../packages/auto-icons/package.json';
@@ -47,6 +48,12 @@ export default defineConfig({
}),
],
markdown: {
config: (md) => {
md.use(footnote);
},
},
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
logo: {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 164 KiB

@@ -62,7 +62,7 @@ import { resolve } from 'node:path';
export default defineRunnerConfig({
// On Windows, the path must be absolute
chromiumArgs: [`--user-data-dir="${resolve(".wxt/chrome-data")}"`
chromiumArgs: [`--user-data-dir="${resolve('.wxt/chrome-data')}"`],
});
```
@@ -9,6 +9,10 @@ WXT supports [dotenv files the same way as Vite](https://vite.dev/guide/env-and-
.env.local
.env.[mode]
.env.[mode].local
.env.[browser]
.env.[browser].local
.env.[mode].[browser]
.env.[mode].[browser].local
```
And any environment variables listed inside them will be available at runtime:
@@ -52,3 +56,26 @@ Vite provides two other environment variables, but they aren't useful in WXT pro
- `import.meta.env.BASE_URL`: Use `browser.runtime.getURL` instead.
- `import.meta.env.SSR`: Always `false`.
:::
## Manifest
To use environment variables in the manifest, you need to use the function syntax:
```ts
export default defineConfig({
extensionApi: 'chrome',
modules: ['@wxt-dev/module-vue'],
manifest: { // [!code --]
oauth2: { // [!code --]
client_id: import.meta.env.WXT_APP_CLIENT_ID // [!code --]
} // [!code --]
} // [!code --]
manifest: () => ({ // [!code ++]
oauth2: { // [!code ++]
client_id: import.meta.env.WXT_APP_CLIENT_ID // [!code ++]
} // [!code ++]
}), // [!code ++]
});
```
WXT can't load your `.env` files until after the config file has been loaded. So by using the function syntax for `manifest`, it defers creating the object until after the `.env` files are loaded into the process.
+2 -1
View File
@@ -239,6 +239,7 @@ export default defineContentScript({
onMount: (container) => {
// Render your app to the UI container
const unmount = render(() => <div>...</div>, container);
return unmount;
},
onRemove: (unmount) => {
// Unmount the app when the UI is removed
@@ -602,7 +603,7 @@ export default defineContentScript({
matches: ['*://*.youtube.com/*'],
main(ctx) {
ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl }) => {
if (watchPattern.matches(newUrl)) mainWatch(ctx);
if (watchPattern.includes(newUrl)) mainWatch(ctx);
});
},
});
+1 -1
View File
@@ -35,7 +35,7 @@ Here's a brief summary of each of these files and directories:
- `hooks/`: Auto-imported by default, contains hooks for React and Solid
- `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/runtime#environment-variables)
- `.env`: Contains [Environment Variables](/guide/essentials/config/environment-variables)
- `.env.publish`: Contains Environment Variables for [publishing](/guide/essentials/publishing)
- `app.config.ts`: Contains [Runtime Config](/guide/essentials/config/runtime)
- `package.json`: The standard file used by your package manager
+1 -1
View File
@@ -5,7 +5,7 @@ WXT is a modern, open-source framework for building web extensions. Inspired by
- Provide an awesome [DX](https://about.gitlab.com/topics/devops/what-is-developer-experience/)
- Provide first-class support for all major browsers
Check out the [comparison](/guide/resources/compare) to see how WXT compares to other tools for building web extnesions.
Check out the [comparison](/guide/resources/compare) to see how WXT compares to other tools for building web extensions.
## Prerequisites
+55 -46
View File
@@ -8,50 +8,59 @@ Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework)
- 🟡 - Partial support
- ❌ - No support
| Features | WXT | Plasmo | CRXJS |
| ------------------------------------------------------- | :--------------: | :-------------: | :--------------: |
| Supports all browsers | ✅ | ✅ | 🟡 <sup>10</sup> |
| MV2 Support | ✅ | ✅ | 🟡 <sup>1</sup> |
| MV3 Support | ✅ | ✅ | 🟡 <sup>1</sup> |
| Create Extension ZIPs | ✅ | ✅ | ❌ |
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
| First-class TypeScript support | ✅ | ✅ | ✅ |
| Entrypoint discovery | ✅ <sup>2</sup> | ✅ <sup>2</sup> | ❌ |
| Inline entrypoint config | ✅ | ✅ | ❌ <sup>9</sup> |
| Auto-imports | ✅ | ❌ | ❌ |
| Reusable module system | ✅ | ❌ | ❌ |
| Supports all frontend frameworks | ✅ | 🟡 <sup>3</sup> | ✅ |
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 <sup>4</sup> | ✅ <sup>5</sup> | ❌ |
| Automated publishing | ✅ | ✅ | ❌ |
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
| Unlisted HTML Pages | ✅ | ✅ | ✅ |
| Unlisted Scripts | ✅ | ❌ | ❌ |
| ESM Content Scripts | ❌ <sup>12</sup> | ❌ | ✅ |
| <strong style="opacity: 50%">Dev Mode</strong> | | |
| `.env` Files | ✅ | ✅ | ✅ |
| Opens browser with extension installed | ✅ | ❌ | ❌ |
| HMR for UIs | ✅ | 🟡 <sup>6</sup> | ✅ |
| Reload HTML Files on Change | ✅ | 🟡 <sup>7</sup> | ✅ |
| Reload Content Scripts on Change | ✅ | 🟡 <sup>7</sup> | ✅ |
| Reload Background on Change | 🟡 <sup>7</sup> | 🟡 <sup>7</sup> | 🟡 <sup>7</sup> |
| Respects Content Script `run_at` | ✅ | ✅ | ❌ <sup>8</sup> |
| <strong style="opacity: 50%">Built-in Wrappers</strong> | | | |
| Storage | ✅ | ✅ | ❌ <sup>11</sup> |
| Messaging | ❌ <sup>11</sup> | ✅ | ❌ <sup>11</sup> |
| Content Script UI | ✅ | ✅ | ❌ <sup>11</sup> |
| I18n | ✅ | ❌ | ❌ |
| Features | WXT | Plasmo | CRXJS |
| ------------------------------------------------------- | :-----: | :-----: | :-----: |
| Supports all browsers | ✅ | ✅ | 🟡 [^j] |
| MV2 Support | ✅ | ✅ | 🟡 [^a] |
| MV3 Support | ✅ | ✅ | 🟡 [^a] |
| Create Extension ZIPs | ✅ | ✅ | ❌ |
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
| First-class TypeScript support | ✅ | ✅ | ✅ |
| Entrypoint discovery | ✅ [^b] | ✅ [^b] | ❌ |
| Inline entrypoint config | ✅ | ✅ | ❌ [^i] |
| Auto-imports | ✅ | ❌ | ❌ |
| Reusable module system | ✅ | ❌ | ❌ |
| Supports all frontend frameworks | ✅ | 🟡 [^c] | ✅ |
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 [^d] | ✅ [^e] | ❌ |
| Automated publishing | ✅ | ✅ | ❌ |
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
| Unlisted HTML Pages | ✅ | ✅ | ✅ |
| Unlisted Scripts | ✅ | ❌ | ❌ |
| ESM Content Scripts | ❌ [^l] | ❌ | ✅ |
| <strong style="opacity: 50%">Dev Mode</strong> | | | |
| `.env` Files | ✅ | ✅ | ✅ |
| Opens browser with extension installed | ✅ | ❌ | ❌ |
| HMR for UIs | ✅ | 🟡 [^f] | ✅ |
| Reload HTML Files on Change | ✅ | 🟡 [^g] | ✅ |
| Reload Content Scripts on Change | ✅ | 🟡 [^g] | ✅ |
| Reload Background on Change | 🟡 [^g] | 🟡 [^g] | 🟡 [^g] |
| Respects Content Script `run_at` | ✅ | ✅ | ❌ [^h] |
| <strong style="opacity: 50%">Built-in Wrappers</strong> | | | |
| Storage | ✅ | ✅ | ❌ [^k] |
| Messaging | ❌ [^k] | ✅ | ❌ [^k] |
| Content Script UI | ✅ | ✅ | ❌ [^k] |
| I18n | ✅ | ❌ | ❌ |
<small>
<sup>1</sup>: Either MV2 or MV3, not both.
<br/><sup>2</sup>: File based.
<br/><sup>3</sup>: Only React, Vue, and Svelte.
<br/><sup>4</sup>: <code>.html</code> <code>.ts</code> <code>.tsx</code>.
<br/><sup>5</sup>: <code>.html</code> <code>.ts</code> <code>.tsx</code>. <code>.vue</code> <code>.svelte</code>.
<br/><sup>6</sup>: React only.
<br/><sup>7</sup>: Reloads entire extension.
<br/><sup>8</sup>: ESM-style loaders run asynchronously.
<br/><sup>9</sup>: Entrypoint options all configured in <code>manifest.json</code>.
<br/><sup>10</sup>: As of <code>v2.0.0-beta.23</code>, but v2 stable hasn't been released yet.
<br/><sup>11</sup>: There is no built-in wrapper around this API. However, you can still access the standard APIs via <code>chrome</code>/<code>browser</code> globals or use any 3rd party NPM package.
<br/><sup>12</sup>: WIP, moving very slowly. Follow <a href="https://github.com/wxt-dev/wxt/issues/357" target="_blank"><code>wxt-dev/wxt#357</code></a> for updates.
</small>
[^a]: Either MV2 or MV3, not both.
[^b]: File based.
[^c]: Only React, Vue, and Svelte.
[^d]: `.html`, `.ts`, `.tsx`.
[^e]: `.html`, `.ts`, `.tsx`, `.vue`, `.svelte`.
[^f]: React only.
[^g]: Reloads entire extension.
[^h]: ESM-style loaders run asynchronously.
[^i]: Entrypoint options all configured in `manifest.json`.
[^j]: As of `v2.0.0-beta.23`, but v2 stable hasn't been released yet.
[^k]: There is no built-in wrapper around this API. However, you can still access the standard APIs via `chrome`/`browser` globals or use any 3rd party NPM package.
[^l]: WIP, moving very slowly. Follow [wxt-dev/wxt#357](https://github.com/wxt-dev/wxt/issues/357) for updates.
+48 -3
View File
@@ -8,9 +8,7 @@ Commonly asked questions about how to use WXT or why it behaves the way it does.
[[toc]]
---
### Why are content scripts not showing up in the manifest?
## Why aren't content scripts added to the manifest?
During development, WXT registers content scripts dynamically so they can be reloaded individually when a file is saved without reloading your entire extension.
@@ -19,3 +17,50 @@ To list the content scripts registered during development, open the service work
```js
await chrome.scripting.getRegisteredContentScripts();
```
## My component library doesn't work in content scripts!
Component libraries place their CSS in the document's `<head>` by default. When using `createShadowRoot`, your UI is isolated from the document's styles because it's inside a ShadowRoot.
To fix this, you need to tell your component library to insert it's CSS inside the shadow root. Here's the docs for a couple of popular libraries:
- React
- Ant Design: [`StyleProvider`](https://ant.design/docs/react/compatible-style#shadow-dom-usage)
- Mantine: [`MantineProvider#getRootElement` and `MantineProvider#cssVariablesSelector`](https://mantine.dev/theming/mantine-provider/)
> If your library isn't listed above, try searching it's docs/issues for "shadow root", "shadow dom", or "css container".
`createShadowRoot` provides it's own `<head>` element inside the shadow root, so that were you should tell the library to add the CSS. Here's an example with Ant Design:
```tsx
import { StyleProvider } from '@ant-design/cssinjs'; // [!code ++]
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
anchor: 'body',
onMount: (container) => { // [!code --]
onMount: (container, shadow) => { // [!code ++]
const cssContainer = shadow.querySelector("head")!; // [!code ++]
const root = ReactDOM.createRoot(container);
root.render(
<StyleProvider container={cssContainer}> // [!code ++]
<App />
</StyleProvider> // [!code ++]
);
return root;
},
onRemove: (root) => {
root?.unmount();
},
});
```
Note that this doesn't effect all component libraries, just ones that inject CSS themselves rather than having you import their CSS. This approach is more prevailent in the React community, but not limited to it. That's why only React libraries are listed above. Vuetify, for example, works just fine because you import its CSS - WXT picks up on this and the CSS is added inside the shadow root automatically:
```ts
import 'vuetify/styles'; // <-- This line imports the CSS, just like importing a .css file
import { createVuetify } from 'vuetify';
```
+31 -8
View File
@@ -55,7 +55,7 @@ For a full list of methods available, see the [API reference](/api/reference/wxt
## Watchers
To listen for storage changes, use the `storage.watch` function. It lets you setup a listener for a single key:
To listen for storage changes, use the `storage.watch` function. It lets you set up a listener for a single key:
```ts
const unwatch = storage.watch<number>('local:counter', (newCount, oldCount) => {
@@ -100,7 +100,7 @@ You can remove all metadata associated with a key, or just specific properties:
// Remove all properties
await storage.removeMeta('local:preference');
// Remove one property
// Remove only the "lastModified" property
await storage.removeMeta('local:preference', 'lastModified');
// Remove multiple properties
@@ -225,13 +225,13 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV3[]>( // [!code
Internally, this uses a metadata property called `v` to track the value's current version.
:::
In this case, we thought that the ignored website list might change in the future, and were able to setup a versioned storage item from the start.
In this case, we thought that the ignored website list might change in the future, and were able to set up a versioned storage item from the start.
Realistically, you won't know a item needs versioned until you need to change it's schema. Thankfully, it's simple to add versioning to an unversioned storage item.
Realistically, you won't know an item needs versioning until you need to change its schema. Thankfully, it's simple to add versioning to an unversioned storage item.
When a previous version isn't found, WXT assumes the version was `1`. That means you just need to set `version: 2` and add a migration for `2`, and it will just work!
Lets look at the same ignored websites example from before, but start with an unversioned item this time:
Let's look at the same ignored websites example from before, but start with an unversioned item this time:
:::code-group
@@ -275,13 +275,13 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code
### Running Migrations
As soon as `storage.defineItem` is called, WXT checks if migrations need to be ran, and if so, runs them. Calls to get or update the storage item's value or metadata (`getValue`, `setValue`, `removeValue`, `getMeta`, etc) will automatically wait for the migration process to finish before actually reading or writing values.
As soon as `storage.defineItem` is called, WXT checks if migrations need to be run, and if so, runs them. Calls to get or update the storage item's value or metadata (`getValue`, `setValue`, `removeValue`, `getMeta`, etc.) will automatically wait for the migration process to finish before actually reading or writing values.
### Default Values
With `storage.defineItem`, there are multiple ways of defining default values:
1. `fallback` - Return this value from `getValue` instead of `null` if the value is missing.
1. **`fallback`** - Return this value from `getValue` instead of `null` if the value is missing.
This option is great for providing default values for settings:
@@ -294,7 +294,7 @@ With `storage.defineItem`, there are multiple ways of defining default values:
});
```
2. `init` - Initialize and save a value in storage if it is not already saved.
2. **`init`** - Initialize and save a value in storage if it is not already saved.
This is great for values that need to be initialized or set once:
@@ -308,3 +308,26 @@ With `storage.defineItem`, there are multiple ways of defining default values:
```
The value is initialized in storage immediately.
## Bulk Operations
When getting or setting multiple values in storage, you can perform bulk operations to improve performance by reducing the number of individual storage calls. The `storage` API provides several methods for performing bulk operations:
- **`getItems`** - Get multiple values at once.
- **`getMetas`** - Get metadata for multiple items at once.
- **`setItems`** - Set multiple values at once.
- **`setMetas`** - Set metadata for multiple items at once.
- **`removeItems`** - Remove multiple values (and optionally metadata) at once.
All these APIs support both string keys and defined storage items:
```ts
const userId = storage.defineItem('local:userId');
await storage.setItems([
{ key: 'local:installDate', value: Date.now() },
{ item: userId, value: generateUserId() },
]);
```
Refer to the [API Reference](/api/reference/wxt/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs.
+4
View File
@@ -26,6 +26,7 @@
"fast-glob": "^3.3.2",
"fs-extra": "^11.2.0",
"lint-staged": "^15.2.10",
"markdown-it-footnote": "^4.0.0",
"nano-spawn": "^0.1.0",
"prettier": "^3.3.3",
"simple-git-hooks": "^2.11.1",
@@ -51,6 +52,9 @@
"@algolia/client-search",
"search-insights"
]
},
"patchedDependencies": {
"markdown-it-footnote": "patches/markdown-it-footnote.patch"
}
}
}
+1 -1
View File
@@ -41,4 +41,4 @@ export default defineConfig({
});
```
Options have JSDocs available in your editor, or you can read them in the source code: [`AutoIconsOptions`](./src/index.ts).
Options have JSDocs available in your editor, or you can read them in the source code: [`AutoIconsOptions`](https://github.com/wxt-dev/wxt/blob/main/packages/auto-icons/src/index.ts).
+29
View File
@@ -1,5 +1,34 @@
# Changelog
## v0.2.3
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.2...i18n-v0.2.3)
### 🩹 Fixes
- Prevent app crashes from parse errors due to incomplete file saves ([#1114](https://github.com/wxt-dev/wxt/pull/1114))
### 📖 Documentation
- Cleanup typos and broken links ([bb5ea34](https://github.com/wxt-dev/wxt/commit/bb5ea34))
### ❤️ Contributors
- Bread Grocery <breadgrocery@gmail.com>
- Aaron ([@aklinker1](http://github.com/aklinker1))
## v0.2.2
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.1...i18n-v0.2.2)
### 🚀 Enhancements
- Add support for configuring the `locales` directory ([#1109](https://github.com/wxt-dev/wxt/pull/1109))
### ❤️ Contributors
- Bread Grocery <breadgrocery@gmail.com>
## v0.2.1
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.0...i18n-v0.2.1)
+18 -3
View File
@@ -98,6 +98,21 @@ And you're done! Using WXT, you get type-safety out of the box.
i18n.t('helloWorld'); // "Hello world!";
```
## Configuration
The module can be configured via the `i18n` config:
```ts
export default defineConfig({
modules: ['@wxt-dev/i18n'],
i18n: {
// ...
},
});
```
Options have JSDocs available in your editor, or you can read them in the source code: [`I18nOptions`](https://github.com/wxt-dev/wxt/blob/main/packages/i18n/src/module.ts).
## Messages File Format
> [!DANGER]
@@ -225,11 +240,11 @@ A key is treated as "verbose" when it is:
{
"appName": {
"message": "GitHub - Better Line Counts",
"description": "The app's name, should not be translated",
"description": "The app's name, should not be translated"
},
"ok": "OK",
"deleteConfirmation": {
"title": "Delete XYZ?"
"title": "Delete XYZ?",
"message": "You cannot undo this action once taken."
}
}
@@ -289,7 +304,7 @@ const messages = {
// ...
};
// Generate JSON files for the browser
// Generate JSON files for the extension
await generateChromeMessagesFile('dist/_locales/en/messages.json', messages.en);
await generateChromeMessagesFile('dist/_locales/de/messages.json', messages.de);
// ...
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/i18n",
"description": "Type-safe wrapper around browser.i18n.getMessage with additional features",
"version": "0.2.1",
"version": "0.2.3",
"type": "module",
"repository": {
"type": "git",
+30 -8
View File
@@ -23,11 +23,11 @@ import { watch } from 'chokidar';
import { GeneratedPublicFile, WxtDirFileEntry } from 'wxt';
import { writeFile } from 'node:fs/promises';
export default defineWxtModule({
export default defineWxtModule<I18nOptions>({
name: '@wxt-dev/i18n',
configKey: 'i18n',
imports: [{ from: '#i18n', name: 'i18n' }],
setup(wxt) {
setup(wxt, options) {
if (wxt.config.manifest.default_locale == null) {
wxt.logger.warn(
`\`[i18n]\` manifest.default_locale not set, \`@wxt-dev/i18n\` disabled.`,
@@ -38,9 +38,12 @@ export default defineWxtModule({
'`[i18n]` Default locale: ' + wxt.config.manifest.default_locale,
);
const { localesDir = resolve(wxt.config.srcDir, 'locales') } =
options ?? {};
const getLocalizationFiles = async () => {
const files = await glob('locales/*', {
cwd: wxt.config.srcDir,
const files = await glob('*.{json,json5,yml,yaml,toml}', {
cwd: localesDir,
absolute: true,
});
return files.map((file) => ({
@@ -71,7 +74,7 @@ export default defineWxtModule({
)!;
if (defaultLocaleFile == null) {
throw Error(
`\`[i18n]\` Required localization file does not exist: \`<srcDir>/locales/${wxt.config.manifest.default_locale}.{json|json5|yml|yaml|toml}\``,
`\`[i18n]\` Required localization file does not exist: \`<localesDir>/${wxt.config.manifest.default_locale}.{json|json5|yml|yaml|toml}\``,
);
}
@@ -154,9 +157,28 @@ export { type GeneratedI18nStructure }
if (wxt.config.command === 'serve') {
wxt.hooks.hookOnce('build:done', () => {
const watcher = watch(resolve(wxt.config.srcDir, 'locales'));
watcher.on('change', updateLocalizations);
const watcher = watch(localesDir);
watcher.on('change', (path) => {
updateLocalizations(path).catch(wxt.logger.error);
});
});
}
},
});
/**
* Options for the i18n module
*/
export interface I18nOptions {
/**
* Directory containing files that define the translations.
* @default "${config.srcDir}/locales"
*/
localesDir?: string;
}
declare module 'wxt' {
export interface InlineConfig {
i18n?: I18nOptions;
}
}
+21
View File
@@ -1,5 +1,26 @@
# Changelog
## v2.0.0
[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v1.0.1...module-svelte-v2.0.0)
### 🚀 Enhancements
- ⚠️ Svelte 5 support ([#1104](https://github.com/wxt-dev/wxt/pull/1104))
### 🏡 Chore
- Add `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))
- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))
#### ⚠️ Breaking Changes
- ⚠️ Svelte 5 support ([#1104](https://github.com/wxt-dev/wxt/pull/1104))
### ❤️ Contributors
- Aaron ([@aklinker1](http://github.com/aklinker1))
## v1.0.1
[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v1.0.0...module-svelte-v1.0.1)
+4 -3
View File
@@ -17,7 +17,7 @@
"email": "aaronklinker1+wxt@gmail.com"
},
"license": "MIT",
"version": "1.0.1",
"version": "2.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
@@ -42,10 +42,11 @@
"check": "pnpm build && check"
},
"peerDependencies": {
"wxt": ">=0.18.6"
"wxt": ">=0.18.6",
"svelte": ">=5"
},
"dependencies": {
"@sveltejs/vite-plugin-svelte": "^3.1.2"
"@sveltejs/vite-plugin-svelte": "^4.0.0"
},
"devDependencies": {
"@aklinker1/check": "^1.4.5",
+1 -1
View File
@@ -44,4 +44,4 @@ export default defineConfig({
});
```
Options have JSDocs available in your editor, or you can read them in the source code: [`UnoCSSOptions`](./src/index.ts).
Options have JSDocs available in your editor, or you can read them in the source code: [`UnoCSSOptions`](https://github.com/wxt-dev/wxt/blob/main/packages/auto-icons/src/index.ts).
+18 -1
View File
@@ -1,5 +1,22 @@
# Changelog
## v0.19.13
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.12...wxt-v0.19.13)
### 🚀 Enhancements
- **env:** Load env from `.env.[browser]` variants ([#1078](https://github.com/wxt-dev/wxt/pull/1078))
### 🩹 Fixes
- Don't use `#private` member variables in `ContentScriptContext` ([#1103](https://github.com/wxt-dev/wxt/pull/1103))
### ❤️ Contributors
- Aaron ([@aklinker1](http://github.com/aklinker1))
- Craig Slusher ([@sleekslush](http://github.com/sleekslush))
## v0.19.12
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.11...wxt-v0.19.12)
@@ -2561,4 +2578,4 @@ Initial release of WXT. Full support for production builds and initial toolkit f
### 🤖 CI
- Create validation workflow ([#12](https://github.com/wxt-dev/wxt/pull/12))
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
@@ -8,6 +8,7 @@ exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file
"ContentScriptContext": true,
"InvalidMatchPattern": true,
"MatchPattern": true,
"MigrationError": true,
"browser": true,
"createIframeUi": true,
"createIntegratedUi": true,
@@ -34,6 +35,7 @@ const globals = {
"ContentScriptContext": true,
"InvalidMatchPattern": true,
"MatchPattern": true,
"MigrationError": true,
"browser": true,
"createIframeUi": true,
"createIntegratedUi": true,
@@ -68,6 +70,7 @@ exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config f
"ContentScriptContext": true,
"InvalidMatchPattern": true,
"MatchPattern": true,
"MigrationError": true,
"browser": true,
"createIframeUi": true,
"createIntegratedUi": true,
@@ -95,6 +98,7 @@ exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = `
"ContentScriptContext": "readonly",
"InvalidMatchPattern": "readonly",
"MatchPattern": "readonly",
"MigrationError": "readonly",
"browser": "readonly",
"createIframeUi": "readonly",
"createIntegratedUi": "readonly",
@@ -20,6 +20,7 @@ describe('Auto Imports', () => {
const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
const InvalidMatchPattern: typeof import('wxt/sandbox')['InvalidMatchPattern']
const MatchPattern: typeof import('wxt/sandbox')['MatchPattern']
const MigrationError: typeof import('wxt/storage')['MigrationError']
const browser: typeof import('wxt/browser')['browser']
const createIframeUi: typeof import('wxt/client')['createIframeUi']
const createIntegratedUi: typeof import('wxt/client')['createIntegratedUi']
@@ -157,4 +157,18 @@ describe('User Config', () => {
await project.fileExists('.output/test-chrome-mv3-development-dev-build'),
).toBe(true);
});
it('should throw error when config file not exist', async () => {
const project = new TestProject();
project.addFile(
'src/entrypoints/background.ts',
`export default defineBackground(
() => console.log('Hello background'),
);`,
);
await expect(
project.build({ configFile: 'foo.config.ts' }),
).rejects.toThrowError(/not found/);
});
});
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.19.12",
"version": "0.19.13",
"description": "Next gen framework for developing web extensions",
"repository": {
"type": "git",
+499 -37
View File
@@ -1,7 +1,7 @@
import { fakeBrowser } from '@webext-core/fake-browser';
import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
import { browser } from 'wxt/browser';
import { WxtStorageItem, storage } from '../storage';
import { MigrationError, WxtStorageItem, storage } from '../storage';
/**
* This works because fakeBrowser is synchronous, and is will finish any number of chained
@@ -38,7 +38,7 @@ describe('Storage Utils', () => {
expect(actual).toBe(expected);
});
it('should return the value if multiple : are use in the key', async () => {
it('should return the value if multiple : are used in the key', async () => {
const expected = 'value';
await fakeBrowser.storage[storageArea].set({ 'some:key': expected });
@@ -64,27 +64,97 @@ describe('Storage Utils', () => {
});
describe('getItems', () => {
it('should return an array of values', async () => {
const expected = [
{ key: `${storageArea}:count`, value: 234 },
{ key: `${storageArea}:installDate`, value: null },
{ key: `${storageArea}:otherValue`, value: 345 },
] as const;
const params = [
expected[0].key,
expected[1].key,
{
key: expected[2].key,
options: { defaultValue: expected[2].value },
},
];
it('should get values from multiple storage keys', async () => {
const item1 = {
key: `${storageArea}:one`,
expectedValue: 1,
} as const;
const item2 = {
key: `${storageArea}:two`,
expectedValue: null,
} as const;
await fakeBrowser.storage[storageArea].set({
count: expected[0].value,
one: item1.expectedValue,
});
const actual = await storage.getItems(params);
const actual = await storage.getItems([item1.key, item2.key]);
expect(actual).toEqual(expected);
expect(actual).toEqual([
{ key: item1.key, value: item1.expectedValue },
{ key: item2.key, value: item2.expectedValue },
]);
});
it('should get values from multiple storage items', async () => {
const item1 = storage.defineItem(`${storageArea}:one`);
const expectedValue1 = 1;
const item2 = storage.defineItem(`${storageArea}:two`);
const expectedValue2 = null;
await fakeBrowser.storage[storageArea].set({
one: expectedValue1,
});
const actual = await storage.getItems([item1, item2]);
expect(actual).toEqual([
{ key: item1.key, value: expectedValue1 },
{ key: item2.key, value: expectedValue2 },
]);
});
it('should get values for a combination of different input types', async () => {
const key1 = `${storageArea}:one` as const;
const expectedValue1 = 1;
const item2 = storage.defineItem<number>(`${storageArea}:two`);
const expectedValue2 = 2;
await fakeBrowser.storage[storageArea].set({
one: expectedValue1,
two: expectedValue2,
});
const actual = await storage.getItems([key1, item2]);
expect(actual).toEqual([
{ key: key1, value: expectedValue1 },
{ key: item2.key, value: expectedValue2 },
]);
});
it('should return fallback values for keys when provided', async () => {
const key1 = `${storageArea}:one` as const;
const expectedValue1 = null;
const key2 = `${storageArea}:two` as const;
const fallback2 = 2;
const expectedValue2 = fallback2;
const actual = await storage.getItems([
key1,
{ key: key2, options: { fallback: fallback2 } },
]);
expect(actual).toEqual([
{ key: key1, value: expectedValue1 },
{ key: key2, value: expectedValue2 },
]);
});
it('should return fallback values for items when provided', async () => {
const item1 = storage.defineItem<number>(`${storageArea}:one`);
const expectedValue1 = null;
const item2 = storage.defineItem(`${storageArea}:two`, {
fallback: 2,
});
const expectedValue2 = item2.fallback;
const actual = await storage.getItems([item1, item2]);
expect(actual).toEqual([
{ key: item1.key, value: expectedValue1 },
{ key: item2.key, value: expectedValue2 },
]);
});
});
@@ -180,6 +250,74 @@ describe('Storage Utils', () => {
);
});
describe('setMetas', () => {
it('should set key metadata correctly', async () => {
const key1 = `${storageArea}:one` as const;
const initialMeta1 = {};
const setMeta1 = { v: 1 };
const expectedMeta1 = setMeta1;
const key2 = `${storageArea}:two` as const;
const initialMeta2 = { v: 1 };
const setMeta2 = { v: 2 };
const expectedMeta2 = setMeta2;
const key3 = `${storageArea}:three` as const;
const initialMeta3 = { v: 1 };
const setMeta3 = { d: Date.now() };
const expectedMeta3 = { ...initialMeta3, ...setMeta3 };
await fakeBrowser.storage[storageArea].set({
one$: initialMeta1,
two$: initialMeta2,
three$: initialMeta3,
});
await storage.setMetas([
{ key: key1, meta: setMeta1 },
{ key: key2, meta: setMeta2 },
{ key: key3, meta: setMeta3 },
]);
expect(await storage.getMeta(key1)).toEqual(expectedMeta1);
expect(await storage.getMeta(key2)).toEqual(expectedMeta2);
expect(await storage.getMeta(key3)).toEqual(expectedMeta3);
});
it('should set item metadata correctly', async () => {
const item1 = storage.defineItem(`${storageArea}:one`);
const initialMeta1 = {};
const setMeta1 = { v: 1 };
const expectedMeta1 = setMeta1;
const item2 = storage.defineItem(`${storageArea}:two`);
const initialMeta2 = { v: 1 };
const setMeta2 = { v: 2 };
const expectedMeta2 = setMeta2;
const item3 = storage.defineItem(`${storageArea}:three`);
const initialMeta3 = { v: 1 };
const setMeta3 = { d: Date.now() };
const expectedMeta3 = { ...initialMeta3, ...setMeta3 };
await fakeBrowser.storage[storageArea].set({
one$: initialMeta1,
two$: initialMeta2,
three$: initialMeta3,
});
await storage.setMetas([
{ item: item1, meta: setMeta1 },
{ item: item2, meta: setMeta2 },
{ item: item3, meta: setMeta3 },
]);
expect(await item1.getMeta()).toEqual(expectedMeta1);
expect(await item2.getMeta()).toEqual(expectedMeta2);
expect(await item3.getMeta()).toEqual(expectedMeta3);
});
});
describe('removeItem', () => {
it('should remove the key from storage', async () => {
await fakeBrowser.storage[storageArea].set({ count: 456 });
@@ -219,29 +357,136 @@ describe('Storage Utils', () => {
});
describe('removeItems', () => {
it('should remove multiple items', async () => {
it('should remove multiple keys', async () => {
const key1 = `${storageArea}:one` as const;
const key2 = `${storageArea}:two` as const;
const key3 = `${storageArea}:three` as const;
await fakeBrowser.storage[storageArea].set({
['one']: '1',
['two']: null,
['two$']: { v: 1 },
['three']: '1',
['three$']: { v: 1 },
one: 1,
two: 2,
});
await storage.removeItems([key1, key2]);
expect(await storage.getItem(key1)).toBeNull();
expect(await storage.getItem(key2)).toBeNull();
});
it('should remove multiple keys and metadata when requested', async () => {
const key1 = `${storageArea}:one` as const;
const key2 = `${storageArea}:two` as const;
await fakeBrowser.storage[storageArea].set({
one: 1,
one$: { v: 1 },
two: 2,
two$: { v: 1 },
});
await storage.removeItems([
key1,
key2,
{ key: key3, options: { removeMeta: true } },
{ key: key2, options: { removeMeta: true } },
]);
expect(await storage.getItem(key1)).toBeNull();
expect(await storage.getMeta(key1)).toEqual({ v: 1 });
expect(await storage.getItem(key2)).toBeNull();
expect(await storage.getMeta(key2)).toEqual({ v: 1 });
expect(await storage.getItem(key3)).toBeNull();
expect(await storage.getMeta(key3)).toEqual({});
expect(await storage.getMeta(key2)).toEqual({});
});
it('should remove multiple items', async () => {
const item1 = storage.defineItem(`${storageArea}:one`);
const item2 = storage.defineItem(`${storageArea}:two`);
await fakeBrowser.storage[storageArea].set({
one: 1,
two: 2,
});
await storage.removeItems([item1, item2]);
expect(await item1.getValue()).toBeNull();
expect(await item2.getValue()).toBeNull();
});
it('should remove multiple items and metadata when requested', async () => {
const item1 = storage.defineItem(`${storageArea}:one`);
const item2 = storage.defineItem(`${storageArea}:two`);
await fakeBrowser.storage[storageArea].set({
one: 1,
one$: { v: 1 },
two: 2,
two$: { v: 1 },
});
await storage.removeItems([
item1,
{ item: item2, options: { removeMeta: true } },
]);
expect(await item1.getValue()).toBeNull();
expect(await item1.getMeta()).toEqual({ v: 1 });
expect(await item2.getValue()).toBeNull();
expect(await item2.getMeta()).toEqual({});
});
it('should remove multiple items', async () => {
const item1 = storage.defineItem(`${storageArea}:one`);
const item2 = storage.defineItem(`${storageArea}:two`);
await fakeBrowser.storage.local.set({
one: 1,
two: 2,
});
await storage.removeItems([item1, item2]);
expect(await item1.getValue()).toBeNull();
expect(await item2.getValue()).toBeNull();
});
it('should remove items using { item: WxtStorageItem, options?: RemoveItemOptions } objects', async () => {
const item1 = storage.defineItem('local:item1');
const item2 = storage.defineItem('session:item2');
await item1.setValue('value1');
await item1.setMeta({ v: 1 });
await item2.setValue('value2');
await item2.setMeta({ v: 1 });
await storage.removeItems([
{ item: item1, options: { removeMeta: true } },
{ item: item2, options: { removeMeta: false } },
]);
expect(await item1.getValue()).toBeNull();
expect(await item1.getMeta()).toEqual({});
expect(await item2.getValue()).toBeNull();
expect(await item2.getMeta()).toEqual({ v: 1 });
});
it('should handle a mix of different input types', async () => {
const item1 = storage.defineItem('local:item1');
const item2 = storage.defineItem('session:item2');
const item3 = storage.defineItem('local:item3');
await item1.setValue('value1');
await item2.setValue('value2');
await item3.setValue('value3');
await storage.removeItems([
'local:item1',
item2,
{ key: 'local:item3', options: { removeMeta: true } },
]);
expect(await storage.getItem('local:item1')).toBeNull();
expect(await item2.getValue()).toBeNull();
expect(await item3.getValue()).toBeNull();
expect(await item3.getMeta()).toEqual({});
});
it('should not throw an error when removing non-existent items', async () => {
const item = storage.defineItem('local:non_existent');
await expect(storage.removeItems([item])).resolves.not.toThrow();
});
});
@@ -400,6 +645,22 @@ describe('Storage Utils', () => {
},
);
describe('Invalid storage areas', () => {
it('should not accept keys without a valid storage area prefix', async () => {
// @ts-expect-error
await storage.getItem('test').catch(() => {});
// @ts-expect-error
await storage.getItem('loca:test').catch(() => {});
});
it('should throw an error when using an invalid storage area', async () => {
// @ts-expect-error: Test passes if there is a type error here
await expect(storage.getItem('invalidArea:key')).rejects.toThrow(
'Invalid area',
);
});
});
describe('defineItem', () => {
describe('versioning', () => {
it('should migrate values to the latest when a version upgrade is detected', async () => {
@@ -553,6 +814,28 @@ describe('Storage Utils', () => {
'Version downgrade detected (v2 -> v1) for "local:count"',
);
});
it('should throw an error when defining an item with an invalid version', () => {
expect(() => storage.defineItem('local:key', { version: 0 })).toThrow(
'Storage item version cannot be less than 1',
);
});
it('should handle errors in migration functions', async () => {
const cause = Error('Some error');
const expectedError = new MigrationError('local:key', 2, { cause });
const item = storage.defineItem<number>('local:key', {
version: 3,
migrations: {
2: () => {
throw cause;
},
},
});
await fakeBrowser.storage.local.set({ key: 1, key$: { v: 1 } });
await expect(item.migrate()).rejects.toThrow(expectedError);
});
});
describe('getValue', () => {
@@ -872,6 +1155,24 @@ describe('Storage Utils', () => {
await expect(storage.getItem('local:test')).resolves.toBe(expected);
});
it("should re-initialize a value on the next call to getValue if it's been removed", async () => {
const init = vi.fn().mockImplementation(Math.random);
const item = storage.defineItem<number>('local:key', { init });
await waitForInit();
await item.removeValue();
// Make sure it's actually blank before running the test
expect(await browser.storage.local.get()).toEqual({});
init.mockClear();
const [value1, value2] = await Promise.all([
item.getValue(),
item.getValue(),
]);
expect(init).toBeCalledTimes(1);
expect(value1).toBe(value2);
});
});
describe('types', () => {
@@ -896,12 +1197,173 @@ describe('Storage Utils', () => {
});
});
describe('types', () => {
it('should not accept keys without a valid storage area prefix', async () => {
// @ts-expect-error: Test passes if there is a type error here
await storage.getItem('test').catch(() => {});
// @ts-expect-error
await storage.getItem('loca:test').catch(() => {});
describe('Multiple Storage Areas', () => {
describe('getItems', () => {
it('should get the values of multiple storage items efficiently', async () => {
const item1 = storage.defineItem<number>('local:item1');
const item2 = storage.defineItem<string>('session:item2');
const item3 = storage.defineItem<boolean>('local:item3');
await item1.setValue(42);
await item2.setValue('hello');
await item3.setValue(false);
const localGetSpy = vi.spyOn(fakeBrowser.storage.local, 'get');
const sessionGetSpy = vi.spyOn(fakeBrowser.storage.session, 'get');
const values = await storage.getItems([item1, item2, item3]);
expect(values).toEqual([
{
key: 'local:item1',
value: 42,
},
{
key: 'session:item2',
value: 'hello',
},
{
key: 'local:item3',
value: false,
},
]);
expect(localGetSpy).toBeCalledTimes(1);
expect(localGetSpy).toBeCalledWith(['item1', 'item3']);
expect(sessionGetSpy).toBeCalledTimes(1);
expect(sessionGetSpy).toBeCalledWith(['item2']);
});
it('should return values in the same order as input', async () => {
const item1 = storage.defineItem<number>('local:item1');
const item2 = storage.defineItem<string>('session:item2');
const item3 = storage.defineItem<boolean>('local:item3');
expect(await storage.getItems([item1, item2, item3])).toEqual([
{ key: item1.key, value: null },
{ key: item2.key, value: null },
{ key: item3.key, value: null },
]);
expect(await storage.getItems([item3, item2, item1])).toEqual([
{ key: item3.key, value: null },
{ key: item2.key, value: null },
{ key: item1.key, value: null },
]);
});
});
describe('getMetas', () => {
it('should get the metadata of multiple storage items efficiently', async () => {
const item1 = storage.defineItem<number, { v: number }>('local:item1');
const item2 = storage.defineItem<string, { date: number }>(
'session:item2',
);
const item3 = storage.defineItem<boolean, { flag: boolean }>(
'local:item3',
);
await item1.setMeta({ v: 1 });
await item2.setMeta({ date: 1234567890 });
// item3 has no meta
const localGetSpy = vi.spyOn(fakeBrowser.storage.local, 'get');
const sessionGetSpy = vi.spyOn(fakeBrowser.storage.session, 'get');
const metas = await storage.getMetas([item1, item2, item3]);
expect(metas).toEqual([
{ key: item1.key, meta: { v: 1 } },
{ key: item2.key, meta: { date: 1234567890 } },
{ key: item3.key, meta: {} },
]);
expect(localGetSpy).toBeCalledTimes(1);
expect(localGetSpy).toBeCalledWith(['item1$', 'item3$']);
expect(sessionGetSpy).toBeCalledTimes(1);
expect(sessionGetSpy).toBeCalledWith(['item2$']);
});
it('should return the metadata in the same order as input', async () => {
const item1 = storage.defineItem<number, { v: number }>('local:item1');
const item2 = storage.defineItem<string, { date: number }>(
'session:item2',
);
const item3 = storage.defineItem<boolean, { flag: boolean }>(
'local:item3',
);
expect(await storage.getMetas([item1, item2, item3])).toEqual([
{ key: item1.key, meta: {} },
{ key: item2.key, meta: {} },
{ key: item3.key, meta: {} },
]);
expect(await storage.getMetas([item3, item2, item1])).toEqual([
{ key: item3.key, meta: {} },
{ key: item2.key, meta: {} },
{ key: item1.key, meta: {} },
]);
});
});
describe('setItems', () => {
it('should set the values of multiple storage items efficiently', async () => {
const item1 = storage.defineItem<number>('local:item1');
const value1 = 100;
const item2 = storage.defineItem<string>('session:item2');
const value2 = 'test';
const item3 = storage.defineItem<boolean>('local:item3');
const value3 = true;
const localSetSpy = vi.spyOn(fakeBrowser.storage.local, 'set');
const sessionSetSpy = vi.spyOn(fakeBrowser.storage.session, 'set');
await storage.setItems([
{ item: item1, value: value1 },
{ item: item2, value: value2 },
{ item: item3, value: value3 },
]);
expect(localSetSpy).toBeCalledTimes(1);
expect(localSetSpy).toBeCalledWith({ item1: value1, item3: value3 });
expect(sessionSetSpy).toBeCalledTimes(1);
expect(sessionSetSpy).toBeCalledWith({ item2: value2 });
});
});
describe('setMetas', () => {
it('should set metadata efficiently', async () => {
const item1 = storage.defineItem<number, { v: number }>('local:one');
const item2 = storage.defineItem<string, { v: number }>('session:two');
const item3 = storage.defineItem<boolean, { v: number }>('local:three');
await waitForInit();
const localGetSpy = vi.spyOn(fakeBrowser.storage.local, 'get');
const sessionGetSpy = vi.spyOn(fakeBrowser.storage.session, 'get');
const localSetSpy = vi.spyOn(fakeBrowser.storage.local, 'set');
const sessionSetSpy = vi.spyOn(fakeBrowser.storage.session, 'set');
await storage.setMetas([
{ item: item1, meta: { v: 1 } },
{ item: item2, meta: { v: 2 } },
{ item: item3, meta: { v: 3 } },
]);
console.log(localGetSpy.mock.calls);
expect(localGetSpy).toBeCalledTimes(1);
expect(localGetSpy).toBeCalledWith(['one$', 'three$']);
expect(sessionGetSpy).toBeCalledTimes(1);
expect(sessionGetSpy).toBeCalledWith(['two$']);
expect(localSetSpy).toBeCalledTimes(1);
expect(localSetSpy).toBeCalledWith({
one$: { v: 1 },
three$: { v: 3 },
});
expect(sessionSetSpy).toBeCalledTimes(1);
expect(sessionSetSpy).toBeCalledWith({
two$: { v: 2 },
});
});
});
it('should return a nullable type when getItem is called without a fallback', async () => {
@@ -96,7 +96,7 @@ export function createIframeUi<TMounted>(
*
* > This function is async because it has to load the CSS via a network call.
*
* @see https://wxt.dev/guide/essentials/content-scripts.html#shadowroot
* @see https://wxt.dev/guide/essentials/content-scripts.html#shadow-root
*/
export async function createShadowRootUi<TMounted>(
ctx: ContentScriptContext,
+4 -1
View File
@@ -53,6 +53,9 @@ export async function resolveConfig(
esmResolve: true,
},
});
if (inlineConfig.configFile && metadata.layers?.length === 0) {
throw Error(`Config file "${inlineConfig.configFile}" not found`);
}
userConfig = loadedConfig ?? {};
userConfigMetadata = metadata;
}
@@ -74,7 +77,7 @@ export async function resolveConfig(
const mode = mergedConfig.mode ?? COMMAND_MODES[command];
const env: ConfigEnv = { browser, command, manifestVersion, mode };
loadEnv(mode); // Load any environment variables used below
loadEnv(mode, browser); // Load any environment variables used below
const root = path.resolve(
inlineConfig.root ?? userConfig.root ?? process.cwd(),
@@ -228,7 +228,7 @@ async function getPopupEntrypoint(
const options = await getHtmlEntrypointOptions<PopupEntrypointOptions>(
info,
{
browserStyle: 'browse_style',
browserStyle: 'browser_style',
exclude: 'exclude',
include: 'include',
defaultIcon: 'default_icon',
@@ -262,7 +262,7 @@ async function getOptionsEntrypoint(
const options = await getHtmlEntrypointOptions<OptionsEntrypointOptions>(
info,
{
browserStyle: 'browse_style',
browserStyle: 'browser_style',
chromeStyle: 'chrome_style',
exclude: 'exclude',
include: 'include',
+14 -3
View File
@@ -1,10 +1,21 @@
import { config } from 'dotenv';
import type { TargetBrowser } from '../../types';
/**
* Load environment files based on the current mode.
* Load environment files based on the current mode and browser.
*/
export function loadEnv(mode: string) {
export function loadEnv(mode: string, browser: TargetBrowser) {
return config({
path: [`.env.${mode}.local`, `.env.${mode}`, `.env.local`, `.env`],
// Files on top override files below
path: [
`.env.${mode}.${browser}.local`,
`.env.${mode}.${browser}`,
`.env.${browser}.local`,
`.env.${browser}`,
`.env.${mode}.local`,
`.env.${mode}`,
`.env.local`,
`.env`,
],
});
}
+194 -46
View File
@@ -44,6 +44,14 @@ function createStorage(): WxtStorage {
};
};
const getMetaKey = (key: string) => key + '$';
const mergeMeta = (oldMeta: any, newMeta: any): any => {
const newFields = { ...oldMeta };
Object.entries(newMeta).forEach(([key, value]) => {
if (value == null) delete newFields[key];
else newFields[key] = value;
});
return newFields;
};
const getValueOrFallback = (value: any, fallback: any) =>
value ?? fallback ?? null;
const getMetaValue = (properties: any) =>
@@ -78,15 +86,7 @@ function createStorage(): WxtStorage {
) => {
const metaKey = getMetaKey(driverKey);
const existingFields = getMetaValue(await driver.getItem(metaKey));
const newFields = { ...existingFields };
Object.entries(properties).forEach(([key, value]) => {
if (value == null) {
delete newFields[key];
} else {
newFields[key] = value;
}
});
await driver.setItem(metaKey, newFields);
await driver.setItem(metaKey, mergeMeta(existingFields, properties));
};
const removeItem = async (
driver: WxtStorageDriver,
@@ -129,98 +129,195 @@ function createStorage(): WxtStorage {
getItems: async (keys) => {
const areaToKeyMap = new Map<StorageArea, string[]>();
const keyToOptsMap = new Map<string, GetItemOptions<any> | undefined>();
const orderedKeys: StorageItemKey[] = [];
keys.forEach((key) => {
let keyStr: StorageItemKey;
let opts: GetItemOptions<any> | undefined;
if (typeof key === 'string') {
// key: string
keyStr = key;
} else if ('getValue' in key) {
// key: WxtStorageItem
keyStr = key.key;
opts = { fallback: key.fallback };
} else {
// key: { key, options }
keyStr = key.key;
opts = key.options;
}
orderedKeys.push(keyStr);
const { driverArea, driverKey } = resolveKey(keyStr);
const keys = areaToKeyMap.get(driverArea) ?? [];
areaToKeyMap.set(driverArea, keys.concat(driverKey));
const areaKeys = areaToKeyMap.get(driverArea) ?? [];
areaToKeyMap.set(driverArea, areaKeys.concat(driverKey));
keyToOptsMap.set(keyStr, opts);
});
const results = await Promise.all(
const resultsMap = new Map<StorageItemKey, any>();
await Promise.all(
Array.from(areaToKeyMap.entries()).map(async ([driverArea, keys]) => {
const driverResults = await drivers[driverArea].getItems(keys);
return driverResults.map((driverResult) => {
driverResults.forEach((driverResult) => {
const key = `${driverArea}:${driverResult.key}` as StorageItemKey;
const opts = keyToOptsMap.get(key);
const value = getValueOrFallback(
driverResult.value,
opts?.fallback ?? opts?.defaultValue,
);
return { key, value };
resultsMap.set(key, value);
});
}),
);
return results.flat();
return orderedKeys.map((key) => ({
key,
value: resultsMap.get(key),
}));
},
getMeta: async (key) => {
const { driver, driverKey } = resolveKey(key);
return await getMeta(driver, driverKey);
},
getMetas: async (args) => {
const keys = args.map((arg) => {
const key = typeof arg === 'string' ? arg : arg.key;
const { driverArea, driverKey } = resolveKey(key);
return {
key,
driverArea,
driverKey,
driverMetaKey: getMetaKey(driverKey),
};
});
const areaToDriverMetaKeysMap = keys.reduce<
Partial<Record<StorageArea, (typeof keys)[number][]>>
>((map, key) => {
map[key.driverArea] ??= [];
map[key.driverArea]!.push(key);
return map;
}, {});
const resultsMap: Record<string, any> = {};
await Promise.all(
Object.entries(areaToDriverMetaKeysMap).map(async ([area, keys]) => {
const areaRes = await browser.storage[area as StorageArea].get(
keys.map((key) => key.driverMetaKey),
);
keys.forEach((key) => {
resultsMap[key.key] = areaRes[key.driverMetaKey] ?? {};
});
}),
);
return keys.map((key) => ({
key: key.key,
meta: resultsMap[key.key],
}));
},
setItem: async (key, value) => {
const { driver, driverKey } = resolveKey(key);
await setItem(driver, driverKey, value);
},
setItems: async (values) => {
const areaToKeyValueMap = new Map<
StorageArea,
Array<{ key: string; value: any }>
>();
values.forEach(({ key, value }) => {
const { driverArea, driverKey } = resolveKey(key);
const values = areaToKeyValueMap.get(driverArea) ?? [];
areaToKeyValueMap.set(
driverArea,
values.concat({ key: driverKey, value }),
setItems: async (items) => {
const areaToKeyValueMap: Partial<
Record<StorageArea, Array<{ key: string; value: any }>>
> = {};
items.forEach((item) => {
const { driverArea, driverKey } = resolveKey(
'key' in item ? item.key : item.item.key,
);
areaToKeyValueMap[driverArea] ??= [];
areaToKeyValueMap[driverArea].push({
key: driverKey,
value: item.value,
});
});
await Promise.all(
Array.from(areaToKeyValueMap.entries()).map(
async ([driverArea, values]) => {
const driver = getDriver(driverArea);
await driver.setItems(values);
},
),
Object.entries(areaToKeyValueMap).map(async ([driverArea, values]) => {
const driver = getDriver(driverArea as StorageArea);
await driver.setItems(values);
}),
);
},
setMeta: async (key, properties) => {
const { driver, driverKey } = resolveKey(key);
await setMeta(driver, driverKey, properties);
},
setMetas: async (items) => {
const areaToMetaUpdatesMap: Partial<
Record<StorageArea, { key: string; properties: any }[]>
> = {};
items.forEach((item) => {
const { driverArea, driverKey } = resolveKey(
'key' in item ? item.key : item.item.key,
);
areaToMetaUpdatesMap[driverArea] ??= [];
areaToMetaUpdatesMap[driverArea].push({
key: driverKey,
properties: item.meta,
});
});
await Promise.all(
Object.entries(areaToMetaUpdatesMap).map(
async ([storageArea, updates]) => {
const driver = getDriver(storageArea as StorageArea);
const metaKeys = updates.map(({ key }) => getMetaKey(key));
console.log(storageArea, metaKeys);
const existingMetas = await driver.getItems(metaKeys);
const existingMetaMap = Object.fromEntries(
existingMetas.map(({ key, value }) => [key, getMetaValue(value)]),
);
const metaUpdates = updates.map(({ key, properties }) => {
const metaKey = getMetaKey(key);
return {
key: metaKey,
value: mergeMeta(existingMetaMap[metaKey] ?? {}, properties),
};
});
await driver.setItems(metaUpdates);
},
),
);
},
removeItem: async (key, opts) => {
const { driver, driverKey } = resolveKey(key);
await removeItem(driver, driverKey, opts);
},
removeItems: async (keys) => {
const areaToKeysMap = new Map<StorageArea, string[]>();
const areaToKeysMap: Partial<Record<StorageArea, string[]>> = {};
keys.forEach((key) => {
let keyStr: StorageItemKey;
let opts: RemoveItemOptions | undefined;
if (typeof key === 'string') {
// key: string
keyStr = key;
} else if ('getValue' in key) {
// key: WxtStorageItem
keyStr = key.key;
} else if ('item' in key) {
// key: { item, options }
keyStr = key.item.key;
opts = key.options;
} else {
// key: { key, options }
keyStr = key.key;
opts = key.options;
}
const { driverArea, driverKey } = resolveKey(keyStr);
const areaKeys = areaToKeysMap.get(driverArea) ?? [];
areaKeys.push(driverKey);
areaToKeysMap[driverArea] ??= [];
areaToKeysMap[driverArea].push(driverKey);
if (opts?.removeMeta) {
areaKeys.push(getMetaKey(driverKey));
areaToKeysMap[driverArea].push(getMetaKey(driverKey));
}
areaToKeysMap.set(driverArea, areaKeys);
});
await Promise.all(
Array.from(areaToKeysMap.entries()).map(async ([driverArea, keys]) => {
const driver = getDriver(driverArea);
Object.entries(areaToKeysMap).map(async ([driverArea, keys]) => {
const driver = getDriver(driverArea as StorageArea);
await driver.removeItems(keys);
}),
);
@@ -284,9 +381,15 @@ function createStorage(): WxtStorage {
);
let migratedValue = value;
for (const migrateToVersion of migrationsToRun) {
migratedValue =
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
migratedValue;
try {
migratedValue =
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
migratedValue;
} catch (err) {
throw Error(`v${migrateToVersion} migration failed for "${key}"`, {
cause: err,
});
}
}
await driver.setItems([
{ key: driverKey, value: migratedValue },
@@ -323,6 +426,7 @@ function createStorage(): WxtStorage {
migrationsDone.then(getOrInitValue);
return {
key,
get defaultValue() {
return getFallback();
},
@@ -480,7 +584,9 @@ export interface WxtStorage {
*/
getItems(
keys: Array<
StorageItemKey | { key: StorageItemKey; options?: GetItemOptions<any> }
| StorageItemKey
| WxtStorageItem<any, any>
| { key: StorageItemKey; options?: GetItemOptions<any> }
>,
): Promise<Array<{ key: StorageItemKey; value: any }>>;
/**
@@ -491,6 +597,15 @@ export interface WxtStorage {
* await storage.getMeta("local:installDate");
*/
getMeta<T extends Record<string, unknown>>(key: StorageItemKey): Promise<T>;
/**
* Get the metadata of multiple storage items.
*
* @param items List of keys or items to get the metadata of.
* @returns An array containing storage keys and their metadata.
*/
getMetas(
keys: Array<StorageItemKey | WxtStorageItem<any, any>>,
): Promise<Array<{ key: StorageItemKey; meta: any }>>;
/**
* Set a value in storage. Setting a value to `null` or `undefined` is equivalent to calling
* `removeItem`.
@@ -508,7 +623,12 @@ export interface WxtStorage {
* { key: "session:someCounter, value: 5 },
* ]);
*/
setItems(values: Array<{ key: StorageItemKey; value: any }>): Promise<void>;
setItems(
values: Array<
| { key: StorageItemKey; value: any }
| { item: WxtStorageItem<any, any>; value: any }
>,
): Promise<void>;
/**
* Sets metadata properties. If some properties are already set, but are not included in the
* `properties` parameter, they will not be removed.
@@ -520,6 +640,17 @@ export interface WxtStorage {
key: StorageItemKey,
properties: T | null,
): Promise<void>;
/**
* Set the metadata of multiple storage items.
*
* @param items List of storage keys or items and metadata to set for each.
*/
setMetas(
metas: Array<
| { key: StorageItemKey; meta: Record<string, any> }
| { item: WxtStorageItem<any, any>; meta: Record<string, any> }
>,
): Promise<void>;
/**
* Removes an item from storage.
*
@@ -532,7 +663,10 @@ export interface WxtStorage {
*/
removeItems(
keys: Array<
StorageItemKey | { key: StorageItemKey; options?: RemoveItemOptions }
| StorageItemKey
| WxtStorageItem<any, any>
| { key: StorageItemKey; options?: RemoveItemOptions }
| { item: WxtStorageItem<any, any>; options?: RemoveItemOptions }
>,
): Promise<void>;
/**
@@ -602,7 +736,11 @@ export interface WxtStorageItem<
TMetadata extends Record<string, unknown>,
> {
/**
* @deprecated Renamed to `fallback`, use it instead.
* The storage key passed when creating the storage item.
*/
key: StorageItemKey;
/**
* @deprecated Renamed to fallback, use it instead.
*/
defaultValue: TValue;
/**
@@ -719,3 +857,13 @@ export type WatchCallback<T> = (newValue: T, oldValue: T) => void;
* Call to remove a watch listener
*/
export type Unwatch = () => void;
export class MigrationError extends Error {
constructor(
public key: string,
public version: number,
options?: ErrorOptions,
) {
super(`v${version} migration failed for "${key}"`, options);
}
}
+1
View File
@@ -0,0 +1 @@
Removed sub-ids from rendered links. When you link to the same footnote multiple times, the link would look like `[3.2]` instead of just `[3]`. Didn't like how that looked, so this patch removes that function.
+79
View File
@@ -0,0 +1,79 @@
diff --git a/dist/index.cjs.js b/dist/index.cjs.js
index 806448c967261a61288f0faa0633a91f77d35968..6812dd84bbbb7176af4115a287677454fa562883 100644
--- a/dist/index.cjs.js
+++ b/dist/index.cjs.js
@@ -13,14 +13,14 @@ function render_footnote_anchor_name(tokens, idx, options, env /*, slf */) {
}
function render_footnote_caption(tokens, idx /*, options, env, slf */) {
let n = Number(tokens[idx].meta.id + 1).toString();
- if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`;
+ // if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`;
return `[${n}]`;
}
function render_footnote_ref(tokens, idx, options, env, slf) {
const id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
const caption = slf.rules.footnote_caption(tokens, idx, options, env, slf);
let refid = id;
- if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`;
+ // if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`;
return `<sup class="footnote-ref"><a href="#fn${id}" id="fnref${refid}">${caption}</a></sup>`;
}
function render_footnote_block_open(tokens, idx, options) {
@@ -31,7 +31,7 @@ function render_footnote_block_close() {
}
function render_footnote_open(tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
- if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
+ // if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
return `<li id="fn${id}" class="footnote-item">`;
}
function render_footnote_close() {
@@ -39,7 +39,7 @@ function render_footnote_close() {
}
function render_footnote_anchor(tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
- if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
+ // if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
/* ↩ with escape code to prevent display as Apple Emoji on iOS */
return ` <a href="#fnref${id}" class="footnote-backref">\u21a9\uFE0E</a>`;
diff --git a/index.mjs b/index.mjs
index 48277ca67206f248b9deb0058e9a7d69dbc07702..718e3e527b2513e4f6f59cba9d4526a36d58f6bc 100644
--- a/index.mjs
+++ b/index.mjs
@@ -17,7 +17,7 @@ function render_footnote_anchor_name (tokens, idx, options, env/*, slf */) {
function render_footnote_caption (tokens, idx/*, options, env, slf */) {
let n = Number(tokens[idx].meta.id + 1).toString()
- if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`
+ // if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`
return `[${n}]`
}
@@ -27,7 +27,7 @@ function render_footnote_ref (tokens, idx, options, env, slf) {
const caption = slf.rules.footnote_caption(tokens, idx, options, env, slf)
let refid = id
- if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`
+ // if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`
return `<sup class="footnote-ref"><a href="#fn${id}" id="fnref${refid}">${caption}</a></sup>`
}
@@ -45,7 +45,7 @@ function render_footnote_block_close () {
function render_footnote_open (tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf)
- if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`
+ // if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`
return `<li id="fn${id}" class="footnote-item">`
}
@@ -57,7 +57,7 @@ function render_footnote_close () {
function render_footnote_anchor (tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf)
- if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`
+ // if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`
/* ↩ with escape code to prevent display as Apple Emoji on iOS */
return ` <a href="#fnref${id}" class="footnote-backref">\u21a9\uFE0E</a>`
+102 -74
View File
@@ -4,6 +4,11 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
patchedDependencies:
markdown-it-footnote:
hash: ue34jdgdx43siqdj557feoepzq
path: patches/markdown-it-footnote.patch
importers:
.:
@@ -35,6 +40,9 @@ importers:
lint-staged:
specifier: ^15.2.10
version: 15.2.10
markdown-it-footnote:
specifier: ^4.0.0
version: 4.0.0(patch_hash=ue34jdgdx43siqdj557feoepzq)
nano-spawn:
specifier: ^0.1.0
version: 0.1.0
@@ -208,8 +216,11 @@ importers:
packages/module-svelte:
dependencies:
'@sveltejs/vite-plugin-svelte':
specifier: ^3.1.2
version: 3.1.2(svelte@4.2.18)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4))
specifier: ^4.0.0
version: 4.0.0(svelte@5.1.6)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4))
svelte:
specifier: '>=5'
version: 5.1.6
devDependencies:
'@aklinker1/check':
specifier: ^1.4.5
@@ -1664,19 +1675,19 @@ packages:
resolution: {integrity: sha512-axlrvsHlHlFmKKMEg4VyvMzFr93JWJj4eIfXY1STVuO2fsImCa7ncaiG5gC8HKOX590AW5RtRsC41/B+OfrSqw==}
engines: {node: '>=14.16'}
'@sveltejs/vite-plugin-svelte-inspector@2.1.0':
resolution: {integrity: sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==}
engines: {node: ^18.0.0 || >=20}
'@sveltejs/vite-plugin-svelte-inspector@3.0.1':
resolution: {integrity: sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22}
peerDependencies:
'@sveltejs/vite-plugin-svelte': ^3.0.0
svelte: ^4.0.0 || ^5.0.0-next.0
'@sveltejs/vite-plugin-svelte': ^4.0.0-next.0||^4.0.0
svelte: ^5.0.0-next.96 || ^5.0.0
vite: ^5.0.0
'@sveltejs/vite-plugin-svelte@3.1.2':
resolution: {integrity: sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==}
engines: {node: ^18.0.0 || >=20}
'@sveltejs/vite-plugin-svelte@4.0.0':
resolution: {integrity: sha512-kpVJwF+gNiMEsoHaw+FJL76IYiwBikkxYU83+BpqQLdVMff19KeRKLd2wisS8niNBMJ2omv5gG+iGDDwd8jzag==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22}
peerDependencies:
svelte: ^4.0.0 || ^5.0.0-next.0
svelte: ^5.0.0-next.96 || ^5.0.0
vite: ^5.0.0
'@szmarczak/http-timer@5.0.1':
@@ -2021,6 +2032,11 @@ packages:
'@webext-core/match-patterns@1.0.3':
resolution: {integrity: sha512-NY39ACqCxdKBmHgw361M9pfJma8e4AZo20w9AY+5ZjIj1W2dvXC8J31G5fjfOGbulW9w4WKpT8fPooi0mLkn9A==}
acorn-typescript@1.4.13:
resolution: {integrity: sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==}
peerDependencies:
acorn: '>=8.9.0'
acorn@8.12.1:
resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==}
engines: {node: '>=0.4.0'}
@@ -2074,8 +2090,9 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
aria-query@5.3.2:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
array-differ@4.0.0:
resolution: {integrity: sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==}
@@ -2106,8 +2123,9 @@ packages:
peerDependencies:
postcss: ^8.1.0
axobject-query@4.0.0:
resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==}
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
babel-plugin-jsx-dom-expressions@0.37.21:
resolution: {integrity: sha512-WbQo1NQ241oki8bYasVzkMXOTSIri5GO/K47rYJb2ZBh8GaPUEWiWbMV3KwXz+96eU2i54N6ThzjQG/f5n8Azw==}
@@ -2335,9 +2353,6 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
code-red@1.0.4:
resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@@ -2702,6 +2717,12 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
esm-env@1.1.4:
resolution: {integrity: sha512-oO82nKPHKkzIj/hbtuDYy/JHqBHFlMIW36SDiPCVsj87ntDLcWN+sJ1erdVryd4NxODacFTsdrIE3b7IamqbOg==}
esrap@1.2.2:
resolution: {integrity: sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
@@ -3340,6 +3361,9 @@ packages:
magic-string@0.30.11:
resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==}
magic-string@0.30.12:
resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==}
magicast@0.3.5:
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
@@ -3353,6 +3377,9 @@ packages:
mark.js@8.11.1:
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
markdown-it-footnote@4.0.0:
resolution: {integrity: sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ==}
marked@4.3.0:
resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==}
engines: {node: '>= 12'}
@@ -3730,9 +3757,6 @@ packages:
perfect-debounce@1.0.0:
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
periscopic@3.1.0:
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
picocolors@1.1.0:
resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==}
@@ -4364,15 +4388,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
svelte-hmr@0.16.0:
resolution: {integrity: sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==}
engines: {node: ^12.20 || ^14.13.1 || >= 16}
peerDependencies:
svelte: ^3.19.0 || ^4.0.0
svelte@4.2.18:
resolution: {integrity: sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==}
engines: {node: '>=16'}
svelte@5.1.6:
resolution: {integrity: sha512-bYS/DpkqXk0j5UZgiNXrEjZYPRZ4Ncd87w4KUSbcZGyojA0+i/Ls9OGUjETHmdLe8RcQ0G8SX/T0PypPpAA/ew==}
engines: {node: '>=18'}
svgo@3.3.2:
resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==}
@@ -4678,6 +4696,14 @@ packages:
vite:
optional: true
vitefu@1.0.3:
resolution: {integrity: sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==}
peerDependencies:
vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0-beta.0
peerDependenciesMeta:
vite:
optional: true
vitepress@1.3.4:
resolution: {integrity: sha512-I1/F6OW1xl3kW4PaIMC6snxjWgf3qfziq2aqsDoFc/Gt41WbcRv++z8zjw8qGRIJ+I4bUW7ZcKFDHHN/jkH9DQ==}
hasBin: true
@@ -4880,6 +4906,9 @@ packages:
yauzl@2.10.0:
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
zimmerframe@1.1.2:
resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==}
zip-dir@2.0.0:
resolution: {integrity: sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg==}
@@ -5884,26 +5913,25 @@ snapshots:
'@sindresorhus/is@5.4.1': {}
'@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.18)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4)))(svelte@4.2.18)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4))':
'@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.6)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4)))(svelte@5.1.6)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4))':
dependencies:
'@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.18)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4))
debug: 4.3.5
svelte: 4.2.18
'@sveltejs/vite-plugin-svelte': 4.0.0(svelte@5.1.6)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4))
debug: 4.3.7
svelte: 5.1.6
vite: 5.4.8(@types/node@20.16.10)(sass@1.79.4)
transitivePeerDependencies:
- supports-color
'@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.18)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4))':
'@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.6)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4))':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.18)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4)))(svelte@4.2.18)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4))
debug: 4.3.5
'@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.6)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4)))(svelte@5.1.6)(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4))
debug: 4.3.7
deepmerge: 4.3.1
kleur: 4.1.5
magic-string: 0.30.11
svelte: 4.2.18
svelte-hmr: 0.16.0(svelte@4.2.18)
magic-string: 0.30.12
svelte: 5.1.6
vite: 5.4.8(@types/node@20.16.10)(sass@1.79.4)
vitefu: 0.2.5(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4))
vitefu: 1.0.3(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4))
transitivePeerDependencies:
- supports-color
@@ -6365,6 +6393,10 @@ snapshots:
'@webext-core/match-patterns@1.0.3': {}
acorn-typescript@1.4.13(acorn@8.12.1):
dependencies:
acorn: 8.12.1
acorn@8.12.1: {}
adm-zip@0.5.10: {}
@@ -6421,9 +6453,7 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.1
aria-query@5.3.0:
dependencies:
dequal: 2.0.3
aria-query@5.3.2: {}
array-differ@4.0.0: {}
@@ -6449,9 +6479,7 @@ snapshots:
postcss: 8.4.39
postcss-value-parser: 4.2.0
axobject-query@4.0.0:
dependencies:
dequal: 2.0.3
axobject-query@4.1.0: {}
babel-plugin-jsx-dom-expressions@0.37.21(@babel/core@7.24.7):
dependencies:
@@ -6727,14 +6755,6 @@ snapshots:
clone@1.0.4: {}
code-red@1.0.4:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
'@types/estree': 1.0.6
acorn: 8.12.1
estree-walker: 3.0.3
periscopic: 3.1.0
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
@@ -7129,6 +7149,13 @@ snapshots:
escape-string-regexp@5.0.0: {}
esm-env@1.1.4: {}
esrap@1.2.2:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
'@types/estree': 1.0.6
estree-walker@2.0.2: {}
estree-walker@3.0.3:
@@ -7789,6 +7816,10 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
magic-string@0.30.12:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
magicast@0.3.5:
dependencies:
'@babel/parser': 7.25.6
@@ -7803,6 +7834,8 @@ snapshots:
mark.js@8.11.1: {}
markdown-it-footnote@4.0.0(patch_hash=ue34jdgdx43siqdj557feoepzq): {}
marked@4.3.0: {}
marky@1.2.5: {}
@@ -8186,12 +8219,6 @@ snapshots:
perfect-debounce@1.0.0: {}
periscopic@3.1.0:
dependencies:
'@types/estree': 1.0.6
estree-walker: 3.0.3
is-reference: 3.0.2
picocolors@1.1.0: {}
picomatch@2.3.1: {}
@@ -8844,26 +8871,21 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
svelte-hmr@0.16.0(svelte@4.2.18):
dependencies:
svelte: 4.2.18
svelte@4.2.18:
svelte@5.1.6:
dependencies:
'@ampproject/remapping': 2.3.0
'@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25
'@types/estree': 1.0.6
acorn: 8.12.1
aria-query: 5.3.0
axobject-query: 4.0.0
code-red: 1.0.4
css-tree: 2.3.1
estree-walker: 3.0.3
acorn-typescript: 1.4.13(acorn@8.12.1)
aria-query: 5.3.2
axobject-query: 4.1.0
esm-env: 1.1.4
esrap: 1.2.2
is-reference: 3.0.2
locate-character: 3.0.0
magic-string: 0.30.11
periscopic: 3.1.0
magic-string: 0.30.12
zimmerframe: 1.1.2
svgo@3.3.2:
dependencies:
@@ -9218,6 +9240,10 @@ snapshots:
optionalDependencies:
vite: 5.4.8(@types/node@20.16.10)(sass@1.79.4)
vitefu@1.0.3(vite@5.4.8(@types/node@20.16.10)(sass@1.79.4)):
optionalDependencies:
vite: 5.4.8(@types/node@20.16.10)(sass@1.79.4)
vitepress@1.3.4(@algolia/client-search@4.20.0)(@types/node@20.16.10)(@types/react@18.3.11)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.79.4)(search-insights@2.15.0)(typescript@5.6.2):
dependencies:
'@docsearch/css': 3.6.2
@@ -9477,6 +9503,8 @@ snapshots:
buffer-crc32: 0.2.13
fd-slicer: 1.1.0
zimmerframe@1.1.2: {}
zip-dir@2.0.0:
dependencies:
async: 3.2.4
+1
View File
@@ -19,6 +19,7 @@
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/chrome": "^0.0.269",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"@wxt-dev/module-react": "^1.1.1",
+1
View File
@@ -18,6 +18,7 @@
"solid-js": "^1.9.1"
},
"devDependencies": {
"@types/chrome": "^0.0.269",
"@wxt-dev/module-solid": "^1.1.2",
"typescript": "^5.6.2",
"wxt": "^0.19.11"
+1
View File
@@ -15,6 +15,7 @@
"postinstall": "wxt prepare"
},
"devDependencies": {
"@types/chrome": "^0.0.269",
"@tsconfig/svelte": "^5.0.4",
"@wxt-dev/module-svelte": "^1.0.1",
"svelte": "^4.2.19",
+1
View File
@@ -15,6 +15,7 @@
"postinstall": "wxt prepare"
},
"devDependencies": {
"@types/chrome": "^0.0.269",
"typescript": "^5.6.2",
"wxt": "^0.19.11"
}
+1
View File
@@ -18,6 +18,7 @@
"vue": "^3.5.11"
},
"devDependencies": {
"@types/chrome": "^0.0.269",
"@wxt-dev/module-vue": "^1.0.1",
"typescript": "^5.6.2",
"vue-tsc": "^2.1.6",