Compare commits

...

18 Commits

Author SHA1 Message Date
GitHub Actions fba8f0d017 chore(release): v0.6.0 2023-09-22 17:45:55 +00:00
Aaron Klinker 3f260ee777 fix: Disable dev logs in production
This closes #119
2023-09-22 12:05:05 -05:00
Aaron Klinker 1f6a931b02 fix: Use the same mode for each build step 2023-09-22 11:57:36 -05:00
Aaron 07891d028b feat!: Require a function for vite configuration (#121)
BREAKING CHANGE: The `vite` config option must now be a function. If you were using an object before, change it from `vite: { ... }` to `vite: () => ({ ... })`.
2023-09-22 11:51:55 -05:00
Aaron Klinker 1f448d1bf0 feat: Export ContentScriptContext from wxt/client 2023-09-22 11:00:28 -05:00
Aaron Klinker 7722986537 Update changelog authors 2023-09-22 10:56:22 -05:00
GitHub Actions 4b73168618 chore(release): v0.5.6 2023-09-22 15:48:35 +00:00
Aaron 16cebc538f feat: Add ContentScriptContext util for stopping invalidated content scripts (#120) 2023-09-21 18:48:07 -05:00
Aaron e4aaba98ab Remove defer from umami script 2023-09-20 21:09:30 -05:00
Aaron c190d8c44c Add umami to docs website 2023-09-20 21:08:51 -05:00
GitHub Actions 5801f96f79 chore(release): v0.5.5 2023-09-19 23:00:53 +00:00
Aaron Klinker 92039b855e fix: Automatically replace vite's process.env.NODE_ENV output in lib mode with the mode 2023-09-19 17:56:10 -05:00
GitHub Actions f145e00cc9 chore(release): v0.5.4 2023-09-19 22:39:57 +00:00
Aaron Klinker 87e8df90f5 fix: Don't warn about deep entrypoint subdirectories not being recognized 2023-09-19 17:32:25 -05:00
Aaron Klinker 419fab8193 fix: Recognize background/index.ts as an entrypoint 2023-09-19 17:32:25 -05:00
GitHub Actions 71743b3bf3 chore(release): v0.5.3 2023-09-19 21:40:41 +00:00
Aaron 8e3169eb1e chore: Refactor how config is resolved (#118) 2023-09-19 16:36:00 -05:00
Aaron Klinker 4ec904ea3c fix: Allow function for vite config 2023-09-19 12:53:35 -05:00
37 changed files with 644 additions and 243 deletions
+78 -3
View File
@@ -1,5 +1,81 @@
# Changelog
## v0.6.0
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.6...v0.6.0)
### 🚀 Enhancements
- Export `ContentScriptContext` from `wxt/client` ([1f448d1](https://github.com/wxt-dev/wxt/commit/1f448d1))
- ⚠️ Require a function for `vite` configuration ([#121](https://github.com/wxt-dev/wxt/pull/121))
### 🩹 Fixes
- Use the same mode for each build step ([1f6a931](https://github.com/wxt-dev/wxt/commit/1f6a931))
- Disable dev logs in production ([3f260ee](https://github.com/wxt-dev/wxt/commit/3f260ee))
#### ⚠️ Breaking Changes
- ⚠️ Require a function for `vite` configuration ([#121](https://github.com/wxt-dev/wxt/pull/121))
### ❤️ Contributors
- Aaron Klinker <aaronklinker1@gmail.com>
- Aaron <aaronklinker1@gmail.com>
## v0.5.6
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.5...v0.5.6)
### 🚀 Enhancements
- Add `ContentScriptContext` util for stopping invalidated content scripts ([#120](https://github.com/wxt-dev/wxt/pull/120))
### ❤️ Contributors
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
## v0.5.5
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.4...v0.5.5)
### 🩹 Fixes
- Automatically replace vite's `process.env.NODE_ENV` output in lib mode with the mode ([92039b8](https://github.com/wxt-dev/wxt/commit/92039b8))
### ❤️ Contributors
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
## v0.5.4
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.3...v0.5.4)
### 🩹 Fixes
- Recognize `background/index.ts` as an entrypoint ([419fab8](https://github.com/wxt-dev/wxt/commit/419fab8))
- Don't warn about deep entrypoint subdirectories not being recognized ([87e8df9](https://github.com/wxt-dev/wxt/commit/87e8df9))
### ❤️ Contributors
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
## v0.5.3
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.2...v0.5.3)
### 🩹 Fixes
- Allow function for vite config ([4ec904e](https://github.com/wxt-dev/wxt/commit/4ec904e))
### 🏡 Chore
- Refactor how config is resolved ([#118](https://github.com/wxt-dev/wxt/pull/118))
### ❤️ Contributors
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
## v0.5.2
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.1...v0.5.2)
@@ -10,7 +86,7 @@
### ❤️ Contributors
- Aaron <aaronklinker1@gmail.com>
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
## v0.5.1
@@ -27,8 +103,7 @@
### ❤️ Contributors
- Aaron <aaronklinker1@gmail.com>
- Aaron Klinker <aaronklinker1@gmail.com>
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
## v0.5.0
+2
View File
@@ -19,4 +19,6 @@ export default defineBackground(() => {
browser.i18n.getMessage('hello', 'Aaron');
browser.i18n.getMessage('bye', ['Aaron']);
browser.i18n.getMessage('@@extension_id');
console.log('WXT MODE:', import.meta.env.MODE);
});
+2
View File
@@ -3,3 +3,5 @@ import 'url:https://code.jquery.com/jquery-3.7.1.slim.min.js';
console.log(browser.runtime.id);
logId();
console.log(2);
console.log('WXT MODE:', import.meta.env.MODE);
@@ -2,9 +2,17 @@ import '../../common/style.css';
export default defineContentScript({
matches: ['*://*/*'],
async main() {
async main(ctx) {
console.log(browser.runtime.id);
logId();
console.log('WXT MODE:', import.meta.env.MODE);
const n = (Math.random() * 100).toFixed(1);
ctx.setInterval(() => {
console.log(n, browser.runtime.id);
}, 1e3);
mountContentScriptUi();
},
});
-3
View File
@@ -2,9 +2,6 @@ import { defineConfig } from 'wxt';
export default defineConfig({
srcDir: 'src',
storeIds: {
chrome: '123',
},
manifest: {
default_locale: 'en',
},
+11
View File
@@ -11,6 +11,17 @@ export default defineConfig({
description: 'Next gen framework for developing web extensions',
lastUpdated: true,
head: [
[
"script",
{
async: "",
"data-website-id": "c1840c18-a12c-4a45-a848-55ae85ef7915",
src: "https://umami.aklinker1.io/script.js",
},
],
],
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
logo: '/logo.svg',
+1
View File
@@ -9,6 +9,7 @@ For MV2, the background is added as a script to the background page. For MV3, th
<EntrypointPatterns
:patterns="[
['background.ts', 'background.js'],
['background/index.ts', 'background.js'],
]"
/>
+39 -2
View File
@@ -34,7 +34,7 @@ export default defineContentScript({
include: undefined | string[],
exclude: undefined | string[],
main() {
main(ctx) {
// Executed when content script is loaded
},
});
@@ -63,7 +63,7 @@ import './style.css';
export default defineContentScript({
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
main() {
main(ctx) {
// ...
},
});
@@ -83,3 +83,40 @@ Any styles imported in your content script will be added to that content script'
]
}
```
## Context
Old content scripts are not automatically stopped when an extension updates and restarts. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API. Since it's not connected to the latest version of your extension, the browser decides to throw an error.
WXT provides a utility for managing this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
```ts
export default defineContentScript({
// ...
main(ctx: ContentScriptContext) {
// Add custom listeners for stopping work
ctx.onInvalidated(() => {
// ...
});
// Stop fetch requests
fetch('...url', { signal: ctx.signal });
// Timeout utilities
ctx.setTimeout(() => {
// ...
}, 5e3);
ctx.setInterval(() => {
// ...
}, 60e3);
},
});
```
The class extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and provides other utilities for stopping a content script's logic once it becomes invalidated.
:::tip
When working with content scripts, **you should always use the `ctx` object to stop any async or future work.**
This prevents old content scripts from interfering with new content scripts, and prevents error messages from the console in production.
:::
+1
View File
@@ -16,6 +16,7 @@ describe('Auto Imports', () => {
// Generated by wxt
export {}
declare global {
const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
const browser: typeof import('wxt/browser')['browser']
const defineBackground: typeof import('wxt/client')['defineBackground']
const defineConfig: typeof import('wxt')['defineConfig']
+14 -8
View File
@@ -43,7 +43,12 @@ describe('Output Directory Structure', () => {
await project.build();
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
expect(
await project.serializeOutput([
'.output/chrome-mv3/content-scripts/one.js',
'.output/chrome-mv3/content-scripts/two.js',
]),
).toMatchInlineSnapshot(`
".output/chrome-mv3/assets/one.css
----------------------------------------
body{color:#00f}
@@ -56,13 +61,11 @@ describe('Output Directory Structure', () => {
================================================================================
.output/chrome-mv3/content-scripts/one.js
----------------------------------------
(function(){\\"use strict\\";function i(n){return n}const s=\\"\\",o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
<contents-ignored>
================================================================================
.output/chrome-mv3/content-scripts/two.js
----------------------------------------
(function(){\\"use strict\\";function i(n){return n}const s=\\"\\",o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
<contents-ignored>
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
@@ -82,11 +85,14 @@ describe('Output Directory Structure', () => {
await project.build();
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
expect(
await project.serializeOutput([
'.output/chrome-mv3/content-scripts/overlay-one.js',
]),
).toMatchInlineSnapshot(`
".output/chrome-mv3/content-scripts/overlay-one.js
----------------------------------------
(function(){\\"use strict\\";function i(n){return n}const o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
<contents-ignored>
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
+1
View File
@@ -22,6 +22,7 @@ describe('TypeScript Project', () => {
const __IS_EDGE__: boolean;
const __IS_OPERA__: boolean;
const __COMMAND__: \\"build\\" | \\"serve\\";
const __ENTRYPOINT__: string;
}
"
`);
File diff suppressed because one or more lines are too long
+23 -6
View File
@@ -80,9 +80,12 @@ export class TestProject {
/**
* Read all the files from the test project's `.output` directory and combine them into a string
* that can be used in a snapshot.
*
* Optionally, provide a list of filenames whose content is not printed (because it's inconsistent
* or not relevant to a test).
*/
serializeOutput(): Promise<string> {
return this.serializeDir('.output');
serializeOutput(ignoreContentsOfFilenames?: string[]): Promise<string> {
return this.serializeDir('.output', ignoreContentsOfFilenames);
}
/**
@@ -95,7 +98,16 @@ export class TestProject {
);
}
private async serializeDir(dir: string): Promise<string> {
/**
* Deeply print the filename and contents of all files in a directory.
*
* Optionally, provide a list of filenames whose content is not printed (because it's inconsistent
* or not relevant to a test).
*/
private async serializeDir(
dir: string,
ignoreContentsOfFilenames?: string[],
): Promise<string> {
const outputFiles = await glob('**/*', {
cwd: resolve(this.root, dir),
ignore: ['**/node_modules', '**/.output'],
@@ -104,19 +116,24 @@ export class TestProject {
const fileContents = [];
for (const file of outputFiles) {
const path = resolve(this.root, dir, file);
fileContents.push(await this.serializeFile(path));
const isContentIgnored = !!ignoreContentsOfFilenames?.find(
(ignoredFile) => normalizePath(path).endsWith(ignoredFile),
);
fileContents.push(await this.serializeFile(path, isContentIgnored));
}
return fileContents.join(`\n${''.padEnd(80, '=')}\n`);
}
/**
* @param path An abosolute path to a file or a path relative to the root.
* @param ignoreContents An optional boolean that, when true, causes this function to not print
* the file contents.
*/
async serializeFile(path: string): Promise<string> {
async serializeFile(path: string, ignoreContents?: boolean): Promise<string> {
const absolutePath = resolve(this.root, path);
return [
normalizePath(relative(this.root, absolutePath)),
await fs.readFile(absolutePath),
ignoreContents ? '<contents-ignored>' : await fs.readFile(absolutePath),
].join(`\n${''.padEnd(40, '-')}\n`);
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.5.2",
"version": "0.6.0",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18.16.0",
+1 -2
View File
@@ -14,10 +14,9 @@ export const build = defineCommand<
},
]
>(async (root, flags) => {
const mode = flags.mode ?? 'production';
const cliConfig: wxt.InlineConfig = {
root,
mode,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
+1 -2
View File
@@ -14,10 +14,9 @@ export const dev = defineCommand<
},
]
>(async (root, flags) => {
const mode = flags.mode ?? 'development';
const cliConfig: wxt.InlineConfig = {
root,
mode,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
+1 -2
View File
@@ -17,10 +17,9 @@ export const zip = defineCommand<
},
]
>(async (root, flags) => {
const mode = flags.mode ?? 'production';
const cliConfig: wxt.InlineConfig = {
root,
mode,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
+1
View File
@@ -1,3 +1,4 @@
export * from './defineContentScript';
export * from './defineBackground';
export * from './mountContentScriptUi';
export * from './utils/ContentScriptContext';
+139
View File
@@ -0,0 +1,139 @@
import { browser } from '../browser';
import { logger } from './logger';
/**
* Extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
* Used to detect and stop content script code when the script is invalidated.
*
* It also provides several utilities like `ctx.setTimeout` and `ctx.setInterval` that should be used in
* content scripts instead of `window.setTimeout` or `window.setInterval`.
*/
export class ContentScriptContext extends AbortController {
static SCRIPT_STARTED_MESSAGE_TYPE = 'wxt:content-script-started';
#isTopFrame = window.self === window.top;
constructor(private readonly contentScriptName: string) {
super();
if (this.#isTopFrame) {
this.#stopOldScripts();
}
this.setTimeout(() => {
// Run on next tick so the listener it adds isn't triggered by stopOldScript
this.#listenForNewerScripts();
});
}
get isInvalid(): boolean {
if (browser.runtime.id == null) {
this.notifyInvalidated(); // Sets `signal.aborted` to true
}
return this.signal.aborted;
}
get isValid(): boolean {
return !this.isInvalid;
}
/**
* Add a listener that is called when the content script's context is invalidated.
*
* @returns A function to remove the listener.
*
* @example
* browser.runtime.onMessage.addListener(cb);
* const removeInvalidatedListener = ctx.onInvalidated(() => {
* browser.runtime.onMessage.removeListener(cb);
* })
* // ...
* removeInvalidatedListener();
*/
onInvalidated(cb: () => void): () => void {
this.signal.addEventListener('abort', cb);
return () => this.signal.removeEventListener('abort', cb);
}
/**
* Wrapper around `window.setInterval` that automatically clears the interval when invalidated.
*/
setInterval(handler: () => void, timeout?: number): number {
const id = setInterval(() => {
if (this.isValid) handler();
}, timeout) as unknown as number;
this.onInvalidated(() => clearInterval(id));
return id;
}
/**
* Wrapper around `window.setTimeout` that automatically clears the interval when invalidated.
*/
setTimeout(handler: () => void, timeout?: number): number {
const id = setTimeout(() => {
if (this.isValid) handler();
}, timeout) as unknown as number;
this.onInvalidated(() => clearTimeout(id));
return id;
}
/**
* Wrapper around `window.requestAnimationFrame` that automatically cancels the request when
* invalidated.
*/
requestAnimationFrame(callback: FrameRequestCallback): number {
const id = requestAnimationFrame((...args) => {
if (this.isValid) callback(...args);
});
this.onInvalidated(() => cancelAnimationFrame(id));
return id;
}
/**
* Wrapper around `window.requestIdleCallback` that automatically cancels the request when
* invalidated.
*/
requestIdleCallback(
callback: IdleRequestCallback,
options?: IdleRequestOptions,
): number {
const id = requestIdleCallback((...args) => {
if (!this.signal.aborted) callback(...args);
}, options);
this.onInvalidated(() => cancelIdleCallback(id));
return id;
}
/**
* Abort the abort controller and execute all `onInvalidated` listeners.
*/
notifyInvalidated() {
this.abort('Content script context invalidated');
logger.debug(
`Content script "${this.contentScriptName}" context invalidated`,
);
}
#stopOldScripts() {
// Use postMessage so it get's sent to all the frames of the page.
window.postMessage({
event: ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE,
contentScriptName: this.contentScriptName,
});
}
#listenForNewerScripts() {
const cb = (event: MessageEvent) => {
if (
event.data?.type === ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE &&
event.data?.contentScriptName === this.contentScriptName
) {
this.notifyInvalidated();
}
};
addEventListener('message', cb);
this.onInvalidated(() => removeEventListener('message', cb));
}
}
+2
View File
@@ -1,4 +1,6 @@
function print(method: (...args: any[]) => void, ...args: any[]) {
if (import.meta.env.MODE === 'production') return;
if (typeof args[0] === 'string') {
const message = args.shift();
method(`[wxt] ${message}`, ...args);
@@ -1,9 +1,12 @@
import definition from 'virtual:user-content-script';
import { logger } from '../utils/logger';
import { ContentScriptContext } from '../utils/ContentScriptContext';
(async () => {
try {
await definition.main();
const ctx = new ContentScriptContext(__ENTRYPOINT__);
await definition.main(ctx);
} catch (err) {
logger.error('The content script crashed on startup!', err);
}
@@ -21,3 +21,4 @@ declare const __DEV_SERVER_PROTOCOL__: string;
declare const __DEV_SERVER_HOSTNAME__: string;
declare const __DEV_SERVER_PORT__: string;
declare const __MANIFEST_VERSION__: 2 | 3;
declare const __ENTRYPOINT__: string;
@@ -214,6 +214,15 @@ describe('findEntrypoints', () => {
outputDir: config.outDir,
},
],
[
'background/index.ts',
{
type: 'background',
name: 'background',
inputPath: resolve(config.entrypointsDir, 'background/index.ts'),
outputDir: config.outDir,
},
],
])(
'should find and load background entrypoint config from %s',
async (path, expected) => {
+14 -2
View File
@@ -12,6 +12,7 @@ import { getEntrypointBundlePath } from '../utils/entrypoints';
import fs from 'fs-extra';
import { dirname, resolve } from 'path';
import { getPublicFiles } from '../utils/public';
import { getEntrypointGlobals } from '../utils/globals';
export async function buildEntrypoints(
groups: EntrypointGroup[],
@@ -78,10 +79,17 @@ async function buildSingleEntrypoint(
},
},
},
define: {
// See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
'process.env.NODE_ENV': JSON.stringify(config.mode),
},
};
for (const global of getEntrypointGlobals(config, entrypoint.name)) {
libMode.define![global.name] = JSON.stringify(global.value);
}
const entryConfig = vite.mergeConfig(
libMode,
config.vite,
await config.vite(config.env),
) as vite.InlineConfig;
const result = await vite.build(entryConfig);
@@ -116,11 +124,15 @@ async function buildMultipleEntrypoints(
},
},
},
define: {},
};
for (const global of getEntrypointGlobals(config, 'html')) {
multiPage.define![global.name] = JSON.stringify(global.value);
}
const entryConfig = vite.mergeConfig(
multiPage,
config.vite,
await config.vite(config.env),
) as vite.UserConfig;
const result = await vite.build(entryConfig);
+3 -2
View File
@@ -350,6 +350,7 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
'devtools/index.html': 'devtools',
'background.ts': 'background',
'background/index.ts': 'background',
[VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background',
'content.ts?(x)': 'content-script',
@@ -374,8 +375,8 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
[`*.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
[`*/index.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
// Don't warn about any files in subdirectories, like CSS or JS entrypoints for HTML files
'*/*': 'ignored',
// Don't warn about any files in subdirectories, like CSS or JS entrypoints for HTML files or tests
'*/**': 'ignored',
};
const CONTENT_SCRIPT_OUT_DIR = 'content-scripts';
+2 -2
View File
@@ -4,7 +4,7 @@ import fs from 'fs-extra';
import { relative, resolve } from 'path';
import { getEntrypointBundlePath } from '../utils/entrypoints';
import { getUnimportOptions } from '../utils/auto-imports';
import { getGlobals } from '../utils/globals';
import { getEntrypointGlobals, getGlobals } from '../utils/globals';
import { getPublicFiles } from '../utils/public';
import { normalizePath } from '../utils/paths';
import path from 'node:path';
@@ -157,7 +157,7 @@ async function writeGlobalsDeclarationFile(
config: InternalConfig,
): Promise<string> {
const filePath = resolve(config.typesDir, 'globals.d.ts');
const globals = getGlobals(config);
const globals = [...getGlobals(config), ...getEntrypointGlobals(config, '')];
await writeFileIfDifferent(
filePath,
[
+1 -1
View File
@@ -37,7 +37,7 @@ export async function setupServer(
const runner = createWebExtRunner();
const viteServer = await vite.createServer(
vite.mergeConfig(serverInfo, config.vite),
vite.mergeConfig(serverInfo, await config.vite(config.env)),
);
const start = async () => {
+15 -12
View File
@@ -3,6 +3,7 @@ import { Manifest, Scripting } from 'webextension-polyfill';
import { UnimportOptions } from 'unimport';
import { EntrypointGroup } from '.';
import { LogLevel } from 'consola';
import { ContentScriptContext } from '../../client/utils/ContentScriptContext';
export interface InlineConfig {
/**
@@ -43,14 +44,6 @@ export interface InlineConfig {
* @default false
*/
debug?: boolean;
/**
* ID of the extension for each store. Used for publishing.
*/
storeIds?: {
chrome?: string;
firefox?: string;
edge?: string;
};
/**
* Explicitly set a mode to run in. This will override the default mode for each command, and can
* be overridden by the command line `--mode` option.
@@ -91,11 +84,16 @@ export interface InlineConfig {
*/
logger?: Logger;
/**
* Custom Vite options, see <https://vitejs.dev/config/shared-options.html>.
* Return custom Vite options from a function. See
* <https://vitejs.dev/config/shared-options.html>.
*
* [`root`](#root), [`configFile`](#configfile), and [`mode`](#mode) should be set in WXT's config.
* [`root`](#root), [`configFile`](#configfile), and [`mode`](#mode) should be set in WXT's config
* instead of Vite's.
*
* This is a function because any vite plugins added need to be recreated for each individual
* build step, incase they have internal state causing them to fail when reused.
*/
vite?: Omit<vite.UserConfig, 'root' | 'configFile' | 'mode'>;
vite?: (env: ConfigEnv) => WxtViteConfig | Promise<WxtViteConfig>;
/**
* Customize the `manifest.json` output. Can be an object, promise, or function that returns an
* object or promise.
@@ -375,7 +373,7 @@ export interface ContentScriptDefinition extends ExcludableEntrypoint {
/**
* Main function executed when the content script is loaded.
*/
main(): void | Promise<void>;
main(ctx: ContentScriptContext): void | Promise<void>;
}
export interface BackgroundScriptDefintition extends ExcludableEntrypoint {
@@ -484,3 +482,8 @@ export interface ExtensionRunnerConfig {
*/
startUrls?: string[];
}
export type WxtViteConfig = Omit<
vite.UserConfig,
'root' | 'configFile' | 'mode'
>;
+3 -6
View File
@@ -7,6 +7,7 @@ import {
TargetManifestVersion,
UserManifest,
ExtensionRunnerConfig,
ConfigEnv,
} from './external';
import { UnimportOptions } from 'unimport';
import { ResolvedConfig } from 'c12';
@@ -21,18 +22,14 @@ export interface InternalConfig {
outBaseDir: string;
outDir: string;
debug: boolean;
storeIds: {
chrome?: string;
firefox?: string;
edge?: string;
};
mode: string;
command: 'build' | 'serve';
browser: TargetBrowser;
manifestVersion: TargetManifestVersion;
env: ConfigEnv;
logger: Logger;
imports: false | Partial<UnimportOptions>;
vite: vite.InlineConfig;
vite: (env: ConfigEnv) => Promise<vite.InlineConfig> | vite.InlineConfig;
manifest: UserManifest;
fsCache: FsCache;
server?: WxtDevServer;
+190 -135
View File
@@ -1,176 +1,113 @@
import { loadConfig } from 'c12';
import {
ConfigEnv,
ExtensionRunnerConfig,
InlineConfig,
InternalConfig,
UserConfig,
UserManifest,
ConfigEnv,
UserManifestFn,
UserManifest,
WxtViteConfig,
ExtensionRunnerConfig,
} from '../types';
import path, { resolve } from 'node:path';
import path from 'node:path';
import * as vite from 'vite';
import { LogLevels, consola } from 'consola';
import * as plugins from '../vite-plugins';
import { createFsCache } from './createFsCache';
import consola, { LogLevels } from 'consola';
import * as plugins from '../vite-plugins';
import { getGlobals } from './globals';
import { loadConfig } from 'c12';
/**
* Given an inline config, discover the config file if necessary, merge the results, resolve any
* relative paths, and apply any defaults.
*
* Inline config always has priority over user config. Cli flags are passed as inline config if set.
* If unset, undefined is passed in, letting this function decide default values.
*/
export async function getInternalConfig(
config: InlineConfig,
inlineConfig: InlineConfig,
command: 'build' | 'serve',
): Promise<InternalConfig> {
// Apply defaults to a base config
const root = config.root ? path.resolve(config.root) : process.cwd();
const mode =
config.mode ?? (command === 'build' ? 'production' : 'development');
const browser = config.browser ?? 'chrome';
const manifestVersion =
config.manifestVersion ?? (browser == 'firefox' ? 2 : 3);
const outBaseDir = path.resolve(root, '.output');
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
const logger = config.logger ?? consola;
const debug = !!config.debug;
if (debug) logger.level = LogLevels.debug;
// Load user config
const baseConfig: InternalConfigNoUserDirs = {
root,
outDir,
outBaseDir,
storeIds: config.storeIds ?? {},
browser,
manifestVersion,
mode,
command,
debug,
logger,
vite: config.vite ?? {},
imports: config.imports ?? {},
runnerConfig: await loadConfig<ExtensionRunnerConfig>({
name: 'web-ext',
cwd: root,
globalRc: true,
rcFile: '.webextrc',
overrides: config.runner,
}),
};
// Load user config from file
let userConfig: UserConfig = {
mode,
};
if (config.configFile !== false) {
let userConfig: UserConfig = {};
if (inlineConfig.configFile !== false) {
const loaded = await loadConfig<UserConfig>({
name: 'wxt',
cwd: root,
cwd: inlineConfig.root ?? process.cwd(),
rcFile: false,
});
userConfig = loaded.config ?? {};
}
// Merge inline and user configs
const merged = vite.mergeConfig(
baseConfig,
userConfig,
) as InternalConfigNoUserDirs;
// Merge it into the inline config
// Apply user config and create final config
const srcDir = userConfig.srcDir ? resolve(root, userConfig.srcDir) : root;
const entrypointsDir = resolve(
srcDir,
userConfig.entrypointsDir ?? 'entrypoints',
const mergedConfig = mergeInlineConfig(inlineConfig, userConfig);
// Apply defaults to make internal config.
const debug = mergedConfig.debug ?? false;
const logger = mergedConfig.logger ?? consola;
if (debug) logger.level = LogLevels.debug;
const browser = mergedConfig.browser ?? 'chrome';
const manifestVersion =
mergedConfig.manifestVersion ?? (browser == 'firefox' ? 2 : 3);
const mode =
mergedConfig.mode ?? (command === 'build' ? 'production' : 'development');
const env: ConfigEnv = { browser, command, manifestVersion, mode };
const root = path.resolve(
inlineConfig.root ?? userConfig.root ?? process.cwd(),
);
const publicDir = resolve(srcDir, userConfig.publicDir ?? 'public');
const wxtDir = resolve(root, '.wxt');
const typesDir = resolve(wxtDir, 'types');
const wxtDir = path.resolve(root, '.wxt');
const srcDir = path.resolve(root, mergedConfig.srcDir ?? root);
const entrypointsDir = path.resolve(
srcDir,
mergedConfig.entrypointsDir ?? 'entrypoints',
);
const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
const typesDir = path.resolve(wxtDir, 'types');
const outBaseDir = path.resolve(root, '.output');
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
// Merge manifest sources
const env: ConfigEnv = { mode, browser, manifestVersion, command };
const userManifest = await resolveManifestConfig(env, userConfig.manifest);
const inlineManifest = await resolveManifestConfig(env, config.manifest);
const manifest = vite.mergeConfig(userManifest, inlineManifest);
const runnerConfig = await loadConfig<ExtensionRunnerConfig>({
name: 'web-ext',
cwd: root,
globalRc: true,
rcFile: '.webextrc',
overrides: mergedConfig.runner,
});
const finalConfig: InternalConfig = {
...merged,
srcDir,
browser,
command,
debug,
entrypointsDir,
publicDir,
wxtDir: wxtDir,
typesDir,
env,
fsCache: createFsCache(wxtDir),
manifest,
zip: {
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
sourcesRoot: root,
...userConfig.zip,
...config.zip,
ignoredSources: [
'**/node_modules',
// WXT files
'**/web-ext.config.ts',
// Hidden files
'**/.*',
// Tests
'**/__tests__/**',
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
// User config
...(userConfig.zip?.ignoredSources ?? []),
...(config.zip?.ignoredSources ?? []),
],
},
imports: mergedConfig.imports ?? {},
logger,
manifest: await resolveManifestConfig(env, mergedConfig.manifest),
manifestVersion,
mode,
outBaseDir,
outDir,
publicDir,
root,
runnerConfig,
srcDir,
typesDir,
vite: () => ({}), // Real value added after this object is initialized.
wxtDir,
zip: resolveInternalZipConfig(root, mergedConfig),
};
// Customize the default vite config
finalConfig.vite.root = root;
finalConfig.vite.configFile = false;
finalConfig.vite.logLevel = 'warn';
finalConfig.vite.build ??= {};
finalConfig.vite.build.outDir = outDir;
finalConfig.vite.build.emptyOutDir = false;
finalConfig.vite.plugins ??= [];
finalConfig.vite.plugins.push(plugins.download(finalConfig));
finalConfig.vite.plugins.push(plugins.devHtmlPrerender(finalConfig));
finalConfig.vite.plugins.push(plugins.unimport(finalConfig));
finalConfig.vite.plugins.push(
plugins.virtualEntrypoin('background', finalConfig),
);
finalConfig.vite.plugins.push(
plugins.virtualEntrypoin('content-script', finalConfig),
);
finalConfig.vite.plugins.push(plugins.devServerGlobals(finalConfig));
finalConfig.vite.plugins.push(plugins.tsconfigPaths(finalConfig));
finalConfig.vite.plugins.push(plugins.noopBackground());
finalConfig.vite.define ??= {};
getGlobals(finalConfig).forEach((global) => {
finalConfig.vite.define![global.name] = JSON.stringify(global.value);
});
finalConfig.vite = (env) =>
resolveInternalViteConfig(env, mergedConfig, finalConfig);
return finalConfig;
}
/**
* Helper type for defining a base config, since user-configurable directories must be set after
* reading in the user config.
*/
type InternalConfigNoUserDirs = Omit<
InternalConfig,
| 'srcDir'
| 'publicDir'
| 'entrypointsDir'
| 'wxtDir'
| 'typesDir'
| 'fsCache'
| 'manifest'
| 'zip'
>;
async function resolveManifestConfig(
env: ConfigEnv,
manifest: UserManifest | Promise<UserManifest> | UserManifestFn | undefined,
@@ -179,3 +116,121 @@ async function resolveManifestConfig(
? manifest(env)
: manifest ?? {});
}
/**
* Merge the inline config and user config. Inline config is given priority. Defaults are not applied here.
*/
function mergeInlineConfig(
inlineConfig: InlineConfig,
userConfig: UserConfig,
): InlineConfig {
let imports: InlineConfig['imports'];
if (inlineConfig.imports === false || userConfig.imports === false) {
imports = false;
} else if (userConfig.imports == null && inlineConfig.imports == null) {
imports = undefined;
} else {
imports = vite.mergeConfig(
userConfig.imports ?? {},
inlineConfig.imports ?? {},
);
}
const manifest: UserManifestFn = async (env) => {
const user = await resolveManifestConfig(env, userConfig.manifest);
const inline = await resolveManifestConfig(env, inlineConfig.manifest);
return vite.mergeConfig(user, inline);
};
const viteConfig = async (env: ConfigEnv): Promise<WxtViteConfig> => {
const user = await userConfig.vite?.(env);
const inline = await inlineConfig.vite?.(env);
return vite.mergeConfig(user ?? {}, inline ?? {});
};
const runner: InlineConfig['runner'] = vite.mergeConfig(
userConfig.runner ?? {},
inlineConfig.runner ?? {},
);
const zip: InlineConfig['zip'] = vite.mergeConfig(
userConfig.zip ?? {},
inlineConfig.zip ?? {},
);
return {
root: inlineConfig.root ?? userConfig.root,
browser: inlineConfig.browser ?? userConfig.browser,
manifestVersion: inlineConfig.manifestVersion ?? userConfig.manifestVersion,
configFile: inlineConfig.configFile,
debug: inlineConfig.debug ?? userConfig.debug,
entrypointsDir: inlineConfig.entrypointsDir ?? userConfig.entrypointsDir,
imports,
logger: inlineConfig.logger ?? userConfig.logger,
manifest,
mode: inlineConfig.mode ?? userConfig.mode,
publicDir: inlineConfig.publicDir ?? userConfig.publicDir,
runner,
srcDir: inlineConfig.srcDir ?? userConfig.srcDir,
vite: viteConfig,
zip,
};
}
function resolveInternalZipConfig(
root: string,
mergedConfig: InlineConfig,
): InternalConfig['zip'] {
return {
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
sourcesRoot: root,
...mergedConfig.zip,
ignoredSources: [
'**/node_modules',
// WXT files
'**/web-ext.config.ts',
// Hidden files
'**/.*',
// Tests
'**/__tests__/**',
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
// From user
...(mergedConfig.zip?.ignoredSources ?? []),
],
};
}
async function resolveInternalViteConfig(
env: ConfigEnv,
mergedConfig: InlineConfig,
finalConfig: InternalConfig,
) {
const internalVite: vite.InlineConfig =
(await mergedConfig.vite?.(env)) ?? {};
internalVite.root = finalConfig.root;
internalVite.configFile = false;
internalVite.logLevel = 'warn';
internalVite.mode = env.mode;
internalVite.build ??= {};
internalVite.build.outDir = finalConfig.outDir;
internalVite.build.emptyOutDir = false;
internalVite.plugins ??= [];
internalVite.plugins.push(plugins.download(finalConfig));
internalVite.plugins.push(plugins.devHtmlPrerender(finalConfig));
internalVite.plugins.push(plugins.unimport(finalConfig));
internalVite.plugins.push(
plugins.virtualEntrypoin('background', finalConfig),
);
internalVite.plugins.push(
plugins.virtualEntrypoin('content-script', finalConfig),
);
internalVite.plugins.push(plugins.devServerGlobals(finalConfig));
internalVite.plugins.push(plugins.tsconfigPaths(finalConfig));
internalVite.plugins.push(plugins.noopBackground());
internalVite.define ??= {};
for (const global of getGlobals(finalConfig)) {
internalVite.define[global.name] = JSON.stringify(global.value);
}
return internalVite;
}
+13
View File
@@ -46,3 +46,16 @@ export function getGlobals(
},
];
}
export function getEntrypointGlobals(
config: InternalConfig,
entrypointName: string,
) {
return [
{
name: '__ENTRYPOINT__',
value: entrypointName,
type: `string`,
},
];
}
+7 -6
View File
@@ -1,7 +1,6 @@
import { BuildOutput, WxtDevServer, InlineConfig } from './core/types';
import { getInternalConfig } from './core/utils/getInternalConfig';
import pc from 'picocolors';
import * as vite from 'vite';
import { detectDevChanges } from './core/utils/detectDevChanges';
import { Mutex } from 'async-mutex';
import { consola } from 'consola';
@@ -38,12 +37,14 @@ export async function createServer(
): Promise<WxtDevServer> {
const serverInfo = await getServerInfo();
const getLatestInternalConfig = () => {
const viteConfig: vite.InlineConfig = vite.mergeConfig(
serverInfo.viteServerConfig,
config?.vite ?? {},
const getLatestInternalConfig = async () => {
return getInternalConfig(
{
...config,
vite: () => serverInfo.viteServerConfig,
},
'serve',
);
return getInternalConfig({ ...config, vite: viteConfig }, 'serve');
};
let internalConfig = await getLatestInternalConfig();
+39 -32
View File
@@ -198,35 +198,42 @@ export function fakeArray<T>(createItem: () => T, count = 3): T[] {
return array;
}
export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => ({
browser: faker.helpers.arrayElement(['chrome', 'firefox']),
command: faker.helpers.arrayElement(['build', 'serve']),
entrypointsDir: fakeDir(),
fsCache: mock<FsCache>(),
imports: {},
logger: mock(),
manifest: fakeManifest(),
manifestVersion: faker.helpers.arrayElement([2, 3]),
mode: faker.helpers.arrayElement(['development', 'production']),
outBaseDir: fakeDir(),
outDir: fakeDir(),
publicDir: fakeDir(),
root: fakeDir(),
runnerConfig: {
config: {},
},
debug: faker.datatype.boolean(),
srcDir: fakeDir(),
storeIds: {},
typesDir: fakeDir(),
vite: {},
wxtDir: fakeDir(),
server: mock<WxtDevServer>(),
zip: {
artifactTemplate: '{{name}}-{{version}}.zip',
ignoredSources: [],
sourcesRoot: fakeDir(),
sourcesTemplate: '{{name}}-sources.zip',
name: faker.person.firstName().toLowerCase(),
},
}));
export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
const browser = faker.helpers.arrayElement(['chrome', 'firefox']);
const command = faker.helpers.arrayElement(['build', 'serve'] as const);
const manifestVersion = faker.helpers.arrayElement([2, 3] as const);
const mode = faker.helpers.arrayElement(['development', 'production']);
return {
browser,
command,
entrypointsDir: fakeDir(),
env: { browser, command, manifestVersion, mode },
fsCache: mock<FsCache>(),
imports: {},
logger: mock(),
manifest: fakeManifest(),
manifestVersion,
mode,
outBaseDir: fakeDir(),
outDir: fakeDir(),
publicDir: fakeDir(),
root: fakeDir(),
runnerConfig: {
config: {},
},
debug: faker.datatype.boolean(),
srcDir: fakeDir(),
typesDir: fakeDir(),
vite: () => ({}),
wxtDir: fakeDir(),
server: mock<WxtDevServer>(),
zip: {
artifactTemplate: '{{name}}-{{version}}.zip',
ignoredSources: [],
sourcesRoot: fakeDir(),
sourcesTemplate: '{{name}}-sources.zip',
name: faker.person.firstName().toLowerCase(),
},
};
});
+2 -2
View File
@@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react';
// See https://wxt.dev/config.html
export default defineConfig({
vite: {
vite: () => ({
plugins: [react()],
},
}),
});
+2 -2
View File
@@ -3,10 +3,10 @@ import Solid from 'vite-plugin-solid';
// See https://wxt.dev/config.html
export default defineConfig({
vite: {
vite: () => ({
build: {
target: 'esnext',
},
plugins: [Solid()],
},
}),
});
+2 -2
View File
@@ -4,7 +4,7 @@ import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte';
// See https://wxt.dev/config.html
export default defineConfig({
srcDir: 'src',
vite: {
vite: () => ({
plugins: [
svelte({
// Using a svelte.config.js file causes a segmentation fault when importing the file
@@ -12,5 +12,5 @@ export default defineConfig({
preprocess: [vitePreprocess()],
}),
],
},
}),
});
+2 -2
View File
@@ -3,7 +3,7 @@ import vue from '@vitejs/plugin-vue';
// See https://wxt.dev/config.html
export default defineConfig({
vite: {
vite: () => ({
plugins: [vue()],
},
}),
});