From bf148dfd4e8470742143d29200996efcda3439a2 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Wed, 24 Dec 2025 15:10:10 +0100 Subject: [PATCH 01/64] fix: add types instead of `any` and improve readability of analytics package --- .../analytics/modules/analytics/client.ts | 108 +++++++++++------- packages/analytics/modules/analytics/index.ts | 7 +- .../analytics/providers/google-analytics-4.ts | 3 +- .../modules/analytics/providers/umami.ts | 1 + 4 files changed, 71 insertions(+), 48 deletions(-) diff --git a/packages/analytics/modules/analytics/client.ts b/packages/analytics/modules/analytics/client.ts index 85bac1097..2cae7a06b 100644 --- a/packages/analytics/modules/analytics/client.ts +++ b/packages/analytics/modules/analytics/client.ts @@ -2,30 +2,62 @@ import { UAParser } from 'ua-parser-js'; import type { Analytics, AnalyticsConfig, + AnalyticsEventMetadata, AnalyticsPageViewEvent, + AnalyticsProvider, AnalyticsStorageItem, AnalyticsTrackEvent, BaseAnalyticsEvent, - AnalyticsEventMetadata, - AnalyticsProvider, } from './types'; import { browser } from '@wxt-dev/browser'; const ANALYTICS_PORT = '@wxt-dev/analytics'; +type TAnalyticsMessage = { + [K in keyof Analytics]: { + fn: K; + args: Parameters; + }; +}[keyof Analytics]; + +type TAnalyticsMethod = + | ((...args: Parameters) => void) + | undefined; + +type TMethodForwarder = ( + fn: K, +) => (...args: Parameters) => void; +const INTERACTIVE_TAGS = new Set([ + 'A', + 'BUTTON', + 'INPUT', + 'SELECT', + 'TEXTAREA', +]); +const INTERACTIVE_ROLES = new Set([ + 'button', + 'link', + 'checkbox', + 'menuitem', + 'tab', + 'radio', +]); + +// This is injected by the build process, it only seems unused export function createAnalytics(config?: AnalyticsConfig): Analytics { if (!browser?.runtime?.id) throw Error( 'Cannot use WXT analytics in contexts without access to the browser.runtime APIs', ); - if (config == null) { + + if (config === null) { console.warn( "[@wxt-dev/analytics] Config not provided to createAnalytics. If you're using WXT, add the 'analytics' property to '/app.config.ts'.", ); } // TODO: This only works for standard WXT extensions, add a more generic - // background script detector that works with non-WXT projects. + // Background script detector that works with non-WXT projects. if (location.pathname === '/background.js') return createBackgroundAnalytics(config); @@ -41,12 +73,14 @@ function createBackgroundAnalytics( // User properties storage const userIdStorage = config?.userId ?? defineStorageItem('wxt-analytics:user-id'); + const userPropertiesStorage = config?.userProperties ?? defineStorageItem>( 'wxt-analytics:user-properties', {}, ); + const enabled = config?.enabled ?? defineStorageItem('local:wxt-analytics:enabled', false); @@ -54,6 +88,7 @@ function createBackgroundAnalytics( // Cached values const platformInfo = browser.runtime.getPlatformInfo(); const userAgent = UAParser(); + let userId = Promise.resolve(userIdStorage.getValue()).then( (id) => id ?? globalThis.crypto.randomUUID(), ); @@ -75,7 +110,7 @@ function createBackgroundAnalytics( const getBaseEvent = async ( meta: AnalyticsEventMetadata, ): Promise => { - const platform = await platformInfo; + const { arch, os } = await platformInfo; return { meta, user: { @@ -84,8 +119,8 @@ function createBackgroundAnalytics( version: config?.version ?? manifest.version_name ?? manifest.version, wxtMode: import.meta.env.MODE, wxtBrowser: import.meta.env.BROWSER, - arch: platform.arch, - os: platform.os, + arch, + os, browser: userAgent.browser.name, browserVersion: userAgent.browser.version, ...(await userProperties), @@ -110,7 +145,9 @@ function createBackgroundAnalytics( ]); // Notify providers const event = await getBaseEvent(meta); + if (config?.debug) console.debug('[@wxt-dev/analytics] identify', event); + if (await enabled.getValue()) { await Promise.allSettled( providers.map((provider) => provider.identify(event)), @@ -134,7 +171,9 @@ function createBackgroundAnalytics( title: meta?.title ?? globalThis.document?.title, }, }; + if (config?.debug) console.debug('[@wxt-dev/analytics] page', event); + if (await enabled.getValue()) { await Promise.allSettled( providers.map((provider) => provider.page(event)), @@ -155,7 +194,9 @@ function createBackgroundAnalytics( ...baseEvent, event: { name: eventName, properties: eventProperties }, }; + if (config?.debug) console.debug('[@wxt-dev/analytics] track', event); + if (await enabled.getValue()) { await Promise.allSettled( providers.map((provider) => provider.track(event)), @@ -181,9 +222,8 @@ function createBackgroundAnalytics( // Listen for messages from the rest of the extension browser.runtime.onConnect.addListener((port) => { if (port.name === ANALYTICS_PORT) { - port.onMessage.addListener(({ fn, args }) => { - // @ts-expect-error: Untyped fn key - void analytics[fn]?.(...args); + port.onMessage.addListener(({ fn, args }: TAnalyticsMessage) => { + void (analytics[fn] as TAnalyticsMethod)?.(...args); }); } }); @@ -201,17 +241,15 @@ function createFrontendAnalytics(): Analytics { sessionId, timestamp: Date.now(), language: navigator.language, - referrer: globalThis.document?.referrer || undefined, - screen: globalThis.window - ? `${globalThis.window.screen.width}x${globalThis.window.screen.height}` - : undefined, + referrer: globalThis.document?.referrer, + screen: `${globalThis.window.screen.width}x${globalThis.window.screen.height}`, url: location.href, - title: document.title || undefined, + title: document.title, }); - const methodForwarder = - (fn: string) => - (...args: any[]) => { + const methodForwarder: TMethodForwarder = + (fn) => + (...args) => { port.postMessage({ fn, args: [...args, getFrontendMetadata()] }); }; @@ -222,20 +260,20 @@ function createFrontendAnalytics(): Analytics { setEnabled: methodForwarder('setEnabled'), autoTrack: (root) => { const onClick = (event: Event) => { - const element = event.target as any; + const element = event.target as HTMLElement | null; if ( !element || (!INTERACTIVE_TAGS.has(element.tagName) && - !INTERACTIVE_ROLES.has(element.getAttribute('role'))) + !INTERACTIVE_ROLES.has(element.getAttribute('role') ?? '')) ) return; void analytics.track('click', { tagName: element.tagName?.toLowerCase(), - id: element.id || undefined, - className: element.className || undefined, - textContent: element.textContent?.substring(0, 50) || undefined, // Limit text content length - href: element.href, + id: element.id, + className: element.className, + textContent: element.textContent?.substring(0, 50), // Limit text content length + href: (element as HTMLAnchorElement).href, }); }; root.addEventListener('click', onClick, { capture: true, passive: true }); @@ -249,32 +287,16 @@ function createFrontendAnalytics(): Analytics { function defineStorageItem( key: string, - defaultValue?: NonNullable, + defaultValue?: T, ): AnalyticsStorageItem { return { getValue: async () => - (await browser.storage.local.get>(key))[key] ?? - defaultValue, + (((await browser.storage.local.get(key)) as Record)[key] ?? + defaultValue) as T, setValue: (newValue) => browser.storage.local.set({ [key]: newValue }), }; } -const INTERACTIVE_TAGS = new Set([ - 'A', - 'BUTTON', - 'INPUT', - 'SELECT', - 'TEXTAREA', -]); -const INTERACTIVE_ROLES = new Set([ - 'button', - 'link', - 'checkbox', - 'menuitem', - 'tab', - 'radio', -]); - export function defineAnalyticsProvider( definition: ( /** The analytics object. */ diff --git a/packages/analytics/modules/analytics/index.ts b/packages/analytics/modules/analytics/index.ts index f5ef4b00c..1e9d04ef4 100644 --- a/packages/analytics/modules/analytics/index.ts +++ b/packages/analytics/modules/analytics/index.ts @@ -25,6 +25,7 @@ export default defineWxtModule({ const clientModuleId = process.env.NPM ? '@wxt-dev/analytics' : resolve(wxt.config.modulesDir, 'analytics/client'); + const pluginModuleId = process.env.NPM ? '@wxt-dev/analytics/background-plugin' : resolve(wxt.config.modulesDir, 'analytics/background-plugin'); @@ -44,10 +45,8 @@ export default defineWxtModule({ ? clientModuleId : relative(wxtAnalyticsFolder, clientModuleId) }';`, - `import { useAppConfig } from '#imports';`, - ``, - `export const analytics = createAnalytics(useAppConfig().analytics);`, - ``, + `import { useAppConfig } from '#imports';\n`, + `export const analytics = createAnalytics(useAppConfig().analytics);\n`, ].join('\n'); addAlias(wxt, '#analytics', wxtAnalyticsIndex); wxt.hook('prepare:types', async (_, entries) => { diff --git a/packages/analytics/modules/analytics/providers/google-analytics-4.ts b/packages/analytics/modules/analytics/providers/google-analytics-4.ts index 278be6794..00055cb6f 100644 --- a/packages/analytics/modules/analytics/providers/google-analytics-4.ts +++ b/packages/analytics/modules/analytics/providers/google-analytics-4.ts @@ -15,11 +15,12 @@ export const googleAnalytics4 = data: BaseAnalyticsEvent, eventName: string, eventProperties: Record | undefined, - ): Promise => { + ) => { const url = new URL( config?.debug ? '/debug/mp/collect' : '/mp/collect', 'https://www.google-analytics.com', ); + if (options.apiSecret) url.searchParams.set('api_secret', options.apiSecret); if (options.measurementId) diff --git a/packages/analytics/modules/analytics/providers/umami.ts b/packages/analytics/modules/analytics/providers/umami.ts index 99a4e4914..86b7e3f09 100644 --- a/packages/analytics/modules/analytics/providers/umami.ts +++ b/packages/analytics/modules/analytics/providers/umami.ts @@ -12,6 +12,7 @@ export const umami = defineAnalyticsProvider( if (config.debug) { console.debug('[@wxt-dev/analytics] Sending event to Umami:', payload); } + return fetch(`${options.apiUrl}/send`, { method: 'POST', headers: { From f7223d56811e5d28193034e0b91efc3ca5e259bf Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Wed, 24 Dec 2025 21:39:25 +0100 Subject: [PATCH 02/64] clean(analysis): make code more readable and fix null check for mappedUserProperties --- packages/analytics/modules/analytics/client.ts | 1 - packages/analytics/modules/analytics/index.ts | 1 + .../modules/analytics/providers/google-analytics-4.ts | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/analytics/modules/analytics/client.ts b/packages/analytics/modules/analytics/client.ts index 2cae7a06b..99bd9cc2d 100644 --- a/packages/analytics/modules/analytics/client.ts +++ b/packages/analytics/modules/analytics/client.ts @@ -43,7 +43,6 @@ const INTERACTIVE_ROLES = new Set([ 'radio', ]); -// This is injected by the build process, it only seems unused export function createAnalytics(config?: AnalyticsConfig): Analytics { if (!browser?.runtime?.id) throw Error( diff --git a/packages/analytics/modules/analytics/index.ts b/packages/analytics/modules/analytics/index.ts index 1e9d04ef4..3ebcf1e81 100644 --- a/packages/analytics/modules/analytics/index.ts +++ b/packages/analytics/modules/analytics/index.ts @@ -61,6 +61,7 @@ export default defineWxtModule({ const hasBackground = entrypoints.find( (entry) => entry.type === 'background', ); + if (!hasBackground) { entrypoints.push({ type: 'background', diff --git a/packages/analytics/modules/analytics/providers/google-analytics-4.ts b/packages/analytics/modules/analytics/providers/google-analytics-4.ts index 00055cb6f..76e6c0895 100644 --- a/packages/analytics/modules/analytics/providers/google-analytics-4.ts +++ b/packages/analytics/modules/analytics/providers/google-analytics-4.ts @@ -23,6 +23,7 @@ export const googleAnalytics4 = if (options.apiSecret) url.searchParams.set('api_secret', options.apiSecret); + if (options.measurementId) url.searchParams.set('measurement_id', options.measurementId); @@ -34,7 +35,7 @@ export const googleAnalytics4 = const mappedUserProperties = Object.fromEntries( Object.entries(userProperties).map(([name, value]) => [ name, - value == null ? undefined : { value }, + value === null ? undefined : { value }, ]), ); From 2a0492ead48b5b48bbb013e87681f97128a8fc4a Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Wed, 24 Dec 2025 21:42:15 +0100 Subject: [PATCH 03/64] clean(analysis): move new types for client.ts to types.ts --- packages/analytics/modules/analytics/client.ts | 17 +++-------------- packages/analytics/modules/analytics/types.ts | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/analytics/modules/analytics/client.ts b/packages/analytics/modules/analytics/client.ts index 99bd9cc2d..46fa4c09b 100644 --- a/packages/analytics/modules/analytics/client.ts +++ b/packages/analytics/modules/analytics/client.ts @@ -8,24 +8,13 @@ import type { AnalyticsStorageItem, AnalyticsTrackEvent, BaseAnalyticsEvent, + TAnalyticsMessage, + TAnalyticsMethod, + TMethodForwarder, } from './types'; import { browser } from '@wxt-dev/browser'; const ANALYTICS_PORT = '@wxt-dev/analytics'; -type TAnalyticsMessage = { - [K in keyof Analytics]: { - fn: K; - args: Parameters; - }; -}[keyof Analytics]; - -type TAnalyticsMethod = - | ((...args: Parameters) => void) - | undefined; - -type TMethodForwarder = ( - fn: K, -) => (...args: Parameters) => void; const INTERACTIVE_TAGS = new Set([ 'A', diff --git a/packages/analytics/modules/analytics/types.ts b/packages/analytics/modules/analytics/types.ts index d14d59f81..c6f0c7a22 100644 --- a/packages/analytics/modules/analytics/types.ts +++ b/packages/analytics/modules/analytics/types.ts @@ -97,3 +97,18 @@ export interface AnalyticsTrackEvent extends BaseAnalyticsEvent { properties?: Record; }; } + +export type TAnalyticsMessage = { + [K in keyof Analytics]: { + fn: K; + args: Parameters; + }; +}[keyof Analytics]; + +export type TAnalyticsMethod = + | ((...args: Parameters) => void) + | undefined; + +export type TMethodForwarder = ( + fn: K, +) => (...args: Parameters) => void; From e159734614e8d9e0dd40dc9d25677a90141dcd7f Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Wed, 24 Dec 2025 21:43:03 +0100 Subject: [PATCH 04/64] clean(analysis): split comment of getBackgroundMeta a little better --- packages/analytics/modules/analytics/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/analytics/modules/analytics/client.ts b/packages/analytics/modules/analytics/client.ts index 46fa4c09b..09be16027 100644 --- a/packages/analytics/modules/analytics/client.ts +++ b/packages/analytics/modules/analytics/client.ts @@ -85,8 +85,8 @@ function createBackgroundAnalytics( const getBackgroundMeta = () => ({ timestamp: Date.now(), - // Don't track sessions for the background, it can be running - // indefinitely, and will inflate session duration stats. + // Don't track sessions for the background, it can be running indefinitely + // and will inflate session duration stats. sessionId: undefined, language: navigator.language, referrer: undefined, From ea303d53b902ab7ae50d976faa6064ccfa2f559a Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Wed, 24 Dec 2025 22:24:54 +0100 Subject: [PATCH 05/64] clean(auto-icons): add TODO suggestion and missing await for ensureDir --- packages/auto-icons/src/index.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/auto-icons/src/index.ts b/packages/auto-icons/src/index.ts index a97aeede5..bd01adcb5 100644 --- a/packages/auto-icons/src/index.ts +++ b/packages/auto-icons/src/index.ts @@ -1,6 +1,6 @@ import 'wxt'; import { defineWxtModule } from 'wxt/modules'; -import { resolve, relative } from 'node:path'; +import { relative, resolve } from 'node:path'; import defu from 'defu'; import sharp from 'sharp'; import { ensureDir, exists } from 'fs-extra'; @@ -19,6 +19,9 @@ export default defineWxtModule({ }, ); + // TODO: MAYBE DROP THIS COMPATIBILITY? + // TODO: It has a while, and for 1.0.0 we should be as much "up to date" as we can, + // TODO: because later it'll be harder to make breaking change? // Backward compatibility for the deprecated option if (options?.grayscaleOnDevelopment !== undefined) { wxt.logger.warn( @@ -37,7 +40,9 @@ export default defineWxtModule({ if (!parsedOptions.enabled) return wxt.logger.warn(`\`[auto-icons]\` ${this.name} disabled`); - if (!(await exists(resolvedPath))) { + // TODO: STH DOESN'T GOOD WITH FS-EXTRA, BECAUSE IT DOESN'T RECOGNIZE TYPES PROPERLY, + // TODO: SIMILAR ISSUE LIKE IN #2015 PR + if (!(await (exists as (path: string) => Promise)(resolvedPath))) { return wxt.logger.warn( `\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`, ); @@ -91,7 +96,7 @@ export default defineWxtModule({ } } - ensureDir(resolve(outputFolder, 'icons')); + await ensureDir(resolve(outputFolder, 'icons')); await resizedImage.toFile(resolve(outputFolder, `icons/${size}.png`)); output.publicAssets.push({ From ba6e87727a840b27b36d38d6bd9b536c27e9db17 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 11:48:53 +0100 Subject: [PATCH 06/64] fix(packages/browser): remove unnecessary argument of transformFile function of generate.ts --- packages/browser/scripts/generate.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/browser/scripts/generate.ts b/packages/browser/scripts/generate.ts index ddbf1d816..b40849d29 100644 --- a/packages/browser/scripts/generate.ts +++ b/packages/browser/scripts/generate.ts @@ -5,12 +5,10 @@ import { dirname, join, resolve, sep } from 'node:path'; import { sep as posixSep } from 'node:path/posix'; // Fetch latest version - console.log('Getting latest version of \x1b[36m@types/chrome\x1b[0m'); await spawn('pnpm', ['i', '--ignore-scripts', '-D', '@types/chrome@latest']); // Generate new package.json - console.log('Generating new \x1b[36mpackage.json\x1b[0m'); const pkgJsonPath = fileURLToPath( @@ -31,7 +29,6 @@ await fs.writeJson(outPkgJsonPath, newPkgJson); await spawn('pnpm', ['-w', 'prettier', '--write', outPkgJsonPath]); // Generate declaration files - console.log('Generating declaration files'); const outDir = resolve('src/gen'); const declarationFileMapping = ( @@ -51,7 +48,7 @@ const declarationFileMapping = ( for (const { file, srcPath, destPath } of declarationFileMapping) { const content = await fs.readFile(srcPath, 'utf8'); - const transformedContent = transformFile(file, content); + const transformedContent = transformFileContent(content); const destDir = dirname(destPath); await fs.mkdir(destDir, { recursive: true }); await fs.writeFile(destPath, transformedContent); @@ -59,12 +56,10 @@ for (const { file, srcPath, destPath } of declarationFileMapping) { } // Done! - console.log('\x1b[32m✔\x1b[0m Done in ' + performance.now().toFixed(0) + ' ms'); // Transformations - -function transformFile(file: string, content: string): string { +function transformFileContent(content: string) { return ( // Add prefix `/* DO NOT EDIT - generated by scripts/generate.ts */\n\n${content}\n` From 7bdaf83ea614fed1c8d383d81d6b7061c5c7ebcf Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 12:09:33 +0100 Subject: [PATCH 07/64] fix(packages/browser): change tests type assertions from deprecated to newer equivalent --- packages/browser/src/__tests__/index.test.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/browser/src/__tests__/index.test.ts b/packages/browser/src/__tests__/index.test.ts index 07b40f518..ad0193dfe 100644 --- a/packages/browser/src/__tests__/index.test.ts +++ b/packages/browser/src/__tests__/index.test.ts @@ -5,19 +5,19 @@ import { browser, type Browser } from '../index'; describe('browser', () => { describe('types', () => { it('should provide types via the Browser import', () => { - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); }); it('should provide values via the browser import', () => { - expectTypeOf(browser.runtime.id).toMatchTypeOf(); - expectTypeOf( - browser.storage.local, - ).toMatchTypeOf(); - expectTypeOf( - browser.i18n.detectLanguage('Hello, world!'), - ).resolves.toMatchTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf< + typeof browser.storage.local + >().toEqualTypeOf(); + expectTypeOf< + typeof browser.i18n.detectLanguage + >().returns.resolves.toEqualTypeOf(); }); }); }); From dc2a35218e8c16f04ddf6ca852bb36f0fe8e182b Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 12:30:52 +0100 Subject: [PATCH 08/64] fix(packages/i18n): remove unnecessary staff from tests and add question --- packages/i18n/src/__tests__/types.test.ts | 4 +++- packages/i18n/src/__tests__/utils.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/i18n/src/__tests__/types.test.ts b/packages/i18n/src/__tests__/types.test.ts index 540732092..2df2ae516 100644 --- a/packages/i18n/src/__tests__/types.test.ts +++ b/packages/i18n/src/__tests__/types.test.ts @@ -4,6 +4,7 @@ import { browser } from '@wxt-dev/browser'; vi.mock('@wxt-dev/browser', async () => { const { vi } = await import('vitest'); + return { browser: { i18n: { @@ -14,7 +15,7 @@ vi.mock('@wxt-dev/browser', async () => { }); const getMessageMock = vi.mocked(browser.i18n.getMessage); -const n: number = 1; +const n = 1; describe('I18n Types', () => { beforeEach(() => { @@ -47,6 +48,7 @@ describe('I18n Types', () => { describe('t', () => { it('should only allow passing valid combinations of arguments', () => { i18n.t('simple'); + // TODO: WHY THERE'S SO MUCH TS-EXPECT-ERRORS? // @ts-expect-error i18n.t('simple', []); // @ts-expect-error diff --git a/packages/i18n/src/__tests__/utils.test.ts b/packages/i18n/src/__tests__/utils.test.ts index 7c44bc46f..d9fd38266 100644 --- a/packages/i18n/src/__tests__/utils.test.ts +++ b/packages/i18n/src/__tests__/utils.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { ChromeMessage } from '../build'; import { applyChromeMessagePlaceholders, @@ -43,7 +43,7 @@ describe('Utils', () => { }); describe('getSubstitutionCount', () => { - it('should return the last substution present in the message', () => { + it('should return the last substitution present in the message', () => { expect(getSubstitutionCount('I like $1, but I like $2 better')).toBe(2); }); From 6dd1874a8cd4b35631faf55cf323c54d88a39bda Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 12:39:44 +0100 Subject: [PATCH 09/64] fix(packages/i18n): improve readability of code --- packages/i18n/src/build.ts | 10 ++++++++-- packages/i18n/src/index.ts | 6 +++++- packages/i18n/src/module.ts | 9 ++++++--- packages/i18n/src/utils.ts | 3 ++- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/i18n/src/build.ts b/packages/i18n/src/build.ts index 65144939e..dd2d8b6b6 100644 --- a/packages/i18n/src/build.ts +++ b/packages/i18n/src/build.ts @@ -6,7 +6,7 @@ */ import { mkdir, readFile, writeFile } from 'node:fs/promises'; -import { parseYAML, parseJSON5, parseTOML } from 'confbox'; +import { parseJSON5, parseTOML, parseYAML } from 'confbox'; import { dirname, extname } from 'node:path'; import { applyChromeMessagePlaceholders, getSubstitutionCount } from './utils'; @@ -118,6 +118,7 @@ export async function parseMessagesFile( ): Promise { const text = await readFile(file, 'utf8'); const ext = extname(file).toLowerCase(); + return parseMessagesText(text, EXT_FORMATS_MAP[ext] ?? 'JSON5'); } @@ -173,13 +174,16 @@ function _parseMessagesObject( `Messages file should not contain \`${object}\` (found at "${path.join('.')}")`, ); } + if (Array.isArray(object)) return object.flatMap((item, i) => _parseMessagesObject(path.concat(String(i)), item, depth + 1), ); + if (isPluralMessage(object)) { const message = Object.values(object).join('|'); const substitutions = getSubstitutionCount(message); + return [ { type: 'plural', @@ -189,9 +193,11 @@ function _parseMessagesObject( }, ]; } + if (depth === 1 && isChromeMessage(object)) { const message = applyChromeMessagePlaceholders(object); const substitutions = getSubstitutionCount(message); + return [ { type: 'chrome', @@ -227,7 +233,7 @@ function isChromeMessage(object: any): object is ChromeMessage { export function generateTypeText(messages: ParsedMessage[]): string { const renderMessageEntry = (message: ParsedMessage): string => { - // Use . for deep keys at runtime and types + // Use '.' for deep keys at runtime and types const key = message.key.join('.'); const features = [ diff --git a/packages/i18n/src/index.ts b/packages/i18n/src/index.ts index 559083b06..a736eb311 100644 --- a/packages/i18n/src/index.ts +++ b/packages/i18n/src/index.ts @@ -2,9 +2,9 @@ * @module @wxt-dev/i18n */ import { - I18nStructure, DefaultI18nStructure, I18n, + I18nStructure, Substitution, } from './types'; import { browser } from '@wxt-dev/browser'; @@ -16,6 +16,7 @@ export function createI18n< // Resolve args let sub: Substitution[] | undefined; let count: number | undefined; + args.forEach((arg, i) => { if (arg == null) { // ignore nullish args @@ -37,6 +38,7 @@ export function createI18n< // Load the localization let message: string; + if (sub?.length) { // Convert all substitutions to strings const stringSubs = sub?.map((sub) => String(sub)); @@ -44,9 +46,11 @@ export function createI18n< } else { message = browser.i18n.getMessage(key.replaceAll('.', '_')); } + if (!message) { console.warn(`[i18n] Message not found: "${key}"`); } + if (count == null) return message; // Apply pluralization diff --git a/packages/i18n/src/module.ts b/packages/i18n/src/module.ts index 68c914894..624722ba3 100644 --- a/packages/i18n/src/module.ts +++ b/packages/i18n/src/module.ts @@ -14,8 +14,8 @@ import 'wxt'; import { addAlias, defineWxtModule } from 'wxt/modules'; import { generateChromeMessagesText, - parseMessagesFile, generateTypeText, + parseMessagesFile, SUPPORTED_LOCALES, } from './build'; import glob from 'fast-glob'; @@ -70,6 +70,7 @@ export default defineWxtModule({ GeneratedPublicFile[] > => { const files = await getLocalizationFiles(); + return await Promise.all( files.map(async ({ file, locale }) => { const messages = await parseMessagesFile(file); @@ -86,13 +87,15 @@ export default defineWxtModule({ const defaultLocaleFile = files.find( ({ locale }) => locale === wxt.config.manifest.default_locale, )!; - if (defaultLocaleFile == null) { + + if (defaultLocaleFile === null) { throw Error( `\`[i18n]\` Required localization file does not exist: \`/${wxt.config.manifest.default_locale}.{json|json5|yml|yaml|toml}\``, ); } const messages = await parseMessagesFile(defaultLocaleFile.file); + return { path: typesPath, text: generateTypeText(messages), @@ -152,7 +155,7 @@ export { type GeneratedI18nStructure } addAlias(wxt, '#i18n', sourcePath); - // Generate separate declaration file containing types - this prevents + // Generate a separate declaration file containing types - this prevents // firing the dev server's default file watcher when updating the types, // which would cause a full rebuild and reload of the extension. diff --git a/packages/i18n/src/utils.ts b/packages/i18n/src/utils.ts index 6856c79ad..1762bf7be 100644 --- a/packages/i18n/src/utils.ts +++ b/packages/i18n/src/utils.ts @@ -1,7 +1,7 @@ import { ChromeMessage } from './build'; export function applyChromeMessagePlaceholders(message: ChromeMessage): string { - if (message.placeholders == null) return message.message; + if (message.placeholders === null) return message.message; return Object.entries(message.placeholders ?? {}).reduce( (text, [name, value]) => { @@ -28,6 +28,7 @@ export function standardizeLocale(locale: string): string { const [is_match, prefix, suffix] = locale.match(/^([a-z]{2})[-_]([a-z]{2,3})$/i) ?? []; + if (is_match) { return `${prefix.toLowerCase()}_${suffix.toUpperCase()}`; } From bbd29eae55d86409c0cd7766e7d65e9aa9b3132c Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 13:22:45 +0100 Subject: [PATCH 10/64] fix(packages/runner): remove unnecessary code --- packages/runner/src/__tests__/options.test.ts | 7 +++---- packages/runner/src/bidi.ts | 1 + packages/runner/src/cdp.ts | 1 + packages/runner/src/install.ts | 13 +++++-------- packages/wxt/src/core/define-web-ext-config.ts | 1 + 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/runner/src/__tests__/options.test.ts b/packages/runner/src/__tests__/options.test.ts index 23ec3e4ab..28a92c71a 100644 --- a/packages/runner/src/__tests__/options.test.ts +++ b/packages/runner/src/__tests__/options.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { beforeAll, describe, expect, it, vi } from 'vitest'; import { ResolvedRunOptions, resolveRunOptions } from '../options'; -import { resolve, join } from 'node:path'; -import { tmpdir, homedir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { homedir, tmpdir } from 'node:os'; import { mkdir } from 'node:fs/promises'; vi.mock('node:os', async () => { @@ -11,7 +11,6 @@ vi.mock('node:os', async () => { return { ...os, tmpdir: () => join(os.tmpdir(), 'tmpdir-mock'), - homedir: () => join(os.tmpdir(), 'homedir-mock'), }; }); diff --git a/packages/runner/src/bidi.ts b/packages/runner/src/bidi.ts index d784d3552..d9b16a19d 100644 --- a/packages/runner/src/bidi.ts +++ b/packages/runner/src/bidi.ts @@ -23,6 +23,7 @@ export async function createBidiConnection( send(method, params, timeout = 10e3) { const id = ++requestId; const command = { id, method, params }; + debugBidi('Sending command:', command); return new Promise((resolve, reject) => { diff --git a/packages/runner/src/cdp.ts b/packages/runner/src/cdp.ts index b96ac3ca8..92475ae84 100644 --- a/packages/runner/src/cdp.ts +++ b/packages/runner/src/cdp.ts @@ -21,6 +21,7 @@ export function createCdpConnection( send(method, params, timeout = 10e3) { const id = ++requestId; const command = { id, method, params }; + debugCdp('Sending command:', command); return new Promise((resolve, reject) => { diff --git a/packages/runner/src/install.ts b/packages/runner/src/install.ts index 2338f22a0..7033f00f5 100644 --- a/packages/runner/src/install.ts +++ b/packages/runner/src/install.ts @@ -17,15 +17,12 @@ export async function installFirefox( await bidi.send('session.new', { capabilities: {} }); // Install the extension - return await bidi.send( - 'webExtension.install', - { - extensionData: { - type: 'path', - path: extensionDir, - }, + return bidi.send('webExtension.install', { + extensionData: { + type: 'path', + path: extensionDir, }, - ); + }); } export type BidiWebExtensionInstallResponse = { diff --git a/packages/wxt/src/core/define-web-ext-config.ts b/packages/wxt/src/core/define-web-ext-config.ts index 7826004fc..f12d0be97 100644 --- a/packages/wxt/src/core/define-web-ext-config.ts +++ b/packages/wxt/src/core/define-web-ext-config.ts @@ -1,6 +1,7 @@ import consola from 'consola'; import { WebExtConfig } from '../types'; +// TODO: MAYBE DROP IT, BEFORE 1.0.0? /** * @deprecated Use `defineWebExtConfig` instead. Same function, different name. */ From 64709e911375171f7b01c19bd211324743149dee Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 13:37:23 +0100 Subject: [PATCH 11/64] fix(packages/runner): make comparing to `null` more strict and fix JDocs for getMetas() and setMetas() --- packages/storage/src/index.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts index b629ec4de..b743b1423 100644 --- a/packages/storage/src/index.ts +++ b/packages/storage/src/index.ts @@ -19,7 +19,7 @@ function createStorage(): WxtStorage { }; const getDriver = (area: StorageArea) => { const driver = drivers[area]; - if (driver == null) { + if (driver === null) { const areaNames = Object.keys(drivers).join(', '); throw Error(`Invalid area "${area}". Options: ${areaNames}`); } @@ -29,7 +29,7 @@ function createStorage(): WxtStorage { const deliminatorIndex = key.indexOf(':'); const driverArea = key.substring(0, deliminatorIndex) as StorageArea; const driverKey = key.substring(deliminatorIndex + 1); - if (driverKey == null) + if (driverKey === null) throw Error( `Storage key should be in the form of "area:key", but received "${key}"`, ); @@ -44,7 +44,7 @@ function createStorage(): WxtStorage { const mergeMeta = (oldMeta: any, newMeta: any): any => { const newFields = { ...oldMeta }; Object.entries(newMeta).forEach(([key, value]) => { - if (value == null) delete newFields[key]; + if (value === null) delete newFields[key]; else newFields[key] = value; }); return newFields; @@ -102,11 +102,11 @@ function createStorage(): WxtStorage { properties: string | string[] | undefined, ) => { const metaKey = getMetaKey(driverKey); - if (properties == null) { + if (properties === null) { await driver.removeItem(metaKey); } else { const newFields = getMetaValue(await driver.getItem(metaKey)); - [properties].flat().forEach((field) => delete newFields[field]); + [properties].flat().forEach((field) => delete newFields[field ?? '']); await driver.setItem(metaKey, newFields); } }; @@ -368,7 +368,7 @@ function createStorage(): WxtStorage { driverKey, driverMetaKey, ]); - if (value == null) return; + if (value === null) return; const currentVersion = meta?.v ?? 1; if (currentVersion > targetVersion) { @@ -380,7 +380,7 @@ function createStorage(): WxtStorage { return; } - if (debug === true) { + if (debug) { console.debug( `[@wxt-dev/storage] Running storage migration for ${key}: v${currentVersion} -> v${targetVersion}`, ); @@ -395,7 +395,7 @@ function createStorage(): WxtStorage { migratedValue = (await migrations?.[migrateToVersion]?.(migratedValue)) ?? migratedValue; - if (debug === true) { + if (debug) { console.debug( `[@wxt-dev/storage] Storage migration processed for version: v${migrateToVersion}`, ); @@ -411,7 +411,7 @@ function createStorage(): WxtStorage { { key: driverMetaKey, value: { ...meta, v: targetVersion } }, ]); - if (debug === true) { + if (debug) { console.debug( `[@wxt-dev/storage] Storage migration completed for ${key} v${targetVersion}`, { migratedValue }, @@ -626,8 +626,8 @@ export interface WxtStorage { /** * 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. + * @param keys List of keys or items to get the metadata of. */ getMetas( keys: Array>, @@ -669,7 +669,7 @@ export interface WxtStorage { /** * Set the metadata of multiple storage items. * - * @param items List of storage keys or items and metadata to set for each. + * @param metas List of storage keys or items and metadata to set for each. */ setMetas( metas: Array< @@ -863,6 +863,7 @@ export interface SnapshotOptions { } export interface WxtStorageItemOptions { + // TODO: MAYBE REMOVE IT BEFORE 1.0 RELEASE? /** * @deprecated Renamed to `fallback`, use it instead. */ From 1d25b85fd10aa7d49c542594a07825d50a2383a2 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 13:39:52 +0100 Subject: [PATCH 12/64] fix(packages/runner): simplify return of createStorage() --- packages/storage/src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts index b743b1423..5d6394d88 100644 --- a/packages/storage/src/index.ts +++ b/packages/storage/src/index.ts @@ -118,7 +118,7 @@ function createStorage(): WxtStorage { return driver.watch(driverKey, cb); }; - const storage: WxtStorage = { + return { getItem: async (key, opts) => { const { driver, driverKey } = resolveKey(key); return await getItem(driver, driverKey, opts); @@ -491,7 +491,6 @@ function createStorage(): WxtStorage { }; }, }; - return storage; } function createDriver(storageArea: StorageArea): WxtStorageDriver { From dd4bc41da915a8c4b5cd794e1f32e44d1511f9d8 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 13:54:41 +0100 Subject: [PATCH 13/64] revert(packages/storage): back to check with `==` instead of `===` --- packages/storage/src/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts index 5d6394d88..b7f8a7e12 100644 --- a/packages/storage/src/index.ts +++ b/packages/storage/src/index.ts @@ -19,7 +19,7 @@ function createStorage(): WxtStorage { }; const getDriver = (area: StorageArea) => { const driver = drivers[area]; - if (driver === null) { + if (driver == null) { const areaNames = Object.keys(drivers).join(', '); throw Error(`Invalid area "${area}". Options: ${areaNames}`); } @@ -29,7 +29,7 @@ function createStorage(): WxtStorage { const deliminatorIndex = key.indexOf(':'); const driverArea = key.substring(0, deliminatorIndex) as StorageArea; const driverKey = key.substring(deliminatorIndex + 1); - if (driverKey === null) + if (driverKey == null) throw Error( `Storage key should be in the form of "area:key", but received "${key}"`, ); @@ -44,7 +44,7 @@ function createStorage(): WxtStorage { const mergeMeta = (oldMeta: any, newMeta: any): any => { const newFields = { ...oldMeta }; Object.entries(newMeta).forEach(([key, value]) => { - if (value === null) delete newFields[key]; + if (value == null) delete newFields[key]; else newFields[key] = value; }); return newFields; @@ -102,11 +102,11 @@ function createStorage(): WxtStorage { properties: string | string[] | undefined, ) => { const metaKey = getMetaKey(driverKey); - if (properties === null) { + if (properties == null) { await driver.removeItem(metaKey); } else { const newFields = getMetaValue(await driver.getItem(metaKey)); - [properties].flat().forEach((field) => delete newFields[field ?? '']); + [properties].flat().forEach((field) => delete newFields[field]); await driver.setItem(metaKey, newFields); } }; @@ -368,7 +368,7 @@ function createStorage(): WxtStorage { driverKey, driverMetaKey, ]); - if (value === null) return; + if (value == null) return; const currentVersion = meta?.v ?? 1; if (currentVersion > targetVersion) { From 24590947cf35f5e4ca4d404c3cd5263324b5cbb4 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 14:35:01 +0100 Subject: [PATCH 14/64] fix(packages/storage): create types to avoid `any` --- packages/storage/src/index.ts | 212 ++++++++++++++++++++++------------ 1 file changed, 138 insertions(+), 74 deletions(-) diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts index b7f8a7e12..056167fde 100644 --- a/packages/storage/src/index.ts +++ b/packages/storage/src/index.ts @@ -41,7 +41,10 @@ function createStorage(): WxtStorage { }; }; const getMetaKey = (key: string) => key + '$'; - const mergeMeta = (oldMeta: any, newMeta: any): any => { + const mergeMeta = ( + oldMeta: Record, + newMeta: Record, + ): Record => { const newFields = { ...oldMeta }; Object.entries(newMeta).forEach(([key, value]) => { if (value == null) delete newFields[key]; @@ -49,41 +52,43 @@ function createStorage(): WxtStorage { }); return newFields; }; - const getValueOrFallback = (value: any, fallback: any) => + const getValueOrFallback = (value: T | null | undefined, fallback: T) => value ?? fallback ?? null; - const getMetaValue = (properties: any) => - typeof properties === 'object' && !Array.isArray(properties) - ? properties + const getMetaValue = (properties: unknown): Record => + typeof properties === 'object' && + properties !== null && + !Array.isArray(properties) + ? (properties as Record) : {}; - const getItem = async ( + const getItem = async ( driver: WxtStorageDriver, driverKey: string, - opts: GetItemOptions | undefined, - ) => { - const res = await driver.getItem(driverKey); - return getValueOrFallback(res, opts?.fallback ?? opts?.defaultValue); + opts: GetItemOptions | undefined, + ): Promise => { + const res = await driver.getItem(driverKey); + return getValueOrFallback(res, (opts?.fallback ?? opts?.defaultValue) as T); }; const getMeta = async (driver: WxtStorageDriver, driverKey: string) => { const metaKey = getMetaKey(driverKey); - const res = await driver.getItem(metaKey); + const res = await driver.getItem>(metaKey); return getMetaValue(res); }; - const setItem = async ( + const setItem = async ( driver: WxtStorageDriver, driverKey: string, - value: any, + value: T, ) => { await driver.setItem(driverKey, value ?? null); }; const setMeta = async ( driver: WxtStorageDriver, driverKey: string, - properties: any | undefined, + properties: Record | undefined, ) => { const metaKey = getMetaKey(driverKey); const existingFields = getMetaValue(await driver.getItem(metaKey)); - await driver.setItem(metaKey, mergeMeta(existingFields, properties)); + await driver.setItem(metaKey, mergeMeta(existingFields, properties ?? {})); }; const removeItem = async ( driver: WxtStorageDriver, @@ -110,12 +115,12 @@ function createStorage(): WxtStorage { await driver.setItem(metaKey, newFields); } }; - const watch = ( + const watch = ( driver: WxtStorageDriver, driverKey: string, - cb: WatchCallback, + cb: WatchCallback, ) => { - return driver.watch(driverKey, cb); + return driver.watch(driverKey, cb); }; return { @@ -125,12 +130,15 @@ function createStorage(): WxtStorage { }, getItems: async (keys) => { const areaToKeyMap = new Map(); - const keyToOptsMap = new Map | undefined>(); + const keyToOptsMap = new Map< + string, + GetItemOptions | undefined + >(); const orderedKeys: StorageItemKey[] = []; keys.forEach((key) => { let keyStr: StorageItemKey; - let opts: GetItemOptions | undefined; + let opts: GetItemOptions | undefined; if (typeof key === 'string') { // key: string keyStr = key; @@ -150,7 +158,7 @@ function createStorage(): WxtStorage { keyToOptsMap.set(keyStr, opts); }); - const resultsMap = new Map(); + const resultsMap = new Map(); await Promise.all( Array.from(areaToKeyMap.entries()).map(async ([driverArea, keys]) => { const driverResults = await drivers[driverArea].getItems(keys); @@ -171,9 +179,9 @@ function createStorage(): WxtStorage { value: resultsMap.get(key), })); }, - getMeta: async (key) => { + getMeta: async >(key: StorageItemKey) => { const { driver, driverKey } = resolveKey(key); - return await getMeta(driver, driverKey); + return (await getMeta(driver, driverKey)) as T; }, getMetas: async (args) => { const keys = args.map((arg) => { @@ -194,14 +202,17 @@ function createStorage(): WxtStorage { return map; }, {}); - const resultsMap: Record = {}; + const resultsMap: Record> = {}; 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] ?? {}; + resultsMap[key.key] = (areaRes[key.driverMetaKey] ?? {}) as Record< + string, + unknown + >; }); }), ); @@ -217,14 +228,14 @@ function createStorage(): WxtStorage { }, setItems: async (items) => { const areaToKeyValueMap: Partial< - Record> + Record> > = {}; items.forEach((item) => { const { driverArea, driverKey } = resolveKey( 'key' in item ? item.key : item.item.key, ); areaToKeyValueMap[driverArea] ??= []; - areaToKeyValueMap[driverArea].push({ + areaToKeyValueMap[driverArea]!.push({ key: driverKey, value: item.value, }); @@ -238,20 +249,23 @@ function createStorage(): WxtStorage { }, setMeta: async (key, properties) => { const { driver, driverKey } = resolveKey(key); - await setMeta(driver, driverKey, properties); + await setMeta(driver, driverKey, properties as Record); }, setMetas: async (items) => { const areaToMetaUpdatesMap: Partial< - Record + Record< + StorageArea, + { key: string; properties: Record }[] + > > = {}; items.forEach((item) => { const { driverArea, driverKey } = resolveKey( 'key' in item ? item.key : item.item.key, ); areaToMetaUpdatesMap[driverArea] ??= []; - areaToMetaUpdatesMap[driverArea].push({ + areaToMetaUpdatesMap[driverArea]!.push({ key: driverKey, - properties: item.meta, + properties: item.meta as Record, }); }); @@ -348,7 +362,13 @@ function createStorage(): WxtStorage { driver.unwatch(); }); }, - defineItem: (key, opts?: WxtStorageItemOptions) => { + defineItem: < + TValue, + TMetadata extends Record = Record, + >( + key: StorageItemKey, + opts?: WxtStorageItemOptions, + ) => { const { driver, driverKey } = resolveKey(key); const { @@ -364,13 +384,15 @@ function createStorage(): WxtStorage { } const migrate = async () => { const driverMetaKey = getMetaKey(driverKey); - const [{ value }, { value: meta }] = await driver.getItems([ + const [itemRes, metaRes] = await driver.getItems([ driverKey, driverMetaKey, ]); + const value = itemRes.value; + const meta = getMetaValue(metaRes.value); if (value == null) return; - const currentVersion = meta?.v ?? 1; + const currentVersion = (meta?.v as number | undefined) ?? 1; if (currentVersion > targetVersion) { throw Error( `Version downgrade detected (v${currentVersion} -> v${targetVersion}) for "${key}"`, @@ -408,7 +430,10 @@ function createStorage(): WxtStorage { } await driver.setItems([ { key: driverKey, value: migratedValue }, - { key: driverMetaKey, value: { ...meta, v: targetVersion } }, + { + key: driverMetaKey, + value: { ...meta, v: targetVersion }, + }, ]); if (debug) { @@ -417,7 +442,7 @@ function createStorage(): WxtStorage { { migratedValue }, ); } - onMigrationComplete?.(migratedValue, targetVersion); + onMigrationComplete?.(migratedValue as TValue, targetVersion); }; const migrationsDone = opts?.migrations == null @@ -435,12 +460,12 @@ function createStorage(): WxtStorage { const getOrInitValue = () => initMutex.runExclusive(async () => { - const value = await driver.getItem(driverKey); + const value = await driver.getItem(driverKey); // Don't init value if it already exists or the init function isn't provided if (value != null || opts?.init == null) return value; const newValue = await opts.init(); - await driver.setItem(driverKey, newValue); + await driver.setItem(driverKey, newValue); return newValue; }); @@ -450,22 +475,25 @@ function createStorage(): WxtStorage { return { key, get defaultValue() { - return getFallback(); + return getFallback() as TValue; }, get fallback() { - return getFallback(); + return getFallback() as TValue; }, getValue: async () => { await migrationsDone; if (opts?.init) { - return await getOrInitValue(); + return (await getOrInitValue()) as TValue; } else { - return await getItem(driver, driverKey, opts); + return (await getItem(driver, driverKey, opts)) as TValue; } }, getMeta: async () => { await migrationsDone; - return await getMeta(driver, driverKey); + return (await getMeta( + driver, + driverKey, + )) as NullablePartial; }, setValue: async (value) => { await migrationsDone; @@ -473,7 +501,11 @@ function createStorage(): WxtStorage { }, setMeta: async (properties) => { await migrationsDone; - return await setMeta(driver, driverKey, properties); + return await setMeta( + driver, + driverKey, + properties as Record, + ); }, removeValue: async (opts) => { await migrationsDone; @@ -483,9 +515,12 @@ function createStorage(): WxtStorage { await migrationsDone; return await removeMeta(driver, driverKey, properties); }, - watch: (cb) => - watch(driver, driverKey, (newValue, oldValue) => - cb(newValue ?? getFallback(), oldValue ?? getFallback()), + watch: (cb: WatchCallback) => + watch(driver, driverKey, (newValue, oldValue) => + cb( + (newValue ?? getFallback()) as TValue, + (oldValue ?? getFallback()) as TValue, + ), ), migrate, }; @@ -517,9 +552,9 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver { }; const watchListeners = new Set<(changes: StorageAreaChanges) => void>(); return { - getItem: async (key) => { - const res = await getStorageArea().get>(key); - return res[key]; + getItem: async (key: string) => { + const res = await getStorageArea().get>(key); + return (res[key] as T) ?? null; }, getItems: async (keys) => { const result = await getStorageArea().get(keys); @@ -557,11 +592,11 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver { restoreSnapshot: async (data) => { await getStorageArea().set(data); }, - watch(key, cb) { + watch(key: string, cb: WatchCallback) { const listener = (changes: StorageAreaChanges) => { const change = changes[key] as { - newValue?: any; - oldValue?: any | null; + newValue?: T; + oldValue?: T | null; } | null; if (change == null) return; if (dequal(change.newValue, change.oldValue)) return; @@ -610,10 +645,10 @@ export interface WxtStorage { getItems( keys: Array< | StorageItemKey - | WxtStorageItem - | { key: StorageItemKey; options?: GetItemOptions } + | WxtStorageItem> + | { key: StorageItemKey; options?: GetItemOptions } >, - ): Promise>; + ): Promise>; /** * Return an object containing metadata about the key. Object is stored at `key + "$"`. If value * is not an object, it returns an empty object. @@ -629,8 +664,10 @@ export interface WxtStorage { * @param keys List of keys or items to get the metadata of. */ getMetas( - keys: Array>, - ): Promise>; + keys: Array< + StorageItemKey | WxtStorageItem> + >, + ): Promise }>>; /** * Set a value in storage. Setting a value to `null` or `undefined` is equivalent to calling * `removeItem`. @@ -650,8 +687,11 @@ export interface WxtStorage { */ setItems( values: Array< - | { key: StorageItemKey; value: any } - | { item: WxtStorageItem; value: any } + | { key: StorageItemKey; value: unknown } + | { + item: WxtStorageItem>; + value: unknown; + } >, ): Promise; /** @@ -672,8 +712,11 @@ export interface WxtStorage { */ setMetas( metas: Array< - | { key: StorageItemKey; meta: Record } - | { item: WxtStorageItem; meta: Record } + | { key: StorageItemKey; meta: Record } + | { + item: WxtStorageItem>; + meta: Record; + } >, ): Promise; /** @@ -689,9 +732,12 @@ export interface WxtStorage { removeItems( keys: Array< | StorageItemKey - | WxtStorageItem + | WxtStorageItem> | { key: StorageItemKey; options?: RemoveItemOptions } - | { item: WxtStorageItem; options?: RemoveItemOptions } + | { + item: WxtStorageItem>; + options?: RemoveItemOptions; + } >, ): Promise; @@ -725,7 +771,10 @@ export interface WxtStorage { * Restores the results of `snapshot`. If new properties have been saved since the snapshot, they are * not overridden. Only values existing in the snapshot are overridden. */ - restoreSnapshot(base: StorageArea, data: any): Promise; + restoreSnapshot( + base: StorageArea, + data: Record, + ): Promise; /** * Watch for changes to a specific key in storage. */ @@ -740,24 +789,39 @@ export interface WxtStorage { * * Read full docs: https://wxt.dev/storage.html#defining-storage-items */ - defineItem = {}>( + defineItem< + TValue, + TMetadata extends Record = Record, + >( key: StorageItemKey, ): WxtStorageItem; - defineItem = {}>( + defineItem< + TValue, + TMetadata extends Record = Record, + >( key: StorageItemKey, options: WxtStorageItemOptions & { fallback: TValue }, ): WxtStorageItem; - defineItem = {}>( + defineItem< + TValue, + TMetadata extends Record = Record, + >( key: StorageItemKey, options: WxtStorageItemOptions & { defaultValue: TValue }, ): WxtStorageItem; - defineItem = {}>( + defineItem< + TValue, + TMetadata extends Record = Record, + >( key: StorageItemKey, options: WxtStorageItemOptions & { init: () => TValue | Promise; }, ): WxtStorageItem; - defineItem = {}>( + defineItem< + TValue, + TMetadata extends Record = Record, + >( key: StorageItemKey, options: WxtStorageItemOptions, ): WxtStorageItem; @@ -765,9 +829,9 @@ export interface WxtStorage { interface WxtStorageDriver { getItem(key: string): Promise; - getItems(keys: string[]): Promise<{ key: string; value: any }[]>; + getItems(keys: string[]): Promise<{ key: string; value: unknown }[]>; setItem(key: string, value: T | null): Promise; - setItems(values: Array<{ key: string; value: any }>): Promise; + setItems(values: Array<{ key: string; value: unknown }>): Promise; removeItem(key: string): Promise; removeItems(keys: string[]): Promise; clear(): Promise; @@ -824,7 +888,7 @@ export interface WxtStorageItem< /** * If there are migrations defined on the storage item, migrate to the latest version. * - * **This function is ran automatically whenever the extension updates**, so you don't have to call it + * **This function is run automatically whenever the extension updates**, so you don't have to call it * manually. */ migrate(): Promise; @@ -886,7 +950,7 @@ export interface WxtStorageItemOptions { /** * A map of version numbers to the functions used to migrate the data to that version. */ - migrations?: Record any>; + migrations?: Record unknown>; /** * Print debug logs, such as migration process. * @default false From a03b9b4138f21b01b63ffad7b90cd2bf24502c79 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 14:42:01 +0100 Subject: [PATCH 15/64] fix(packages/runner): create types to avoid `any` --- packages/runner/src/__tests__/options.test.ts | 2 +- packages/runner/src/bidi.ts | 6 +++++- packages/runner/src/cdp.ts | 6 +++++- packages/runner/src/debug.ts | 5 +++-- packages/runner/src/web-socket.ts | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/runner/src/__tests__/options.test.ts b/packages/runner/src/__tests__/options.test.ts index 28a92c71a..8c4e87532 100644 --- a/packages/runner/src/__tests__/options.test.ts +++ b/packages/runner/src/__tests__/options.test.ts @@ -6,7 +6,7 @@ import { mkdir } from 'node:fs/promises'; vi.mock('node:os', async () => { const { vi } = await import('vitest'); - const os: any = await vi.importActual('node:os'); + const os = (await vi.importActual('node:os')) as typeof import('node:os'); const { join } = await import('node:path'); return { ...os, diff --git a/packages/runner/src/bidi.ts b/packages/runner/src/bidi.ts index d9b16a19d..468801e3b 100644 --- a/packages/runner/src/bidi.ts +++ b/packages/runner/src/bidi.ts @@ -4,7 +4,11 @@ import { debug } from './debug'; const debugBidi = debug.scoped('bidi'); export interface BidiConnection extends Disposable { - send(method: string, params: any, timeout?: number): Promise; + send( + method: string, + params: Record, + timeout?: number, + ): Promise; close(): void; } diff --git a/packages/runner/src/cdp.ts b/packages/runner/src/cdp.ts index 92475ae84..ee2a074ae 100644 --- a/packages/runner/src/cdp.ts +++ b/packages/runner/src/cdp.ts @@ -5,7 +5,11 @@ import { debug } from './debug'; const debugCdp = debug.scoped('cdp'); export interface CDPConnection extends Disposable { - send(method: string, params: any, timeout?: number): Promise; + send( + method: string, + params: Record, + timeout?: number, + ): Promise; close(): void; } diff --git a/packages/runner/src/debug.ts b/packages/runner/src/debug.ts index fbf91e395..ee308572a 100644 --- a/packages/runner/src/debug.ts +++ b/packages/runner/src/debug.ts @@ -1,10 +1,11 @@ export interface Debug { - (...args: any[]): void; scoped: (scope: string) => Debug; + + (...args: unknown[]): void; } function createDebug(scopes: string[]): Debug { - const debug = (...args: any[]) => { + const debug = (...args: unknown[]) => { const scope = scopes.join(':'); if ( process.env.DEBUG === '1' || diff --git a/packages/runner/src/web-socket.ts b/packages/runner/src/web-socket.ts index baf5bc1f9..be758d2ad 100644 --- a/packages/runner/src/web-socket.ts +++ b/packages/runner/src/web-socket.ts @@ -25,7 +25,7 @@ export function openWebSocket(url: string): Promise { ), ); }; - const onError = (error: any) => { + const onError = (error: unknown) => { cleanup(); reject(new Error('Error connecting to WebSocket', { cause: error })); }; From 553628efdc8a20e705a5a00655ba898f16e5cb12 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 14:43:00 +0100 Subject: [PATCH 16/64] fix(packages/runner): add clearingTimeout on createBidiConnection -> send(), for avoid unnecessary code run --- packages/runner/src/bidi.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/runner/src/bidi.ts b/packages/runner/src/bidi.ts index 468801e3b..f08f18774 100644 --- a/packages/runner/src/bidi.ts +++ b/packages/runner/src/bidi.ts @@ -36,7 +36,7 @@ export async function createBidiConnection( webSocket.removeEventListener('error', onError); }; - setTimeout(() => { + const timeoutId = setTimeout(() => { cleanup(); reject( new Error( @@ -49,12 +49,14 @@ export async function createBidiConnection( const data = JSON.parse(event.data); if (data.id === id) { debugBidi('Received response:', data); + clearTimeout(timeoutId); cleanup(); if (data.type === 'success') resolve(data.result); else reject(Error(data.message, { cause: data })); } }; - const onError = (error: any) => { + const onError = (error: unknown) => { + clearTimeout(timeoutId); cleanup(); reject(new Error('Error sending request', { cause: error })); }; From 604e895da8e6da13e76dde34188bd3ae2ea94776 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 15:26:01 +0100 Subject: [PATCH 17/64] fix(packages/runner): change `any` to more strict types and by this change, avoids @ts-expect-error for tests --- packages/i18n/src/__tests__/types.test.ts | 25 ------------------- packages/i18n/src/build.ts | 29 +++++++++++++---------- packages/i18n/src/index.ts | 4 ++-- packages/i18n/src/types.ts | 21 +++++++++------- 4 files changed, 30 insertions(+), 49 deletions(-) diff --git a/packages/i18n/src/__tests__/types.test.ts b/packages/i18n/src/__tests__/types.test.ts index 2df2ae516..160eac4d2 100644 --- a/packages/i18n/src/__tests__/types.test.ts +++ b/packages/i18n/src/__tests__/types.test.ts @@ -48,66 +48,41 @@ describe('I18n Types', () => { describe('t', () => { it('should only allow passing valid combinations of arguments', () => { i18n.t('simple'); - // TODO: WHY THERE'S SO MUCH TS-EXPECT-ERRORS? - // @ts-expect-error i18n.t('simple', []); - // @ts-expect-error i18n.t('simple', ['one']); - // @ts-expect-error i18n.t('simple', n); i18n.t('simpleSub1', ['one']); - // @ts-expect-error i18n.t('simpleSub1'); - // @ts-expect-error i18n.t('simpleSub1', []); - // @ts-expect-error i18n.t('simpleSub1', ['one', 'two']); - // @ts-expect-error i18n.t('simpleSub1', n); i18n.t('simpleSub2', ['one', 'two']); - // @ts-expect-error i18n.t('simpleSub2'); - // @ts-expect-error i18n.t('simpleSub2', ['one']); - // @ts-expect-error i18n.t('simpleSub2', ['one', 'two', 'three']); - // @ts-expect-error i18n.t('simpleSub2', n); i18n.t('plural', n); - // @ts-expect-error i18n.t('plural'); - // @ts-expect-error i18n.t('plural', []); - // @ts-expect-error i18n.t('plural', ['one']); - // @ts-expect-error i18n.t('plural', n, ['sub']); i18n.t('pluralSub1', n); i18n.t('pluralSub1', n, undefined); i18n.t('pluralSub1', n, ['one']); - // @ts-expect-error i18n.t('pluralSub1'); - // @ts-expect-error i18n.t('pluralSub1', ['one']); - // @ts-expect-error i18n.t('pluralSub1', n, []); - // @ts-expect-error i18n.t('pluralSub1', n, ['one', 'two']); i18n.t('pluralSub2', n, ['one', 'two']); - // @ts-expect-error i18n.t('pluralSub2'); - // @ts-expect-error i18n.t('pluralSub2', ['one', 'two']); - // @ts-expect-error i18n.t('pluralSub2', n, ['one']); - // @ts-expect-error i18n.t('pluralSub2', n, ['one', 'two', 'three']); - // @ts-expect-error i18n.t('pluralSub2', n); }); }); diff --git a/packages/i18n/src/build.ts b/packages/i18n/src/build.ts index dd2d8b6b6..5bc62f0cd 100644 --- a/packages/i18n/src/build.ts +++ b/packages/i18n/src/build.ts @@ -94,7 +94,7 @@ const EXT_FORMATS_MAP: Record = { '.toml': 'TOML', }; -const PARSERS: Record any> = { +const PARSERS: Record unknown> = { YAML: parseYAML, JSON5: parseJSON5, TOML: parseTOML, @@ -135,11 +135,11 @@ export function parseMessagesText( /** * Given the JS object form of a raw messages file, extract the messages. */ -export function parseMessagesObject(object: any): ParsedMessage[] { +export function parseMessagesObject(object: unknown): ParsedMessage[] { return _parseMessagesObject( [], { - ...object, + ...(object as Record), ...PREDEFINED_MESSAGES, }, 0, @@ -148,7 +148,7 @@ export function parseMessagesObject(object: any): ParsedMessage[] { function _parseMessagesObject( path: string[], - object: any, + object: unknown, depth: number, ): ParsedMessage[] { switch (typeof object) { @@ -169,7 +169,7 @@ function _parseMessagesObject( ]; } case 'object': - if ([null, undefined].includes(object)) { + if (object === null || object === undefined) { throw new Error( `Messages file should not contain \`${object}\` (found at "${path.join('.')}")`, ); @@ -189,13 +189,13 @@ function _parseMessagesObject( type: 'plural', key: path, substitutions, - plurals: object, + plurals: object as Record, }, ]; } - if (depth === 1 && isChromeMessage(object)) { - const message = applyChromeMessagePlaceholders(object); + if (depth === 1 && isChromeMessage(object as object)) { + const message = applyChromeMessagePlaceholders(object as ChromeMessage); const substitutions = getSubstitutionCount(message); return [ @@ -203,25 +203,28 @@ function _parseMessagesObject( type: 'chrome', key: path, substitutions, - ...object, + ...(object as ChromeMessage), }, ]; } - return Object.entries(object).flatMap(([key, value]) => - _parseMessagesObject(path.concat(key), value, depth + 1), + return Object.entries(object as Record).flatMap( + ([key, value]) => + _parseMessagesObject(path.concat(key), value, depth + 1), ); default: throw Error(`"Could not parse object of type "${typeof object}"`); } } -function isPluralMessage(object: any): object is Record { +function isPluralMessage( + object: object, +): object is Record { return Object.keys(object).every( (key) => key === 'n' || isFinite(Number(key)), ); } -function isChromeMessage(object: any): object is ChromeMessage { +function isChromeMessage(object: object): object is ChromeMessage { return Object.keys(object).every((key) => ALLOWED_CHROME_MESSAGE_KEYS.has(key), ); diff --git a/packages/i18n/src/index.ts b/packages/i18n/src/index.ts index a736eb311..5abedca15 100644 --- a/packages/i18n/src/index.ts +++ b/packages/i18n/src/index.ts @@ -10,9 +10,9 @@ import { import { browser } from '@wxt-dev/browser'; export function createI18n< - T extends I18nStructure = DefaultI18nStructure, + T extends I18nStructure | DefaultI18nStructure = DefaultI18nStructure, >(): I18n { - const t = (key: string, ...args: any[]) => { + const t = (key: string, ...args: unknown[]) => { // Resolve args let sub: Substitution[] | undefined; let count: number | undefined; diff --git a/packages/i18n/src/types.ts b/packages/i18n/src/types.ts index d742c873c..905c5fed9 100644 --- a/packages/i18n/src/types.ts +++ b/packages/i18n/src/types.ts @@ -7,13 +7,12 @@ export type I18nStructure = { [K: string]: I18nFeatures; }; -export type DefaultI18nStructure = { - [K: string]: any; -}; +export type DefaultI18nStructure = Record; // prettier-ignore export type SubstitutionTuple = - T extends 1 ? [$1: Substitution] + T extends 0 ? [] + : T extends 1 ? [$1: Substitution] : T extends 2 ? [$1: Substitution, $2: Substitution] : T extends 3 ? [$1: Substitution, $2: Substitution, $3: Substitution] : T extends 4 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution] @@ -22,7 +21,7 @@ export type SubstitutionTuple = : T extends 7 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution] : T extends 8 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution, $8: Substitution] : T extends 9 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution, $8: Substitution, $9: Substitution] - : never + : [] export type TFunction = { // Non-plural, no substitutions @@ -37,7 +36,7 @@ export type TFunction = { key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: SubstitutionCount } ? P : never; }[keyof T], substitutions: T[K] extends I18nFeatures ? SubstitutionTuple - : never, + : [], ): string; // Plural with 1 substitution @@ -62,12 +61,16 @@ export type TFunction = { n: number, substitutions: T[K] extends I18nFeatures ? SubstitutionTuple - : never, + : [], ): string; }; -export interface I18n { - t: TFunction; +export interface I18n< + T extends I18nStructure | DefaultI18nStructure = DefaultI18nStructure, +> { + t: T extends DefaultI18nStructure + ? (key: string, ...args: unknown[]) => string + : TFunction>; } export type Substitution = string | number; From d417287ed91336f1295fec658410c862f3d53abc Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 15:39:54 +0100 Subject: [PATCH 18/64] fix(packages/wxt): add missing `lang` prop for for couple e2e tests --- packages/wxt/e2e/tests/analysis.test.ts | 20 ++++++------- packages/wxt/e2e/tests/auto-imports.test.ts | 30 +++++++++---------- packages/wxt/e2e/tests/hooks.test.ts | 12 ++++---- packages/wxt/e2e/tests/modules.test.ts | 6 ++-- .../wxt/e2e/tests/output-structure.test.ts | 23 +++++++------- .../wxt/e2e/tests/typescript-project.test.ts | 22 +++++++------- packages/wxt/e2e/tests/user-config.test.ts | 2 +- 7 files changed, 59 insertions(+), 56 deletions(-) diff --git a/packages/wxt/e2e/tests/analysis.test.ts b/packages/wxt/e2e/tests/analysis.test.ts index 0ba662f9f..b14cfb344 100644 --- a/packages/wxt/e2e/tests/analysis.test.ts +++ b/packages/wxt/e2e/tests/analysis.test.ts @@ -17,8 +17,8 @@ describe('Analysis', () => { it('should output a stats.html with no part files by default', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ''); - project.addFile('entrypoints/options.html', ''); + project.addFile('entrypoints/popup.html', ''); + project.addFile('entrypoints/options.html', ''); project.addFile( 'entrypoints/background.ts', 'export default defineBackground(() => {});', @@ -38,8 +38,8 @@ describe('Analysis', () => { it('should save part files when requested', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ''); - project.addFile('entrypoints/options.html', ''); + project.addFile('entrypoints/popup.html', ''); + project.addFile('entrypoints/options.html', ''); project.addFile( 'entrypoints/background.ts', 'export default defineBackground(() => {});', @@ -59,8 +59,8 @@ describe('Analysis', () => { it('should support customizing the stats output directory', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ''); - project.addFile('entrypoints/options.html', ''); + project.addFile('entrypoints/popup.html', ''); + project.addFile('entrypoints/options.html', ''); project.addFile( 'entrypoints/background.ts', 'export default defineBackground(() => {});', @@ -78,8 +78,8 @@ describe('Analysis', () => { it('should place artifacts next to the custom output file', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ''); - project.addFile('entrypoints/options.html', ''); + project.addFile('entrypoints/popup.html', ''); + project.addFile('entrypoints/options.html', ''); project.addFile( 'entrypoints/background.ts', 'export default defineBackground(() => {});', @@ -100,8 +100,8 @@ describe('Analysis', () => { it('should open the stats in the browser when requested', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ''); - project.addFile('entrypoints/options.html', ''); + project.addFile('entrypoints/popup.html', ''); + project.addFile('entrypoints/options.html', ''); project.addFile( 'entrypoints/background.ts', 'export default defineBackground(() => {});', diff --git a/packages/wxt/e2e/tests/auto-imports.test.ts b/packages/wxt/e2e/tests/auto-imports.test.ts index 8746656ff..6b20e2721 100644 --- a/packages/wxt/e2e/tests/auto-imports.test.ts +++ b/packages/wxt/e2e/tests/auto-imports.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { TestProject } from '../utils'; import spawn from 'nano-spawn'; @@ -6,7 +6,7 @@ describe('Auto Imports', () => { describe('imports: { ... }', () => { it('should generate a declaration file, imports.d.ts, for auto-imports', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ``); + project.addFile('entrypoints/popup.html', ``); await project.prepare(); @@ -70,7 +70,7 @@ describe('Auto Imports', () => { it('should include auto-imports in the project', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ``); + project.addFile('entrypoints/popup.html', ``); await project.prepare(); @@ -91,7 +91,7 @@ describe('Auto Imports', () => { it('should generate the #imports module', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ``); + project.addFile('entrypoints/popup.html', ``); // Project auto-imports should also be present project.addFile( 'utils/time.ts', @@ -138,7 +138,7 @@ describe('Auto Imports', () => { project.setConfigFileConfig({ imports: false, }); - project.addFile('entrypoints/popup.html', ``); + project.addFile('entrypoints/popup.html', ``); await project.prepare(); @@ -150,8 +150,7 @@ describe('Auto Imports', () => { project.setConfigFileConfig({ imports: false, }); - project.addFile('entrypoints/popup.html', ``); - + project.addFile('entrypoints/popup.html', ``); await project.prepare(); expect( @@ -176,7 +175,7 @@ describe('Auto Imports', () => { project.setConfigFileConfig({ imports: false, }); - project.addFile('entrypoints/popup.html', ``); + project.addFile('entrypoints/popup.html', ``); // Project auto-imports should also be present project.addFile( 'utils/time.ts', @@ -219,7 +218,7 @@ describe('Auto Imports', () => { describe('eslintrc', () => { it('"enabled: true" should output a JSON config file compatible with ESlint 8', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ``); + project.addFile('entrypoints/popup.html', ``); await project.prepare({ imports: { @@ -236,7 +235,7 @@ describe('Auto Imports', () => { it('"enabled: 8" should output a JSON config file compatible with ESlint 8', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ``); + project.addFile('entrypoints/popup.html', ``); await project.prepare({ imports: { @@ -253,7 +252,7 @@ describe('Auto Imports', () => { it('"enabled: 9" should output a flat config file compatible with ESlint 9', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ``); + project.addFile('entrypoints/popup.html', ``); await project.prepare({ imports: { @@ -270,7 +269,7 @@ describe('Auto Imports', () => { it('"enabled: false" should NOT output an ESlint config file', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ``); + project.addFile('entrypoints/popup.html', ``); await project.prepare({ imports: { @@ -290,7 +289,7 @@ describe('Auto Imports', () => { it('should NOT output an ESlint config file by default', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ``); + project.addFile('entrypoints/popup.html', ``); await project.prepare(); @@ -304,7 +303,7 @@ describe('Auto Imports', () => { it('should allow customizing the output', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ``); + project.addFile('entrypoints/popup.html', ``); await project.prepare({ imports: { @@ -331,7 +330,8 @@ describe('Auto Imports', () => { await project.prepare({ imports: { eslintrc: { enabled: version } }, }); - return await spawn('pnpm', ['eslint', 'entrypoints/background.js'], { + + return spawn('pnpm', ['eslint', 'entrypoints/background.js'], { cwd: project.root, }); } diff --git a/packages/wxt/e2e/tests/hooks.test.ts b/packages/wxt/e2e/tests/hooks.test.ts index 9442c4088..ef063c1c9 100644 --- a/packages/wxt/e2e/tests/hooks.test.ts +++ b/packages/wxt/e2e/tests/hooks.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { TestProject } from '../utils'; -import { WxtHooks } from '../../src/types'; +import { WxtHooks } from '../../src'; const hooks: WxtHooks = { ready: vi.fn(), @@ -48,7 +48,7 @@ describe('Hooks', () => { it('prepare should call hooks', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ''); + project.addFile('entrypoints/popup.html', ''); await project.prepare({ hooks }); @@ -80,7 +80,7 @@ describe('Hooks', () => { it('build should call hooks', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ''); + project.addFile('entrypoints/popup.html', ''); await project.build({ hooks }); @@ -112,7 +112,7 @@ describe('Hooks', () => { it('zip should call hooks', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ''); + project.addFile('entrypoints/popup.html', ''); await project.zip({ hooks }); @@ -144,7 +144,7 @@ describe('Hooks', () => { it('zip -b firefox should call hooks', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ''); + project.addFile('entrypoints/popup.html', ''); await project.zip({ hooks, browser: 'firefox' }); @@ -176,7 +176,7 @@ describe('Hooks', () => { it('server.start should call hooks', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ''); + project.addFile('entrypoints/popup.html', ''); const server = await project.startServer({ hooks, diff --git a/packages/wxt/e2e/tests/modules.test.ts b/packages/wxt/e2e/tests/modules.test.ts index dcbe6c90a..ece6a65ee 100644 --- a/packages/wxt/e2e/tests/modules.test.ts +++ b/packages/wxt/e2e/tests/modules.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { TestProject } from '../utils'; -import type { GenericEntrypoint, InlineConfig } from '../../src/types'; +import type { GenericEntrypoint, InlineConfig } from '../../src'; import { readFile } from 'fs-extra'; import { normalizePath } from '../../src/core/utils/paths'; @@ -201,7 +201,7 @@ describe('Module Helpers', () => { project.addFile( 'entrypoints/popup/index.html', ` - + `, diff --git a/packages/wxt/e2e/tests/output-structure.test.ts b/packages/wxt/e2e/tests/output-structure.test.ts index 0db84f041..76ff8b2a8 100644 --- a/packages/wxt/e2e/tests/output-structure.test.ts +++ b/packages/wxt/e2e/tests/output-structure.test.ts @@ -5,9 +5,12 @@ describe('Output Directory Structure', () => { it('should not output hidden files and directories that start with "."', async () => { const project = new TestProject(); project.addFile('entrypoints/.DS_Store'); - project.addFile('entrypoints/.hidden1/index.html', ''); - project.addFile('entrypoints/.hidden2.html', ''); - project.addFile('entrypoints/unlisted.html', ''); + project.addFile( + 'entrypoints/.hidden1/index.html', + '', + ); + project.addFile('entrypoints/.hidden2.html', ''); + project.addFile('entrypoints/unlisted.html', ''); await project.build(); @@ -23,7 +26,7 @@ describe('Output Directory Structure', () => { ================================================================================ .output/chrome-mv3/unlisted.html ---------------------------------------- - + " `); }); @@ -113,7 +116,7 @@ describe('Output Directory Structure', () => { it('should not include an entrypoint if the target browser is not in the list of included targets', async () => { const project = new TestProject(); - project.addFile('entrypoints/options.html', ''); + project.addFile('entrypoints/options.html', ''); project.addFile( 'entrypoints/background.ts', ` @@ -133,7 +136,7 @@ describe('Output Directory Structure', () => { it('should not include an entrypoint if the target browser is in the list of excluded targets', async () => { const project = new TestProject(); - project.addFile('entrypoints/options.html', ''); + project.addFile('entrypoints/options.html', ''); project.addFile( 'entrypoints/background.ts', ` @@ -163,7 +166,7 @@ describe('Output Directory Structure', () => { 'entrypoints/background.ts', `export default defineBackground(() => {});`, ); - project.addFile('entrypoints/popup.html', ''); + project.addFile('entrypoints/popup.html', ''); project.addFile( 'entrypoints/overlay.content.ts', `export default defineContentScript({ @@ -293,7 +296,7 @@ describe('Output Directory Structure', () => { it("should output to a custom directory when overriding 'outDir'", async () => { const project = new TestProject(); - project.addFile('entrypoints/unlisted.html', ''); + project.addFile('entrypoints/unlisted.html', ''); project.setConfigFileConfig({ outDir: 'dist', }); @@ -324,7 +327,7 @@ describe('Output Directory Structure', () => { ); project.addFile( 'entrypoints/popup/index.html', - ` + ` @@ -397,7 +400,7 @@ describe('Output Directory Structure', () => { ); project.addFile( 'entrypoints/popup/index.html', - ` + ` diff --git a/packages/wxt/e2e/tests/typescript-project.test.ts b/packages/wxt/e2e/tests/typescript-project.test.ts index 35d35a6aa..0327c38b5 100644 --- a/packages/wxt/e2e/tests/typescript-project.test.ts +++ b/packages/wxt/e2e/tests/typescript-project.test.ts @@ -4,7 +4,7 @@ import { TestProject } from '../utils'; describe('TypeScript Project', () => { it('should generate defined constants correctly', async () => { const project = new TestProject(); - project.addFile('entrypoints/unlisted.html', ''); + project.addFile('entrypoints/unlisted.html', ''); await project.prepare(); @@ -33,9 +33,9 @@ describe('TypeScript Project', () => { it('should augment the types for browser.runtime.getURL', async () => { const project = new TestProject(); - project.addFile('entrypoints/popup.html', ''); - project.addFile('entrypoints/options.html', ''); - project.addFile('entrypoints/sandbox.html', ''); + project.addFile('entrypoints/popup.html', ''); + project.addFile('entrypoints/options.html', ''); + project.addFile('entrypoints/sandbox.html', ''); await project.prepare(); @@ -65,7 +65,7 @@ describe('TypeScript Project', () => { it('should augment the types for browser.i18n.getMessage', async () => { const project = new TestProject(); - project.addFile('entrypoints/unlisted.html', ''); + project.addFile('entrypoints/unlisted.html', ''); project.addFile( 'public/_locales/en/messages.json', JSON.stringify({ @@ -227,7 +227,7 @@ describe('TypeScript Project', () => { it('should reference all the required types in a single declaration file', async () => { const project = new TestProject(); - project.addFile('entrypoints/unlisted.html', ''); + project.addFile('entrypoints/unlisted.html', ''); await project.prepare(); @@ -248,7 +248,7 @@ describe('TypeScript Project', () => { it('should generate a TSConfig file for the project', async () => { const project = new TestProject(); - project.addFile('entrypoints/unlisted.html', ''); + project.addFile('entrypoints/unlisted.html', ''); await project.prepare(); @@ -289,7 +289,7 @@ describe('TypeScript Project', () => { it('should generate correct path aliases for a custom srcDir', async () => { const project = new TestProject(); - project.addFile('src/entrypoints/unlisted.html', ''); + project.addFile('src/entrypoints/unlisted.html', ''); project.setConfigFileConfig({ srcDir: 'src', }); @@ -333,7 +333,7 @@ describe('TypeScript Project', () => { it('should add additional path aliases listed in the alias config, preventing defaults from being overridden', async () => { const project = new TestProject(); - project.addFile('src/entrypoints/unlisted.html', ''); + project.addFile('src/entrypoints/unlisted.html', ''); project.setConfigFileConfig({ srcDir: 'src', alias: { @@ -383,7 +383,7 @@ describe('TypeScript Project', () => { it('should start path aliases with "./" for paths inside the .wxt dir', async () => { const project = new TestProject(); - project.addFile('src/entrypoints/unlisted.html', ''); + project.addFile('src/entrypoints/unlisted.html', ''); project.setConfigFileConfig({ srcDir: 'src', alias: { @@ -399,7 +399,7 @@ describe('TypeScript Project', () => { it('should set correct import.meta.env.BROWSER type based on targetBrowsers', async () => { const project = new TestProject(); - project.addFile('entrypoints/unlisted.html', ''); + project.addFile('entrypoints/unlisted.html', ''); project.setConfigFileConfig({ targetBrowsers: ['firefox', 'chrome'], }); diff --git a/packages/wxt/e2e/tests/user-config.test.ts b/packages/wxt/e2e/tests/user-config.test.ts index 7eecaae4a..b781d7ac0 100644 --- a/packages/wxt/e2e/tests/user-config.test.ts +++ b/packages/wxt/e2e/tests/user-config.test.ts @@ -60,7 +60,7 @@ describe('User Config', () => { it('should merge inline and user config based manifests', async () => { const project = new TestProject(); - project.addFile('entrypoints/unlisted.html', ''); + project.addFile('entrypoints/unlisted.html', ''); project.addFile( 'wxt.config.ts', `import { defineConfig } from 'wxt'; From a994bbe247ed4330ad962ef4d48f5d816d538948 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 16:22:54 +0100 Subject: [PATCH 19/64] clean(packages/wxt): make code more readable and add 2 questions --- packages/wxt/e2e/tests/dev.test.ts | 1 + packages/wxt/e2e/tests/hooks.test.ts | 4 +++- packages/wxt/e2e/tests/modules.test.ts | 2 ++ packages/wxt/e2e/tests/output-structure.test.ts | 2 +- packages/wxt/e2e/tests/remote-code.test.ts | 10 +++++----- .../src/core/builders/vite/plugins/bundleAnalysis.ts | 1 + 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/wxt/e2e/tests/dev.test.ts b/packages/wxt/e2e/tests/dev.test.ts index c782bb94c..40539f855 100644 --- a/packages/wxt/e2e/tests/dev.test.ts +++ b/packages/wxt/e2e/tests/dev.test.ts @@ -10,6 +10,7 @@ describe('Dev Mode', () => { ); const server = await project.startServer({ + // TODO: MAYBE REMOVE IT BEFORE 1.0.0? runner: { disabled: true, }, diff --git a/packages/wxt/e2e/tests/hooks.test.ts b/packages/wxt/e2e/tests/hooks.test.ts index ef063c1c9..a5f068eb0 100644 --- a/packages/wxt/e2e/tests/hooks.test.ts +++ b/packages/wxt/e2e/tests/hooks.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { TestProject } from '../utils'; import { WxtHooks } from '../../src'; @@ -34,6 +34,7 @@ function expectHooksToBeCalled( const hookName = key as keyof WxtHooks; const value = called[hookName]; const times = typeof value === 'number' ? value : value ? 1 : 0; + expect( hooks[hookName], `Expected "${hookName}" to be called ${times} time(s)`, @@ -184,6 +185,7 @@ describe('Hooks', () => { disabled: true, }, }); + expect(hooks['server:closed']).not.toBeCalled(); await server.stop(); diff --git a/packages/wxt/e2e/tests/modules.test.ts b/packages/wxt/e2e/tests/modules.test.ts index ece6a65ee..d1d837846 100644 --- a/packages/wxt/e2e/tests/modules.test.ts +++ b/packages/wxt/e2e/tests/modules.test.ts @@ -255,12 +255,14 @@ describe('Module Helpers', () => { customImport(); });`, ); + const utils = project.addFile( 'custom.ts', `export function customImport() { console.log("${expectedText}") }`, ); + project.addFile( 'modules/test.ts', `import { defineWxtModule } from 'wxt/modules'; diff --git a/packages/wxt/e2e/tests/output-structure.test.ts b/packages/wxt/e2e/tests/output-structure.test.ts index 76ff8b2a8..94a76f401 100644 --- a/packages/wxt/e2e/tests/output-structure.test.ts +++ b/packages/wxt/e2e/tests/output-structure.test.ts @@ -411,7 +411,7 @@ describe('Output Directory Structure', () => { await project.build({ vite: () => ({ build: { - // Make output for snapshot readible + // Make output for snapshot readable minify: false, }, }), diff --git a/packages/wxt/e2e/tests/remote-code.test.ts b/packages/wxt/e2e/tests/remote-code.test.ts index c445cc564..6517e2da1 100644 --- a/packages/wxt/e2e/tests/remote-code.test.ts +++ b/packages/wxt/e2e/tests/remote-code.test.ts @@ -1,13 +1,13 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { TestProject } from '../utils'; describe('Remote Code', () => { it('should download "url:*" modules and include them in the final bundle', async () => { - const url = 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js'; + const URL = 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js'; const project = new TestProject(); project.addFile( 'entrypoints/popup.ts', - `import "url:${url}" + `import "url:${URL}" export default defineUnlistedScript(() => {})`, ); @@ -18,9 +18,9 @@ describe('Remote Code', () => { // Some text that will hopefully be in future versions of this script '__lodash_placeholder__', ); - expect(output).not.toContain(url); + expect(output).not.toContain(URL); expect( - await project.fileExists(`.wxt/cache/${encodeURIComponent(url)}`), + await project.fileExists(`.wxt/cache/${encodeURIComponent(URL)}`), ).toBe(true); }); }); diff --git a/packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts b/packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts index 744e3032a..266252e85 100644 --- a/packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts +++ b/packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts @@ -15,6 +15,7 @@ export function bundleAnalysis(config: ResolvedConfig): vite.Plugin { }) as vite.Plugin; } +// TODO: MAYBE REMOVE IT BEFORE 1.0.0? /** * @deprecated FOR TESTING ONLY. */ From 754d36dcf5f41bbd6181f0afaf93e00ac54166bd Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 16:23:33 +0100 Subject: [PATCH 20/64] fix(packages/wxt): add `example` props for InlineConfig for avoid ts-expect-error --- packages/wxt/e2e/tests/modules.test.ts | 1 - packages/wxt/src/types.ts | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/wxt/e2e/tests/modules.test.ts b/packages/wxt/e2e/tests/modules.test.ts index d1d837846..51e137cda 100644 --- a/packages/wxt/e2e/tests/modules.test.ts +++ b/packages/wxt/e2e/tests/modules.test.ts @@ -33,7 +33,6 @@ describe('Module Helpers', () => { ); await project.build({ - // @ts-expect-error: untyped field for testing example: options, }); diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index 284af1d78..5ff354097 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -374,6 +374,10 @@ export interface InlineConfig { * "wxt-module-analytics"). */ modules?: string[]; + /** + * Field only for testing purposes, don't use it for other cases + */ + example?: { key: string }; } // TODO: Extract to @wxt/vite-builder and use module augmentation to include the vite field From 4ecae1dace0126dbc23907d582b28373fe11aaaf Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 16:28:10 +0100 Subject: [PATCH 21/64] clean(packages/wxt): remove unnecessary serializeWxtDir --- packages/wxt/e2e/utils.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/wxt/e2e/utils.ts b/packages/wxt/e2e/utils.ts index 8a44c9859..61ca3315f 100644 --- a/packages/wxt/e2e/utils.ts +++ b/packages/wxt/e2e/utils.ts @@ -138,14 +138,6 @@ export class TestProject { return this.serializeDir('.output', ignoreContentsOfFilenames); } - /** - * Read all the files from the test project's `.wxt` directory and combine them into a string - * that can be used in a snapshot. - */ - serializeWxtDir(): Promise { - return this.serializeDir(resolve(this.root, '.wxt/types')); - } - /** * Deeply print the filename and contents of all files in a directory. * From db82216ac09d6fbad118958f75f74f3d808de69c Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 16:42:41 +0100 Subject: [PATCH 22/64] fix(packages/wxt): change deprecated exists with pathExists --- packages/wxt/e2e/utils.ts | 2 +- packages/wxt/src/core/generate-wxt-dir.ts | 2 +- packages/wxt/src/core/resolve-config.ts | 2 +- packages/wxt/src/core/utils/fs.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/wxt/e2e/utils.ts b/packages/wxt/e2e/utils.ts index 61ca3315f..ff34c9a3e 100644 --- a/packages/wxt/e2e/utils.ts +++ b/packages/wxt/e2e/utils.ts @@ -178,7 +178,7 @@ export class TestProject { } fileExists(...path: string[]): Promise { - return fs.exists(this.resolvePath(...path)); + return fs.pathExists(this.resolvePath(...path)); } async getOutputManifest( diff --git a/packages/wxt/src/core/generate-wxt-dir.ts b/packages/wxt/src/core/generate-wxt-dir.ts index ad7aeb2ef..921f1c130 100644 --- a/packages/wxt/src/core/generate-wxt-dir.ts +++ b/packages/wxt/src/core/generate-wxt-dir.ts @@ -136,7 +136,7 @@ declare module "wxt/browser" { 'messages.json', ); let messages: Message[]; - if (await fs.exists(defaultLocalePath)) { + if (await fs.pathExists(defaultLocalePath)) { const content = JSON.parse(await fs.readFile(defaultLocalePath, 'utf-8')); messages = parseI18nMessages(content); } else { diff --git a/packages/wxt/src/core/resolve-config.ts b/packages/wxt/src/core/resolve-config.ts index 5e14b4e8b..574d535e7 100644 --- a/packages/wxt/src/core/resolve-config.ts +++ b/packages/wxt/src/core/resolve-config.ts @@ -550,7 +550,7 @@ function resolveWxtModuleDir() { } async function isDirMissing(dir: string) { - return !(await fs.exists(dir)); + return !(await fs.pathExists(dir)); } function logMissingDir(logger: Logger, name: string, expected: string) { diff --git a/packages/wxt/src/core/utils/fs.ts b/packages/wxt/src/core/utils/fs.ts index ff9cdfdc5..3d3f28878 100644 --- a/packages/wxt/src/core/utils/fs.ts +++ b/packages/wxt/src/core/utils/fs.ts @@ -28,7 +28,7 @@ export async function writeFileIfDifferent( * `config.publicDir`. */ export async function getPublicFiles(): Promise { - if (!(await fs.exists(wxt.config.publicDir))) return []; + if (!(await fs.pathExists(wxt.config.publicDir))) return []; const files = await glob('**/*', { cwd: wxt.config.publicDir }); return files.map(unnormalizePath); From 941b5475ea15add097c5dc01569e3da6f487df10 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Thu, 25 Dec 2025 16:46:37 +0100 Subject: [PATCH 23/64] fix(packages/wxt): change HtmlPublicPath from `type` to `const` for fix `Unresolved variable or type HtmlPublicPath` error --- packages/wxt/e2e/tests/typescript-project.test.ts | 2 +- packages/wxt/src/core/generate-wxt-dir.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wxt/e2e/tests/typescript-project.test.ts b/packages/wxt/e2e/tests/typescript-project.test.ts index 0327c38b5..330a32b19 100644 --- a/packages/wxt/e2e/tests/typescript-project.test.ts +++ b/packages/wxt/e2e/tests/typescript-project.test.ts @@ -53,7 +53,7 @@ describe('TypeScript Project', () => { | "/options.html" | "/popup.html" | "/sandbox.html" - type HtmlPublicPath = Extract + const HtmlPublicPath = Extract export interface WxtRuntime { getURL(path: PublicPath): string; getURL(path: \`\${HtmlPublicPath}\${string}\`): string; diff --git a/packages/wxt/src/core/generate-wxt-dir.ts b/packages/wxt/src/core/generate-wxt-dir.ts index 921f1c130..65c57e744 100644 --- a/packages/wxt/src/core/generate-wxt-dir.ts +++ b/packages/wxt/src/core/generate-wxt-dir.ts @@ -92,7 +92,7 @@ import "wxt/browser"; declare module "wxt/browser" { export type PublicPath = {{ union }} - type HtmlPublicPath = Extract + const HtmlPublicPath = Extract export interface WxtRuntime { getURL(path: PublicPath): string; getURL(path: \`\${HtmlPublicPath}\${string}\`): string; From 8bd9f8ff628e294b4cc1d2c2be61754ce999bff3 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 09:56:35 +0100 Subject: [PATCH 24/64] clean(packages/wxt): simplify ValidationError checking on cli-utils.ts --- packages/wxt/src/cli/cli-utils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/wxt/src/cli/cli-utils.ts b/packages/wxt/src/cli/cli-utils.ts index 2f525ae6f..3a772c434 100644 --- a/packages/wxt/src/cli/cli-utils.ts +++ b/packages/wxt/src/cli/cli-utils.ts @@ -19,8 +19,7 @@ export function wrapAction( }, ) { return async (...args: any[]) => { - // Enable consola's debug mode globally at the start of all commands when the `--debug` flag is - // passed + // Enable consola's debug mode globally at the start of all commands when the `--debug` flag is passed const isDebug = !!args.find((arg) => arg?.debug); if (isDebug) { consola.level = LogLevels.debug; @@ -40,11 +39,11 @@ export function wrapAction( consola.fail( `Command failed after ${formatDuration(Date.now() - startTime)}`, ); - if (err instanceof ValidationError) { - // Don't log these errors, they've already been logged - } else { + + if (!(err instanceof ValidationError)) { consola.error(err); } + process.exit(1); } }; @@ -98,6 +97,7 @@ export function createAliasedCommand( }); aliasCommandNames.add(aliasedCommand.name); } + export function isAliasedCommand(command: Command | undefined): boolean { return !!command && aliasCommandNames.has(command.name); } From 5285c2eaabd48820dbcea74bfdb9db604b315885 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 10:03:12 +0100 Subject: [PATCH 25/64] fix(packages/wxt): add missing `lang` to `html` on devHtmlPrerender.test.ts --- .../builders/vite/plugins/__tests__/devHtmlPrerender.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wxt/src/core/builders/vite/plugins/__tests__/devHtmlPrerender.test.ts b/packages/wxt/src/core/builders/vite/plugins/__tests__/devHtmlPrerender.test.ts index db51f3c5b..e10c12e22 100644 --- a/packages/wxt/src/core/builders/vite/plugins/__tests__/devHtmlPrerender.test.ts +++ b/packages/wxt/src/core/builders/vite/plugins/__tests__/devHtmlPrerender.test.ts @@ -32,7 +32,7 @@ describe('Dev HTML Prerender Plugin', () => { // URLs should not be changed ['https://example.com/style.css', 'https://example.com/style.css'], ])('should transform "%s" into "%s"', (input, expected) => { - const { document } = parseHTML(''); + const { document } = parseHTML(''); const root = '/some/root'; const config = fakeResolvedConfig({ root, From 41b7998be2c66d70101e51e573a0622bc71275c5 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 10:09:15 +0100 Subject: [PATCH 26/64] fix(packages/wxt): change `==` to `===` check, for real `null`, to fix possibly undefined document.head of wxtPluginLoader.ts --- .../core/builders/vite/plugins/wxtPluginLoader.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts b/packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts index c6808bac5..3e335c106 100644 --- a/packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts +++ b/packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts @@ -35,11 +35,11 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { if (id === resolvedVirtualHtmlModuleId) { return `import { initPlugins } from '${virtualModuleId}'; -try { - initPlugins(); -} catch (err) { - console.error("[wxt] Failed to initialize plugins", err); -}`; + try { + initPlugins(); + } catch (err) { + console.error("[wxt] Failed to initialize plugins", err); + }`; } }, transformIndexHtml: { @@ -59,10 +59,11 @@ try { script.type = 'module'; script.src = src; - if (document.head == null) { + if (document.head === null) { const newHead = document.createElement('head'); document.documentElement.prepend(newHead); } + document.head.prepend(script); return document.toString(); }, From b1547beb096c9de0301c98ec837cafa703ed951f Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 10:13:23 +0100 Subject: [PATCH 27/64] fix(packages/wxt): change deprecated `exists` to `pathExists` of `fs-extra` --- .../wxt/src/core/builders/vite/plugins/resolveAppConfig.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts b/packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts index e2b4e670e..a76c2c3d9 100644 --- a/packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts +++ b/packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts @@ -1,4 +1,4 @@ -import { exists } from 'fs-extra'; +import { pathExists } from 'fs-extra'; import { resolve } from 'node:path'; import type * as vite from 'vite'; import { ResolvedConfig } from '../../../../types'; @@ -25,7 +25,7 @@ export function resolveAppConfig(config: ResolvedConfig): vite.Plugin { async resolveId(id) { if (id !== virtualModuleId) return; - return (await exists(appConfigFile)) + return (await pathExists(appConfigFile)) ? appConfigFile : resolvedVirtualModuleId; }, From 2df5305c7046ac8c626fd838ed93ede2a2f571f0 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 10:17:18 +0100 Subject: [PATCH 28/64] fix(packages/wxt): add missing await for removeEmptyDirs of builders/vite/index.ts --- packages/wxt/src/core/builders/vite/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts index c798749da..78c071faf 100644 --- a/packages/wxt/src/core/builders/vite/index.ts +++ b/packages/wxt/src/core/builders/vite/index.ts @@ -453,7 +453,7 @@ async function moveHtmlFiles( ); // TODO: Optimize and only delete old path directories - removeEmptyDirs(config.outDir); + await removeEmptyDirs(config.outDir); return movedChunks; } @@ -463,9 +463,11 @@ async function moveHtmlFiles( */ export async function removeEmptyDirs(dir: string): Promise { const files = await fs.readdir(dir); + for (const file of files) { const filePath = join(dir, file); const stats = await fs.stat(filePath); + if (stats.isDirectory()) { await removeEmptyDirs(filePath); } From db0df10fd598b5a322bf85a86d4278756e9fceed Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 10:17:51 +0100 Subject: [PATCH 29/64] clean(packages/wxt): improve readability of plugins --- .../src/core/builders/vite/plugins/entrypointGroupGlobals.ts | 1 + packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/wxt/src/core/builders/vite/plugins/entrypointGroupGlobals.ts b/packages/wxt/src/core/builders/vite/plugins/entrypointGroupGlobals.ts index a9e3e1aea..b203fe1ad 100644 --- a/packages/wxt/src/core/builders/vite/plugins/entrypointGroupGlobals.ts +++ b/packages/wxt/src/core/builders/vite/plugins/entrypointGroupGlobals.ts @@ -13,6 +13,7 @@ export function entrypointGroupGlobals( config() { const define: vite.InlineConfig['define'] = {}; let name = Array.isArray(entrypointGroup) ? 'html' : entrypointGroup.name; + for (const global of getEntrypointGlobals(name)) { define[`import.meta.env.${global.name}`] = JSON.stringify(global.value); } diff --git a/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts b/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts index ee46f4ef1..ef9cb0d7b 100644 --- a/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts +++ b/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts @@ -27,7 +27,7 @@ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption { ], }, ssr: { - // Inline all WXT modules subdependencies can be mocked + // Inline all WXT modules sub dependencies can be mocked noExternal: ['wxt'], }, }; From d7fc6c2a3526198b543a5d02a03a40c8d9ab3591 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 10:21:30 +0100 Subject: [PATCH 30/64] fix(packages/wxt): change deprecated `exists` to `pathExists` on npm.test.ts --- packages/wxt/src/core/package-managers/__tests__/npm.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wxt/src/core/package-managers/__tests__/npm.test.ts b/packages/wxt/src/core/package-managers/__tests__/npm.test.ts index de2e8e2ae..6117d946f 100644 --- a/packages/wxt/src/core/package-managers/__tests__/npm.test.ts +++ b/packages/wxt/src/core/package-managers/__tests__/npm.test.ts @@ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest'; import path from 'node:path'; import { npm } from '../npm'; import spawn from 'nano-spawn'; -import { exists } from 'fs-extra'; +import { pathExists } from 'fs-extra'; describe('NPM Package Management Utils', () => { describe('listDependencies', () => { @@ -41,7 +41,7 @@ describe('NPM Package Management Utils', () => { const actual = await npm.downloadDependency(id, downloadDir); expect(actual).toEqual(expected); - expect(await exists(actual)).toBe(true); + expect(await pathExists(actual)).toBe(true); }); }); }); From c676a0cc71eb6dff76193494eb581d537de852db Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 10:25:43 +0100 Subject: [PATCH 31/64] clean(packages/wxt): improve readability of code for core/package-managers --- packages/wxt/src/core/package-managers/npm.ts | 8 ++++++++ packages/wxt/src/core/package-managers/pnpm.ts | 2 ++ packages/wxt/src/core/package-managers/yarn.ts | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/packages/wxt/src/core/package-managers/npm.ts b/packages/wxt/src/core/package-managers/npm.ts index 7ad5386f8..1b1989205 100644 --- a/packages/wxt/src/core/package-managers/npm.ts +++ b/packages/wxt/src/core/package-managers/npm.ts @@ -12,13 +12,16 @@ export const npm: WxtPackageManagerImpl = { cwd: downloadDir, }); const packed: PackedDependency[] = JSON.parse(res.stdout); + return path.resolve(downloadDir, packed[0].filename); }, async listDependencies(options) { const args = ['ls', '--json']; + if (options?.all) { args.push('--depth', 'Infinity'); } + const res = await spawn('npm', args, { cwd: options?.cwd }); const project: NpmListProject = JSON.parse(res.stdout); @@ -30,12 +33,15 @@ export function flattenNpmListOutput(projects: NpmListProject[]): Dependency[] { const queue: Record[] = projects.flatMap( (project) => { const acc: Record[] = []; + if (project.dependencies) acc.push(project.dependencies); if (project.devDependencies) acc.push(project.devDependencies); + return acc; }, ); const dependencies: Dependency[] = []; + while (queue.length > 0) { Object.entries(queue.pop()!).forEach(([name, meta]) => { dependencies.push({ @@ -46,11 +52,13 @@ export function flattenNpmListOutput(projects: NpmListProject[]): Dependency[] { if (meta.devDependencies) queue.push(meta.devDependencies); }); } + return dedupeDependencies(dependencies); } export function dedupeDependencies(dependencies: Dependency[]): Dependency[] { const hashes = new Set(); + return dependencies.filter((dep) => { const hash = `${dep.name}@${dep.version}`; if (hashes.has(hash)) { diff --git a/packages/wxt/src/core/package-managers/pnpm.ts b/packages/wxt/src/core/package-managers/pnpm.ts index 32d89b609..ed6a023ef 100644 --- a/packages/wxt/src/core/package-managers/pnpm.ts +++ b/packages/wxt/src/core/package-managers/pnpm.ts @@ -9,6 +9,7 @@ export const pnpm: WxtPackageManagerImpl = { }, async listDependencies(options) { const args = ['ls', '-r', '--json']; + if (options?.all) { args.push('--depth', 'Infinity'); } @@ -20,6 +21,7 @@ export const pnpm: WxtPackageManagerImpl = { ) { args.push('--ignore-workspace'); } + const res = await spawn('pnpm', args, { cwd: options?.cwd }); const projects: NpmListProject[] = JSON.parse(res.stdout); diff --git a/packages/wxt/src/core/package-managers/yarn.ts b/packages/wxt/src/core/package-managers/yarn.ts index 8b5449329..b2d85101d 100644 --- a/packages/wxt/src/core/package-managers/yarn.ts +++ b/packages/wxt/src/core/package-managers/yarn.ts @@ -10,14 +10,17 @@ export const yarn: WxtPackageManagerImpl = { }, async listDependencies(options) { const args = ['list', '--json']; + if (options?.all) { args.push('--depth', 'Infinity'); } + const res = await spawn('yarn', args, { cwd: options?.cwd }); const tree = res.stdout .split('\n') .map((line) => JSON.parse(line)) .find((line) => line.type === 'tree')?.data as JsonLineTree | undefined; + if (tree == null) throw Error("'yarn list --json' did not output a tree"); const queue = [...tree.trees]; @@ -26,10 +29,12 @@ export const yarn: WxtPackageManagerImpl = { while (queue.length > 0) { const { name: treeName, children } = queue.pop()!; const match = /(@?\S+)@(\S+)$/.exec(treeName); + if (match) { const [_, name, version] = match; dependencies.push({ name, version }); } + if (children != null) { queue.push(...children); } From 9fac3fc67289b43ed6b049f16ed408e765d44057 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 10:27:04 +0100 Subject: [PATCH 32/64] fix(packages/wxt): add question for core/runners manual.ts --- packages/wxt/src/core/runners/manual.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wxt/src/core/runners/manual.ts b/packages/wxt/src/core/runners/manual.ts index 8dfee1a55..fb5773096 100644 --- a/packages/wxt/src/core/runners/manual.ts +++ b/packages/wxt/src/core/runners/manual.ts @@ -16,6 +16,7 @@ export function createManualRunner(): ExtensionRunner { ); }, async closeBrowser() { + // TODO: THIS ISN'T USED ANYWHERE, MAYBE REMOVE? // noop }, }; From 780ff175b60d8eea22a19e3cf65fcd5389bd1b07 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 11:09:45 +0100 Subject: [PATCH 33/64] fix(packages/wxt): remove @ts-expect-error from manifest.test.ts and fix typo --- packages/wxt/src/core/utils/__tests__/manifest.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/wxt/src/core/utils/__tests__/manifest.test.ts b/packages/wxt/src/core/utils/__tests__/manifest.test.ts index e900fdde5..efc35eda3 100644 --- a/packages/wxt/src/core/utils/__tests__/manifest.test.ts +++ b/packages/wxt/src/core/utils/__tests__/manifest.test.ts @@ -37,12 +37,11 @@ describe('Manifest Utils', () => { const popupEntrypoint = (type?: ActionType) => fakePopupEntrypoint({ options: { - // @ts-expect-error: Force this to be undefined instead of inheriting the random value - mv2Key: type ?? null, + mv2Key: type, defaultIcon: { '16': '/icon/16.png', }, - defaultTitle: 'Default Iitle', + defaultTitle: 'Default Title', }, outputDir: outDir, skipped: false, From a47b88e21b3f214cae35e29811f8c84d99188c92 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 12:01:07 +0100 Subject: [PATCH 34/64] fix(packages/wxt): add support for `null` type for `version` of fake WxtManifest --- packages/browser/src/gen/index.d.ts | 2 +- packages/wxt/src/core/utils/__tests__/manifest.test.ts | 1 - packages/wxt/src/core/utils/manifest.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/gen/index.d.ts b/packages/browser/src/gen/index.d.ts index 456894b6e..bd9a1a198 100644 --- a/packages/browser/src/gen/index.d.ts +++ b/packages/browser/src/gen/index.d.ts @@ -9239,7 +9239,7 @@ export namespace Browser { // Required manifest_version: number; name: string; - version: string; + version: string | null; // Recommended default_locale?: string | undefined; diff --git a/packages/wxt/src/core/utils/__tests__/manifest.test.ts b/packages/wxt/src/core/utils/__tests__/manifest.test.ts index efc35eda3..823927f36 100644 --- a/packages/wxt/src/core/utils/__tests__/manifest.test.ts +++ b/packages/wxt/src/core/utils/__tests__/manifest.test.ts @@ -1180,7 +1180,6 @@ describe('Manifest Utils', () => { setFakeWxt({ config: { manifest: { - // @ts-ignore: Purposefully removing version from fake object version: null, }, }, diff --git a/packages/wxt/src/core/utils/manifest.ts b/packages/wxt/src/core/utils/manifest.ts index 3496ac13b..4d6aaebfc 100644 --- a/packages/wxt/src/core/utils/manifest.ts +++ b/packages/wxt/src/core/utils/manifest.ts @@ -60,7 +60,7 @@ export async function generateManifest( wxt.config.manifest.version_name ?? wxt.config.manifest.version ?? pkg?.version; - if (versionName == null) { + if (!versionName) { versionName = '0.0.0'; wxt.logger.warn( 'Extension version not found, defaulting to "0.0.0". Add a version to your `package.json` or `wxt.config.ts` file. For more details, see: https://wxt.dev/guide/key-concepts/manifest.html#version-and-version-name', From 52f1cd84c8313d407d9829fd6e454f3e2f2beb18 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 12:32:03 +0100 Subject: [PATCH 35/64] clean(packages/wxt): make code more readable and make number and string const UPPER_CASE, like real const --- .../src/core/utils/__tests__/manifest.test.ts | 207 +++++++++++------- 1 file changed, 131 insertions(+), 76 deletions(-) diff --git a/packages/wxt/src/core/utils/__tests__/manifest.test.ts b/packages/wxt/src/core/utils/__tests__/manifest.test.ts index 823927f36..216761b0d 100644 --- a/packages/wxt/src/core/utils/__tests__/manifest.test.ts +++ b/packages/wxt/src/core/utils/__tests__/manifest.test.ts @@ -23,8 +23,8 @@ import { wxt } from '../../wxt'; import { mock } from 'vitest-mock-extended'; import type { Browser } from '@wxt-dev/browser'; -const outDir = '/output'; -const contentScriptOutDir = '/output/content-scripts'; +const OUT_DIR = '/output'; +const CONTENT_SCRIPT_OUT_DIR = '/output/content-scripts'; describe('Manifest Utils', () => { beforeEach(() => { @@ -43,7 +43,7 @@ describe('Manifest Utils', () => { }, defaultTitle: 'Default Title', }, - outputDir: outDir, + outputDir: OUT_DIR, skipped: false, }); @@ -54,9 +54,10 @@ describe('Manifest Utils', () => { setFakeWxt({ config: { manifestVersion: 3, - outDir, + outDir: OUT_DIR, }, }); + const expected: Partial = { action: { default_icon: popup.options.defaultIcon, @@ -85,12 +86,14 @@ describe('Manifest Utils', () => { async ({ inputType, expectedType }) => { const popup = popupEntrypoint(inputType); const buildOutput = fakeBuildOutput(); + setFakeWxt({ config: { manifestVersion: 2, - outDir, + outDir: OUT_DIR, }, }); + const expected = { default_icon: popup.options.defaultIcon, default_title: popup.options.defaultTitle, @@ -110,9 +113,10 @@ describe('Manifest Utils', () => { describe('action without popup', () => { it('should respect the action field in the manifest without a popup', async () => { const buildOutput = fakeBuildOutput(); + setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, manifestVersion: 3, manifest: { action: { @@ -132,9 +136,10 @@ describe('Manifest Utils', () => { it('should generate `browser_action` for MV2 when only `action` is defined', async () => { const buildOutput = fakeBuildOutput(); + setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, manifestVersion: 2, manifest: { action: { @@ -153,9 +158,10 @@ describe('Manifest Utils', () => { it('should keep the `page_action` for MV2 when both `action` and `page_action` are defined', async () => { const buildOutput = fakeBuildOutput(); + setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, manifestVersion: 2, manifest: { action: { @@ -177,9 +183,10 @@ describe('Manifest Utils', () => { it('should keep the custom `browser_action` for MV2 when both `action` and `browser_action` are defined', async () => { const buildOutput = fakeBuildOutput(); + setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, manifestVersion: 2, manifest: { action: { @@ -204,7 +211,7 @@ describe('Manifest Utils', () => { describe('options', () => { const options = fakeOptionsEntrypoint({ - outputDir: outDir, + outputDir: OUT_DIR, options: { openInTab: false, chromeStyle: true, @@ -217,10 +224,11 @@ describe('Manifest Utils', () => { setFakeWxt({ config: { manifestVersion: 3, - outDir, + outDir: OUT_DIR, browser: 'chrome', }, }); + const buildOutput = fakeBuildOutput(); const expected = { open_in_tab: false, @@ -241,9 +249,10 @@ describe('Manifest Utils', () => { config: { manifestVersion: 3, browser: 'firefox', - outDir, + outDir: OUT_DIR, }, }); + const buildOutput = fakeBuildOutput(); const expected = { open_in_tab: false, @@ -262,7 +271,7 @@ describe('Manifest Utils', () => { describe('background', () => { const background = fakeBackgroundEntrypoint({ - outputDir: outDir, + outputDir: OUT_DIR, options: { persistent: true, type: 'module', @@ -276,11 +285,12 @@ describe('Manifest Utils', () => { async (browser) => { setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, manifestVersion: 3, browser, }, }); + const buildOutput = fakeBuildOutput(); const expected = { type: 'module', @@ -299,11 +309,12 @@ describe('Manifest Utils', () => { it('should include a background script and type for firefox', async () => { setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, manifestVersion: 3, browser: 'firefox', }, }); + const buildOutput = fakeBuildOutput(); const expected = { type: 'module', @@ -325,11 +336,12 @@ describe('Manifest Utils', () => { async (browser) => { setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, manifestVersion: 2, browser, }, }); + const buildOutput = fakeBuildOutput(); const expected = { persistent: true, @@ -348,11 +360,12 @@ describe('Manifest Utils', () => { it('should include a background script and persistent for firefox mv2', async () => { setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, manifestVersion: 2, browser: 'firefox', }, }); + const buildOutput = fakeBuildOutput(); const expected = { persistent: true, @@ -431,6 +444,7 @@ describe('Manifest Utils', () => { 32: 'logo-32.png', 48: 'logo-48.png', }; + setFakeWxt({ config: { manifest: { @@ -454,7 +468,7 @@ describe('Manifest Utils', () => { type: 'content-script', name: 'one', inputPath: 'entrypoints/one.content/index.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://google.com/*'], }, @@ -464,11 +478,12 @@ describe('Manifest Utils', () => { type: 'asset', fileName: 'content-scripts/one.css', }; + const cs2: ContentScriptEntrypoint = { type: 'content-script', name: 'two', inputPath: 'entrypoints/two.content/index.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://google.com/*'], runAt: 'document_end', @@ -479,11 +494,12 @@ describe('Manifest Utils', () => { type: 'asset', fileName: 'content-scripts/two.css', }; + const cs3: ContentScriptEntrypoint = { type: 'content-script', name: 'three', inputPath: 'entrypoints/three.content/index.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://google.com/*'], runAt: 'document_end', @@ -494,11 +510,12 @@ describe('Manifest Utils', () => { type: 'asset', fileName: 'content-scripts/three.css', }; + const cs4: ContentScriptEntrypoint = { type: 'content-script', name: 'four', inputPath: 'entrypoints/four.content/index.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://duckduckgo.com/*'], runAt: 'document_end', @@ -509,11 +526,12 @@ describe('Manifest Utils', () => { type: 'asset', fileName: 'content-scripts/four.css', }; + const cs5: ContentScriptEntrypoint = { type: 'content-script', name: 'five', inputPath: 'entrypoints/five.content/index.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://google.com/*'], world: 'MAIN', @@ -529,10 +547,11 @@ describe('Manifest Utils', () => { setFakeWxt({ config: { command: 'build', - outDir, + outDir: OUT_DIR, manifestVersion: 3, }, }); + const buildOutput: Omit = { publicAssets: [], steps: [ @@ -554,18 +573,21 @@ describe('Manifest Utils', () => { css: ['content-scripts/one.css'], js: ['content-scripts/one.js'], }); + expect(actual.content_scripts).toContainEqual({ matches: ['*://google.com/*'], run_at: 'document_end', css: ['content-scripts/two.css', 'content-scripts/three.css'], js: ['content-scripts/two.js', 'content-scripts/three.js'], }); + expect(actual.content_scripts).toContainEqual({ matches: ['*://duckduckgo.com/*'], run_at: 'document_end', css: ['content-scripts/four.css'], js: ['content-scripts/four.js'], }); + expect(actual.content_scripts).toContainEqual({ matches: ['*://google.com/*'], css: ['content-scripts/five.css'], @@ -579,16 +601,18 @@ describe('Manifest Utils', () => { type: 'content-script', name: 'one', inputPath: 'entrypoints/one.content.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://google.com/*'], }, skipped: false, }; + const generatedContentScript = { matches: ['*://google.com/*'], js: ['content-scripts/one.js'], }; + const userContentScript = { css: ['content-scripts/two.css'], matches: ['*://*.google.com/*'], @@ -596,9 +620,10 @@ describe('Manifest Utils', () => { const entrypoints = [cs]; const buildOutput = fakeBuildOutput(); + setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, command: 'build', manifest: { content_scripts: [userContentScript], @@ -623,13 +648,14 @@ describe('Manifest Utils', () => { type: 'content-script', name: 'one', inputPath: 'entrypoints/one.content.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://google.com/*'], cssInjectionMode, }, skipped: false, }; + const styles: OutputAsset = { type: 'asset', fileName: 'content-scripts/one.css', @@ -640,9 +666,10 @@ describe('Manifest Utils', () => { publicAssets: [], steps: [{ entrypoints: cs, chunks: [styles] }], }; + setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, command: 'build', }, }); @@ -669,13 +696,14 @@ describe('Manifest Utils', () => { type: 'content-script', name: 'one', inputPath: 'entrypoints/one.content.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://google.com/*'], cssInjectionMode, }, skipped: false, }; + const styles: OutputAsset = { type: 'asset', fileName: 'content-scripts/one.css', @@ -686,9 +714,10 @@ describe('Manifest Utils', () => { publicAssets: [], steps: [{ entrypoints: cs, chunks: [styles] }], }; + setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, command: 'build', }, }); @@ -712,13 +741,14 @@ describe('Manifest Utils', () => { type: 'content-script', name: 'one', inputPath: 'entrypoints/one.content.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://google.com/*'], cssInjectionMode: 'ui', }, skipped: false, }; + const styles: OutputAsset = { type: 'asset', fileName: 'content-scripts/one.css', @@ -729,9 +759,10 @@ describe('Manifest Utils', () => { publicAssets: [], steps: [{ entrypoints: cs, chunks: [styles] }], }; + setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, command: 'build', manifestVersion: 3, }, @@ -756,13 +787,14 @@ describe('Manifest Utils', () => { type: 'content-script', name: 'one', inputPath: 'entrypoints/one.content.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://google.com/*'], cssInjectionMode: 'ui', }, skipped: false, }; + const styles: OutputAsset = { type: 'asset', fileName: 'content-scripts/one.css', @@ -773,9 +805,10 @@ describe('Manifest Utils', () => { publicAssets: [], steps: [{ entrypoints: cs, chunks: [styles] }], }; + setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, command: 'build', manifestVersion: 2, }, @@ -796,13 +829,14 @@ describe('Manifest Utils', () => { type: 'content-script', name: 'one', inputPath: 'entrypoints/one.content.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://play.google.com/books/*'], cssInjectionMode: 'ui', }, skipped: false, }; + const styles: OutputAsset = { type: 'asset', fileName: 'content-scripts/one.css', @@ -813,9 +847,10 @@ describe('Manifest Utils', () => { publicAssets: [], steps: [{ entrypoints: cs, chunks: [styles] }], }; + setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, command: 'build', manifestVersion: 3, }, @@ -842,7 +877,7 @@ describe('Manifest Utils', () => { type: 'content-script', name: 'one', inputPath: 'entrypoints/one.content.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://google.com/*'], registration: 'runtime', @@ -859,10 +894,11 @@ describe('Manifest Utils', () => { publicAssets: [], steps: [{ entrypoints: cs, chunks: [styles] }], }; + setFakeWxt({ config: { manifestVersion: 3, - outDir, + outDir: OUT_DIR, command: 'build', }, }); @@ -883,7 +919,7 @@ describe('Manifest Utils', () => { 'should include the side_panel and permission, ignoring all options for %s', async (browser) => { const sidepanel = fakeSidepanelEntrypoint({ - outputDir: outDir, + outputDir: OUT_DIR, skipped: false, }); const buildOutput = fakeBuildOutput(); @@ -892,10 +928,11 @@ describe('Manifest Utils', () => { config: { manifestVersion: 3, browser, - outDir, + outDir: OUT_DIR, command: 'build', }, }); + const expected = { side_panel: { default_path: 'sidepanel.html', @@ -916,7 +953,7 @@ describe('Manifest Utils', () => { 'should include a sidebar_action for %s', async (browser) => { const sidepanel = fakeSidepanelEntrypoint({ - outputDir: outDir, + outputDir: OUT_DIR, skipped: false, }); const buildOutput = fakeBuildOutput(); @@ -925,9 +962,10 @@ describe('Manifest Utils', () => { config: { manifestVersion: 3, browser, - outDir, + outDir: OUT_DIR, }, }); + const expected = { sidebar_action: { default_panel: 'sidepanel.html', @@ -954,13 +992,14 @@ describe('Manifest Utils', () => { type: 'content-script', name: 'one', inputPath: 'entrypoints/one.content.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://google.com/*'], cssInjectionMode: 'ui', }, skipped: false, }; + const styles: OutputAsset = { type: 'asset', fileName: 'content-scripts/one.css', @@ -971,9 +1010,10 @@ describe('Manifest Utils', () => { publicAssets: [], steps: [{ entrypoints: cs, chunks: [styles] }], }; + setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, command: 'build', manifestVersion: 3, manifest: { @@ -1004,13 +1044,14 @@ describe('Manifest Utils', () => { type: 'content-script', name: 'one', inputPath: 'entrypoints/one.content.ts', - outputDir: contentScriptOutDir, + outputDir: CONTENT_SCRIPT_OUT_DIR, options: { matches: ['*://google.com/*'], cssInjectionMode: 'ui', }, skipped: false, }; + const styles: OutputAsset = { type: 'asset', fileName: 'content-scripts/one.css', @@ -1021,9 +1062,10 @@ describe('Manifest Utils', () => { publicAssets: [], steps: [{ entrypoints: cs, chunks: [styles] }], }; + setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, command: 'build', manifestVersion: 2, manifest: { @@ -1046,7 +1088,7 @@ describe('Manifest Utils', () => { it('should convert mv3 items to mv2 strings automatically', async () => { setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, manifestVersion: 2, manifest: { web_accessible_resources: [ @@ -1077,7 +1119,7 @@ describe('Manifest Utils', () => { it('should convert mv2 strings to mv3 items with a warning automatically', async () => { setFakeWxt({ config: { - outDir, + outDir: OUT_DIR, manifestVersion: 3, manifest: { web_accessible_resources: ['/icon.svg'], @@ -1097,16 +1139,17 @@ describe('Manifest Utils', () => { it.each(['chrome', 'safari', 'edge'] as const)( 'should include version and version_name as is on %s', async (browser) => { - const version = '1.0.0'; - const versionName = '1.0.0-alpha1'; + const VERSION = '1.0.0'; + const VERSION_NAME = '1.0.0-alpha1'; const entrypoints: Entrypoint[] = []; const buildOutput = fakeBuildOutput(); + setFakeWxt({ config: { browser, manifest: { - version, - version_name: versionName, + version: VERSION, + version_name: VERSION_NAME, }, }, }); @@ -1116,24 +1159,25 @@ describe('Manifest Utils', () => { buildOutput, ); - expect(actual.version).toBe(version); - expect(actual.version_name).toBe(versionName); + expect(actual.version).toBe(VERSION); + expect(actual.version_name).toBe(VERSION_NAME); }, ); it.each(['firefox'] as const)( 'should not include a version_name on %s because it is unsupported', async (browser) => { - const version = '1.0.0'; - const versionName = '1.0.0-alpha1'; + const VERSION = '1.0.0'; + const VERSION_NAME = '1.0.0-alpha1'; const entrypoints: Entrypoint[] = []; const buildOutput = fakeBuildOutput(); + setFakeWxt({ config: { browser, manifest: { - version, - version_name: versionName, + version: VERSION, + version_name: VERSION_NAME, }, }, }); @@ -1143,7 +1187,7 @@ describe('Manifest Utils', () => { buildOutput, ); - expect(actual.version).toBe(version); + expect(actual.version).toBe(VERSION); expect(actual.version_name).toBeUndefined(); }, ); @@ -1151,15 +1195,16 @@ describe('Manifest Utils', () => { it.each(['chrome', 'firefox', 'safari', 'edge'])( 'should not include the version_name if it is equal to version', async (browser) => { - const version = '1.0.0'; + const VERSION = '1.0.0'; const entrypoints: Entrypoint[] = []; const buildOutput = fakeBuildOutput(); + setFakeWxt({ config: { browser, manifest: { - version, - version_name: version, + version: VERSION, + version_name: VERSION, }, }, }); @@ -1169,7 +1214,7 @@ describe('Manifest Utils', () => { buildOutput, ); - expect(actual.version).toBe(version); + expect(actual.version).toBe(VERSION); expect(actual.version_name).toBeUndefined(); }, ); @@ -1177,6 +1222,7 @@ describe('Manifest Utils', () => { it('should log a warning if the version could not be detected', async () => { const entrypoints: Entrypoint[] = []; const buildOutput = fakeBuildOutput(); + setFakeWxt({ config: { manifest: { @@ -1200,7 +1246,7 @@ describe('Manifest Utils', () => { }); describe('commands', () => { - const reloadCommandName = 'wxt:reload-extension'; + const RELOAD_COMMAND_NAME = 'wxt:reload-extension'; const reloadCommand = { description: expect.any(String), suggested_key: { @@ -1221,7 +1267,7 @@ describe('Manifest Utils', () => { ); expect(actual.commands).toEqual({ - [reloadCommandName]: reloadCommand, + [RELOAD_COMMAND_NAME]: reloadCommand, }); }); @@ -1243,7 +1289,7 @@ describe('Manifest Utils', () => { ); expect(actual.commands).toEqual({ - [reloadCommandName]: { + [RELOAD_COMMAND_NAME]: { ...reloadCommand, suggested_key: { default: 'Ctrl+E', @@ -1273,18 +1319,20 @@ describe('Manifest Utils', () => { }); it('should not override any existing commands when adding the one to reload the extension', async () => { - const customCommandName = 'custom-command'; + const CUSTOM_COMMAND_NAME = 'custom-command'; const customCommand = fakeManifestCommand(); + setFakeWxt({ config: { command: 'serve', manifest: { commands: { - [customCommandName]: customCommand, + [CUSTOM_COMMAND_NAME]: customCommand, }, }, }, }); + const output = fakeBuildOutput(); const entrypoints = fakeArray(fakeEntrypoint); @@ -1294,8 +1342,8 @@ describe('Manifest Utils', () => { ); expect(actual.commands).toEqual({ - [reloadCommandName]: reloadCommand, - [customCommandName]: customCommand, + [RELOAD_COMMAND_NAME]: reloadCommand, + [CUSTOM_COMMAND_NAME]: customCommand, }); }); @@ -1328,6 +1376,7 @@ describe('Manifest Utils', () => { setFakeWxt({ config: { command: 'build' }, }); + const output = fakeBuildOutput(); const entrypoints = fakeArray(fakeEntrypoint); @@ -1413,6 +1462,7 @@ describe('Manifest Utils', () => { it('should keep host_permissions as-is for MV3', async () => { const expectedHostPermissions = ['https://google.com/*']; const expectedPermissions = ['scripting']; + setFakeWxt({ config: { manifest: { @@ -1423,6 +1473,7 @@ describe('Manifest Utils', () => { command: 'build', }, }); + const output = fakeBuildOutput(); const { manifest: actual } = await generateManifest([], output); @@ -1437,6 +1488,7 @@ describe('Manifest Utils', () => { '*://*.youtube.com/*', 'https://google.com/*', ]; + setFakeWxt({ config: { manifest: { @@ -1447,6 +1499,7 @@ describe('Manifest Utils', () => { command: 'build', }, }); + const output = fakeBuildOutput(); const { manifest: actual } = await generateManifest([], output); @@ -1520,6 +1573,7 @@ describe('Manifest Utils', () => { origin: 'http://localhost:3000', }), }); + const output = fakeBuildOutput(); const entrypoints: Entrypoint[] = []; @@ -1543,9 +1597,9 @@ describe('Manifest Utils', () => { it('should convert MV3 CSP object to MV2 CSP string with localhost for MV2', async () => { const entrypoints: Entrypoint[] = []; const buildOutput = fakeBuildOutput(); - const inputCsp = + const INPUT_CSP = "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"; - const expectedCsp = + const EXPECTED_CSP = "script-src 'self' 'wasm-unsafe-eval' http://localhost:3000; object-src 'self';"; // Setup WXT for Firefox and serve command @@ -1556,7 +1610,7 @@ describe('Manifest Utils', () => { manifestVersion: 2, manifest: { content_security_policy: { - extension_pages: inputCsp, + extension_pages: INPUT_CSP, }, }, }, @@ -1572,7 +1626,7 @@ describe('Manifest Utils', () => { buildOutput, ); - expect(actual.content_security_policy).toEqual(expectedCsp); + expect(actual.content_security_policy).toEqual(EXPECTED_CSP); }); }); @@ -1602,12 +1656,13 @@ describe('Manifest Utils', () => { describe('manifest_version', () => { it('should ignore and log a warning when someone sets `manifest_version` inside the manifest', async () => { const buildOutput = fakeBuildOutput(); - const expectedVersion = 2; + const EXPECTED_VERSION = 2; + setFakeWxt({ logger: mock(), config: { command: 'build', - manifestVersion: expectedVersion, + manifestVersion: EXPECTED_VERSION, manifest: { manifest_version: 3, }, @@ -1616,7 +1671,7 @@ describe('Manifest Utils', () => { const { manifest } = await generateManifest([], buildOutput); - expect(manifest.manifest_version).toBe(expectedVersion); + expect(manifest.manifest_version).toBe(EXPECTED_VERSION); expect(wxt.logger.warn).toBeCalledTimes(1); expect(wxt.logger.warn).toBeCalledWith( expect.stringContaining( From 1f161d6bd58672448ee8bff9c98d21a899e35022 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Fri, 26 Dec 2025 22:54:58 +0100 Subject: [PATCH 36/64] clean(packages/wxt): make code more readable and make number and string const UPPER_CASE, like real const and remove unnecessary staff --- packages/i18n/src/__tests__/index.test.ts | 6 +- packages/storage/src/__tests__/index.test.ts | 235 +++++++++++------- packages/wxt/src/__tests__/modules.test.ts | 10 +- packages/wxt/src/cli/__tests__/index.test.ts | 21 +- packages/wxt/src/cli/index.ts | 4 +- .../builders/vite/__tests__/fixtures/test.ts | 2 +- .../__tests__/devHtmlPrerender.test.ts | 19 +- .../builders/vite/plugins/devHtmlPrerender.ts | 22 +- .../builders/vite/plugins/extensionApiMock.ts | 13 +- .../builders/vite/plugins/noopBackground.ts | 9 +- .../builders/vite/plugins/resolveAppConfig.ts | 12 +- .../vite/plugins/resolveVirtualModules.ts | 21 +- .../builders/vite/plugins/wxtPluginLoader.ts | 23 +- packages/wxt/src/core/clean.ts | 2 + packages/wxt/src/core/generate-wxt-dir.ts | 22 +- .../package-managers/__tests__/npm.test.ts | 4 +- packages/wxt/src/core/package-managers/npm.ts | 6 +- .../core/utils/__tests__/entrypoints.test.ts | 25 +- .../__tests__/minimatch-multiple.test.ts | 24 +- .../src/core/utils/__tests__/package.test.ts | 4 +- .../src/core/utils/__tests__/strings.test.ts | 11 +- .../core/utils/__tests__/transform.test.ts | 65 ++--- .../__tests__/detect-dev-changes.test.ts | 88 ++++--- .../test-entrypoints/no-default-export.ts | 0 .../src/core/utils/building/internal-build.ts | 26 +- .../wxt/src/core/utils/log/printHeader.ts | 5 +- packages/wxt/src/core/utils/manifest.ts | 36 ++- .../wxt/src/core/utils/minimatch-multiple.ts | 1 + packages/wxt/src/core/utils/network.ts | 3 +- packages/wxt/src/core/utils/syntax-errors.ts | 2 + packages/wxt/src/core/utils/transform.ts | 12 +- packages/wxt/src/core/utils/validation.ts | 2 + packages/wxt/src/core/wxt.ts | 1 + packages/wxt/src/core/zip.ts | 26 +- packages/wxt/src/modules.ts | 6 +- packages/wxt/src/types.ts | 2 +- .../__tests__/content-script-context.test.ts | 9 +- .../__tests__/split-shadow-root-css.test.ts | 20 +- .../wxt/src/utils/content-script-context.ts | 4 +- .../content-script-ui/__tests__/index.test.ts | 65 ++++- .../wxt/src/utils/content-script-ui/iframe.ts | 3 +- .../src/utils/content-script-ui/integrated.ts | 3 +- .../utils/content-script-ui/shadow-root.ts | 4 +- .../wxt/src/utils/content-script-ui/shared.ts | 22 +- packages/wxt/src/utils/define-background.ts | 2 + .../wxt/src/utils/define-unlisted-script.ts | 2 + .../wxt/src/utils/internal/custom-events.ts | 4 +- .../utils/internal/dev-server-websocket.ts | 4 +- packages/wxt/src/version.ts | 2 +- .../wxt/src/virtual/background-entrypoint.ts | 3 +- .../content-script-main-world-entrypoint.ts | 2 +- packages/wxt/src/virtual/reload-html.ts | 2 +- .../virtual/utils/reload-content-scripts.ts | 10 + packages/wxt/vitest.globalSetup.ts | 9 +- 54 files changed, 592 insertions(+), 348 deletions(-) delete mode 100644 packages/wxt/src/core/utils/building/__tests__/test-entrypoints/no-default-export.ts diff --git a/packages/i18n/src/__tests__/index.test.ts b/packages/i18n/src/__tests__/index.test.ts index 11ed8d60b..6f9fe7791 100644 --- a/packages/i18n/src/__tests__/index.test.ts +++ b/packages/i18n/src/__tests__/index.test.ts @@ -53,13 +53,13 @@ describe('createI18n', () => { (rawMessage, count, expected) => { const i18n = createI18n(); getMessageMock.mockReturnValue(rawMessage); - const key = 'items'; + const KEY = 'items'; - const actual = i18n.t(key, count); + const actual = i18n.t(KEY, count); expect(actual).toBe(expected); expect(getMessageMock).toBeCalledTimes(1); - expect(getMessageMock).toBeCalledWith(key, [String(count)]); + expect(getMessageMock).toBeCalledWith(KEY, [String(count)]); }, ); diff --git a/packages/storage/src/__tests__/index.test.ts b/packages/storage/src/__tests__/index.test.ts index 1ba64e3cd..27ac26d78 100644 --- a/packages/storage/src/__tests__/index.test.ts +++ b/packages/storage/src/__tests__/index.test.ts @@ -30,21 +30,21 @@ describe('Storage Utils', () => { (storageArea) => { describe('getItem', () => { it('should return the value from the correct storage area', async () => { - const expected = 123; - await fakeBrowser.storage[storageArea].set({ count: expected }); + const EXPECTED = 123; + await fakeBrowser.storage[storageArea].set({ count: EXPECTED }); const actual = await storage.getItem(`${storageArea}:count`); - expect(actual).toBe(expected); + expect(actual).toBe(EXPECTED); }); it('should return the value if multiple : are used in the key', async () => { - const expected = 'value'; - await fakeBrowser.storage[storageArea].set({ 'some:key': expected }); + const EXPECTED = 'value'; + await fakeBrowser.storage[storageArea].set({ 'some:key': EXPECTED }); const actual = await storage.getItem(`${storageArea}:some:key`); - expect(actual).toBe(expected); + expect(actual).toBe(EXPECTED); }); it("should return null if the value doesn't exist", async () => { @@ -54,12 +54,12 @@ describe('Storage Utils', () => { }); it('should return the default value if passed in options', async () => { - const expected = 0; + const EXPECTED = 0; const actual = await storage.getItem(`${storageArea}:count`, { - defaultValue: expected, + defaultValue: EXPECTED, }); - expect(actual).toBe(expected); + expect(actual).toBe(EXPECTED); }); }); @@ -69,6 +69,7 @@ describe('Storage Utils', () => { key: `${storageArea}:one`, expectedValue: 1, } as const; + const item2 = { key: `${storageArea}:two`, expectedValue: null, @@ -88,72 +89,72 @@ describe('Storage Utils', () => { it('should get values from multiple storage items', async () => { const item1 = storage.defineItem(`${storageArea}:one`); - const expectedValue1 = 1; + const EXPECTED_VALUE_1 = 1; const item2 = storage.defineItem(`${storageArea}:two`); - const expectedValue2 = null; + const EXPECTED_VALUE_2 = null; await fakeBrowser.storage[storageArea].set({ - one: expectedValue1, + one: EXPECTED_VALUE_1, }); const actual = await storage.getItems([item1, item2]); expect(actual).toEqual([ - { key: item1.key, value: expectedValue1 }, - { key: item2.key, value: expectedValue2 }, + { key: item1.key, value: EXPECTED_VALUE_1 }, + { key: item2.key, value: EXPECTED_VALUE_2 }, ]); }); it('should get values for a combination of different input types', async () => { const key1 = `${storageArea}:one` as const; - const expectedValue1 = 1; + const EXPECTED_VALUE_1 = 1; const item2 = storage.defineItem(`${storageArea}:two`); - const expectedValue2 = 2; + const EXPECTED_VALUE_2 = 2; await fakeBrowser.storage[storageArea].set({ - one: expectedValue1, - two: expectedValue2, + one: EXPECTED_VALUE_1, + two: EXPECTED_VALUE_2, }); const actual = await storage.getItems([key1, item2]); expect(actual).toEqual([ - { key: key1, value: expectedValue1 }, - { key: item2.key, value: expectedValue2 }, + { key: key1, value: EXPECTED_VALUE_1 }, + { key: item2.key, value: EXPECTED_VALUE_2 }, ]); }); it('should return fallback values for keys when provided', async () => { const key1 = `${storageArea}:one` as const; - const expectedValue1 = null; + const EXPECTED_VALUE_1 = null; const key2 = `${storageArea}:two` as const; - const fallback2 = 2; - const expectedValue2 = fallback2; + const FALLBACK_2 = 2; + const EXPECTED_VALUE_2 = FALLBACK_2; const actual = await storage.getItems([ key1, - { key: key2, options: { fallback: fallback2 } }, + { key: key2, options: { fallback: FALLBACK_2 } }, ]); expect(actual).toEqual([ - { key: key1, value: expectedValue1 }, - { key: key2, value: expectedValue2 }, + { key: key1, value: EXPECTED_VALUE_1 }, + { key: key2, value: EXPECTED_VALUE_2 }, ]); }); it('should return fallback values for items when provided', async () => { const item1 = storage.defineItem(`${storageArea}:one`); - const expectedValue1 = null; + const EXPECTED_VALUE_1 = null; const item2 = storage.defineItem(`${storageArea}:two`, { fallback: 2, }); - const expectedValue2 = item2.fallback; + const EXPECTED_VALUE_2 = item2.fallback; const actual = await storage.getItems([item1, item2]); expect(actual).toEqual([ - { key: item1.key, value: expectedValue1 }, - { key: item2.key, value: expectedValue2 }, + { key: item1.key, value: EXPECTED_VALUE_1 }, + { key: item2.key, value: EXPECTED_VALUE_2 }, ]); }); }); @@ -178,9 +179,9 @@ describe('Storage Utils', () => { describe('setItem', () => { it('should set the value in the correct storage area', async () => { const key = `${storageArea}:count` as const; - const value = 321; + const VALUE = 321; - await storage.setItem(key, value); + await storage.setItem(key, VALUE); }); it.each([undefined, null])( @@ -203,12 +204,14 @@ describe('Storage Utils', () => { { key: `${storageArea}:count` as const, value: 234 }, { key: `${storageArea}:installDate` as const, value: null }, ]; + await fakeBrowser.storage[storageArea].set({ count: 123, installDate: 321, }); await storage.setItems(expected); + const actual = await storage.getItems( expected.map((item) => item.key), ); @@ -223,7 +226,9 @@ describe('Storage Utils', () => { describe('setMeta', () => { it('should set metadata at key+$', async () => { const existing = { v: 1 }; + await browser.storage[storageArea].set({ count$: existing }); + const newValues = { date: Date.now(), }; @@ -239,7 +244,9 @@ describe('Storage Utils', () => { 'should remove any properties set to %s', async (version) => { const existing = { v: 1 }; + await browser.storage[storageArea].set({ count$: existing }); + const expected = {}; await storage.setMeta(`${storageArea}:count`, { v: version }); @@ -330,6 +337,7 @@ describe('Storage Utils', () => { it('should not remove the metadata by default', async () => { const expected = { v: 1 }; + await fakeBrowser.storage[storageArea].set({ count$: expected, count: 3, @@ -360,6 +368,7 @@ describe('Storage Utils', () => { it('should remove multiple keys', async () => { const key1 = `${storageArea}:one` as const; const key2 = `${storageArea}:two` as const; + await fakeBrowser.storage[storageArea].set({ one: 1, two: 2, @@ -374,6 +383,7 @@ describe('Storage Utils', () => { 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 }, @@ -409,6 +419,7 @@ describe('Storage Utils', () => { 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 }, @@ -562,10 +573,12 @@ describe('Storage Utils', () => { one: 'one', two: 'two', }; + const existing = { two: 'two-two', three: 'three', }; + await fakeBrowser.storage[storageArea].set(existing); await storage.restoreSnapshot(storageArea, data); @@ -581,15 +594,18 @@ describe('Storage Utils', () => { v: 2, }, }; + const data = { count$: { restoredAt: Date.now(), }, }; + const expected = { ...existing, count$: data.count$, }; + await fakeBrowser.storage[storageArea].set(existing); await storage.restoreSnapshot(storageArea, data); @@ -622,14 +638,14 @@ describe('Storage Utils', () => { it('should call the callback when the value changes', async () => { const cb = vi.fn(); - const newValue = '123'; - const oldValue = null; + const NEW_VALUE = '123'; + const OLD_VALUE = null; storage.watch(`${storageArea}:key`, cb); - await storage.setItem(`${storageArea}:key`, newValue); + await storage.setItem(`${storageArea}:key`, NEW_VALUE); expect(cb).toBeCalledTimes(1); - expect(cb).toBeCalledWith(newValue, oldValue); + expect(cb).toBeCalledWith(NEW_VALUE, OLD_VALUE); }); it('should remove the listener when calling the returned function', async () => { @@ -637,6 +653,7 @@ describe('Storage Utils', () => { const unwatch = storage.watch(`${storageArea}:key`, cb); unwatch(); + await storage.setItem(`${storageArea}:key`, '123'); expect(cb).not.toBeCalled(); @@ -680,6 +697,7 @@ describe('Storage Utils', () => { count: 2, count$: { v: 1 }, }); + const migrateToV2 = vi.fn((oldCount) => oldCount * 2); const migrateToV3 = vi.fn((oldCount) => oldCount * 3); @@ -711,6 +729,7 @@ describe('Storage Utils', () => { count: 2, count$: { v: 1 }, }); + const migrateToV2 = vi.fn((oldCount) => oldCount * 2); const migrateToV3 = vi.fn((oldCount) => oldCount * 3); const onMigrationComplete = vi.fn((count, _v) => count); @@ -742,6 +761,7 @@ describe('Storage Utils', () => { 3: migrateToV3, }, }); + await waitForMigrations(); const actualValue = await item.getValue(); @@ -758,6 +778,7 @@ describe('Storage Utils', () => { await fakeBrowser.storage.local.set({ count: 2, }); + const migrateToV2 = vi.fn((oldCount) => oldCount * 2); const item = storage.defineItem(`local:count`, { @@ -767,6 +788,7 @@ describe('Storage Utils', () => { 2: migrateToV2, }, }); + await waitForMigrations(); const actualValue = await item.getValue(); @@ -784,6 +806,7 @@ describe('Storage Utils', () => { count: 2, count$: { v: 3 }, }); + const migrateToV2 = vi.fn((oldCount) => oldCount * 2); const migrateToV3 = vi.fn((oldCount) => oldCount * 3); @@ -817,6 +840,7 @@ describe('Storage Utils', () => { 3: migrateToV3, }, }); + await waitForMigrations(); const actualValue = await item.getValue(); @@ -833,17 +857,19 @@ describe('Storage Utils', () => { }); it('should throw an error if the new version is less than the previous version', async () => { - const prevVersion = 2; - const nextVersion = 1; + const PREV_VERSION = 2; + const NEXT_VERSION = 1; + await fakeBrowser.storage.local.set({ count: 0, - count$: { v: prevVersion }, + count$: { v: PREV_VERSION }, }); const item = storage.defineItem(`local:count`, { defaultValue: 0, - version: nextVersion, + version: NEXT_VERSION, }); + await waitForMigrations(); await expect(item.migrate()).rejects.toThrow( @@ -878,6 +904,7 @@ describe('Storage Utils', () => { count: 2, count$: { v: 1 }, }); + const migrateToV2 = vi.fn((oldCount) => oldCount * 2); const migrateToV3 = vi.fn((oldCount) => oldCount * 3); const consoleSpy = vi.spyOn(console, 'debug'); @@ -891,6 +918,7 @@ describe('Storage Utils', () => { }, debug: true, }); + await waitForMigrations(); expect(consoleSpy).toHaveBeenCalledTimes(4); @@ -915,6 +943,7 @@ describe('Storage Utils', () => { count2: 2, count2$: { v: 1 }, }); + const migrateToV2 = vi.fn((oldCount) => oldCount * 2); const migrateToV3 = vi.fn((oldCount) => oldCount * 3); const consoleSpy = vi.spyOn(console, 'debug'); @@ -936,20 +965,22 @@ describe('Storage Utils', () => { }, debug: false, }); + await waitForMigrations(); + expect(consoleSpy).toHaveBeenCalledTimes(0); }); }); describe('getValue', () => { it('should return the value from storage', async () => { - const expected = 2; + const EXPECTED = 2; const item = storage.defineItem(`local:count`); - await fakeBrowser.storage.local.set({ count: expected }); + await fakeBrowser.storage.local.set({ count: EXPECTED }); const actual = await item.getValue(); - expect(actual).toBe(expected); + expect(actual).toBe(EXPECTED); }); it('should return null if missing', async () => { @@ -961,14 +992,15 @@ describe('Storage Utils', () => { }); it('should return the provided default value if missing', async () => { - const expected = 0; + const EXPECTED0 = 0; + const item = storage.defineItem(`local:count`, { - defaultValue: expected, + defaultValue: EXPECTED0, }); const actual = await item.getValue(); - expect(actual).toEqual(expected); + expect(actual).toEqual(EXPECTED0); }); }); @@ -976,6 +1008,7 @@ describe('Storage Utils', () => { it('should return the value from storage at key+$', async () => { const expected = { v: 2 }; const item = storage.defineItem(`local:count`); + await fakeBrowser.storage.local.set({ count$: expected }); const actual = await item.getMeta(); @@ -995,13 +1028,13 @@ describe('Storage Utils', () => { describe('setValue', () => { it('should set the value in storage', async () => { - const expected = 1; + const EXPECTED = 1; const item = storage.defineItem(`local:count`); - await item.setValue(expected); + await item.setValue(EXPECTED); const actual = await item.getValue(); - expect(actual).toBe(expected); + expect(actual).toBe(EXPECTED); }); it.each([undefined, null])( @@ -1020,15 +1053,15 @@ describe('Storage Utils', () => { describe('setMeta', () => { it('should set metadata at key+$', async () => { - const expected = { date: Date.now() }; + const EXPECTED = { date: Date.now() }; const item = storage.defineItem( `local:count`, ); - await item.setMeta(expected); + await item.setMeta(EXPECTED); const actual = await item.getMeta(); - expect(actual).toEqual(expected); + expect(actual).toEqual(EXPECTED); }); it('should add to metadata if already present', async () => { @@ -1038,6 +1071,7 @@ describe('Storage Utils', () => { const item = storage.defineItem( `local:count`, ); + await fakeBrowser.storage.local.set({ count$: existing, }); @@ -1052,6 +1086,7 @@ describe('Storage Utils', () => { describe('removeValue', () => { it('should remove the key from storage', async () => { const item = storage.defineItem(`local:count`); + await fakeBrowser.storage.local.set({ count: 456 }); await item.removeValue(); @@ -1063,6 +1098,7 @@ describe('Storage Utils', () => { it('should not remove the metadata by default', async () => { const item = storage.defineItem(`local:count`); const expected = { v: 1 }; + await fakeBrowser.storage.local.set({ count$: expected, count: 3, @@ -1076,6 +1112,7 @@ describe('Storage Utils', () => { it('should remove the metadata when requested', async () => { const item = storage.defineItem(`local:count`); + await fakeBrowser.storage.local.set({ count$: { v: 1 }, count: 3, @@ -1091,6 +1128,7 @@ describe('Storage Utils', () => { describe('removeMeta', () => { it('should remove all metadata', async () => { const item = storage.defineItem(`local:count`); + await fakeBrowser.storage.local.set({ count$: { v: 4 } }); await item.removeMeta(); @@ -1103,6 +1141,7 @@ describe('Storage Utils', () => { const item = storage.defineItem( `local:count`, ); + await fakeBrowser.storage.local.set({ count$: { v: 4, d: Date.now() }, }); @@ -1126,60 +1165,66 @@ describe('Storage Utils', () => { }); it("should not trigger if the value doesn't change", async () => { + const VALUE = '123'; + const item = storage.defineItem(`local:key`); const cb = vi.fn(); - const value = '123'; - await item.setValue(value); + await item.setValue(VALUE); item.watch(cb); - await item.setValue(value); + await item.setValue(VALUE); expect(cb).not.toBeCalled(); }); it('should call the callback when the value changes', async () => { + const NEW_VALUE = '123'; + const OLD_VALUE = null; + const item = storage.defineItem(`local:key`); const cb = vi.fn(); - const newValue = '123'; - const oldValue = null; item.watch(cb); - await item.setValue(newValue); + await item.setValue(NEW_VALUE); expect(cb).toBeCalledTimes(1); - expect(cb).toBeCalledWith(newValue, oldValue); + expect(cb).toBeCalledWith(NEW_VALUE, OLD_VALUE); }); it('should use the default value for the newValue when the item is removed', async () => { - const defaultValue = 'default'; + const OLD_VALUE = '123'; + const DEFAULT_VALUE = 'default'; + const item = storage.defineItem(`local:key`, { - defaultValue, + defaultValue: DEFAULT_VALUE, }); + const cb = vi.fn(); - const oldValue = '123'; - await item.setValue(oldValue); + await item.setValue(OLD_VALUE); item.watch(cb); await item.removeValue(); expect(cb).toBeCalledTimes(1); - expect(cb).toBeCalledWith(defaultValue, oldValue); + expect(cb).toBeCalledWith(DEFAULT_VALUE, OLD_VALUE); }); it("should use the default value for the oldItem when the item didn't exist in storage yet", async () => { - const defaultValue = 'default'; + const DEFAULT_VALUE = 'default'; + const NEW_VALUE = '123'; + const item = storage.defineItem(`local:key`, { - defaultValue, + defaultValue: DEFAULT_VALUE, }); + const cb = vi.fn(); - const newValue = '123'; await item.removeValue(); item.watch(cb); - await item.setValue(newValue); + await item.setValue(NEW_VALUE); expect(cb).toBeCalledTimes(1); - expect(cb).toBeCalledWith(newValue, defaultValue); + expect(cb).toBeCalledWith(NEW_VALUE, DEFAULT_VALUE); }); it('should remove the listener when calling the returned function', async () => { @@ -1188,6 +1233,7 @@ describe('Storage Utils', () => { const unwatch = item.watch(cb); unwatch(); + await item.setValue('123'); expect(cb).not.toBeCalled(); @@ -1201,6 +1247,7 @@ describe('Storage Utils', () => { item.watch(cb); storage.unwatch(); + await item.setValue('123'); expect(cb).not.toBeCalled(); @@ -1211,13 +1258,14 @@ describe('Storage Utils', () => { '%s option', (fallbackKey) => { it('should return the default value when provided', () => { - const fallback = 123; + const FALLBACK = 123; + const item = storage.defineItem(`local:test`, { - [fallbackKey]: fallback, + [fallbackKey]: FALLBACK, }); - expect(item.fallback).toBe(fallback); - expect(item.defaultValue).toBe(fallback); + expect(item.fallback).toBe(FALLBACK); + expect(item.defaultValue).toBe(FALLBACK); }); it('should return null when not provided', () => { @@ -1231,11 +1279,12 @@ describe('Storage Utils', () => { describe('init option', () => { it('should only call init once (per JS context) when calling getValue successively, avoiding race conditions', async () => { - const expected = 1; + const EXPECTED = 1; + const init = vi .fn() - .mockResolvedValueOnce(expected) - .mockResolvedValue('not' + expected); + .mockResolvedValueOnce(EXPECTED) + .mockResolvedValue('not' + EXPECTED); const item = storage.defineItem('local:test', { init }); await waitForInit(); @@ -1243,25 +1292,27 @@ describe('Storage Utils', () => { const p1 = item.getValue(); const p2 = item.getValue(); - await expect(p1).resolves.toBe(expected); - await expect(p2).resolves.toBe(expected); + await expect(p1).resolves.toBe(EXPECTED); + await expect(p2).resolves.toBe(EXPECTED); expect(init).toBeCalledTimes(1); }); it('should initialize the value in storage immediately', async () => { - const expected = 1; - const init = vi.fn().mockReturnValue(expected); + const EXPECTED = 1; + + const init = vi.fn().mockReturnValue(EXPECTED); storage.defineItem('local:test', { init }); await waitForInit(); - await expect(storage.getItem('local:test')).resolves.toBe(expected); + 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('local:key', { init }); + await waitForInit(); await item.removeValue(); @@ -1273,6 +1324,7 @@ describe('Storage Utils', () => { item.getValue(), item.getValue(), ]); + expect(init).toBeCalledTimes(1); expect(value1).toBe(value2); }); @@ -1441,25 +1493,27 @@ describe('Storage Utils', () => { describe('setItems', () => { it('should set the values of multiple storage items efficiently', async () => { const item1 = storage.defineItem('local:item1'); - const value1 = 100; + const VALUE_1 = 100; + const item2 = storage.defineItem('session:item2'); - const value2 = 'test'; + const VALUE_2 = 'test'; + const item3 = storage.defineItem('local:item3'); - const value3 = true; + const VALUE_3 = 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 }, + { item: item1, value: VALUE_1 }, + { item: item2, value: VALUE_2 }, + { item: item3, value: VALUE_3 }, ]); expect(localSetSpy).toBeCalledTimes(1); - expect(localSetSpy).toBeCalledWith({ item1: value1, item3: value3 }); + expect(localSetSpy).toBeCalledWith({ item1: VALUE_1, item3: VALUE_3 }); expect(sessionSetSpy).toBeCalledTimes(1); - expect(sessionSetSpy).toBeCalledWith({ item2: value2 }); + expect(sessionSetSpy).toBeCalledWith({ item2: VALUE_2 }); }); }); @@ -1468,10 +1522,12 @@ describe('Storage Utils', () => { const item1 = storage.defineItem('local:one'); const item2 = storage.defineItem('session:two'); const item3 = storage.defineItem('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'); @@ -1482,6 +1538,7 @@ describe('Storage Utils', () => { ]); console.log(localGetSpy.mock.calls); + expect(localGetSpy).toBeCalledTimes(1); expect(localGetSpy).toBeCalledWith(['one$', 'three$']); expect(sessionGetSpy).toBeCalledTimes(1); diff --git a/packages/wxt/src/__tests__/modules.test.ts b/packages/wxt/src/__tests__/modules.test.ts index 6481e0f7c..a9e62090c 100644 --- a/packages/wxt/src/__tests__/modules.test.ts +++ b/packages/wxt/src/__tests__/modules.test.ts @@ -40,19 +40,19 @@ describe('Module Utilities', () => { describe('addImportPreset', () => { it('should add the import to the config', async () => { - const preset = 'vue'; + const PRESET = 'vue'; const wxt = fakeWxt({ hooks: createHooks() }); - addImportPreset(wxt, preset); + addImportPreset(wxt, PRESET); await wxt.hooks.callHook('config:resolved', wxt); expect(wxt.config.imports && wxt.config.imports.presets).toContain( - preset, + PRESET, ); }); it('should not add duplicate presets', async () => { - const preset = 'vue'; + const PRESET = 'vue'; const wxt = fakeWxt({ hooks: createHooks(), config: { @@ -62,7 +62,7 @@ describe('Module Utilities', () => { }, }); - addImportPreset(wxt, preset); + addImportPreset(wxt, PRESET); await wxt.hooks.callHook('config:resolved', wxt); expect(wxt.config.imports && wxt.config.imports.presets).toHaveLength(2); diff --git a/packages/wxt/src/cli/__tests__/index.test.ts b/packages/wxt/src/cli/__tests__/index.test.ts index 074934338..13385ad31 100644 --- a/packages/wxt/src/cli/__tests__/index.test.ts +++ b/packages/wxt/src/cli/__tests__/index.test.ts @@ -1,10 +1,12 @@ import { describe, it, vi, beforeEach, expect } from 'vitest'; -import { build } from '../../core/build'; -import { createServer } from '../../core/create-server'; -import { zip } from '../../core/zip'; -import { prepare } from '../../core/prepare'; -import { clean } from '../../core/clean'; -import { initialize } from '../../core/initialize'; +import { + build, + createServer, + zip, + prepare, + clean, + initialize, +} from '../../core'; import { mock } from 'vitest-mock-extended'; import consola from 'consola'; @@ -117,14 +119,15 @@ describe('CLI', () => { }); it('should respect passing --port', async () => { - const expectedPort = 3100; - mockArgv('--port', String(expectedPort)); + const EXPECTED_PORT = 3100; + + mockArgv('--port', String(EXPECTED_PORT)); await importCli(); expect(createServerMock).toBeCalledWith({ dev: { server: { - port: expectedPort, + port: EXPECTED_PORT, }, }, }); diff --git a/packages/wxt/src/cli/index.ts b/packages/wxt/src/cli/index.ts index d9b7ddfa9..6e7f2780f 100644 --- a/packages/wxt/src/cli/index.ts +++ b/packages/wxt/src/cli/index.ts @@ -1,5 +1,5 @@ import cli from './commands'; -import { version } from '../version'; +import { VERSION } from '../version'; import { isAliasedCommand } from './cli-utils'; // Grab the command that we're trying to run @@ -8,7 +8,7 @@ cli.parse(process.argv, { run: false }); // If it's not an alias, add the help and version options, then parse again if (!isAliasedCommand(cli.matchedCommand)) { cli.help(); - cli.version(version); + cli.version(VERSION); cli.parse(process.argv, { run: false }); } diff --git a/packages/wxt/src/core/builders/vite/__tests__/fixtures/test.ts b/packages/wxt/src/core/builders/vite/__tests__/fixtures/test.ts index a68289eed..c9bf45d77 100644 --- a/packages/wxt/src/core/builders/vite/__tests__/fixtures/test.ts +++ b/packages/wxt/src/core/builders/vite/__tests__/fixtures/test.ts @@ -1,2 +1,2 @@ console.log('Side-effect in test.ts'); -export const a = 'a'; +export const A = 'a'; diff --git a/packages/wxt/src/core/builders/vite/plugins/__tests__/devHtmlPrerender.test.ts b/packages/wxt/src/core/builders/vite/plugins/__tests__/devHtmlPrerender.test.ts index e10c12e22..84822570b 100644 --- a/packages/wxt/src/core/builders/vite/plugins/__tests__/devHtmlPrerender.test.ts +++ b/packages/wxt/src/core/builders/vite/plugins/__tests__/devHtmlPrerender.test.ts @@ -33,16 +33,17 @@ describe('Dev HTML Prerender Plugin', () => { ['https://example.com/style.css', 'https://example.com/style.css'], ])('should transform "%s" into "%s"', (input, expected) => { const { document } = parseHTML(''); - const root = '/some/root'; + const ROOT = '/some/root'; + const config = fakeResolvedConfig({ - root, + root: ROOT, alias: { '~local': '.', - '~absolute': `${root}/assets`, - '~file': `${root}/example.css`, - '~outside': `${root}/../non-root`, - '~~': root, - '~': root, + '~absolute': `${ROOT}/assets`, + '~file': `${ROOT}/example.css`, + '~outside': `${ROOT}/../non-root`, + '~~': ROOT, + '~': ROOT, }, }); const server = fakeDevServer({ @@ -50,10 +51,10 @@ describe('Dev HTML Prerender Plugin', () => { port: 5173, origin: 'http://localhost:5173', }); - const id = root + '/entrypoints/popup/index.html'; + const ID = ROOT + '/entrypoints/popup/index.html'; document.head.innerHTML = ``; - pointToDevServer(config, server, id, document, 'link', 'href'); + pointToDevServer(config, server, ID, document, 'link', 'href'); const actual = document.querySelector('link')!; expect(actual.getAttribute('href')).toBe(expected); diff --git a/packages/wxt/src/core/builders/vite/plugins/devHtmlPrerender.ts b/packages/wxt/src/core/builders/vite/plugins/devHtmlPrerender.ts index 0830cd86a..28e641fbd 100644 --- a/packages/wxt/src/core/builders/vite/plugins/devHtmlPrerender.ts +++ b/packages/wxt/src/core/builders/vite/plugins/devHtmlPrerender.ts @@ -16,14 +16,15 @@ export function devHtmlPrerender( config: ResolvedConfig, server: WxtDevServer | undefined, ): vite.PluginOption { - const htmlReloadId = '@wxt/reload-html'; + const HTML_RELOAD_ID = '@wxt/reload-html'; + const resolvedHtmlReloadId = resolve( config.wxtModuleDir, 'dist/virtual/reload-html.mjs', ); - const virtualInlineScript = 'virtual:wxt-inline-script'; - const resolvedVirtualInlineScript = '\0' + virtualInlineScript; + const VIRTUAL_INLINE_SCRIPT = 'virtual:wxt-inline-script'; + const RESOLVED_VIRTUAL_INLINE_SCRIPT = '\0' + VIRTUAL_INLINE_SCRIPT; return [ { @@ -33,7 +34,7 @@ export function devHtmlPrerender( return { resolve: { alias: { - [htmlReloadId]: resolvedHtmlReloadId, + [HTML_RELOAD_ID]: resolvedHtmlReloadId, }, }, }; @@ -57,7 +58,7 @@ export function devHtmlPrerender( // Add a script to add page reloading const reloader = document.createElement('script'); - reloader.src = htmlReloadId; + reloader.src = HTML_RELOAD_ID; reloader.type = 'module'; document.head.appendChild(reloader); @@ -65,6 +66,7 @@ export function devHtmlPrerender( config.logger.debug('transform ' + id); config.logger.debug('Old HTML:\n' + code); config.logger.debug('New HTML:\n' + newHtml); + return newHtml; }, @@ -76,6 +78,7 @@ export function devHtmlPrerender( const name = getEntrypointName(config.entrypointsDir, ctx.filename); const url = `${server.origin}/${name}.html`; const serverHtml = await server.transformHtml(url, html, originalUrl); + const { document } = parseHTML(serverHtml); // Replace inline script with virtual module served via dev server. @@ -91,7 +94,8 @@ export function devHtmlPrerender( // Replace unsafe inline script const virtualScript = document.createElement('script'); virtualScript.type = 'module'; - virtualScript.src = `${server.origin}/@id/${virtualInlineScript}?${key}`; + virtualScript.src = `${server.origin}/@id/${VIRTUAL_INLINE_SCRIPT}?${key}`; + script.replaceWith(virtualScript); }); @@ -99,6 +103,7 @@ export function devHtmlPrerender( const viteClientScript = document.querySelector( "script[src='/@vite/client']", ); + if (viteClientScript) { viteClientScript.src = `${server.origin}${viteClientScript.src}`; } @@ -115,7 +120,7 @@ export function devHtmlPrerender( apply: 'serve', resolveId(id) { // Resolve inline scripts - if (id.startsWith(virtualInlineScript)) { + if (id.startsWith(VIRTUAL_INLINE_SCRIPT)) { return '\0' + id; } @@ -126,7 +131,7 @@ export function devHtmlPrerender( }, load(id) { // Resolve virtualized inline scripts - if (id.startsWith(resolvedVirtualInlineScript)) { + if (id.startsWith(RESOLVED_VIRTUAL_INLINE_SCRIPT)) { // id="virtual:wxt-inline-script?" const key = id.substring(id.indexOf('?') + 1); return inlineScriptContents[key]; @@ -166,6 +171,7 @@ export function pointToDevServer( const matchingAlias = Object.entries(config.alias).find(([key]) => src.startsWith(key), ); + if (matchingAlias) { // Matches a import alias const [alias, replacement] = matchingAlias; diff --git a/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts b/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts index ef9cb0d7b..71ecf884c 100644 --- a/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts +++ b/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts @@ -6,8 +6,8 @@ import { ResolvedConfig } from '../../../../types'; * Mock `wxt/browser` and stub the global `browser`/`chrome` types with a fake version of the extension APIs */ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption { - const virtualSetupModule = 'virtual:wxt-setup'; - const resolvedVirtualSetupModule = '\0' + virtualSetupModule; + const VIRTUAL_SETUP_MODULE = 'virtual:wxt-setup'; + const RESOLVED_VIRTUAL_SETUP_MODULE = '\0' + VIRTUAL_SETUP_MODULE; return { name: 'wxt:extension-api-mock', @@ -18,7 +18,7 @@ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption { ); return { test: { - setupFiles: [virtualSetupModule], + setupFiles: [VIRTUAL_SETUP_MODULE], }, resolve: { alias: [ @@ -33,15 +33,16 @@ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption { }; }, resolveId(id) { - if (id.endsWith(virtualSetupModule)) return resolvedVirtualSetupModule; + if (id.endsWith(VIRTUAL_SETUP_MODULE)) + return RESOLVED_VIRTUAL_SETUP_MODULE; }, load(id) { - if (id === resolvedVirtualSetupModule) return setupTemplate; + if (id === RESOLVED_VIRTUAL_SETUP_MODULE) return SETUP_TEMPLATE; }, }; } -const setupTemplate = ` +const SETUP_TEMPLATE = ` import { vi } from 'vitest'; import { fakeBrowser } from 'wxt/testing/fake-browser'; diff --git a/packages/wxt/src/core/builders/vite/plugins/noopBackground.ts b/packages/wxt/src/core/builders/vite/plugins/noopBackground.ts index 660399e3e..532e6d4bb 100644 --- a/packages/wxt/src/core/builders/vite/plugins/noopBackground.ts +++ b/packages/wxt/src/core/builders/vite/plugins/noopBackground.ts @@ -6,15 +6,16 @@ import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../../../utils/constants'; * connection is setup and the extension reloads HTML pages and content scripts correctly. */ export function noopBackground(): Plugin { - const virtualModuleId = VIRTUAL_NOOP_BACKGROUND_MODULE_ID; - const resolvedVirtualModuleId = '\0' + virtualModuleId; + const VIRTUAL_MODULE_ID = VIRTUAL_NOOP_BACKGROUND_MODULE_ID; + const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; + return { name: 'wxt:noop-background', resolveId(id) { - if (id === virtualModuleId) return resolvedVirtualModuleId; + if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID; }, load(id) { - if (id === resolvedVirtualModuleId) { + if (id === RESOLVED_VIRTUAL_MODULE_ID) { return `import { defineBackground } from 'wxt/utils/define-background';\nexport default defineBackground(() => void 0)`; } }, diff --git a/packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts b/packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts index a76c2c3d9..0fca3e6b6 100644 --- a/packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts +++ b/packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts @@ -7,8 +7,8 @@ import { ResolvedConfig } from '../../../../types'; * When importing `virtual:app-config`, resolve it to the `app.config.ts` file in the project. */ export function resolveAppConfig(config: ResolvedConfig): vite.Plugin { - const virtualModuleId = 'virtual:app-config'; - const resolvedVirtualModuleId = '\0' + virtualModuleId; + const VIRTUAL_MODULE_ID = 'virtual:app-config'; + const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; const appConfigFile = resolve(config.srcDir, 'app.config.ts'); return { @@ -18,19 +18,19 @@ export function resolveAppConfig(config: ResolvedConfig): vite.Plugin { optimizeDeps: { // Prevent ESBuild from attempting to resolve the virtual module // while optimizing WXT. - exclude: [virtualModuleId], + exclude: [VIRTUAL_MODULE_ID], }, }; }, async resolveId(id) { - if (id !== virtualModuleId) return; + if (id !== VIRTUAL_MODULE_ID) return; return (await pathExists(appConfigFile)) ? appConfigFile - : resolvedVirtualModuleId; + : RESOLVED_VIRTUAL_MODULE_ID; }, load(id) { - if (id === resolvedVirtualModuleId) return `export default {}`; + if (id === RESOLVED_VIRTUAL_MODULE_ID) return `export default {}`; }, }; } diff --git a/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts b/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts index 231d932df..193ec07d8 100644 --- a/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts +++ b/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts @@ -13,28 +13,31 @@ import { resolve } from 'path'; */ export function resolveVirtualModules(config: ResolvedConfig): Plugin[] { return virtualModuleNames.map((name) => { - const virtualId: `${VirtualModuleId}?` = `virtual:wxt-${name}?`; - const resolvedVirtualId = '\0' + virtualId; + const VIRTUAL_ID: `${VirtualModuleId}?` = `virtual:wxt-${name}?`; + const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_ID; + return { name: `wxt:resolve-virtual-${name}`, resolveId(id) { // Id doesn't start with prefix, it looks like this: // /path/to/project/virtual:wxt-background?/path/to/project/entrypoints/background.ts - const index = id.indexOf(virtualId); + const index = id.indexOf(VIRTUAL_ID); if (index === -1) return; - const inputPath = normalizePath(id.substring(index + virtualId.length)); - return resolvedVirtualId + inputPath; + const inputPath = normalizePath( + id.substring(index + VIRTUAL_ID.length), + ); + return RESOLVED_VIRTUAL_ID + inputPath; }, async load(id) { - if (!id.startsWith(resolvedVirtualId)) return; + if (!id.startsWith(RESOLVED_VIRTUAL_ID)) return; - const inputPath = id.replace(resolvedVirtualId, ''); - const template = await fs.readFile( + const inputPath = id.replace(RESOLVED_VIRTUAL_ID, ''); + const TEMPLATE = await fs.readFile( resolve(config.wxtModuleDir, `dist/virtual/${name}.mjs`), 'utf-8', ); - return template.replace(`virtual:user-${name}`, inputPath); + return TEMPLATE.replace(`virtual:user-${name}`, inputPath); }, }; }); diff --git a/packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts b/packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts index 3e335c106..e3231e2cd 100644 --- a/packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts +++ b/packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts @@ -7,19 +7,19 @@ import { ResolvedConfig } from '../../../../types'; * Resolve and load plugins for each entrypoint. This handles both JS entrypoints via the `virtual:wxt-plugins` import, and HTML files by adding `virtual:wxt-html-plugins` to the document's `` */ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { - const virtualModuleId = 'virtual:wxt-plugins'; - const resolvedVirtualModuleId = '\0' + virtualModuleId; - const virtualHtmlModuleId = 'virtual:wxt-html-plugins'; - const resolvedVirtualHtmlModuleId = '\0' + virtualHtmlModuleId; + const VIRTUAL_MODULE_ID = 'virtual:wxt-plugins'; + const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; + const VIRTUAL_HTML_MODULE_ID = 'virtual:wxt-html-plugins'; + const RESOLVED_VIRTUAL_HTML_MODULE_ID = '\0' + VIRTUAL_HTML_MODULE_ID; return { name: 'wxt:plugin-loader', resolveId(id) { - if (id === virtualModuleId) return resolvedVirtualModuleId; - if (id === virtualHtmlModuleId) return resolvedVirtualHtmlModuleId; + if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID; + if (id === VIRTUAL_HTML_MODULE_ID) return RESOLVED_VIRTUAL_HTML_MODULE_ID; }, load(id) { - if (id === resolvedVirtualModuleId) { + if (id === RESOLVED_VIRTUAL_MODULE_ID) { // Import and init all plugins const imports = config.plugins .map( @@ -32,8 +32,8 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { .join('\n'); return `${imports}\n\nexport function initPlugins() {\n${initCalls}\n}`; } - if (id === resolvedVirtualHtmlModuleId) { - return `import { initPlugins } from '${virtualModuleId}'; + if (id === RESOLVED_VIRTUAL_HTML_MODULE_ID) { + return `import { initPlugins } from '${VIRTUAL_MODULE_ID}'; try { initPlugins(); @@ -48,11 +48,12 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { handler(html, _ctx) { const src = config.command === 'serve' - ? `${config.dev.server?.origin}/@id/${virtualHtmlModuleId}` - : virtualHtmlModuleId; + ? `${config.dev.server?.origin}/@id/${VIRTUAL_HTML_MODULE_ID}` + : VIRTUAL_HTML_MODULE_ID; const { document } = parseHTML(html); const existing = document.querySelector(`script[src='${src}']`); + if (existing) return; const script = document.createElement('script'); diff --git a/packages/wxt/src/core/clean.ts b/packages/wxt/src/core/clean.ts index f3e9c0984..8e93d3985 100644 --- a/packages/wxt/src/core/clean.ts +++ b/packages/wxt/src/core/clean.ts @@ -42,7 +42,9 @@ export async function clean(config?: string | InlineConfig) { '**/.wxt', `${path.relative(root, wxt.config.outBaseDir)}/*`, ]; + wxt.logger.debug('Looking for:', tempDirs.map(pc.cyan).join(', ')); + const directories = await glob(tempDirs, { cwd: root, absolute: true, diff --git a/packages/wxt/src/core/generate-wxt-dir.ts b/packages/wxt/src/core/generate-wxt-dir.ts index 65c57e744..0c922be5c 100644 --- a/packages/wxt/src/core/generate-wxt-dir.ts +++ b/packages/wxt/src/core/generate-wxt-dir.ts @@ -86,7 +86,7 @@ async function getPathsDeclarationEntry( .map((path) => ` | "/${path}"`), ].join('\n'); - const template = `// Generated by wxt + const TEMPLATE = `// Generated by wxt import "wxt/browser"; declare module "wxt/browser" { @@ -102,14 +102,14 @@ declare module "wxt/browser" { return { path: 'types/paths.d.ts', - text: template.replace('{{ union }}', unions || ' | never'), + text: TEMPLATE.replace('{{ union }}', unions || ' | never'), tsReference: true, }; } async function getI18nDeclarationEntry(): Promise { const defaultLocale = wxt.config.manifest.default_locale; - const template = `// Generated by wxt + const TEMPLATE_I18N = `// Generated by wxt import "wxt/browser"; declare module "wxt/browser" { @@ -136,6 +136,7 @@ declare module "wxt/browser" { 'messages.json', ); let messages: Message[]; + if (await fs.pathExists(defaultLocalePath)) { const content = JSON.parse(await fs.readFile(defaultLocalePath, 'utf-8')); messages = parseI18nMessages(content); @@ -149,15 +150,19 @@ declare module "wxt/browser" { translation?: string, ) => { const commentLines: string[] = []; + if (description) commentLines.push(...description.split('\n')); + if (translation) { if (commentLines.length > 0) commentLines.push(''); commentLines.push(`"${translation}"`); } + const comment = commentLines.length > 0 ? `/**\n${commentLines.map((line) => ` * ${line}`.trimEnd()).join('\n')}\n */\n ` : ''; + return ` ${comment}getMessage( messageName: ${keyType}, substitutions?: string | string[], @@ -166,7 +171,7 @@ declare module "wxt/browser" { }; const overrides = [ - // Generate individual overloads for each message so JSDoc contains description and base translation. + // Generate individual overloads for each message, so JSDoc contains description and base translation. ...messages.map((message) => renderGetMessageOverload( `"${message.name}"`, @@ -183,13 +188,14 @@ declare module "wxt/browser" { return { path: 'types/i18n.d.ts', - text: template.replace('{{ overrides }}', overrides.join('\n')), + text: TEMPLATE_I18N.replace('{{ overrides }}', overrides.join('\n')), tsReference: true, }; } async function getGlobalsDeclarationEntry(): Promise { const globals = [...getGlobals(wxt.config), ...getEntrypointGlobals('')]; + return { path: 'types/globals.d.ts', text: [ @@ -208,6 +214,7 @@ async function getGlobalsDeclarationEntry(): Promise { function getMainDeclarationEntry(references: WxtDirEntry[]): WxtDirFileEntry { const lines = ['// Generated by wxt']; + references.forEach((ref) => { if ('module' in ref) { return lines.push(`/// `); @@ -217,6 +224,7 @@ function getMainDeclarationEntry(references: WxtDirEntry[]): WxtDirFileEntry { lines.push(`/// `); } }); + return { path: 'wxt.d.ts', text: lines.join('\n') + '\n', @@ -241,7 +249,7 @@ async function getTsConfigEntry(): Promise { .map((line) => ` ${line}`) .join(',\n'); - const text = `{ + const TEXT = `{ "compilerOptions": { "target": "ESNext", "module": "ESNext", @@ -265,6 +273,6 @@ ${paths} return { path: 'tsconfig.json', - text, + text: TEXT, }; } diff --git a/packages/wxt/src/core/package-managers/__tests__/npm.test.ts b/packages/wxt/src/core/package-managers/__tests__/npm.test.ts index 6117d946f..024cde5ef 100644 --- a/packages/wxt/src/core/package-managers/__tests__/npm.test.ts +++ b/packages/wxt/src/core/package-managers/__tests__/npm.test.ts @@ -34,11 +34,11 @@ describe('NPM Package Management Utils', () => { const cwd = path.resolve(__dirname, 'fixtures/simple-npm-project'); it('should download the dependency as a tarball', async () => { + const ID = 'mime-db@1.52.0'; const downloadDir = path.resolve(cwd, 'dist'); - const id = 'mime-db@1.52.0'; const expected = path.resolve(downloadDir, 'mime-db-1.52.0.tgz'); - const actual = await npm.downloadDependency(id, downloadDir); + const actual = await npm.downloadDependency(ID, downloadDir); expect(actual).toEqual(expected); expect(await pathExists(actual)).toBe(true); diff --git a/packages/wxt/src/core/package-managers/npm.ts b/packages/wxt/src/core/package-managers/npm.ts index 1b1989205..68ba93055 100644 --- a/packages/wxt/src/core/package-managers/npm.ts +++ b/packages/wxt/src/core/package-managers/npm.ts @@ -60,11 +60,11 @@ export function dedupeDependencies(dependencies: Dependency[]): Dependency[] { const hashes = new Set(); return dependencies.filter((dep) => { - const hash = `${dep.name}@${dep.version}`; - if (hashes.has(hash)) { + const HASH = `${dep.name}@${dep.version}`; + if (hashes.has(HASH)) { return false; } else { - hashes.add(hash); + hashes.add(HASH); return true; } }); diff --git a/packages/wxt/src/core/utils/__tests__/entrypoints.test.ts b/packages/wxt/src/core/utils/__tests__/entrypoints.test.ts index 963be40d8..366cbc6f2 100644 --- a/packages/wxt/src/core/utils/__tests__/entrypoints.test.ts +++ b/packages/wxt/src/core/utils/__tests__/entrypoints.test.ts @@ -9,35 +9,36 @@ import { resolve } from 'path'; describe('Entrypoint Utils', () => { describe('getEntrypointName', () => { - const entrypointsDir = '/entrypoints'; + const ENTRYPOINTS_DIR = '/entrypoints'; it.each<[string, string]>([ - [resolve(entrypointsDir, 'popup.html'), 'popup'], - [resolve(entrypointsDir, 'options/index.html'), 'options'], - [resolve(entrypointsDir, 'example.sandbox/index.html'), 'example'], - [resolve(entrypointsDir, 'some.content/index.ts'), 'some'], - [resolve(entrypointsDir, 'overlay.content.ts'), 'overlay'], + [resolve(ENTRYPOINTS_DIR, 'popup.html'), 'popup'], + [resolve(ENTRYPOINTS_DIR, 'options/index.html'), 'options'], + [resolve(ENTRYPOINTS_DIR, 'example.sandbox/index.html'), 'example'], + [resolve(ENTRYPOINTS_DIR, 'some.content/index.ts'), 'some'], + [resolve(ENTRYPOINTS_DIR, 'overlay.content.ts'), 'overlay'], ])('should convert %s to %s', (inputPath, expected) => { - const actual = getEntrypointName(entrypointsDir, inputPath); + const actual = getEntrypointName(ENTRYPOINTS_DIR, inputPath); expect(actual).toBe(expected); }); }); describe('getEntrypointOutputFile', () => { - const outDir = '/.output'; + const OUT_DIR = '/.output'; + it.each<{ expected: string; name: string; ext: string; outputDir: string }>( [ { name: 'popup', ext: '.html', - outputDir: outDir, - expected: resolve(outDir, 'popup.html'), + outputDir: OUT_DIR, + expected: resolve(OUT_DIR, 'popup.html'), }, { name: 'overlay', ext: '.ts', - outputDir: resolve(outDir, 'content-scripts'), - expected: resolve(outDir, 'content-scripts', 'overlay.ts'), + outputDir: resolve(OUT_DIR, 'content-scripts'), + expected: resolve(OUT_DIR, 'content-scripts', 'overlay.ts'), }, ], )('should return %s', ({ name, ext, expected, outputDir }) => { diff --git a/packages/wxt/src/core/utils/__tests__/minimatch-multiple.test.ts b/packages/wxt/src/core/utils/__tests__/minimatch-multiple.test.ts index 34c078e13..88e28bb54 100644 --- a/packages/wxt/src/core/utils/__tests__/minimatch-multiple.test.ts +++ b/packages/wxt/src/core/utils/__tests__/minimatch-multiple.test.ts @@ -4,43 +4,43 @@ import { minimatchMultiple } from '../minimatch-multiple'; describe('minimatchMultiple', () => { it('should return false if the pattern array is undefined', () => { const patterns = undefined; - const search = 'test.json'; + const SEARCH = 'test.json'; - expect(minimatchMultiple(search, patterns)).toBe(false); + expect(minimatchMultiple(SEARCH, patterns)).toBe(false); }); it('should return false if the pattern array is empty', () => { const patterns: string[] = []; - const search = 'test.json'; + const SEARCH = 'test.json'; - expect(minimatchMultiple(search, patterns)).toBe(false); + expect(minimatchMultiple(SEARCH, patterns)).toBe(false); }); it('should return true if the pattern array contains a match', () => { const patterns = ['test.yml', 'test.json']; - const search = 'test.json'; + const SEARCH = 'test.json'; - expect(minimatchMultiple(search, patterns)).toBe(true); + expect(minimatchMultiple(SEARCH, patterns)).toBe(true); }); it('should return false if the pattern array does not contain a match', () => { const patterns = ['test.yml', 'test.json']; - const search = 'test.txt'; + const SEARCH = 'test.txt'; - expect(minimatchMultiple(search, patterns)).toBe(false); + expect(minimatchMultiple(SEARCH, patterns)).toBe(false); }); it('should return false if the pattern matches a negative pattern', () => { const patterns = ['test.*', '!test.json']; - const search = 'test.json'; + const SEARCH = 'test.json'; - expect(minimatchMultiple(search, patterns)).toBe(false); + expect(minimatchMultiple(SEARCH, patterns)).toBe(false); }); it('should return false if the pattern matches a negative pattern, regardless of order', () => { const patterns = ['!test.json', 'test.*']; - const search = 'test.json'; + const SEARCH = 'test.json'; - expect(minimatchMultiple(search, patterns)).toBe(false); + expect(minimatchMultiple(SEARCH, patterns)).toBe(false); }); }); diff --git a/packages/wxt/src/core/utils/__tests__/package.test.ts b/packages/wxt/src/core/utils/__tests__/package.test.ts index 4711c06ae..97d14675f 100644 --- a/packages/wxt/src/core/utils/__tests__/package.test.ts +++ b/packages/wxt/src/core/utils/__tests__/package.test.ts @@ -20,10 +20,10 @@ describe('Package JSON Utils', () => { }); it("should return an empty object when /package.json doesn't exist", async () => { - const root = '/some/path/that/does/not/exist'; + const ROOT = '/some/path/that/does/not/exist'; const logger = mock(); setFakeWxt({ - config: { root, logger }, + config: { root: ROOT, logger }, logger, }); diff --git a/packages/wxt/src/core/utils/__tests__/strings.test.ts b/packages/wxt/src/core/utils/__tests__/strings.test.ts index f1dbce8b2..84654173b 100644 --- a/packages/wxt/src/core/utils/__tests__/strings.test.ts +++ b/packages/wxt/src/core/utils/__tests__/strings.test.ts @@ -32,6 +32,7 @@ describe('String utils', () => { "should convert '%s' to '%s', which can be used for a variable name", (input, expected) => { const actual = safeVarName(input); + expect(actual).toBe(expected); }, ); @@ -39,7 +40,7 @@ describe('String utils', () => { describe('removeImportStatements', () => { it('should remove all import formats', () => { - const imports = ` + const IMPORTS = ` import { registerGithubService, createGithubApi } from "@/utils/github"; import { registerGithubService, @@ -54,16 +55,18 @@ import"@/utils/github" import'@/utils/github'; import * as abc from "@/utils/github" `; - expect(removeImportStatements(imports).trim()).toEqual(''); + + expect(removeImportStatements(IMPORTS).trim()).toEqual(''); }); it('should not remove import.meta or inline import statements', () => { - const imports = ` + const IMPORTS = ` import.meta.env.DEV const a = await import("example"); import("example"); `; - expect(removeImportStatements(imports)).toEqual(imports); + + expect(removeImportStatements(IMPORTS)).toEqual(IMPORTS); }); }); }); diff --git a/packages/wxt/src/core/utils/__tests__/transform.test.ts b/packages/wxt/src/core/utils/__tests__/transform.test.ts index a623aa2fc..2c4232610 100644 --- a/packages/wxt/src/core/utils/__tests__/transform.test.ts +++ b/packages/wxt/src/core/utils/__tests__/transform.test.ts @@ -6,32 +6,32 @@ describe('Transform Utils', () => { it.each(['defineBackground', 'defineUnlistedScript'])( 'should remove the first arrow function argument for %s', (def) => { - const input = ` + const INPUT = ` export default ${def}(() => { console.log(); }) `; - const expected = `export default ${def}();`; + const EXPECTED = `export default ${def}();`; - const actual = removeMainFunctionCode(input).code; + const actual = removeMainFunctionCode(INPUT).code; - expect(actual).toEqual(expected); + expect(actual).toEqual(EXPECTED); }, ); it.each(['defineBackground', 'defineUnlistedScript'])( 'should remove the first function argument for %s', (def) => { - const input = ` + const INPUT = ` export default ${def}(function () { console.log(); }) `; - const expected = `export default ${def}();`; + const EXPECTED = `export default ${def}();`; - const actual = removeMainFunctionCode(input).code; + const actual = removeMainFunctionCode(INPUT).code; - expect(actual).toEqual(expected); + expect(actual).toEqual(EXPECTED); }, ); @@ -40,57 +40,57 @@ describe('Transform Utils', () => { 'defineContentScript', 'defineUnlistedScript', ])('should remove the main field from %s', (def) => { - const input = ` + const INPUT = ` export default ${def}({ asdf: "asdf", main: () => {}, }) `; - const expected = `export default ${def}({ + const EXPECTED = `export default ${def}({ asdf: "asdf" })`; - const actual = removeMainFunctionCode(input).code; + const actual = removeMainFunctionCode(INPUT).code; - expect(actual).toEqual(expected); + expect(actual).toEqual(EXPECTED); }); it('should remove unused imports', () => { - const input = ` + const INPUT = ` import { defineBackground } from "#imports" import { test1 } from "somewhere1" import test2 from "somewhere2" export default defineBackground(() => {}) `; - const expected = `import { defineBackground } from "#imports" + const EXPECTED = `import { defineBackground } from "#imports" export default defineBackground();`; - const actual = removeMainFunctionCode(input).code; + const actual = removeMainFunctionCode(INPUT).code; - expect(actual).toEqual(expected); + expect(actual).toEqual(EXPECTED); }); it('should remove explict side-effect imports', () => { - const input = ` + const INPUT = ` import { defineBackground } from "#imports" import "my-polyfill" import "./style.css" export default defineBackground(() => {}) `; - const expected = `import { defineBackground } from "#imports" + const EXPECTED = `import { defineBackground } from "#imports" export default defineBackground();`; - const actual = removeMainFunctionCode(input).code; + const actual = removeMainFunctionCode(INPUT).code; - expect(actual).toEqual(expected); + expect(actual).toEqual(EXPECTED); }); it("should remove any functions delcared outside the main function that aren't used", () => { - const input = ` + const INPUT = ` function getMatches() { return ["*://*/*"] } @@ -104,7 +104,7 @@ export default defineBackground();`; main: () => {}, }) `; - const expected = `function getMatches() { + const EXPECTED = `function getMatches() { return ["*://*/*"] } @@ -112,13 +112,13 @@ export default defineContentScript({ matches: getMatches() })`; - const actual = removeMainFunctionCode(input).code; + const actual = removeMainFunctionCode(INPUT).code; - expect(actual).toEqual(expected); + expect(actual).toEqual(EXPECTED); }); it("should remove any variables delcared outside the main function that aren't used", () => { - const input = ` + const INPUT = ` const unused1 = "a", matches = ["*://*/*"]; let unused2 = unused1 + "b"; @@ -127,19 +127,19 @@ export default defineContentScript({ main: () => {} }) `; - const expected = `const matches = ["*://*/*"]; + const EXPECTED = `const matches = ["*://*/*"]; export default defineContentScript({ matches })`; - const actual = removeMainFunctionCode(input).code; + const actual = removeMainFunctionCode(INPUT).code; - expect(actual).toEqual(expected); + expect(actual).toEqual(EXPECTED); }); it('should not remove any variables delcared outside the main function that are used', () => { - const input = ` + const INPUT = ` const [ a ] = [ 123, 456 ]; const { b } = { b: 123 }; const { c: { d } } = { c: { d: 123 } }; @@ -154,7 +154,8 @@ export default defineContentScript({ export default defineBackground(() => { console.log('Hello background!', { id: browser.runtime.id }); });`; - const expected = `const [ a ] = [ 123, 456 ]; + + const EXPECTED = `const [ a ] = [ 123, 456 ]; const { b } = { b: 123 }; const { c: { d } } = { c: { d: 123 } }; const { e, ...rest } = { e: 123, f: 456 }; @@ -167,8 +168,8 @@ console.log(rest); export default defineBackground();`; - const actual = removeMainFunctionCode(input).code; - expect(actual).toEqual(expected); + const actual = removeMainFunctionCode(INPUT).code; + expect(actual).toEqual(EXPECTED); }); }); }); diff --git a/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts b/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts index 3dad59a82..9be848265 100644 --- a/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts +++ b/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { DevModeChange, detectDevChanges } from '../../../utils/building'; +import { DevModeChange, detectDevChanges } from '../detect-dev-changes'; import { fakeBackgroundEntrypoint, fakeContentScriptEntrypoint, @@ -12,7 +12,7 @@ import { fakeOutputChunk, fakeWxt, setFakeWxt, -} from '../../../utils/testing/fake-objects'; +} from '../../testing/fake-objects'; import { BuildOutput, BuildStepOutput } from '../../../../types'; import { setWxtForTesting } from '../../../wxt'; @@ -24,6 +24,7 @@ describe('Detect Dev Changes', () => { describe('No changes', () => { it("should return 'no-change' when the changed file isn't used by any of the entrypoints", () => { const changes = ['/some/path.ts']; + const currentOutput: BuildOutput = { manifest: fakeManifest(), publicAssets: [], @@ -47,15 +48,17 @@ describe('Detect Dev Changes', () => { describe('wxt.config.ts', () => { it("should return 'full-restart' when one of the changed files is the config file", () => { - const configFile = '/root/wxt.config.ts'; + const CONFIG_FILE = '/root/wxt.config.ts'; + setFakeWxt({ config: { userConfigMetadata: { - configFile, + configFile: CONFIG_FILE, }, }, }); - const changes = ['/root/src/public/image.svg', configFile]; + + const changes = ['/root/src/public/image.svg', CONFIG_FILE]; const currentOutput: BuildOutput = { manifest: fakeManifest(), publicAssets: [], @@ -73,16 +76,19 @@ describe('Detect Dev Changes', () => { describe('modules/*', () => { it("should return 'full-restart' when one of the changed files is in the WXT modules folder", () => { - const modulesDir = '/root/modules'; + const MODULES_DIR = '/root/modules'; + setFakeWxt({ config: { - modulesDir, + modulesDir: MODULES_DIR, }, }); + const changes = [ '/root/src/public/image.svg', - `${modulesDir}/example.ts`, + `${MODULES_DIR}/example.ts`, ]; + const currentOutput: BuildOutput = { manifest: fakeManifest(), publicAssets: [], @@ -100,15 +106,17 @@ describe('Detect Dev Changes', () => { describe('web-ext.config.ts', () => { it("should return 'browser-restart' when one of the changed files is the config file", () => { - const runnerFile = '/root/web-ext.config.ts'; + const RUNNER_FILE = '/root/web-ext.config.ts'; + setFakeWxt({ config: { runnerConfig: { - configFile: runnerFile, + configFile: RUNNER_FILE, }, }, }); - const changes = ['/root/src/public/image.svg', runnerFile]; + + const changes = ['/root/src/public/image.svg', RUNNER_FILE]; const currentOutput: BuildOutput = { manifest: fakeManifest(), publicAssets: [], @@ -127,22 +135,27 @@ describe('Detect Dev Changes', () => { describe('Public Assets', () => { it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => { const changes = ['/root/src/public/image.svg']; + setFakeWxt({ config: { publicDir: '/root/src/public', }, }); + const asset1 = fakeOutputAsset({ fileName: 'image.svg', }); + const asset2 = fakeOutputAsset({ fileName: 'some-other-image.svg', }); + const currentOutput: BuildOutput = { manifest: fakeManifest(), publicAssets: [asset1, asset2], steps: [], }; + const expected: DevModeChange = { type: 'extension-reload', rebuildGroups: [], @@ -157,10 +170,12 @@ describe('Detect Dev Changes', () => { describe('Background', () => { it("should rebuild the background and reload the extension when the changed file in it's chunks' `moduleIds` field", () => { - const changedPath = '/root/utils/shared.ts'; + const CHANGED_PATH = '/root/utils/shared.ts'; + const contentScript = fakeContentScriptEntrypoint({ inputPath: '/root/overlay.content.ts', }); + const background = fakeBackgroundEntrypoint({ inputPath: '/root/background.ts', }); @@ -177,7 +192,7 @@ describe('Detect Dev Changes', () => { entrypoints: background, chunks: [ fakeOutputChunk({ - moduleIds: [fakeFile(), changedPath, fakeFile()], + moduleIds: [fakeFile(), CHANGED_PATH, fakeFile()], }), ], }; @@ -196,7 +211,7 @@ describe('Detect Dev Changes', () => { rebuildGroups: [background], }; - const actual = detectDevChanges([changedPath], currentOutput); + const actual = detectDevChanges([CHANGED_PATH], currentOutput); expect(actual).toEqual(expected); }); @@ -204,9 +219,10 @@ describe('Detect Dev Changes', () => { describe('HTML Pages', () => { it('should detect changes to entrypoints/.html files', async () => { - const changedPath = '/root/page1.html'; + const CHANGED_PATH = '/root/page1.html'; + const htmlPage1 = fakePopupEntrypoint({ - inputPath: changedPath, + inputPath: CHANGED_PATH, }); const htmlPage2 = fakeOptionsEntrypoint({ inputPath: '/root/page2.html', @@ -247,19 +263,22 @@ describe('Detect Dev Changes', () => { rebuildGroups: [[htmlPage1, htmlPage2]], }; - const actual = detectDevChanges([changedPath], currentOutput); + const actual = detectDevChanges([CHANGED_PATH], currentOutput); expect(actual).toEqual(expected); }); it('should detect changes to entrypoints//index.html files', async () => { - const changedPath = '/root/page1/index.html'; + const CHANGED_PATH = '/root/page1/index.html'; + const htmlPage1 = fakePopupEntrypoint({ - inputPath: changedPath, + inputPath: CHANGED_PATH, }); + const htmlPage2 = fakeOptionsEntrypoint({ inputPath: '/root/page2/index.html', }); + const htmlPage3 = fakeGenericEntrypoint({ type: 'sandbox', inputPath: '/root/page3/index.html', @@ -273,6 +292,7 @@ describe('Detect Dev Changes', () => { }), ], }; + const step2: BuildStepOutput = { entrypoints: [htmlPage3], chunks: [ @@ -287,6 +307,7 @@ describe('Detect Dev Changes', () => { publicAssets: [], steps: [step1, step2], }; + const expected: DevModeChange = { type: 'html-reload', cachedOutput: { @@ -296,7 +317,7 @@ describe('Detect Dev Changes', () => { rebuildGroups: [[htmlPage1, htmlPage2]], }; - const actual = detectDevChanges([changedPath], currentOutput); + const actual = detectDevChanges([CHANGED_PATH], currentOutput); expect(actual).toEqual(expected); }); @@ -304,13 +325,16 @@ describe('Detect Dev Changes', () => { describe('Content Scripts', () => { it('should rebuild then reload only the effected content scripts', async () => { - const changedPath = '/root/utils/shared.ts'; + const CHANGED_PATH = '/root/utils/shared.ts'; + const script1 = fakeContentScriptEntrypoint({ inputPath: '/root/overlay1.content/index.ts', }); + const script2 = fakeContentScriptEntrypoint({ inputPath: '/root/overlay2.ts', }); + const script3 = fakeContentScriptEntrypoint({ inputPath: '/root/overlay3.content/index.ts', }); @@ -319,10 +343,11 @@ describe('Detect Dev Changes', () => { entrypoints: script1, chunks: [ fakeOutputChunk({ - moduleIds: [fakeFile(), changedPath], + moduleIds: [fakeFile(), CHANGED_PATH], }), ], }; + const step2: BuildStepOutput = { entrypoints: script2, chunks: [ @@ -331,11 +356,12 @@ describe('Detect Dev Changes', () => { }), ], }; + const step3: BuildStepOutput = { entrypoints: script3, chunks: [ fakeOutputChunk({ - moduleIds: [changedPath, fakeFile(), fakeFile()], + moduleIds: [CHANGED_PATH, fakeFile(), fakeFile()], }), ], }; @@ -355,20 +381,23 @@ describe('Detect Dev Changes', () => { rebuildGroups: [script1, script3], }; - const actual = detectDevChanges([changedPath], currentOutput); + const actual = detectDevChanges([CHANGED_PATH], currentOutput); expect(actual).toEqual(expected); }); it('should detect changes to import files with `?suffix`', () => { - const importedPath = '/root/utils/shared.css?inline'; - const changedPath = '/root/utils/shared.css'; + const IMPORTED_PATH = '/root/utils/shared.css?inline'; + const CHANGED_PATH = '/root/utils/shared.css'; + const script1 = fakeContentScriptEntrypoint({ inputPath: '/root/overlay1.content/index.ts', }); + const script2 = fakeContentScriptEntrypoint({ inputPath: '/root/overlay2.ts', }); + const script3 = fakeContentScriptEntrypoint({ inputPath: '/root/overlay3.content/index.ts', }); @@ -377,7 +406,7 @@ describe('Detect Dev Changes', () => { entrypoints: script1, chunks: [ fakeOutputChunk({ - moduleIds: [fakeFile(), importedPath], + moduleIds: [fakeFile(), IMPORTED_PATH], }), ], }; @@ -393,7 +422,7 @@ describe('Detect Dev Changes', () => { entrypoints: script3, chunks: [ fakeOutputChunk({ - moduleIds: [importedPath, fakeFile(), fakeFile()], + moduleIds: [IMPORTED_PATH, fakeFile(), fakeFile()], }), ], }; @@ -403,6 +432,7 @@ describe('Detect Dev Changes', () => { publicAssets: [], steps: [step1, step2, step3], }; + const expected: DevModeChange = { type: 'content-script-reload', cachedOutput: { @@ -413,7 +443,7 @@ describe('Detect Dev Changes', () => { rebuildGroups: [script1, script3], }; - const actual = detectDevChanges([changedPath], currentOutput); + const actual = detectDevChanges([CHANGED_PATH], currentOutput); expect(actual).toEqual(expected); }); diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/no-default-export.ts b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/no-default-export.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/wxt/src/core/utils/building/internal-build.ts b/packages/wxt/src/core/utils/building/internal-build.ts index da551d8af..8fb88eee4 100644 --- a/packages/wxt/src/core/utils/building/internal-build.ts +++ b/packages/wxt/src/core/utils/building/internal-build.ts @@ -3,10 +3,10 @@ import { BuildOutput, Entrypoint } from '../../../types'; import pc from 'picocolors'; import fs from 'fs-extra'; import { groupEntrypoints } from './group-entrypoints'; -import { formatDuration } from '../../utils/time'; -import { printBuildSummary } from '../../utils/log'; +import { formatDuration } from '../time'; +import { printBuildSummary } from '../log'; import glob from 'fast-glob'; -import { unnormalizePath } from '../../utils/paths'; +import { unnormalizePath } from '../paths'; import { rebuild } from './rebuild'; import { relative } from 'node:path'; import { @@ -32,10 +32,11 @@ import { isCI } from 'ci-info'; export async function internalBuild(): Promise { await wxt.hooks.callHook('build:before', wxt); - const verb = wxt.config.command === 'serve' ? 'Pre-rendering' : 'Building'; - const target = `${wxt.config.browser}-mv${wxt.config.manifestVersion}`; + const VERB = wxt.config.command === 'serve' ? 'Pre-rendering' : 'Building'; + const TARGET = `${wxt.config.browser}-mv${wxt.config.manifestVersion}`; + wxt.logger.info( - `${verb} ${pc.cyan(target)} for ${pc.cyan(wxt.config.mode)} with ${pc.green( + `${VERB} ${pc.cyan(TARGET)} for ${pc.cyan(wxt.config.mode)} with ${pc.green( `${wxt.builder.name} ${wxt.builder.version}`, )}`, ); @@ -78,16 +79,18 @@ export async function internalBuild(): Promise { if (wxt.config.analysis.enabled) { await combineAnalysisStats(); const statsPath = relative(wxt.config.root, wxt.config.analysis.outputFile); + wxt.logger.info( `Analysis complete:\n ${pc.gray('└─')} ${pc.yellow(statsPath)}`, ); + if (wxt.config.analysis.open) { if (isCI) { wxt.logger.debug(`Skipped opening ${pc.yellow(statsPath)} in CI`); } else { wxt.logger.info(`Opening ${pc.yellow(statsPath)} in browser...`); const { default: open } = await import('open'); - open(wxt.config.analysis.outputFile); + await open(wxt.config.analysis.outputFile); } } } @@ -133,12 +136,13 @@ function printValidationResults({ }, new Map()); Array.from(entrypointErrors.entries()).forEach(([entrypoint, errors]) => { - wxt.logger.log(relative(cwd, entrypoint.inputPath)); - console.log(); + wxt.logger.log(relative(cwd, entrypoint.inputPath) + '\n'); + errors.forEach((err) => { const type = err.type === 'error' ? pc.red('ERROR') : pc.yellow('WARN'); - const recieved = pc.dim(`(recieved: ${JSON.stringify(err.value)})`); - wxt.logger.log(` - ${type} ${err.message} ${recieved}`); + const received = pc.dim(`(received: ${JSON.stringify(err.value)})`); + + wxt.logger.log(` - ${type} ${err.message} ${received}`); }); console.log(); }); diff --git a/packages/wxt/src/core/utils/log/printHeader.ts b/packages/wxt/src/core/utils/log/printHeader.ts index 5d7dbd7c6..25307f73f 100644 --- a/packages/wxt/src/core/utils/log/printHeader.ts +++ b/packages/wxt/src/core/utils/log/printHeader.ts @@ -1,8 +1,7 @@ import pc from 'picocolors'; -import { version } from '../../../version'; +import { VERSION } from '../../../version'; import { consola } from 'consola'; export function printHeader() { - console.log(); - consola.log(`${pc.gray('WXT')} ${pc.gray(pc.bold(version))}`); + consola.log(`\n${pc.gray('WXT')} ${pc.gray(pc.bold(VERSION))}`); } diff --git a/packages/wxt/src/core/utils/manifest.ts b/packages/wxt/src/core/utils/manifest.ts index 4d6aaebfc..f7aa5991a 100644 --- a/packages/wxt/src/core/utils/manifest.ts +++ b/packages/wxt/src/core/utils/manifest.ts @@ -60,12 +60,14 @@ export async function generateManifest( wxt.config.manifest.version_name ?? wxt.config.manifest.version ?? pkg?.version; + if (!versionName) { versionName = '0.0.0'; wxt.logger.warn( 'Extension version not found, defaulting to "0.0.0". Add a version to your `package.json` or `wxt.config.ts` file. For more details, see: https://wxt.dev/guide/key-concepts/manifest.html#version-and-version-name', ); } + const version = wxt.config.manifest.version ?? simplifyVersion(versionName); const baseManifest: Browser.runtime.Manifest = { @@ -76,7 +78,9 @@ export async function generateManifest( short_name: pkg?.shortName, icons: discoverIcons(buildOutput), }; + const userManifest = wxt.config.manifest; + if (userManifest.manifest_version) { delete userManifest.manifest_version; wxt.logger.warn( @@ -142,6 +146,7 @@ export async function generateManifest( throw Error( "Manifest 'name' is missing. Either:\n1. Set the name in your /package.json\n2. Set a name via the manifest option in your wxt.config.ts", ); + if (manifest.version == null) { throw Error( "Manifest 'version' is missing. Either:\n1. Add a version in your /package.json\n2. Pass the version via the manifest option in your wxt.config.ts", @@ -274,13 +279,17 @@ function addEntrypoints( '.html', ); const options: Browser.runtime.ManifestAction = {}; - if (popup.options.defaultIcon) + + if (popup.options.defaultIcon) { options.default_icon = popup.options.defaultIcon; - if (popup.options.defaultTitle) + } + if (popup.options.defaultTitle) { options.default_title = popup.options.defaultTitle; - if (popup.options.browserStyle) + } + if (popup.options.browserStyle) { // @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox options.browser_style = popup.options.browserStyle; + } if (manifest.manifest_version === 3) { manifest.action = { ...manifest.action, @@ -397,6 +406,7 @@ function addEntrypoints( getContentScriptCssFiles(scripts, cssMap), ), ); + if (manifestContentScripts.length >= 0) { manifest.content_scripts ??= []; manifest.content_scripts.push(...manifestContentScripts); @@ -459,19 +469,21 @@ function discoverIcons( } function addDevModeCsp(manifest: Browser.runtime.Manifest): void { - let permissonUrl = wxt.server?.origin; - if (permissonUrl) { - const permissionUrlInstance = new URL(permissonUrl); + let permissionUrl = wxt.server?.origin; + + if (permissionUrl) { + const permissionUrlInstance = new URL(permissionUrl); permissionUrlInstance.port = ''; - permissonUrl = permissionUrlInstance.toString(); + permissionUrl = permissionUrlInstance.toString(); } - const permission = `${permissonUrl}*`; + + const PERMISSION = `${permissionUrl}*`; const allowedCsp = wxt.server?.origin ?? 'http://localhost:*'; if (manifest.manifest_version === 3) { - addHostPermission(manifest, permission); + addHostPermission(manifest, PERMISSION); } else { - addPermission(manifest, permission); + addPermission(manifest, PERMISSION); } const extensionPagesCsp = new ContentSecurityPolicy( @@ -669,7 +681,7 @@ function convertCspToMv2(manifest: Browser.runtime.Manifest): void { } /** - * Make sure all resources are in MV3 format. If not, add a wanring + * Make sure all resources are in MV3 format. If not, add a warning */ function validateMv3WebAccessibleResources( manifest: Browser.runtime.Manifest, @@ -693,8 +705,10 @@ function validateMv3WebAccessibleResources( */ function stripKeys(manifest: Browser.runtime.Manifest): void { let keysToRemove: string[] = []; + if (wxt.config.manifestVersion === 2) { keysToRemove.push(...mv3OnlyKeys); + if (wxt.config.browser === 'firefox') keysToRemove.push(...firefoxMv3OnlyKeys); } else { diff --git a/packages/wxt/src/core/utils/minimatch-multiple.ts b/packages/wxt/src/core/utils/minimatch-multiple.ts index be1bbf0e8..7bae9e021 100644 --- a/packages/wxt/src/core/utils/minimatch-multiple.ts +++ b/packages/wxt/src/core/utils/minimatch-multiple.ts @@ -22,6 +22,7 @@ export function minimatchMultiple( const negatePatterns: string[] = []; const positivePatterns: string[] = []; + for (const pattern of patterns) { if (pattern[0] === '!') negatePatterns.push(pattern.slice(1)); else positivePatterns.push(pattern); diff --git a/packages/wxt/src/core/utils/network.ts b/packages/wxt/src/core/utils/network.ts index 57a7918b4..e73f55149 100644 --- a/packages/wxt/src/core/utils/network.ts +++ b/packages/wxt/src/core/utils/network.ts @@ -2,7 +2,7 @@ import dns from 'node:dns'; import { ResolvedConfig } from '../../types'; import { withTimeout } from './time'; -function isOffline(): Promise { +async function isOffline(): Promise { const isOffline = new Promise((res) => { dns.resolve('google.com', (err) => { if (err == null) { @@ -32,6 +32,7 @@ export async function fetchCached( if (await isOnline()) { const res = await fetch(url); + if (res.status < 300) { content = await res.text(); await config.fsCache.set(url, content); diff --git a/packages/wxt/src/core/utils/syntax-errors.ts b/packages/wxt/src/core/utils/syntax-errors.ts index d0aef21a9..9ee863230 100644 --- a/packages/wxt/src/core/utils/syntax-errors.ts +++ b/packages/wxt/src/core/utils/syntax-errors.ts @@ -21,10 +21,12 @@ export function logBabelSyntaxError(error: BabelSyntaxError) { if (filename.startsWith('..')) { filename = error.id; } + let message = error.message.replace( /\(\d+:\d+\)$/, `(${filename}:${error.loc.line}:${error.loc.column + 1})`, ); + if (error.frame) { message += '\n\n' + pc.red(error.frame); } diff --git a/packages/wxt/src/core/utils/transform.ts b/packages/wxt/src/core/utils/transform.ts index e8593c346..23ad194f2 100644 --- a/packages/wxt/src/core/utils/transform.ts +++ b/packages/wxt/src/core/utils/transform.ts @@ -15,13 +15,14 @@ export function removeMainFunctionCode(code: string): { emptyMainFunction(mod); let removedCount = 0; let depth = 0; - const maxDepth = 10; + const MAX_DEPTH = 10; + do { removedCount = 0; removedCount += removeUnusedTopLevelVariables(mod); removedCount += removeUnusedTopLevelFunctions(mod); removedCount += removeUnusedImports(mod); - } while (removedCount > 0 && depth++ <= maxDepth); + } while (removedCount > 0 && depth++ <= MAX_DEPTH); removeSideEffectImports(mod); return mod.generate(); } @@ -57,8 +58,10 @@ function removeUnusedTopLevelVariables(mod: ProxifiedModule): number { const cleanArrayPattern = (pattern: any): boolean => { const elements = pattern.elements; + for (let i = elements.length - 1; i >= 0; i--) { const el = elements[i]; + if (el?.type === 'Identifier' && !isUsed(el)) { elements.splice(i, 1); deletedCount++; @@ -69,6 +72,7 @@ function removeUnusedTopLevelVariables(mod: ProxifiedModule): number { const cleanObjectPattern = (pattern: any): boolean => { const properties = pattern.properties; + for (let i = properties.length - 1; i >= 0; i--) { const prop = properties[i]; @@ -82,6 +86,7 @@ function removeUnusedTopLevelVariables(mod: ProxifiedModule): number { } } else if (value.type === 'ArrayPattern') { const isEmpty = cleanArrayPattern(value); + if (isEmpty) { properties.splice(i, 1); } @@ -91,6 +96,7 @@ function removeUnusedTopLevelVariables(mod: ProxifiedModule): number { } } else if (prop.type === 'RestElement') { const arg = prop.argument; + if (arg.type === 'Identifier' && !isUsed(arg)) { properties.splice(i, 1); deletedCount++; @@ -136,6 +142,7 @@ function removeUnusedTopLevelFunctions(mod: ProxifiedModule): number { let deletedCount = 0; const ast = mod.$ast as any; + for (let i = ast.body.length - 1; i >= 0; i--) { if ( ast.body[i].type === 'FunctionDeclaration' && @@ -145,6 +152,7 @@ function removeUnusedTopLevelFunctions(mod: ProxifiedModule): number { deletedCount++; } } + return deletedCount; } diff --git a/packages/wxt/src/core/utils/validation.ts b/packages/wxt/src/core/utils/validation.ts index 331fe80a6..41433e341 100644 --- a/packages/wxt/src/core/utils/validation.ts +++ b/packages/wxt/src/core/utils/validation.ts @@ -14,6 +14,7 @@ export function validateEntrypoints( let errorCount = 0; let warningCount = 0; + for (const err of errors) { if (err.type === 'warning') warningCount++; else errorCount++; @@ -30,6 +31,7 @@ function validateContentScriptEntrypoint( definition: ContentScriptEntrypoint, ): ValidationResult[] { const errors = validateBaseEntrypoint(definition); + if ( definition.options.registration !== 'runtime' && definition.options.matches == null diff --git a/packages/wxt/src/core/wxt.ts b/packages/wxt/src/core/wxt.ts index eafc9429d..bbff462bd 100644 --- a/packages/wxt/src/core/wxt.ts +++ b/packages/wxt/src/core/wxt.ts @@ -85,6 +85,7 @@ export async function initWxtModules() { async function initWxtModule(module: WxtModule): Promise { if (module.hooks) wxt.hooks.addHooks(module.hooks); + await module.setup?.( wxt, // @ts-expect-error: Untyped configKey field diff --git a/packages/wxt/src/core/zip.ts b/packages/wxt/src/core/zip.ts index 70626d6ae..12dfb84bd 100644 --- a/packages/wxt/src/core/zip.ts +++ b/packages/wxt/src/core/zip.ts @@ -4,7 +4,7 @@ import fs from 'fs-extra'; import { safeFilename } from './utils/strings'; import { getPackageJson } from './utils/package'; import { formatDuration } from './utils/time'; -import { printFileList } from './utils/log/printFileList'; +import { printFileList } from './utils/log'; import { findEntrypoints, internalBuild } from './utils/building'; import { registerWxt, wxt } from './wxt'; import JSZip from 'jszip'; @@ -30,15 +30,16 @@ export async function zip(config?: InlineConfig): Promise { const projectName = wxt.config.zip.name ?? safeFilename(packageJson?.name || path.basename(process.cwd())); + const applyTemplate = (template: string): string => template .replaceAll('{{name}}', projectName) .replaceAll('{{browser}}', wxt.config.browser) .replaceAll( '{{version}}', - output.manifest.version_name ?? output.manifest.version, + String(output.manifest.version_name ?? output.manifest.version), ) - .replaceAll('{{packageVersion}}', packageJson?.version) + .replaceAll('{{packageVersion}}', String(packageJson?.version)) .replaceAll('{{mode}}', wxt.config.mode) .replaceAll('{{manifestVersion}}', `mv${wxt.config.manifestVersion}`); @@ -46,8 +47,10 @@ export async function zip(config?: InlineConfig): Promise { // ZIP output directory await wxt.hooks.callHook('zip:extension:start', wxt); + const outZipFilename = applyTemplate(wxt.config.zip.artifactTemplate); const outZipPath = path.resolve(wxt.config.outBaseDir, outZipFilename); + await zipDir(wxt.config.outDir, outZipPath, { exclude: wxt.config.zip.exclude, }); @@ -57,20 +60,25 @@ export async function zip(config?: InlineConfig): Promise { if (wxt.config.zip.zipSources) { const entrypoints = await findEntrypoints(); const skippedEntrypoints = entrypoints.filter((entry) => entry.skipped); + const excludeSources = [ ...wxt.config.zip.excludeSources, ...skippedEntrypoints.map((entry) => path.relative(wxt.config.zip.sourcesRoot, entry.inputPath), ), ].map((paths) => paths.replaceAll('\\', '/')); + await wxt.hooks.callHook('zip:sources:start', wxt); + const { overrides, files: downloadedPackages } = await downloadPrivatePackages(); + const sourcesZipFilename = applyTemplate(wxt.config.zip.sourcesTemplate); const sourcesZipPath = path.resolve( wxt.config.outBaseDir, sourcesZipFilename, ); + await zipDir(wxt.config.zip.sourcesRoot, sourcesZipPath, { include: wxt.config.zip.includeSources, exclude: excludeSources, @@ -81,6 +89,7 @@ export async function zip(config?: InlineConfig): Promise { }, additionalFiles: downloadedPackages, }); + zipFiles.push(sourcesZipPath); await wxt.hooks.callHook('zip:sources:done', wxt, sourcesZipPath); } @@ -132,8 +141,10 @@ async function zipDir( path.relative(directory, file), ), ]; + for (const file of filesToZip) { const absolutePath = path.resolve(directory, file); + if (file.endsWith('.json')) { const content = await fs.readFile(absolutePath, 'utf-8'); archive.file( @@ -145,6 +156,7 @@ async function zipDir( archive.file(file, content); } } + await options?.additionalWork?.(archive); await new Promise((resolve, reject) => @@ -180,13 +192,13 @@ async function downloadPrivatePackages() { for (const pkg of downloadPackages) { wxt.logger.info(`Downloading package: ${pkg.name}@${pkg.version}`); - const id = `${pkg.name}@${pkg.version}`; + const ID = `${pkg.name}@${pkg.version}`; const tgzPath = await wxt.pm.downloadDependency( - id, + ID, wxt.config.zip.downloadedPackagesDir, ); files.push(tgzPath); - overrides[id] = tgzPath; + overrides[ID] = tgzPath; } } @@ -206,9 +218,11 @@ function addOverridesToPackageJson( ...oldPackage, [wxt.pm.overridesKey]: { ...oldPackage[wxt.pm.overridesKey] }, }; + Object.entries(overrides).forEach(([key, absolutePath]) => { newPackage[wxt.pm.overridesKey][key] = 'file://./' + normalizePath(path.relative(packageJsonDir, absolutePath)); }); + return JSON.stringify(newPackage, null, 2); } diff --git a/packages/wxt/src/modules.ts b/packages/wxt/src/modules.ts index 2cb12a071..c1bd87d1e 100644 --- a/packages/wxt/src/modules.ts +++ b/packages/wxt/src/modules.ts @@ -108,9 +108,11 @@ export function addViteConfig( ): void { wxt.hooks.hook('config:resolved', (wxt) => { const userVite = wxt.config.vite; + wxt.config.vite = async (env) => { const fromUser = await userVite(env); const fromModule = viteConfig(env) ?? {}; + return vite.mergeConfig(fromModule, fromUser); }; }); @@ -167,11 +169,12 @@ export function addImportPreset( preset: UnimportOptions['presets'][0], ): void { wxt.hooks.hook('config:resolved', (wxt) => { + // TODO: BUT IN NEWER VERSIONS OF WXT, THIS SHOULD NEVER HAPPENED, YEAH? // In older versions of WXT, `wxt.config.imports` could be false if (!wxt.config.imports) return; wxt.config.imports.presets ??= []; - // De-dupelicate built-in named presets + // De-duplicate built-in named presets if (wxt.config.imports.presets.includes(preset)) return; wxt.config.imports.presets.push(preset); @@ -208,6 +211,7 @@ export function addImportPreset( export function addAlias(wxt: Wxt, alias: string, path: string) { wxt.hooks.hook('config:resolved', (wxt) => { const target = resolve(wxt.config.root, path); + if (wxt.config.alias[alias] != null && wxt.config.alias[alias] !== target) { wxt.logger.warn( `Skipped adding alias (${alias} => ${target}) because an alias with the same name already exists: ${alias} => ${wxt.config.alias[alias]}`, diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index 5ff354097..d09f72b9c 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -21,7 +21,7 @@ export interface InlineConfig { * Directory containing all source code. Set to `"src"` to move all source code to a `src/` * directory. * - * After changing, don't forget to move the `public/` and `entrypoints/` directories into the new + * After changing, remember to move the `public/` and `entrypoints/` directories into the new * source dir. * * @default config.root diff --git a/packages/wxt/src/utils/__tests__/content-script-context.test.ts b/packages/wxt/src/utils/__tests__/content-script-context.test.ts index f7a5ffc86..9993ebcb7 100644 --- a/packages/wxt/src/utils/__tests__/content-script-context.test.ts +++ b/packages/wxt/src/utils/__tests__/content-script-context.test.ts @@ -33,16 +33,18 @@ describe('Content Script Context', () => { }); it('should invalidate the current content script when a new context is created', async () => { - const name = 'test'; + const NAME = 'test'; + const onInvalidated = vi.fn(); - const ctx = new ContentScriptContext(name); + const ctx = new ContentScriptContext(NAME); + ctx.onInvalidated(onInvalidated); // Wait for events to run before next tick next tick await waitForEventsToFire(); // Create a new context after first is initialized, and wait for it to initialize - new ContentScriptContext(name); + new ContentScriptContext(NAME); await waitForEventsToFire(); expect(onInvalidated).toBeCalled(); @@ -52,6 +54,7 @@ describe('Content Script Context', () => { it('should not invalidate the current content script when a new context is created with a different name', async () => { const onInvalidated = vi.fn(); const ctx = new ContentScriptContext('test1'); + ctx.onInvalidated(onInvalidated); // Wait for events to run before next tick next tick diff --git a/packages/wxt/src/utils/__tests__/split-shadow-root-css.test.ts b/packages/wxt/src/utils/__tests__/split-shadow-root-css.test.ts index b1e5a3c56..84b90434b 100644 --- a/packages/wxt/src/utils/__tests__/split-shadow-root-css.test.ts +++ b/packages/wxt/src/utils/__tests__/split-shadow-root-css.test.ts @@ -4,15 +4,15 @@ import { splitShadowRootCss } from '../split-shadow-root-css'; describe('Shadow Root Utils', () => { describe('splitShadowRootCss', () => { it('should extract @property and @font-face declarations from minified tailwindcss v4', () => { - const css = `/*! tailwindcss v4.0.13 | MIT License | https://tailwindcss.com */@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--spacing:.25rem;--container-3xl:48rem;--container-5xl:64rem;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-light:300;--font-weight-semibold:600;--font-weight-bold:700;--radius-md:.375rem;--radius-xl:.75rem;--aspect-video:16/9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-font-feature-settings:var(--font-sans--font-feature-settings);--default-font-variation-settings:var(--font-sans--font-variation-settings);--default-mono-font-family:var(--font-mono);--default-mono-font-feature-settings:var(--font-mono--font-feature-settings);--default-mono-font-variation-settings:var(--font-mono--font-variation-settings);--color-primary:#ffc700;--color-primary-content:#000;--color-secondary:#c00;--color-secondary-content:#fff;--color-base:#000;--color-base-content:#fff;--color-neutral:#1c1c1c;--color-neutral-content:#fff;--color-white:#fff;--color-black:#000;--font-poppins:Poppins,sans-serif;--spacing-main-navigation:calc(20*var(--spacing))}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1;color:color-mix(in oklab,currentColor 50%,transparent)}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{min-width:0;min-height:0}body{background-color:var(--color-base);color:var(--color-base-content)}}@layer components{.btn{--btn-bg:var(--color-primary);--btn-text:var(--color-primary-content);background-color:var(--btn-bg);color:var(--btn-text);border-radius:calc(2*var(--spacing));font-weight:500;font-family:var(--font-poppins);padding:calc(3*var(--spacing))calc(6*var(--spacing));justify-content:center;align-items:center;gap:calc(3*var(--spacing));cursor:pointer;transition:transform .2s;display:flex}.btn:hover{transform:scale(1.05)}.btn:active{transform:scale(.97)}.btn-neutral{--btn-bg:var(--color-neutral);--btn-text:var(--color-neutral-content)}.btn-base{--btn-bg:var(--color-base);--btn-text:var(--color-base-content)}.btn-white{--btn-bg:var(--color-white);--btn-text:var(--color-black)}.btn-secondary{--btn-bg:var(--color-secondary);--btn-text:var(--color-secondary-content)}.btn-square{padding:calc(3*var(--spacing))}.nav-link{font-family:var(--font-poppins);font-size:var(--text-xl);transition:color .2s;position:relative}.nav-link:hover,.nav-link.active{color:var(--color-primary)}.nav-link.active:after{content:"";bottom:calc(-1*var(--spacing));background-color:var(--color-primary);width:100%;height:2px;position:absolute;left:0}.link{color:color-mix(in srgb,var(--color-secondary),white 30%);font-weight:500;text-decoration:underline}.link-white{color:var(--color-white)}}@layer utilities{.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.z-0{z-index:0}.z-1{z-index:1}.z-10{z-index:10}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-main-navigation{margin-top:var(--spacing-main-navigation)}.-mr-2{margin-right:calc(var(--spacing)*-2)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.-ml-2{margin-left:calc(var(--spacing)*-2)}.i-heroicons-bars-3{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.i-heroicons-calendar-days{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12zM12 15h.008v.008H12zm0 2.25h.008v.008H12zM9.75 15h.008v.008H9.75zm0 2.25h.008v.008H9.75zM7.5 15h.008v.008H7.5zm0 2.25h.008v.008H7.5zm6.75-4.5h.008v.008h-.008zm0 2.25h.008v.008h-.008zm0 2.25h.008v.008h-.008zm2.25-4.5h.008v.008H16.5zm0 2.25h.008v.008H16.5z'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.i-heroicons-chevron-right{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m8.25 4.5l7.5 7.5l-7.5 7.5'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.i-heroicons-x-mark{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 18L18 6M6 6l12 12'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.flex{display:flex}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1}.aspect-video{aspect-ratio:var(--aspect-video)}.h-12{height:calc(var(--spacing)*12)}.h-20{height:calc(var(--spacing)*20)}.h-\\[70vh\\]{height:70vh}.h-\\[95vh\\]{height:95vh}.h-\\[100vh\\]{height:100vh}.h-full{height:100%}.h-main-navigation{height:var(--spacing-main-navigation)}.w-24{width:calc(var(--spacing)*24)}.w-40{width:calc(var(--spacing)*40)}.w-full{width:100%}.max-w-3xl{max-width:var(--container-3xl)}.max-w-5xl{max-width:var(--container-5xl)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x)var(--tw-rotate-y)var(--tw-rotate-z)var(--tw-skew-x)var(--tw-skew-y)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-items-center{justify-items:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}.gap-8{gap:calc(var(--spacing)*8)}.gap-16{gap:calc(var(--spacing)*16)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-base>:not(:last-child)){border-color:var(--color-base)}.overflow-hidden{overflow:hidden}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-white\\/50{border-color:color-mix(in oklab,var(--color-white)50%,transparent)}.bg-base\\/0{background-color:color-mix(in oklab,var(--color-base)0%,transparent)}.bg-base\\/100{background-color:color-mix(in oklab,var(--color-base)100%,transparent)}.bg-neutral{background-color:var(--color-neutral)}.object-cover{object-fit:cover}.object-center{object-position:center}.p-8{padding:calc(var(--spacing)*8)}.p-16{padding:calc(var(--spacing)*16)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-20{padding-block:calc(var(--spacing)*20)}.pt-main-navigation{padding-top:var(--spacing-main-navigation)}.text-center{text-align:center}.font-poppins{font-family:var(--font-poppins)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.leading-\\[3\\]{--tw-leading:3;line-height:3}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-primary{color:var(--color-primary)}.text-secondary{color:var(--color-secondary)}.text-white{color:var(--color-white)}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-4{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(4px + var(--tw-ring-offset-width))var(--tw-ring-color,currentColor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-primary\\/50{--tw-shadow-color:color-mix(in oklab,var(--color-primary)50%,transparent)}.ring-primary\\/30{--tw-ring-color:color-mix(in oklab,var(--color-primary)30%,transparent)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.hover\\:text-primary:hover{color:var(--color-primary)}}@media (width>=48rem){.md\\:mx-16{margin-inline:calc(var(--spacing)*16)}.md\\:block{display:block}.md\\:flex{display:flex}.md\\:hidden{display:none}.md\\:aspect-square{aspect-ratio:1}.md\\:h-56{height:calc(var(--spacing)*56)}.md\\:h-full{height:100%}.md\\:w-\\[unset\\]{width:unset}.md\\:flex-1{flex:1}.md\\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\\:flex-row-reverse{flex-direction:row-reverse}.md\\:items-start{align-items:flex-start}.md\\:justify-start{justify-content:flex-start}.md\\:justify-items-start{justify-items:start}.md\\:gap-12{gap:calc(var(--spacing)*12)}.md\\:px-16{padding-inline:calc(var(--spacing)*16)}.md\\:pr-\\[30vw\\]{padding-right:30vw}.md\\:text-left{text-align:left}}@media (width>=64rem){.lg\\:aspect-\\[4\\/3\\]{aspect-ratio:4/3}.lg\\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-rotate-x{syntax:"*";inherits:false;initial-value:rotateX(0)}@property --tw-rotate-y{syntax:"*";inherits:false;initial-value:rotateY(0)}@property --tw-rotate-z{syntax:"*";inherits:false;initial-value:rotateZ(0)}@property --tw-skew-x{syntax:"*";inherits:false;initial-value:skewX(0)}@property --tw-skew-y{syntax:"*";inherits:false;initial-value:skewY(0)}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@font-face{font-family: "custom-font";font-display: swap;font-weight: 500;src: url("fonts/custom-font.otf") format("opentype");}`; + const CSS = `/*! tailwindcss v4.0.13 | MIT License | https://tailwindcss.com */@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--spacing:.25rem;--container-3xl:48rem;--container-5xl:64rem;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-light:300;--font-weight-semibold:600;--font-weight-bold:700;--radius-md:.375rem;--radius-xl:.75rem;--aspect-video:16/9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-font-feature-settings:var(--font-sans--font-feature-settings);--default-font-variation-settings:var(--font-sans--font-variation-settings);--default-mono-font-family:var(--font-mono);--default-mono-font-feature-settings:var(--font-mono--font-feature-settings);--default-mono-font-variation-settings:var(--font-mono--font-variation-settings);--color-primary:#ffc700;--color-primary-content:#000;--color-secondary:#c00;--color-secondary-content:#fff;--color-base:#000;--color-base-content:#fff;--color-neutral:#1c1c1c;--color-neutral-content:#fff;--color-white:#fff;--color-black:#000;--font-poppins:Poppins,sans-serif;--spacing-main-navigation:calc(20*var(--spacing))}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1;color:color-mix(in oklab,currentColor 50%,transparent)}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{min-width:0;min-height:0}body{background-color:var(--color-base);color:var(--color-base-content)}}@layer components{.btn{--btn-bg:var(--color-primary);--btn-text:var(--color-primary-content);background-color:var(--btn-bg);color:var(--btn-text);border-radius:calc(2*var(--spacing));font-weight:500;font-family:var(--font-poppins);padding:calc(3*var(--spacing))calc(6*var(--spacing));justify-content:center;align-items:center;gap:calc(3*var(--spacing));cursor:pointer;transition:transform .2s;display:flex}.btn:hover{transform:scale(1.05)}.btn:active{transform:scale(.97)}.btn-neutral{--btn-bg:var(--color-neutral);--btn-text:var(--color-neutral-content)}.btn-base{--btn-bg:var(--color-base);--btn-text:var(--color-base-content)}.btn-white{--btn-bg:var(--color-white);--btn-text:var(--color-black)}.btn-secondary{--btn-bg:var(--color-secondary);--btn-text:var(--color-secondary-content)}.btn-square{padding:calc(3*var(--spacing))}.nav-link{font-family:var(--font-poppins);font-size:var(--text-xl);transition:color .2s;position:relative}.nav-link:hover,.nav-link.active{color:var(--color-primary)}.nav-link.active:after{content:"";bottom:calc(-1*var(--spacing));background-color:var(--color-primary);width:100%;height:2px;position:absolute;left:0}.link{color:color-mix(in srgb,var(--color-secondary),white 30%);font-weight:500;text-decoration:underline}.link-white{color:var(--color-white)}}@layer utilities{.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.z-0{z-index:0}.z-1{z-index:1}.z-10{z-index:10}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-main-navigation{margin-top:var(--spacing-main-navigation)}.-mr-2{margin-right:calc(var(--spacing)*-2)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.-ml-2{margin-left:calc(var(--spacing)*-2)}.i-heroicons-bars-3{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.i-heroicons-calendar-days{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12zM12 15h.008v.008H12zm0 2.25h.008v.008H12zM9.75 15h.008v.008H9.75zm0 2.25h.008v.008H9.75zM7.5 15h.008v.008H7.5zm0 2.25h.008v.008H7.5zm6.75-4.5h.008v.008h-.008zm0 2.25h.008v.008h-.008zm0 2.25h.008v.008h-.008zm2.25-4.5h.008v.008H16.5zm0 2.25h.008v.008H16.5z'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.i-heroicons-chevron-right{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m8.25 4.5l7.5 7.5l-7.5 7.5'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.i-heroicons-x-mark{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 18L18 6M6 6l12 12'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.flex{display:flex}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1}.aspect-video{aspect-ratio:var(--aspect-video)}.h-12{height:calc(var(--spacing)*12)}.h-20{height:calc(var(--spacing)*20)}.h-\\[70vh\\]{height:70vh}.h-\\[95vh\\]{height:95vh}.h-\\[100vh\\]{height:100vh}.h-full{height:100%}.h-main-navigation{height:var(--spacing-main-navigation)}.w-24{width:calc(var(--spacing)*24)}.w-40{width:calc(var(--spacing)*40)}.w-full{width:100%}.max-w-3xl{max-width:var(--container-3xl)}.max-w-5xl{max-width:var(--container-5xl)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x)var(--tw-rotate-y)var(--tw-rotate-z)var(--tw-skew-x)var(--tw-skew-y)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-items-center{justify-items:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}.gap-8{gap:calc(var(--spacing)*8)}.gap-16{gap:calc(var(--spacing)*16)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-base>:not(:last-child)){border-color:var(--color-base)}.overflow-hidden{overflow:hidden}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-white\\/50{border-color:color-mix(in oklab,var(--color-white)50%,transparent)}.bg-base\\/0{background-color:color-mix(in oklab,var(--color-base)0%,transparent)}.bg-base\\/100{background-color:color-mix(in oklab,var(--color-base)100%,transparent)}.bg-neutral{background-color:var(--color-neutral)}.object-cover{object-fit:cover}.object-center{object-position:center}.p-8{padding:calc(var(--spacing)*8)}.p-16{padding:calc(var(--spacing)*16)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-20{padding-block:calc(var(--spacing)*20)}.pt-main-navigation{padding-top:var(--spacing-main-navigation)}.text-center{text-align:center}.font-poppins{font-family:var(--font-poppins)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.leading-\\[3\\]{--tw-leading:3;line-height:3}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-primary{color:var(--color-primary)}.text-secondary{color:var(--color-secondary)}.text-white{color:var(--color-white)}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-4{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(4px + var(--tw-ring-offset-width))var(--tw-ring-color,currentColor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-primary\\/50{--tw-shadow-color:color-mix(in oklab,var(--color-primary)50%,transparent)}.ring-primary\\/30{--tw-ring-color:color-mix(in oklab,var(--color-primary)30%,transparent)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.hover\\:text-primary:hover{color:var(--color-primary)}}@media (width>=48rem){.md\\:mx-16{margin-inline:calc(var(--spacing)*16)}.md\\:block{display:block}.md\\:flex{display:flex}.md\\:hidden{display:none}.md\\:aspect-square{aspect-ratio:1}.md\\:h-56{height:calc(var(--spacing)*56)}.md\\:h-full{height:100%}.md\\:w-\\[unset\\]{width:unset}.md\\:flex-1{flex:1}.md\\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\\:flex-row-reverse{flex-direction:row-reverse}.md\\:items-start{align-items:flex-start}.md\\:justify-start{justify-content:flex-start}.md\\:justify-items-start{justify-items:start}.md\\:gap-12{gap:calc(var(--spacing)*12)}.md\\:px-16{padding-inline:calc(var(--spacing)*16)}.md\\:pr-\\[30vw\\]{padding-right:30vw}.md\\:text-left{text-align:left}}@media (width>=64rem){.lg\\:aspect-\\[4\\/3\\]{aspect-ratio:4/3}.lg\\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-rotate-x{syntax:"*";inherits:false;initial-value:rotateX(0)}@property --tw-rotate-y{syntax:"*";inherits:false;initial-value:rotateY(0)}@property --tw-rotate-z{syntax:"*";inherits:false;initial-value:rotateZ(0)}@property --tw-skew-x{syntax:"*";inherits:false;initial-value:skewX(0)}@property --tw-skew-y{syntax:"*";inherits:false;initial-value:skewY(0)}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@font-face{font-family: "custom-font";font-display: swap;font-weight: 500;src: url("fonts/custom-font.otf") format("opentype");}`; - const actual = splitShadowRootCss(css); + const actual = splitShadowRootCss(CSS); expect(actual).toMatchSnapshot(); }); it('should maintain the same rule ordering and spacing as the original', () => { - const css = ` + const CSS = ` .one { color: blue; } @@ -35,8 +35,9 @@ describe('Shadow Root Utils', () => { font-weight: 500; src: url("~~/assets/fonts/custom-font.otf") format("opentype"); } - `.trim(); - const expectedShadowCss = ` +`.trim(); + + const EXPECTED_SHADOW_CSS = ` .one { color: blue; } @@ -45,7 +46,8 @@ describe('Shadow Root Utils', () => { color: red; } `.trim(); - const expectedDocumentCss = ` + + const EXPECTED_DOCUMENT_CSS = ` @property --test-1 { initial-value: 0; } @@ -62,11 +64,11 @@ describe('Shadow Root Utils', () => { } `.trim(); - const actual = splitShadowRootCss(css); + const actual = splitShadowRootCss(CSS); expect(actual).toEqual({ - shadowCss: expectedShadowCss, - documentCss: expectedDocumentCss, + shadowCss: EXPECTED_SHADOW_CSS, + documentCss: EXPECTED_DOCUMENT_CSS, }); }); }); diff --git a/packages/wxt/src/utils/content-script-context.ts b/packages/wxt/src/utils/content-script-context.ts index fdcbe857d..bd2de28dd 100644 --- a/packages/wxt/src/utils/content-script-context.ts +++ b/packages/wxt/src/utils/content-script-context.ts @@ -1,7 +1,7 @@ /** @module wxt/utils/content-script-context */ import { ContentScriptDefinition } from '../types'; import { browser } from 'wxt/browser'; -import { logger } from '../utils/internal/logger'; +import { logger } from './internal/logger'; import { WxtLocationChangeEvent, getUniqueEventName, @@ -260,6 +260,7 @@ export class ContentScriptContext implements AbortController { const isSameContentScript = event.data?.contentScriptName === this.contentScriptName; const isNotDuplicate = !this.receivedMessageIds.has(event.data?.messageId); + return isScriptStartedEvent && isSameContentScript && isNotDuplicate; } @@ -272,6 +273,7 @@ export class ContentScriptContext implements AbortController { const wasFirst = isFirst; isFirst = false; + if (wasFirst && options?.ignoreFirstEvent) return; this.notifyInvalidated(); diff --git a/packages/wxt/src/utils/content-script-ui/__tests__/index.test.ts b/packages/wxt/src/utils/content-script-ui/__tests__/index.test.ts index 1e349ef86..95751a79b 100644 --- a/packages/wxt/src/utils/content-script-ui/__tests__/index.test.ts +++ b/packages/wxt/src/utils/content-script-ui/__tests__/index.test.ts @@ -10,7 +10,7 @@ import { ContentScriptUi } from '../types'; * Util for floating promise. */ async function runMicrotasks() { - return await new Promise((resolve) => setTimeout(resolve, 0)); + return new Promise((resolve) => setTimeout(resolve, 0)); } function appendTestApp(container: HTMLElement) { @@ -25,14 +25,17 @@ function appendTestElement({ id: string; }) { const parent = document.querySelector('#parent'); + if (!parent) { throw Error( 'Parent element not found. Please check the testing environment DOM', ); } + const element = document.createElement(tagName); element.id = id; parent.append(element); + return element; } @@ -148,6 +151,7 @@ describe('Content Script UIs', () => { position: 'overlay', page: '/page.html', }); + ui.mount(); expect(ui.wrapper.outerHTML).toMatchInlineSnapshot( @@ -161,6 +165,7 @@ describe('Content Script UIs', () => { page: '/page.html', alignment: 'top-left', }); + ui.mount(); expect(ui.wrapper.outerHTML).toMatchInlineSnapshot( @@ -174,6 +179,7 @@ describe('Content Script UIs', () => { page: '/page.html', alignment: 'top-right', }); + ui.mount(); expect(ui.wrapper.outerHTML).toMatchInlineSnapshot( @@ -187,6 +193,7 @@ describe('Content Script UIs', () => { page: '/page.html', alignment: 'bottom-right', }); + ui.mount(); expect(ui.wrapper.outerHTML).toMatchInlineSnapshot( @@ -200,6 +207,7 @@ describe('Content Script UIs', () => { page: '/page.html', alignment: 'bottom-left', }); + ui.mount(); expect(ui.wrapper.outerHTML).toMatchInlineSnapshot( @@ -208,15 +216,17 @@ describe('Content Script UIs', () => { }); it('should respect the provided zIndex', () => { - const zIndex = 123; + const Z_INDEX = 123; + const ui = createIframeUi(ctx, { position: 'overlay', page: '/page.html', - zIndex, + zIndex: Z_INDEX, }); + ui.mount(); - expect(ui.wrapper.style.zIndex).toBe(String(zIndex)); + expect(ui.wrapper.style.zIndex).toBe(String(Z_INDEX)); }); }); @@ -226,6 +236,7 @@ describe('Content Script UIs', () => { position: 'modal', page: '/page.html', }); + ui.mount(); expect(ui.wrapper.outerHTML).toMatchInlineSnapshot( @@ -234,15 +245,17 @@ describe('Content Script UIs', () => { }); it('should respect the provided zIndex', () => { - const zIndex = 123; + const Z_INDEX = 123; + const ui = createIframeUi(ctx, { position: 'modal', page: '/page.html', - zIndex, + zIndex: Z_INDEX, }); + ui.mount(); - expect(ui.wrapper.style.zIndex).toBe(String(zIndex)); + expect(ui.wrapper.style.zIndex).toBe(String(Z_INDEX)); }); }); }); @@ -254,6 +267,7 @@ describe('Content Script UIs', () => { position: 'inline', onMount: appendTestApp, }); + ui.mount(); expect(document.body.children).toContain(ui.wrapper); @@ -267,6 +281,7 @@ describe('Content Script UIs', () => { onMount: appendTestApp, anchor: '#parent', }); + ui.mount(); expect(document.querySelector('#parent')!.children).toContain( @@ -285,6 +300,7 @@ describe('Content Script UIs', () => { onMount: appendTestApp, anchor: '//p[@id="three"]', }); + ui.mount(); expect(document.querySelector('#three')!.children[0]).toBe(ui.wrapper); @@ -298,6 +314,7 @@ describe('Content Script UIs', () => { onMount: appendTestApp, anchor: document.getElementById('parent'), }); + ui.mount(); expect(document.querySelector('#parent')!.children).toContain( @@ -313,6 +330,7 @@ describe('Content Script UIs', () => { onMount: appendTestApp, anchor: () => document.getElementById('parent'), }); + ui.mount(); expect(document.querySelector('#parent')!.children).toContain( @@ -341,6 +359,7 @@ describe('Content Script UIs', () => { append, onMount: appendTestApp, }); + ui.mount(); expect( @@ -357,6 +376,7 @@ describe('Content Script UIs', () => { append: 'first', onMount: appendTestApp, }); + ui.mount(); expect( @@ -373,6 +393,7 @@ describe('Content Script UIs', () => { append: 'replace', onMount: appendTestApp, }); + ui.mount(); expect(document.body.children).toContain(ui.wrapper); @@ -388,6 +409,7 @@ describe('Content Script UIs', () => { append: 'before', onMount: appendTestApp, }); + ui.mount(); expect(document.querySelector('#parent')!.firstElementChild).toBe( @@ -404,6 +426,7 @@ describe('Content Script UIs', () => { append: 'after', onMount: appendTestApp, }); + ui.mount(); expect(document.querySelector('#parent')!.lastElementChild).toBe( @@ -422,10 +445,10 @@ describe('Content Script UIs', () => { }, onMount: appendTestApp, }); + ui.mount(); expect(document.body.children).toContain(ui.wrapper); - expect(document.querySelector('#parent')).toBeNull(); }); }); @@ -440,6 +463,7 @@ describe('Content Script UIs', () => { position: 'inline', onMount: () => expected, }); + expect(ui.mounted).toBeUndefined(); ui.mount(); @@ -459,6 +483,7 @@ describe('Content Script UIs', () => { position: 'inline', onMount: () => expected, }); + expect(ui.mounted).toBeUndefined(); ui.mount(); @@ -478,6 +503,7 @@ describe('Content Script UIs', () => { position: 'inline', onMount: () => expected, }); + expect(ui.mounted).toBeUndefined(); ui.mount(); @@ -495,6 +521,7 @@ describe('Content Script UIs', () => { describe('auto mount', () => { const DYNAMIC_CHILD_ID = 'dynamic-child'; let ui: ContentScriptUi; + beforeEach(async () => { ui?.remove(); await runMicrotasks(); @@ -551,6 +578,7 @@ describe('Content Script UIs', () => { it('should mount when an anchor is dynamically added and unmount when an anchor is removed', async () => { const onRemove = vi.fn(); + ui = await createUiFunction(ctx, { position: 'inline', onMount, @@ -559,6 +587,7 @@ describe('Content Script UIs', () => { page: name === 'iframe' ? '/page.html' : undefined, name: 'test-component', }); + let dynamicEl; ui.autoMount(); await runMicrotasks(); @@ -585,6 +614,7 @@ describe('Content Script UIs', () => { describe('options', () => { it('should auto-mount only once mount and remove when the `once` option is true', async () => { const onRemove = vi.fn(); + ui = await createUiFunction(ctx, { position: 'inline', onMount, @@ -596,11 +626,13 @@ describe('Content Script UIs', () => { let dynamicEl; ui.autoMount({ once: true }); await runMicrotasks(); + await expect .poll(() => document.querySelector(uiSelector)) .toBeNull(); dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID }); + await runMicrotasks(); await expect .poll(() => document.querySelector(uiSelector)) @@ -608,17 +640,17 @@ describe('Content Script UIs', () => { dynamicEl.remove(); await runMicrotasks(); + expect(onMount).toHaveBeenCalledTimes(1); expect(onRemove).toHaveBeenCalledTimes(1); - // re-append after once cycle - dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID }); await runMicrotasks(); // expect stop automount await expect .poll(() => document.querySelector(uiSelector)) .toBeNull(); + expect(onMount).toHaveBeenCalledTimes(1); expect(onRemove).toHaveBeenCalledTimes(1); }); @@ -633,6 +665,7 @@ describe('Content Script UIs', () => { page: name === 'iframe' ? '/page.html' : undefined, name: 'test-component', }); + expect(() => ui.autoMount()).toThrowError( 'autoMount and Element anchor option cannot be combined. Avoid passing `Element` directly or `() => Element` to the anchor.', ); @@ -646,6 +679,7 @@ describe('Content Script UIs', () => { page: name === 'iframe' ? '/page.html' : undefined, name: 'test-component', }); + expect(() => ui.autoMount()).toThrowError( 'autoMount and Element anchor option cannot be combined. Avoid passing `Element` directly or `() => Element` to the anchor.', ); @@ -655,6 +689,7 @@ describe('Content Script UIs', () => { describe('StopAutoMount', () => { it('should stop auto-mounting and remove ui when `ui.remove` is called', async () => { const onRemove = vi.fn(); + ui = await createUiFunction(ctx, { position: 'inline', onMount, @@ -663,18 +698,21 @@ describe('Content Script UIs', () => { page: name === 'iframe' ? '/page.html' : undefined, name: 'test-component', }); + let dynamicEl; ui.autoMount(); await runMicrotasks(); dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID }); await runMicrotasks(); + await expect .poll(() => document.querySelector(uiSelector)) .not.toBeNull(); dynamicEl.remove(); await runMicrotasks(); + expect(onMount).toHaveBeenCalledTimes(1); expect(onRemove).toHaveBeenCalledTimes(1); @@ -683,6 +721,7 @@ describe('Content Script UIs', () => { dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID }); dynamicEl.remove(); await runMicrotasks(); + expect(onMount).toHaveBeenCalledTimes(1); expect(onRemove).toHaveBeenCalledTimes(2); }); @@ -690,6 +729,7 @@ describe('Content Script UIs', () => { it('should call internal StopAutoMount when `ui.remove` is called', async () => { const onRemove = vi.fn(); const onStop = vi.fn(); + ui = await createUiFunction(ctx, { position: 'inline', onMount, @@ -698,8 +738,10 @@ describe('Content Script UIs', () => { page: name === 'iframe' ? '/page.html' : undefined, name: 'test-component', }); + ui.autoMount({ onStop }); ui.remove(); + expect(onStop).toHaveBeenCalledTimes(1); expect(onRemove).toHaveBeenCalledTimes(1); }); @@ -712,7 +754,9 @@ describe('Content Script UIs', () => { page: name === 'iframe' ? '/page.html' : undefined, name: 'test-component', }); + const onStop = vi.fn(); + ui.autoMount({ onStop }); ui.autoMount({ onStop }); @@ -720,7 +764,6 @@ describe('Content Script UIs', () => { expect(onStop).toBeCalledTimes(1); ui.autoMount({ onStop }); - ui.remove(); expect(onStop).toBeCalledTimes(2); }); diff --git a/packages/wxt/src/utils/content-script-ui/iframe.ts b/packages/wxt/src/utils/content-script-ui/iframe.ts index aa968f9bd..109c55bc7 100644 --- a/packages/wxt/src/utils/content-script-ui/iframe.ts +++ b/packages/wxt/src/utils/content-script-ui/iframe.ts @@ -19,13 +19,14 @@ export function createIframeUi( iframe.src = browser.runtime.getURL(options.page); wrapper.appendChild(iframe); - let mounted: TMounted | undefined = undefined; + let mounted: TMounted | undefined; const mount = () => { applyPosition(wrapper, iframe, options); options.onBeforeMount?.(wrapper, iframe); mountUi(wrapper, options); mounted = options.onMount?.(wrapper, iframe); }; + const remove = () => { options.onRemove?.(mounted); wrapper.remove(); diff --git a/packages/wxt/src/utils/content-script-ui/integrated.ts b/packages/wxt/src/utils/content-script-ui/integrated.ts index 37689ec67..f81e5f29c 100644 --- a/packages/wxt/src/utils/content-script-ui/integrated.ts +++ b/packages/wxt/src/utils/content-script-ui/integrated.ts @@ -12,14 +12,15 @@ export function createIntegratedUi( ctx: ContentScriptContext, options: IntegratedContentScriptUiOptions, ): IntegratedContentScriptUi { + let mounted: TMounted | undefined; const wrapper = document.createElement(options.tag || 'div'); - let mounted: TMounted | undefined = undefined; const mount = () => { applyPosition(wrapper, undefined, options); mountUi(wrapper, options); mounted = options.onMount?.(wrapper); }; + const remove = () => { options.onRemove?.(mounted); wrapper.replaceChildren(); diff --git a/packages/wxt/src/utils/content-script-ui/shadow-root.ts b/packages/wxt/src/utils/content-script-ui/shadow-root.ts index 8d1c37d86..047fbacc4 100644 --- a/packages/wxt/src/utils/content-script-ui/shadow-root.ts +++ b/packages/wxt/src/utils/content-script-ui/shadow-root.ts @@ -24,9 +24,11 @@ export async function createShadowRootUi( if (!options.inheritStyles) { css.push(`/* WXT Shadow Root Reset */ :host{all:initial !important;}`); } + if (options.css) { css.push(options.css); } + if (ctx.options?.cssInjectionMode === 'ui') { const entryCss = await loadCss(); // Replace :root selectors with :host since we're in a shadow root @@ -124,7 +126,7 @@ async function loadCss(): Promise { .getURL(`/content-scripts/${import.meta.env.ENTRYPOINT}.css`); try { const res = await fetch(url); - return await res.text(); + return res.text(); } catch (err) { logger.warn( `Failed to load styles @ ${url}. Did you forget to import the stylesheet in your entrypoint?`, diff --git a/packages/wxt/src/utils/content-script-ui/shared.ts b/packages/wxt/src/utils/content-script-ui/shared.ts index c23b52eb9..6f38476c3 100644 --- a/packages/wxt/src/utils/content-script-ui/shared.ts +++ b/packages/wxt/src/utils/content-script-ui/shared.ts @@ -12,7 +12,7 @@ import { isExist as mountDetector, isNotExist as removeDetector, } from '@1natsu/wait-element/detectors'; -import { logger } from '../../utils/internal/logger'; +import { logger } from '../internal/logger'; export function applyPosition( root: HTMLElement, @@ -33,13 +33,18 @@ export function applyPosition( if (positionedElement) { if (options.position === 'overlay') { positionedElement.style.position = 'absolute'; - if (options.alignment?.startsWith('bottom-')) + + if (options.alignment?.startsWith('bottom-')) { positionedElement.style.bottom = '0'; - else positionedElement.style.top = '0'; + } else { + positionedElement.style.top = '0'; + } - if (options.alignment?.endsWith('-right')) + if (options.alignment?.endsWith('-right')) { positionedElement.style.right = '0'; - else positionedElement.style.left = '0'; + } else { + positionedElement.style.left = '0'; + } } else { positionedElement.style.position = 'fixed'; positionedElement.style.top = '0'; @@ -84,6 +89,7 @@ export function mountUi( options: ContentScriptAnchoredOptions, ): void { const anchor = getAnchor(options); + if (anchor == null) throw Error( 'Failed to mount content script UI: could not find anchor element', @@ -108,7 +114,6 @@ export function mountUi( break; default: options.append(anchor, root); - break; } } @@ -116,7 +121,7 @@ export function createMountFunctions( baseFunctions: BaseMountFunctions, options: ContentScriptUiOptions, ): MountFunctions { - let autoMountInstance: AutoMount | undefined = undefined; + let autoMountInstance: AutoMount | undefined; const stopAutoMount = () => { autoMountInstance?.stopAutoMount(); @@ -171,6 +176,7 @@ function autoMountUi( let resolvedAnchor = typeof options.anchor === 'function' ? options.anchor() : options.anchor; + if (resolvedAnchor instanceof Element) { throw Error( 'autoMount and Element anchor option cannot be combined. Avoid passing `Element` directly or `() => Element` to the anchor.', @@ -193,6 +199,7 @@ function autoMountUi( signal: abortController.signal, }); isAnchorExist = !!changedAnchor; + if (isAnchorExist) { uiCallbacks.mount(); } else { @@ -213,6 +220,7 @@ function autoMountUi( } } } + observeElement(resolvedAnchor); return { stopAutoMount: _stopAutoMount }; diff --git a/packages/wxt/src/utils/define-background.ts b/packages/wxt/src/utils/define-background.ts index 18a8c92ee..f1d6a7225 100644 --- a/packages/wxt/src/utils/define-background.ts +++ b/packages/wxt/src/utils/define-background.ts @@ -2,9 +2,11 @@ import type { BackgroundDefinition } from '../types'; export function defineBackground(main: () => void): BackgroundDefinition; + export function defineBackground( definition: BackgroundDefinition, ): BackgroundDefinition; + export function defineBackground( arg: (() => void) | BackgroundDefinition, ): BackgroundDefinition { diff --git a/packages/wxt/src/utils/define-unlisted-script.ts b/packages/wxt/src/utils/define-unlisted-script.ts index 6ee1dbba4..e3e43ca38 100644 --- a/packages/wxt/src/utils/define-unlisted-script.ts +++ b/packages/wxt/src/utils/define-unlisted-script.ts @@ -4,9 +4,11 @@ import type { UnlistedScriptDefinition } from '../types'; export function defineUnlistedScript( main: () => void, ): UnlistedScriptDefinition; + export function defineUnlistedScript( definition: UnlistedScriptDefinition, ): UnlistedScriptDefinition; + export function defineUnlistedScript( arg: (() => void) | UnlistedScriptDefinition, ): UnlistedScriptDefinition { diff --git a/packages/wxt/src/utils/internal/custom-events.ts b/packages/wxt/src/utils/internal/custom-events.ts index 08ca87a26..3240fefe9 100644 --- a/packages/wxt/src/utils/internal/custom-events.ts +++ b/packages/wxt/src/utils/internal/custom-events.ts @@ -1,13 +1,13 @@ import { browser } from 'wxt/browser'; export class WxtLocationChangeEvent extends Event { - static EVENT_NAME = getUniqueEventName('wxt:locationchange'); + static eventName = getUniqueEventName('wxt:locationchange'); constructor( readonly newUrl: URL, readonly oldUrl: URL, ) { - super(WxtLocationChangeEvent.EVENT_NAME, {}); + super(WxtLocationChangeEvent.eventName, {}); } } diff --git a/packages/wxt/src/utils/internal/dev-server-websocket.ts b/packages/wxt/src/utils/internal/dev-server-websocket.ts index 83a159805..1f63f6fd4 100644 --- a/packages/wxt/src/utils/internal/dev-server-websocket.ts +++ b/packages/wxt/src/utils/internal/dev-server-websocket.ts @@ -29,9 +29,6 @@ let ws: WxtWebSocket | undefined; /** * Connect to the websocket and listen for messages. - * - * @param onMessage Optional callback that is called when a message is recieved and we've verified - * it's structure is what we expect. */ export function getDevServerWebSocket(): WxtWebSocket { if (import.meta.env.COMMAND !== 'serve') @@ -61,6 +58,7 @@ export function getDevServerWebSocket(): WxtWebSocket { ws.addEventListener('message', (e) => { try { const message = JSON.parse(e.data) as WebSocketMessage; + if (message.type === 'custom') { ws?.dispatchEvent( new CustomEvent(message.event, { detail: message.data }), diff --git a/packages/wxt/src/version.ts b/packages/wxt/src/version.ts index 526225a09..d456ac9cb 100644 --- a/packages/wxt/src/version.ts +++ b/packages/wxt/src/version.ts @@ -1 +1 @@ -export const version = '{{version}}'; +export const VERSION = '{{version}}'; diff --git a/packages/wxt/src/virtual/background-entrypoint.ts b/packages/wxt/src/virtual/background-entrypoint.ts index 44c4be0ad..75d8f637c 100644 --- a/packages/wxt/src/virtual/background-entrypoint.ts +++ b/packages/wxt/src/virtual/background-entrypoint.ts @@ -9,6 +9,7 @@ import { reloadContentScript } from './utils/reload-content-scripts'; if (import.meta.env.COMMAND === 'serve') { try { const ws = getDevServerWebSocket(); + ws.addWxtEventListener('wxt:reload-extension', () => { browser.runtime.reload(); }); @@ -41,7 +42,7 @@ let result; try { initPlugins(); result = definition.main(); - // @ts-expect-error: res shouldn't be a promise, but we're checking it anyways + // @ts-expect-error: Res shouldn't be a promise, but we're checking it anyway if (result instanceof Promise) { console.warn( "The background's main() function return a promise, but it must be synchronous", diff --git a/packages/wxt/src/virtual/content-script-main-world-entrypoint.ts b/packages/wxt/src/virtual/content-script-main-world-entrypoint.ts index 1241bad64..f52d3821d 100644 --- a/packages/wxt/src/virtual/content-script-main-world-entrypoint.ts +++ b/packages/wxt/src/virtual/content-script-main-world-entrypoint.ts @@ -5,7 +5,7 @@ import { initPlugins } from 'virtual:wxt-plugins'; const result = (async () => { try { initPlugins(); - return await definition.main(); + return definition.main(); } catch (err) { logger.error( `The content script "${import.meta.env.ENTRYPOINT}" crashed on startup!`, diff --git a/packages/wxt/src/virtual/reload-html.ts b/packages/wxt/src/virtual/reload-html.ts index 3f3742bb2..eb4c36c1b 100644 --- a/packages/wxt/src/virtual/reload-html.ts +++ b/packages/wxt/src/virtual/reload-html.ts @@ -4,8 +4,8 @@ import { getDevServerWebSocket } from '../utils/internal/dev-server-websocket'; if (import.meta.env.COMMAND === 'serve') { try { const ws = getDevServerWebSocket(); + ws.addWxtEventListener('wxt:reload-page', (event) => { - // "popup.html" === "/popup.html".substring(1) if (event.detail === location.pathname.substring(1)) location.reload(); }); } catch (err) { diff --git a/packages/wxt/src/virtual/utils/reload-content-scripts.ts b/packages/wxt/src/virtual/utils/reload-content-scripts.ts index 5d3b4363e..eb331d1f7 100644 --- a/packages/wxt/src/virtual/utils/reload-content-scripts.ts +++ b/packages/wxt/src/virtual/utils/reload-content-scripts.ts @@ -5,6 +5,7 @@ import type { ReloadContentScriptPayload } from '../../utils/internal/dev-server export function reloadContentScript(payload: ReloadContentScriptPayload) { const manifest = browser.runtime.getManifest(); + if (manifest.manifest_version == 2) { void reloadContentScriptMv2(payload); } else { @@ -29,14 +30,18 @@ export async function reloadManifestContentScriptMv3( contentScript: ContentScript, ) { const id = `wxt:${contentScript.js![0]}`; + logger.log('Reloading content script:', contentScript); + const registered = await browser.scripting.getRegisteredContentScripts(); + logger.debug('Existing scripts:', registered); const existing = registered.find((cs) => cs.id === id); if (existing) { logger.debug('Updating content script', existing); + await browser.scripting.updateContentScripts([ { ...contentScript, @@ -46,6 +51,7 @@ export async function reloadManifestContentScriptMv3( ]); } else { logger.debug('Registering new content script...'); + await browser.scripting.registerContentScripts([ { ...contentScript, @@ -62,7 +68,9 @@ export async function reloadRuntimeContentScriptMv3( contentScript: ContentScript, ) { logger.log('Reloading content script:', contentScript); + const registered = await browser.scripting.getRegisteredContentScripts(); + logger.debug('Existing scripts:', registered); const matches = registered.filter((cs) => { @@ -90,7 +98,9 @@ async function reloadTabsForContentScript(contentScript: ContentScript) { ); const matchingTabs = allTabs.filter((tab) => { const url = tab.url; + if (!url) return false; + return !!matchPatterns.find((pattern) => pattern.includes(url)); }); await Promise.all( diff --git a/packages/wxt/vitest.globalSetup.ts b/packages/wxt/vitest.globalSetup.ts index 3847f26ad..3da3b5ff5 100644 --- a/packages/wxt/vitest.globalSetup.ts +++ b/packages/wxt/vitest.globalSetup.ts @@ -1,4 +1,4 @@ -import { exists, rm } from 'fs-extra'; +import { pathExists, rm } from 'fs-extra'; let setupHappened = false; @@ -12,8 +12,9 @@ export async function setup() { // @ts-expect-error globalThis.__ENTRYPOINT__ = 'test'; - const e2eDistPath = './e2e/dist/'; - if (await exists(e2eDistPath)) { - await rm(e2eDistPath, { recursive: true, force: true }); + const E2E_DIST_PATH = './e2e/dist/'; + + if (await pathExists(E2E_DIST_PATH)) { + await rm(E2E_DIST_PATH, { recursive: true, force: true }); } } From a79bdb3c78f0ca144c8b39812109c3c168c77d85 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sat, 27 Dec 2025 20:41:53 +0100 Subject: [PATCH 37/64] clean(packages/wxt-demo): make code more readable and make number and string const UPPER_CASE, like real const and remove unnecessary staff --- packages/wxt-demo/modules/example.ts | 13 +------------ .../src/entrypoints/__tests__/background.test.ts | 6 +++--- .../src/entrypoints/automount.content/index.ts | 9 +++++++++ packages/wxt-demo/src/entrypoints/background.ts | 3 --- .../wxt-demo/src/entrypoints/ui.content/index.ts | 2 ++ packages/wxt-demo/wxt.config.ts | 1 - packages/wxt/src/types.ts | 2 +- 7 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/wxt-demo/modules/example.ts b/packages/wxt-demo/modules/example.ts index 8ac1cf51e..ff05310d4 100644 --- a/packages/wxt-demo/modules/example.ts +++ b/packages/wxt-demo/modules/example.ts @@ -1,17 +1,6 @@ import { defineWxtModule } from 'wxt/modules'; -// Example of adding option types to wxt config file -export interface ExampleModuleOptions { - a: string; - b?: string; -} -declare module 'wxt' { - interface InlineConfig { - example?: ExampleModuleOptions; - } -} - -export default defineWxtModule({ +export default defineWxtModule({ configKey: 'example', setup(wxt, options) { wxt.logger.info('Example module with options:', options); diff --git a/packages/wxt-demo/src/entrypoints/__tests__/background.test.ts b/packages/wxt-demo/src/entrypoints/__tests__/background.test.ts index c46f84d2b..5d575523a 100644 --- a/packages/wxt-demo/src/entrypoints/__tests__/background.test.ts +++ b/packages/wxt-demo/src/entrypoints/__tests__/background.test.ts @@ -12,12 +12,12 @@ describe('Background Entrypoint', () => { }); it("should log the extension's runtime ID", () => { - const id = 'some-id'; - fakeBrowser.runtime.id = id; + const ID = 'some-id'; + fakeBrowser.runtime.id = ID; background.main(); - expect(logMock).toBeCalledWith(id); + expect(logMock).toBeCalledWith(ID); }); it('should set the start time in storage', async () => { diff --git a/packages/wxt-demo/src/entrypoints/automount.content/index.ts b/packages/wxt-demo/src/entrypoints/automount.content/index.ts index 7cac00330..5713cbd79 100644 --- a/packages/wxt-demo/src/entrypoints/automount.content/index.ts +++ b/packages/wxt-demo/src/entrypoints/automount.content/index.ts @@ -13,9 +13,12 @@ export default defineContentScript({ onMount: (container) => { const app = document.createElement('div'); container.id = 'automount-anchor'; + app.classList.add('m-4', 'text-center', 'text-red-500'); app.textContent = i18n.t('prompt_for_name'); + container.append(app); + return { container, app }; }, onRemove() { @@ -30,8 +33,10 @@ export default defineContentScript({ onMount: (container) => { const app = document.createElement('div'); app.id = 'automount-ui'; + app.classList.add('m-0', 'text-center', 'text-blue-500'); app.textContent = `Hello, I'm automount UI.`; + container.append(app); }, onRemove() { @@ -51,14 +56,18 @@ export default defineContentScript({ anchor: 'form[role=search]', onMount: (container) => { const app = document.createElement('button'); + container.classList.add('flex', 'flex-justify-center'); app.classList.add('mt-4', 'p-2'); app.textContent = 'Stop auto-mount'; + app.onclick = (e) => { e.preventDefault(); autoMountUi.remove(); }; + container.append(app); + return { container, app }; }, }); diff --git a/packages/wxt-demo/src/entrypoints/background.ts b/packages/wxt-demo/src/entrypoints/background.ts index 096261015..4aaeef7c9 100644 --- a/packages/wxt-demo/src/entrypoints/background.ts +++ b/packages/wxt-demo/src/entrypoints/background.ts @@ -19,12 +19,9 @@ export default defineBackground({ browser.runtime.getURL('/icons/128.png'); browser.runtime.getURL('/example.html#hash'); browser.runtime.getURL('/example.html?query=param'); - // @ts-expect-error: should only accept entrypoints or public assets browser.runtime.getURL('/unknown'); - // @ts-expect-error: should only allow hashes/query params on HTML files browser.runtime.getURL('/icon-128.png?query=param'); - // @ts-expect-error: should only accept known message names i18n.t('test'); i18n.t('prompt_for_name'); i18n.t('hello', ['test']); diff --git a/packages/wxt-demo/src/entrypoints/ui.content/index.ts b/packages/wxt-demo/src/entrypoints/ui.content/index.ts index 1104aed38..85ab7a9cb 100644 --- a/packages/wxt-demo/src/entrypoints/ui.content/index.ts +++ b/packages/wxt-demo/src/entrypoints/ui.content/index.ts @@ -18,8 +18,10 @@ export default defineContentScript({ anchor: 'form[role=search]', onMount: (container) => { const app = document.createElement('div'); + app.classList.add('m-4', 'text-red-500'); app.textContent = i18n.t('prompt_for_name'); + container.append(app); }, }); diff --git a/packages/wxt-demo/wxt.config.ts b/packages/wxt-demo/wxt.config.ts index e68b93b46..4eb2113c1 100644 --- a/packages/wxt-demo/wxt.config.ts +++ b/packages/wxt-demo/wxt.config.ts @@ -25,7 +25,6 @@ export default defineConfig({ }, example: { a: 'a', - // @ts-expect-error: c is not defined, this should be a type error, but it should show up in the module c: 'c', }, unocss: { diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index d09f72b9c..df4d042d0 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -377,7 +377,7 @@ export interface InlineConfig { /** * Field only for testing purposes, don't use it for other cases */ - example?: { key: string }; + example?: Record; } // TODO: Extract to @wxt/vite-builder and use module augmentation to include the vite field From 832b2864728cbc84cffdbf743af42dadcd7a05dd Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sat, 27 Dec 2025 20:42:16 +0100 Subject: [PATCH 38/64] clean(packages/storage): add question about remove deprecated --- packages/storage/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts index 056167fde..abbecdaa3 100644 --- a/packages/storage/src/index.ts +++ b/packages/storage/src/index.ts @@ -849,6 +849,7 @@ export interface WxtStorageItem< * The storage key passed when creating the storage item. */ key: StorageItemKey; + // TODO: MAYBE REMOVE IT BEFORE 1.0.0 RELEASE? /** * @deprecated Renamed to fallback, use it instead. */ From 715d876d13094856572437ee7b98c4f20457685b Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sat, 27 Dec 2025 20:43:00 +0100 Subject: [PATCH 39/64] clean(packages/i18n): make number and string const UPPER_CASE --- packages/i18n/src/__tests__/types.test.ts | 32 +++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/i18n/src/__tests__/types.test.ts b/packages/i18n/src/__tests__/types.test.ts index 160eac4d2..c34b33484 100644 --- a/packages/i18n/src/__tests__/types.test.ts +++ b/packages/i18n/src/__tests__/types.test.ts @@ -15,7 +15,7 @@ vi.mock('@wxt-dev/browser', async () => { }); const getMessageMock = vi.mocked(browser.i18n.getMessage); -const n = 1; +const N = 1; describe('I18n Types', () => { beforeEach(() => { @@ -30,7 +30,7 @@ describe('I18n Types', () => { i18n.t('any'); i18n.t('any', ['one']); i18n.t('any', ['one', 'two']); - i18n.t('any', n, ['one', 'two']); + i18n.t('any', N, ['one', 'two']); }); }); }); @@ -50,40 +50,40 @@ describe('I18n Types', () => { i18n.t('simple'); i18n.t('simple', []); i18n.t('simple', ['one']); - i18n.t('simple', n); + i18n.t('simple', N); i18n.t('simpleSub1', ['one']); i18n.t('simpleSub1'); i18n.t('simpleSub1', []); i18n.t('simpleSub1', ['one', 'two']); - i18n.t('simpleSub1', n); + i18n.t('simpleSub1', N); i18n.t('simpleSub2', ['one', 'two']); i18n.t('simpleSub2'); i18n.t('simpleSub2', ['one']); i18n.t('simpleSub2', ['one', 'two', 'three']); - i18n.t('simpleSub2', n); + i18n.t('simpleSub2', N); - i18n.t('plural', n); + i18n.t('plural', N); i18n.t('plural'); i18n.t('plural', []); i18n.t('plural', ['one']); - i18n.t('plural', n, ['sub']); + i18n.t('plural', N, ['sub']); - i18n.t('pluralSub1', n); - i18n.t('pluralSub1', n, undefined); - i18n.t('pluralSub1', n, ['one']); + i18n.t('pluralSub1', N); + i18n.t('pluralSub1', N, undefined); + i18n.t('pluralSub1', N, ['one']); i18n.t('pluralSub1'); i18n.t('pluralSub1', ['one']); - i18n.t('pluralSub1', n, []); - i18n.t('pluralSub1', n, ['one', 'two']); + i18n.t('pluralSub1', N, []); + i18n.t('pluralSub1', N, ['one', 'two']); - i18n.t('pluralSub2', n, ['one', 'two']); + i18n.t('pluralSub2', N, ['one', 'two']); i18n.t('pluralSub2'); i18n.t('pluralSub2', ['one', 'two']); - i18n.t('pluralSub2', n, ['one']); - i18n.t('pluralSub2', n, ['one', 'two', 'three']); - i18n.t('pluralSub2', n); + i18n.t('pluralSub2', N, ['one']); + i18n.t('pluralSub2', N, ['one', 'two', 'three']); + i18n.t('pluralSub2', N); }); }); }); From 2ced9ec1b6806a9163634bb3ee323b11a3381b4f Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sat, 27 Dec 2025 22:16:37 +0100 Subject: [PATCH 40/64] fix(packages/wxt): import of a to A, after UPPER_CASE conversion --- .../wxt/src/core/builders/vite/__tests__/fixtures/module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wxt/src/core/builders/vite/__tests__/fixtures/module.ts b/packages/wxt/src/core/builders/vite/__tests__/fixtures/module.ts index 1ca73a9d8..e4f456e2e 100644 --- a/packages/wxt/src/core/builders/vite/__tests__/fixtures/module.ts +++ b/packages/wxt/src/core/builders/vite/__tests__/fixtures/module.ts @@ -1,4 +1,4 @@ -import { a } from './test'; +import { A } from './test'; function defineSomething(config: T): T { return config; @@ -7,6 +7,6 @@ function defineSomething(config: T): T { export default defineSomething({ option: 'some value', main: () => { - console.log('main', a); + console.log('main', A); }, }); From 9581ffede369418e687691a56f0fa05d63bef0ab Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sat, 27 Dec 2025 22:20:39 +0100 Subject: [PATCH 41/64] fix(packages/wxt): add missing `null` to BaseAnalyticsEvent, `properties` key --- packages/analytics/modules/analytics/providers/umami.ts | 2 +- packages/analytics/modules/analytics/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/analytics/modules/analytics/providers/umami.ts b/packages/analytics/modules/analytics/providers/umami.ts index 86b7e3f09..89f5fa888 100644 --- a/packages/analytics/modules/analytics/providers/umami.ts +++ b/packages/analytics/modules/analytics/providers/umami.ts @@ -67,5 +67,5 @@ interface UmamiPayload { url?: string; website: string; name: string; - data?: Record; + data?: Record; } diff --git a/packages/analytics/modules/analytics/types.ts b/packages/analytics/modules/analytics/types.ts index c6f0c7a22..312f9601c 100644 --- a/packages/analytics/modules/analytics/types.ts +++ b/packages/analytics/modules/analytics/types.ts @@ -60,7 +60,7 @@ export interface BaseAnalyticsEvent { meta: AnalyticsEventMetadata; user: { id: string; - properties: Record; + properties: Record; }; } From 8ee11288b2d1984fca07b75bf3885d93e1a041b7 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sat, 27 Dec 2025 22:27:54 +0100 Subject: [PATCH 42/64] fix(packages/wxt-demo): change deprecated function `presetUno` to `presetWind3` --- packages/wxt-demo/wxt.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wxt-demo/wxt.config.ts b/packages/wxt-demo/wxt.config.ts index 4eb2113c1..83503833a 100644 --- a/packages/wxt-demo/wxt.config.ts +++ b/packages/wxt-demo/wxt.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from 'wxt'; -import { presetUno } from 'unocss'; +import { presetWind3 } from 'unocss'; export default defineConfig({ srcDir: 'src', @@ -52,7 +52,7 @@ export default defineConfig({ ], }, }, - presets: [presetUno()], + presets: [presetWind3()], }, }, }); From 7aad34404b3f013f10d90d5dc808f9ea61e512c5 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sun, 28 Dec 2025 12:26:47 +0100 Subject: [PATCH 43/64] clean(packages/analytics): improve code readability --- packages/analytics/entrypoints/popup/index.html | 2 +- packages/analytics/modules/analytics/client.ts | 11 ++++++++--- packages/analytics/modules/analytics/index.ts | 3 ++- .../modules/analytics/providers/google-analytics-4.ts | 1 + 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/analytics/entrypoints/popup/index.html b/packages/analytics/entrypoints/popup/index.html index 6725f56e6..3c701dd96 100644 --- a/packages/analytics/entrypoints/popup/index.html +++ b/packages/analytics/entrypoints/popup/index.html @@ -10,7 +10,7 @@  Analytics enabled - + diff --git a/packages/analytics/modules/analytics/client.ts b/packages/analytics/modules/analytics/client.ts index 09be16027..496daeca2 100644 --- a/packages/analytics/modules/analytics/client.ts +++ b/packages/analytics/modules/analytics/client.ts @@ -23,6 +23,7 @@ const INTERACTIVE_TAGS = new Set([ 'SELECT', 'TEXTAREA', ]); + const INTERACTIVE_ROLES = new Set([ 'button', 'link', @@ -61,14 +62,12 @@ function createBackgroundAnalytics( // User properties storage const userIdStorage = config?.userId ?? defineStorageItem('wxt-analytics:user-id'); - const userPropertiesStorage = config?.userProperties ?? defineStorageItem>( 'wxt-analytics:user-properties', {}, ); - const enabled = config?.enabled ?? defineStorageItem('local:wxt-analytics:enabled', false); @@ -81,6 +80,7 @@ function createBackgroundAnalytics( (id) => id ?? globalThis.crypto.randomUUID(), ); let userProperties = userPropertiesStorage.getValue(); + const manifest = browser.runtime.getManifest(); const getBackgroundMeta = () => ({ @@ -99,6 +99,7 @@ function createBackgroundAnalytics( meta: AnalyticsEventMetadata, ): Promise => { const { arch, os } = await platformInfo; + return { meta, user: { @@ -249,12 +250,14 @@ function createFrontendAnalytics(): Analytics { autoTrack: (root) => { const onClick = (event: Event) => { const element = event.target as HTMLElement | null; + if ( !element || (!INTERACTIVE_TAGS.has(element.tagName) && !INTERACTIVE_ROLES.has(element.getAttribute('role') ?? '')) - ) + ) { return; + } void analytics.track('click', { tagName: element.tagName?.toLowerCase(), @@ -265,11 +268,13 @@ function createFrontendAnalytics(): Analytics { }); }; root.addEventListener('click', onClick, { capture: true, passive: true }); + return () => { root.removeEventListener('click', onClick); }; }, }; + return analytics; } diff --git a/packages/analytics/modules/analytics/index.ts b/packages/analytics/modules/analytics/index.ts index 3ebcf1e81..116dcf95a 100644 --- a/packages/analytics/modules/analytics/index.ts +++ b/packages/analytics/modules/analytics/index.ts @@ -22,10 +22,10 @@ export default defineWxtModule({ // Paths const wxtAnalyticsFolder = resolve(wxt.config.wxtDir, 'analytics'); const wxtAnalyticsIndex = resolve(wxtAnalyticsFolder, 'index.ts'); + const clientModuleId = process.env.NPM ? '@wxt-dev/analytics' : resolve(wxt.config.modulesDir, 'analytics/client'); - const pluginModuleId = process.env.NPM ? '@wxt-dev/analytics/background-plugin' : resolve(wxt.config.modulesDir, 'analytics/background-plugin'); @@ -48,6 +48,7 @@ export default defineWxtModule({ `import { useAppConfig } from '#imports';\n`, `export const analytics = createAnalytics(useAppConfig().analytics);\n`, ].join('\n'); + addAlias(wxt, '#analytics', wxtAnalyticsIndex); wxt.hook('prepare:types', async (_, entries) => { entries.push({ diff --git a/packages/analytics/modules/analytics/providers/google-analytics-4.ts b/packages/analytics/modules/analytics/providers/google-analytics-4.ts index 76e6c0895..61fe66ff3 100644 --- a/packages/analytics/modules/analytics/providers/google-analytics-4.ts +++ b/packages/analytics/modules/analytics/providers/google-analytics-4.ts @@ -32,6 +32,7 @@ export const googleAnalytics4 = screen: data.meta.screen, ...data.user.properties, }; + const mappedUserProperties = Object.fromEntries( Object.entries(userProperties).map(([name, value]) => [ name, From 87a3baeca819ec5bb25862c3d609d355c34301ac Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sun, 28 Dec 2025 12:31:49 +0100 Subject: [PATCH 44/64] clean(packages/analytics): improve code readability and simplify if statement and change deprecated `exists` to `pathExists` --- packages/auto-icons/src/index.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/auto-icons/src/index.ts b/packages/auto-icons/src/index.ts index bd01adcb5..3508249f7 100644 --- a/packages/auto-icons/src/index.ts +++ b/packages/auto-icons/src/index.ts @@ -3,7 +3,7 @@ import { defineWxtModule } from 'wxt/modules'; import { relative, resolve } from 'node:path'; import defu from 'defu'; import sharp from 'sharp'; -import { ensureDir, exists } from 'fs-extra'; +import { ensureDir, pathExists } from 'fs-extra'; export default defineWxtModule({ name: '@wxt-dev/auto-icons', @@ -37,22 +37,22 @@ export default defineWxtModule({ const resolvedPath = resolve(wxt.config.srcDir, parsedOptions.baseIconPath); - if (!parsedOptions.enabled) + if (!parsedOptions.enabled) { return wxt.logger.warn(`\`[auto-icons]\` ${this.name} disabled`); + } - // TODO: STH DOESN'T GOOD WITH FS-EXTRA, BECAUSE IT DOESN'T RECOGNIZE TYPES PROPERLY, - // TODO: SIMILAR ISSUE LIKE IN #2015 PR - if (!(await (exists as (path: string) => Promise)(resolvedPath))) { + if (!(await pathExists(resolvedPath))) { return wxt.logger.warn( `\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`, ); } wxt.hooks.hook('build:manifestGenerated', async (wxt, manifest) => { - if (manifest.icons) + if (manifest.icons) { return wxt.logger.warn( '`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons', ); + } manifest.icons = Object.fromEntries( parsedOptions.sizes.map((size) => [size, `icons/${size}.png`]), @@ -70,8 +70,9 @@ export default defineWxtModule({ resizedImage.grayscale(); } else if (parsedOptions.developmentIndicator === 'overlay') { // Helper to build an overlay that places a yellow rectangle at the bottom - // of the icon with the text "DEV" in black. The overlay has the same - // dimensions as the icon so we can composite it with default gravity. + // of the icon with the text "DEV" in black. + // The overlay has the same dimensions as the icon, + // so we can composite it with default gravity. const buildDevOverlay = (size: number) => { const rectHeight = Math.round(size * 0.5); const fontSize = Math.round(size * 0.35); @@ -82,6 +83,7 @@ export default defineWxtModule({ DEV `); }; + const overlayBuffer = await sharp(buildDevOverlay(size)) .png() .toBuffer(); From b9fb718c7b8316c82c13ea89666c485ad68a7c1e Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sun, 28 Dec 2025 13:10:11 +0100 Subject: [PATCH 45/64] clean(packages/i18n): improve code readability --- packages/i18n/src/__tests__/build.test.ts | 1 + packages/i18n/src/module.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/i18n/src/__tests__/build.test.ts b/packages/i18n/src/__tests__/build.test.ts index ef05be0c4..13ff659bf 100644 --- a/packages/i18n/src/__tests__/build.test.ts +++ b/packages/i18n/src/__tests__/build.test.ts @@ -56,6 +56,7 @@ describe('Built Tools', () => { const messages = await parseMessagesFile(`file.yml`); await generateChromeMessagesFile('output.json', messages); await generateTypeFile('output.d.ts', messages); + const actualChromeMessagesFile = mockWriteFile.mock.calls[0][1]; const actualDtsFile = mockWriteFile.mock.calls[1][1]; diff --git a/packages/i18n/src/module.ts b/packages/i18n/src/module.ts index 624722ba3..bc09549d7 100644 --- a/packages/i18n/src/module.ts +++ b/packages/i18n/src/module.ts @@ -36,6 +36,7 @@ export default defineWxtModule({ ); return; } + wxt.logger.info( '`[i18n]` Default locale: ' + wxt.config.manifest.default_locale, ); @@ -54,7 +55,9 @@ export default defineWxtModule({ const res = files.map((file) => { const rawLocale = basename(file).replace(extname(file), ''); const locale = standardizeLocale(rawLocale); + if (!SUPPORTED_LOCALES.has(locale)) unsupportedLocales.push(locale); + return { file, locale }; }); @@ -71,7 +74,7 @@ export default defineWxtModule({ > => { const files = await getLocalizationFiles(); - return await Promise.all( + return Promise.all( files.map(async ({ file, locale }) => { const messages = await parseMessagesFile(file); return { From 9960479144a1c8304586d52468250d5217250e99 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sun, 28 Dec 2025 13:12:17 +0100 Subject: [PATCH 46/64] clean(packages/analytics): improve code readability --- packages/analytics/modules/analytics/client.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/analytics/modules/analytics/client.ts b/packages/analytics/modules/analytics/client.ts index 496daeca2..f08676aed 100644 --- a/packages/analytics/modules/analytics/client.ts +++ b/packages/analytics/modules/analytics/client.ts @@ -226,6 +226,7 @@ function createBackgroundAnalytics( function createFrontendAnalytics(): Analytics { const port = browser.runtime.connect({ name: ANALYTICS_PORT }); const sessionId = Date.now(); + const getFrontendMetadata = (): AnalyticsEventMetadata => ({ sessionId, timestamp: Date.now(), From c665a488deff03789b857e4be6dc1acedd22fe4d Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sun, 28 Dec 2025 13:13:51 +0100 Subject: [PATCH 47/64] clean(packages/module-react): improve code readability --- packages/module-react/components/App.tsx | 2 ++ packages/module-react/entrypoints/content/index.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/module-react/components/App.tsx b/packages/module-react/components/App.tsx index 2124b6126..d39faafe2 100644 --- a/packages/module-react/components/App.tsx +++ b/packages/module-react/components/App.tsx @@ -1,5 +1,7 @@ export default function () { const [count, setCount] = useState(0); + const increment = () => setCount((count) => count + 1); + return ; } diff --git a/packages/module-react/entrypoints/content/index.tsx b/packages/module-react/entrypoints/content/index.tsx index 87ac4e917..cb36ed3d9 100644 --- a/packages/module-react/entrypoints/content/index.tsx +++ b/packages/module-react/entrypoints/content/index.tsx @@ -22,6 +22,7 @@ function createUi(ctx: ContentScriptContext) { append: 'first', onMount(container) { const root = ReactDOM.createRoot(container); + root.render( From 7a132380a0f8d3cf867ea78e13f46b4d6ef0e4f6 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sun, 28 Dec 2025 13:14:49 +0100 Subject: [PATCH 48/64] clean(packages/module-solid): improve code readability --- packages/module-solid/components/App.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/module-solid/components/App.tsx b/packages/module-solid/components/App.tsx index c7c50c6ce..87361faa8 100644 --- a/packages/module-solid/components/App.tsx +++ b/packages/module-solid/components/App.tsx @@ -2,6 +2,8 @@ import { Component } from 'solid-js'; export const App: Component = () => { const [count, setCount] = createSignal(0); + const increment = () => setCount((count) => count + 1); + return ; }; From 69de8847f583730c1aeecdfeb33cdb3f71ee85b9 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sun, 28 Dec 2025 13:30:32 +0100 Subject: [PATCH 49/64] clean(packages/runner): improve code readability, make strings UPPER_CASE like real const and remove unnecessary `exists` func and use `pathExists` instead --- packages/runner/src/__tests__/install.test.ts | 42 +++++++++++-------- packages/runner/src/__tests__/options.test.ts | 22 ++++++++++ packages/runner/src/bidi.ts | 10 ++++- packages/runner/src/cdp.ts | 1 + packages/runner/src/debug.ts | 1 + packages/runner/src/install.ts | 2 +- packages/runner/src/options.ts | 24 +++++------ packages/runner/src/run.ts | 3 ++ packages/runner/src/web-socket.ts | 3 ++ 9 files changed, 74 insertions(+), 34 deletions(-) diff --git a/packages/runner/src/__tests__/install.test.ts b/packages/runner/src/__tests__/install.test.ts index 4ac12d9ba..95626303b 100644 --- a/packages/runner/src/__tests__/install.test.ts +++ b/packages/runner/src/__tests__/install.test.ts @@ -14,54 +14,62 @@ const createBidiConnectionMock = vi.mocked(createBidiConnection); describe('Install', () => { describe('Chromium', () => { it('Should send the install command to the process', async () => { + const EXTENSION_DIR = '/path/to/extension'; + const EXPECTED_EXTENSION_ID = 'chromium-extension-id'; + const browserProcess = mock(); const connection = mock({ [Symbol.dispose]: vi.fn(), }); - const extensionDir = '/path/to/extension'; - const expectedExtensionId = 'chromium-extension-id'; createCdpConnectionMock.mockReturnValue(connection); connection.send.mockImplementation(async (method) => { - if (method === 'Extensions.loadUnpacked') - return { id: expectedExtensionId }; + if (method === 'Extensions.loadUnpacked') { + return { id: EXPECTED_EXTENSION_ID }; + } + throw Error('Unknown method'); }); - const res = await installChromium(browserProcess, extensionDir); + const res = await installChromium(browserProcess, EXTENSION_DIR); expect(createCdpConnectionMock).toBeCalledTimes(1); expect(createCdpConnectionMock).toBeCalledWith(browserProcess); expect(connection.send).toBeCalledTimes(1); expect(connection.send).toBeCalledWith('Extensions.loadUnpacked', { - path: extensionDir, + path: EXTENSION_DIR, }); - expect(res).toEqual({ id: expectedExtensionId }); + expect(res).toEqual({ id: EXPECTED_EXTENSION_ID }); }); }); describe('Firefox', () => { it('Should connect to the server, start a session, then install the extension', async () => { - const debuggerUrl = 'http://127.0.0.1:9222'; - const extensionDir = '/path/to/extension'; - const expectedExtensionId = 'firefox-extension-id'; + const DEBUGGER_URL = 'http://127.0.0.1:9222'; + const EXTENSION_DIR = '/path/to/extension'; + const EXPECTED_EXTENSION_ID = 'firefox-extension-id'; + const connection = mock({ [Symbol.dispose]: vi.fn(), }); createBidiConnectionMock.mockResolvedValue(connection); connection.send.mockImplementation(async (method) => { - if (method === 'session.new') return { sessionId: 'session-id' }; - if (method === 'webExtension.install') - return { extension: expectedExtensionId }; + if (method === 'session.new') { + return { sessionId: 'session-id' }; + } + + if (method === 'webExtension.install') { + return { extension: EXPECTED_EXTENSION_ID }; + } }); - const res = await installFirefox(debuggerUrl, extensionDir); + const res = await installFirefox(DEBUGGER_URL, EXTENSION_DIR); expect(createBidiConnectionMock).toBeCalledTimes(1); - expect(createBidiConnectionMock).toBeCalledWith(debuggerUrl); + expect(createBidiConnectionMock).toBeCalledWith(DEBUGGER_URL); expect(connection.send).toBeCalledTimes(2); expect(connection.send).toBeCalledWith('session.new', { @@ -70,11 +78,11 @@ describe('Install', () => { expect(connection.send).toBeCalledWith('webExtension.install', { extensionData: { type: 'path', - path: extensionDir, + path: EXTENSION_DIR, }, }); - expect(res).toEqual({ extension: expectedExtensionId }); + expect(res).toEqual({ extension: EXPECTED_EXTENSION_ID }); }); }); }); diff --git a/packages/runner/src/__tests__/options.test.ts b/packages/runner/src/__tests__/options.test.ts index 8c4e87532..4d578a7e1 100644 --- a/packages/runner/src/__tests__/options.test.ts +++ b/packages/runner/src/__tests__/options.test.ts @@ -8,6 +8,7 @@ vi.mock('node:os', async () => { const { vi } = await import('vitest'); const os = (await vi.importActual('node:os')) as typeof import('node:os'); const { join } = await import('node:path'); + return { ...os, tmpdir: () => join(os.tmpdir(), 'tmpdir-mock'), @@ -24,6 +25,7 @@ describe('Options', () => { describe('extensionDir', () => { it('should default to the current working directory', async () => { const actual = await resolveRunOptions({}); + expect(actual).toMatchObject>({ extensionDir: process.cwd(), }); @@ -33,6 +35,7 @@ describe('Options', () => { const actual = await resolveRunOptions({ extensionDir: './path/to/extension', }); + expect(actual).toMatchObject>({ extensionDir: resolve(process.cwd(), './path/to/extension'), }); @@ -44,6 +47,7 @@ describe('Options', () => { const actual = await resolveRunOptions({ extensionDir: 'path/to/extension', }); + expect(actual).toMatchObject>({ target: 'chrome', }); @@ -57,6 +61,7 @@ describe('Options', () => { custom: '/path/to/custom/browser', }, }); + expect(actual).toMatchObject>({ target: 'custom', }); @@ -67,6 +72,7 @@ describe('Options', () => { extensionDir: 'path/to/extension', target: 'custom', }); + await expect(actual).rejects.toThrow('Could not find "custom" binary.'); }); }); @@ -81,6 +87,7 @@ describe('Options', () => { process.platform === 'win32' ? 'C:\\path\\to\\custom\\browser.exe' : path; + const actual = await resolveRunOptions({ extensionDir: 'path/to/extension', target: 'custom', @@ -88,6 +95,7 @@ describe('Options', () => { custom: path, }, }); + expect(actual).toMatchObject>({ browserBinary: expectedPath, }); @@ -100,6 +108,7 @@ describe('Options', () => { await resolveRunOptions({ chromiumArgs: ['--user-data-dir=some/custom/path'], }); + expect(warnSpy).toBeCalledTimes(1); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining( @@ -113,6 +122,7 @@ describe('Options', () => { await resolveRunOptions({ chromiumArgs: ['--remote-debugging-port=9222'], }); + expect(warnSpy).toBeCalledTimes(1); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining( @@ -125,6 +135,7 @@ describe('Options', () => { const actual = await resolveRunOptions({ chromiumArgs: ['--window-size=1920,1080'], }); + expect(actual.chromiumArgs).toEqual([ // Defaults '--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculateNativeWinOcclusion,InterestFeedContentSuggestions,CertificateTransparencyComponentUpdater,AutofillServerCommunication,PrivacySandboxSettings4', @@ -163,6 +174,7 @@ describe('Options', () => { await resolveRunOptions({ firefoxArgs: ['--profile=some/custom/path'], }); + expect(warnSpy).toBeCalledTimes(1); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining('Custom Firefox --profile argument ignored'), @@ -174,6 +186,7 @@ describe('Options', () => { await resolveRunOptions({ firefoxArgs: ['--remote-debugging-port=9222'], }); + expect(warnSpy).toBeCalledTimes(1); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining( @@ -186,6 +199,7 @@ describe('Options', () => { const actual = await resolveRunOptions({ firefoxArgs: ['--window-size=1920,1080'], }); + expect(actual.firefoxArgs).toEqual([ // Defaults '--new-instance', @@ -203,6 +217,7 @@ describe('Options', () => { describe('chromiumRemoteDebuggingPort', () => { it('should default to 0', async () => { const actual = await resolveRunOptions({}); + expect(actual).toMatchObject>({ chromiumRemoteDebuggingPort: 0, chromiumArgs: expect.arrayContaining([`--remote-debugging-port=0`]), @@ -213,6 +228,7 @@ describe('Options', () => { const actual = await resolveRunOptions({ chromiumRemoteDebuggingPort: 9222, }); + expect(actual).toMatchObject>({ chromiumRemoteDebuggingPort: 9222, chromiumArgs: expect.arrayContaining([`--remote-debugging-port=9222`]), @@ -223,6 +239,7 @@ describe('Options', () => { describe('firefoxRemoteDebuggingPort', () => { it('should default to 0', async () => { const actual = await resolveRunOptions({}); + expect(actual).toMatchObject>({ firefoxRemoteDebuggingPort: 0, firefoxArgs: expect.arrayContaining([`--remote-debugging-port=0`]), @@ -233,6 +250,7 @@ describe('Options', () => { const actual = await resolveRunOptions({ firefoxRemoteDebuggingPort: 9222, }); + expect(actual).toMatchObject>({ firefoxRemoteDebuggingPort: 9222, firefoxArgs: expect.arrayContaining([`--remote-debugging-port=9222`]), @@ -243,6 +261,7 @@ describe('Options', () => { describe('dataPersistence', () => { it('should default to "none"', async () => { const actual = await resolveRunOptions({}); + expect(actual).toMatchObject>({ dataPersistence: 'none', }); @@ -252,6 +271,7 @@ describe('Options', () => { const actual = await resolveRunOptions({ dataPersistence: 'none', }); + expect(actual).toMatchObject>({ dataPersistence: 'none', dataDir: expect.stringContaining(join(tmpdir(), 'wxt-runner-')), @@ -270,6 +290,7 @@ describe('Options', () => { const actual = await resolveRunOptions({ dataPersistence: 'project', }); + expect(actual).toMatchObject>({ dataPersistence: 'project', dataDir: expect.stringContaining(join(process.cwd(), '.wxt-runner')), @@ -288,6 +309,7 @@ describe('Options', () => { const actual = await resolveRunOptions({ dataPersistence: 'user', }); + expect(actual).toMatchObject>({ dataPersistence: 'user', dataDir: expect.stringContaining(join(homedir(), '.wxt-runner')), diff --git a/packages/runner/src/bidi.ts b/packages/runner/src/bidi.ts index f08f18774..dd433a8ed 100644 --- a/packages/runner/src/bidi.ts +++ b/packages/runner/src/bidi.ts @@ -47,17 +47,23 @@ export async function createBidiConnection( const onMessage = (event: MessageEvent) => { const data = JSON.parse(event.data); + if (data.id === id) { debugBidi('Received response:', data); clearTimeout(timeoutId); cleanup(); - if (data.type === 'success') resolve(data.result); - else reject(Error(data.message, { cause: data })); + + if (data.type === 'success') { + resolve(data.result); + } else { + reject(Error(data.message, { cause: data })); + } } }; const onError = (error: unknown) => { clearTimeout(timeoutId); cleanup(); + reject(new Error('Error sending request', { cause: error })); }; diff --git a/packages/runner/src/cdp.ts b/packages/runner/src/cdp.ts index ee2a074ae..252796259 100644 --- a/packages/runner/src/cdp.ts +++ b/packages/runner/src/cdp.ts @@ -41,6 +41,7 @@ export function createCdpConnection( if (res.id !== id) return; debugCdp('Received response:', res); + clearTimeout(timer); outputStream.removeListener('data', onData); diff --git a/packages/runner/src/debug.ts b/packages/runner/src/debug.ts index ee308572a..72de694a4 100644 --- a/packages/runner/src/debug.ts +++ b/packages/runner/src/debug.ts @@ -7,6 +7,7 @@ export interface Debug { function createDebug(scopes: string[]): Debug { const debug = (...args: unknown[]) => { const scope = scopes.join(':'); + if ( process.env.DEBUG === '1' || process.env.DEBUG === 'true' || diff --git a/packages/runner/src/install.ts b/packages/runner/src/install.ts index 7033f00f5..b29096fda 100644 --- a/packages/runner/src/install.ts +++ b/packages/runner/src/install.ts @@ -43,7 +43,7 @@ export async function installChromium( extensionDir: string, ): Promise { using cdp = createCdpConnection(browserProcess); - return await cdp.send( + return cdp.send( 'Extensions.loadUnpacked', { path: extensionDir, diff --git a/packages/runner/src/options.ts b/packages/runner/src/options.ts index 9aab2bf52..96c18820b 100644 --- a/packages/runner/src/options.ts +++ b/packages/runner/src/options.ts @@ -7,7 +7,8 @@ import { import { resolve, join } from 'node:path'; import { homedir, tmpdir } from 'node:os'; import { debug } from './debug'; -import { mkdtemp, open } from 'node:fs/promises'; +import { mkdtemp } from 'node:fs/promises'; +import { pathExists } from 'fs-extra'; const debugOptions = debug.scoped('options'); @@ -59,6 +60,7 @@ export async function resolveRunOptions( const _browserBinary = options?.browserBinaries?.[target] ?? (await findBrowserBinary(target)); + if (!_browserBinary) throw Error( `Could not find "${target}" binary.\n\nIf it is installed in a custom location, you can specify the path with the browserPaths option.`, @@ -101,20 +103,24 @@ export async function resolveRunOptions( target, }; debugOptions('Resolved options:', resolved); + return resolved; } async function findBrowserBinary(target: string): Promise { const targets = new Set([target as KnownTarget]); + FALLBACK_TARGETS[target as KnownTarget]?.forEach((fallback) => targets.add(fallback), ); + const platform = getPlatform(); for (const target of targets) { const potentialPaths = KNOWN_BROWSER_PATHS[target]?.[platform] ?? []; + for (const path of potentialPaths) { - if (await exists(path)) return path; + if (await pathExists(path)) return path; } } } @@ -192,10 +198,11 @@ function deduplicateArgs( return arg.startsWith('--') ? arg.split('=')[0] : arg; }; const alreadyAdded = new Set(requiredArgs.map(getKey)); - const args = [...requiredArgs]; + userArgs?.forEach((arg) => { const key = getKey(arg); + if (alreadyAdded.has(key)) { if (warnings[key]) console.warn(`[@wxt-dev/runner] ${warnings[key]}`); } else { @@ -207,17 +214,6 @@ function deduplicateArgs( return args; } -async function exists(path: string): Promise { - try { - await open(path, 'r'); - return true; - } catch (err) { - // @ts-expect-error: Unknown error type - if (err?.code === 'ENOENT') return false; - throw err; - } -} - /** * Copied from https://github.com/GoogleChrome/chrome-launcher/blob/main/src/flags.ts * with some flags commented out. Run tests after updating to compare. diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index 580ac149a..29e7ed969 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -44,6 +44,7 @@ async function runFirefox(options: ResolvedRunOptions): Promise { shell: true, }, ); + const debugFirefoxStderr = debugFirefox.scoped('stderr'); browserProcess.stderr.on('data', (data: string) => { const message = data.toString().trim(); @@ -54,6 +55,7 @@ async function runFirefox(options: ResolvedRunOptions): Promise { urlRes.resolve(message.slice(28)); } }); + const debugFirefoxStdout = debugFirefox.scoped('stdout'); browserProcess.stdout.on('data', (data: string) => { const message = data.toString().trim(); @@ -96,6 +98,7 @@ async function runChromium(options: ResolvedRunOptions): Promise { opened.resolve(); } }); + const debugChromeStdout = debugChrome.scoped('stdout'); browserProcess.stdout!.on('data', (data: string) => { const message = data.toString().trim(); diff --git a/packages/runner/src/web-socket.ts b/packages/runner/src/web-socket.ts index be758d2ad..f2443a64e 100644 --- a/packages/runner/src/web-socket.ts +++ b/packages/runner/src/web-socket.ts @@ -13,10 +13,12 @@ export function openWebSocket(url: string): Promise { webSocket.removeEventListener('error', onError); webSocket.removeEventListener('close', onClose); }; + const onOpen = async () => { cleanup(); resolve(webSocket); }; + const onClose = (event: CloseEvent) => { cleanup(); reject( @@ -25,6 +27,7 @@ export function openWebSocket(url: string): Promise { ), ); }; + const onError = (error: unknown) => { cleanup(); reject(new Error('Error connecting to WebSocket', { cause: error })); From 55360499b1fc75277259b3ff22bdf08e4956ea0e Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sun, 28 Dec 2025 14:48:08 +0100 Subject: [PATCH 50/64] clean(packages/storage): improve code readability, remove unnecessary `await` and add question --- packages/storage/src/__tests__/index.test.ts | 136 +++++++++++-------- packages/storage/src/index.ts | 90 +++++++++--- 2 files changed, 150 insertions(+), 76 deletions(-) diff --git a/packages/storage/src/__tests__/index.test.ts b/packages/storage/src/__tests__/index.test.ts index 27ac26d78..0a6d5ea61 100644 --- a/packages/storage/src/__tests__/index.test.ts +++ b/packages/storage/src/__tests__/index.test.ts @@ -31,6 +31,7 @@ describe('Storage Utils', () => { describe('getItem', () => { it('should return the value from the correct storage area', async () => { const EXPECTED = 123; + await fakeBrowser.storage[storageArea].set({ count: EXPECTED }); const actual = await storage.getItem(`${storageArea}:count`); @@ -55,6 +56,7 @@ describe('Storage Utils', () => { it('should return the default value if passed in options', async () => { const EXPECTED = 0; + const actual = await storage.getItem(`${storageArea}:count`, { defaultValue: EXPECTED, }); @@ -88,11 +90,12 @@ describe('Storage Utils', () => { }); it('should get values from multiple storage items', async () => { - const item1 = storage.defineItem(`${storageArea}:one`); const EXPECTED_VALUE_1 = 1; - const item2 = storage.defineItem(`${storageArea}:two`); const EXPECTED_VALUE_2 = null; + const item1 = storage.defineItem(`${storageArea}:one`); + const item2 = storage.defineItem(`${storageArea}:two`); + await fakeBrowser.storage[storageArea].set({ one: EXPECTED_VALUE_1, }); @@ -106,11 +109,12 @@ describe('Storage Utils', () => { }); it('should get values for a combination of different input types', async () => { - const key1 = `${storageArea}:one` as const; const EXPECTED_VALUE_1 = 1; - const item2 = storage.defineItem(`${storageArea}:two`); const EXPECTED_VALUE_2 = 2; + const key1 = `${storageArea}:one` as const; + const item2 = storage.defineItem(`${storageArea}:two`); + await fakeBrowser.storage[storageArea].set({ one: EXPECTED_VALUE_1, two: EXPECTED_VALUE_2, @@ -125,15 +129,15 @@ describe('Storage Utils', () => { }); it('should return fallback values for keys when provided', async () => { - const key1 = `${storageArea}:one` as const; const EXPECTED_VALUE_1 = null; + const key1 = `${storageArea}:one` as const; + const key2 = `${storageArea}:two` as const; - const FALLBACK_2 = 2; - const EXPECTED_VALUE_2 = FALLBACK_2; + const EXPECTED_VALUE_2 = 2; const actual = await storage.getItems([ key1, - { key: key2, options: { fallback: FALLBACK_2 } }, + { key: key2, options: { fallback: EXPECTED_VALUE_2 } }, ]); expect(actual).toEqual([ @@ -144,10 +148,11 @@ describe('Storage Utils', () => { it('should return fallback values for items when provided', async () => { const item1 = storage.defineItem(`${storageArea}:one`); - const EXPECTED_VALUE_1 = null; const item2 = storage.defineItem(`${storageArea}:two`, { fallback: 2, }); + + const EXPECTED_VALUE_1 = null; const EXPECTED_VALUE_2 = item2.fallback; const actual = await storage.getItems([item1, item2]); @@ -161,7 +166,7 @@ describe('Storage Utils', () => { describe('getMeta', () => { it('should return item metadata from key+$', async () => { - const expected = { v: 1 }; + const expected = { v: 1 } as const; await fakeBrowser.storage[storageArea].set({ count$: expected }); const actual = await storage.getMeta(`${storageArea}:count`); @@ -225,7 +230,7 @@ describe('Storage Utils', () => { describe('setMeta', () => { it('should set metadata at key+$', async () => { - const existing = { v: 1 }; + const existing = { v: 1 } as const; await browser.storage[storageArea].set({ count$: existing }); @@ -243,7 +248,7 @@ describe('Storage Utils', () => { it.each([undefined, null])( 'should remove any properties set to %s', async (version) => { - const existing = { v: 1 }; + const existing = { v: 1 } as const; await browser.storage[storageArea].set({ count$: existing }); @@ -261,17 +266,17 @@ describe('Storage Utils', () => { it('should set key metadata correctly', async () => { const key1 = `${storageArea}:one` as const; const initialMeta1 = {}; - const setMeta1 = { v: 1 }; + const setMeta1 = { v: 1 } as const; const expectedMeta1 = setMeta1; const key2 = `${storageArea}:two` as const; - const initialMeta2 = { v: 1 }; - const setMeta2 = { v: 2 }; + const initialMeta2 = { v: 1 } as const; + const setMeta2 = { v: 2 } as const; const expectedMeta2 = setMeta2; const key3 = `${storageArea}:three` as const; - const initialMeta3 = { v: 1 }; - const setMeta3 = { d: Date.now() }; + const initialMeta3 = { v: 1 } as const; + const setMeta3 = { d: Date.now() } as const; const expectedMeta3 = { ...initialMeta3, ...setMeta3 }; await fakeBrowser.storage[storageArea].set({ @@ -294,16 +299,16 @@ describe('Storage Utils', () => { it('should set item metadata correctly', async () => { const item1 = storage.defineItem(`${storageArea}:one`); const initialMeta1 = {}; - const setMeta1 = { v: 1 }; + const setMeta1 = { v: 1 } as const; const expectedMeta1 = setMeta1; const item2 = storage.defineItem(`${storageArea}:two`); - const initialMeta2 = { v: 1 }; - const setMeta2 = { v: 2 }; + const initialMeta2 = { v: 1 } as const; + const setMeta2 = { v: 2 } as const; const expectedMeta2 = setMeta2; const item3 = storage.defineItem(`${storageArea}:three`); - const initialMeta3 = { v: 1 }; + const initialMeta3 = { v: 1 } as const; const setMeta3 = { d: Date.now() }; const expectedMeta3 = { ...initialMeta3, ...setMeta3 }; @@ -336,7 +341,7 @@ describe('Storage Utils', () => { }); it('should not remove the metadata by default', async () => { - const expected = { v: 1 }; + const expected = { v: 1 } as const; await fakeBrowser.storage[storageArea].set({ count$: expected, @@ -405,6 +410,7 @@ describe('Storage Utils', () => { 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, @@ -626,20 +632,22 @@ describe('Storage Utils', () => { }); it("should not trigger if the value doesn't change", async () => { + const VALUE = '123'; + const cb = vi.fn(); - const value = '123'; - await storage.setItem(`${storageArea}:key`, value); + await storage.setItem(`${storageArea}:key`, VALUE); storage.watch(`${storageArea}:key`, cb); - await storage.setItem(`${storageArea}:key`, value); + await storage.setItem(`${storageArea}:key`, VALUE); expect(cb).not.toBeCalled(); }); it('should call the callback when the value changes', async () => { - const cb = vi.fn(); - const NEW_VALUE = '123'; const OLD_VALUE = null; + const NEW_VALUE = '123'; + + const cb = vi.fn(); storage.watch(`${storageArea}:key`, cb); await storage.setItem(`${storageArea}:key`, NEW_VALUE); @@ -676,6 +684,8 @@ describe('Storage Utils', () => { describe('Invalid storage areas', () => { it('should not accept keys without a valid storage area prefix', async () => { + // TODO: THIS HAVE SENSE? + // TODO: TS CHECKING IS ENOUGH TO PREVENT THIS CASE // @ts-expect-error await storage.getItem('test').catch(() => {}); // @ts-expect-error @@ -683,6 +693,7 @@ describe('Storage Utils', () => { }); it('should throw an error when using an invalid storage area', async () => { + // TODO: THE SAME QUESTION AS ABOVE // @ts-expect-error: Test passes if there is a type error here await expect(storage.getItem('invalidArea:key')).rejects.toThrow( 'Invalid area', @@ -709,6 +720,7 @@ describe('Storage Utils', () => { 3: migrateToV3, }, }); + await waitForMigrations(); const actualValue = await item.getValue(); @@ -743,6 +755,7 @@ describe('Storage Utils', () => { }, onMigrationComplete, }); + await waitForMigrations(); expect(onMigrationComplete).toBeCalledTimes(1); @@ -829,6 +842,7 @@ describe('Storage Utils', () => { count: 2, count$: { v: 0 }, }); + const migrateToV1 = vi.fn((oldCount) => oldCount * 1); const migrateToV3 = vi.fn((oldCount) => oldCount * 3); @@ -894,6 +908,7 @@ describe('Storage Utils', () => { }, }, }); + await fakeBrowser.storage.local.set({ key: 1, key$: { v: 1 } }); await expect(item.migrate()).rejects.toThrow(expectedError); @@ -975,6 +990,7 @@ describe('Storage Utils', () => { describe('getValue', () => { it('should return the value from storage', async () => { const EXPECTED = 2; + const item = storage.defineItem(`local:count`); await fakeBrowser.storage.local.set({ count: EXPECTED }); @@ -992,21 +1008,21 @@ describe('Storage Utils', () => { }); it('should return the provided default value if missing', async () => { - const EXPECTED0 = 0; + const EXPECTED = 0; const item = storage.defineItem(`local:count`, { - defaultValue: EXPECTED0, + defaultValue: EXPECTED, }); const actual = await item.getValue(); - expect(actual).toEqual(EXPECTED0); + expect(actual).toEqual(EXPECTED); }); }); describe('getMeta', () => { it('should return the value from storage at key+$', async () => { - const expected = { v: 2 }; + const expected = { v: 2 } as const; const item = storage.defineItem(`local:count`); await fakeBrowser.storage.local.set({ count$: expected }); @@ -1017,7 +1033,8 @@ describe('Storage Utils', () => { }); it('should return an empty object if missing', async () => { - const expected = {}; + const expected = {} as const; + const item = storage.defineItem(`local:count`); const actual = await item.getMeta(); @@ -1029,9 +1046,10 @@ describe('Storage Utils', () => { describe('setValue', () => { it('should set the value in storage', async () => { const EXPECTED = 1; - const item = storage.defineItem(`local:count`); + const item = storage.defineItem(`local:count`); await item.setValue(EXPECTED); + const actual = await item.getValue(); expect(actual).toBe(EXPECTED); @@ -1041,9 +1059,8 @@ describe('Storage Utils', () => { 'should remove the value in storage when %s is passed in', async (value) => { const item = storage.defineItem(`local:count`); + await item.setValue(value!); - // @ts-expect-error: undefined is not assignable to null, but we're testing that case on purpose - await item.setValue(value); const actual = await item.getValue(); expect(actual).toBeNull(); @@ -1053,21 +1070,22 @@ describe('Storage Utils', () => { describe('setMeta', () => { it('should set metadata at key+$', async () => { - const EXPECTED = { date: Date.now() }; + const expected = { date: Date.now() } as const; const item = storage.defineItem( `local:count`, ); - await item.setMeta(EXPECTED); + await item.setMeta(expected); const actual = await item.getMeta(); - expect(actual).toEqual(EXPECTED); + expect(actual).toEqual(expected); }); it('should add to metadata if already present', async () => { - const existing = { v: 2 }; - const newFields = { date: Date.now() }; - const expected = { ...existing, ...newFields }; + const existing = { v: 2 } as const; + const newFields = { date: Date.now() } as const; + const expected = { ...existing, ...newFields } as const; + const item = storage.defineItem( `local:count`, ); @@ -1254,28 +1272,25 @@ describe('Storage Utils', () => { }); }); - describe.each(['fallback', 'defaultValue'] as const)( - '%s option', - (fallbackKey) => { - it('should return the default value when provided', () => { - const FALLBACK = 123; - - const item = storage.defineItem(`local:test`, { - [fallbackKey]: FALLBACK, - }); + describe.each(['fallback', 'defaultValue'])('%s option', (fallbackKey) => { + it('should return the default value when provided', () => { + const FALLBACK = 123; - expect(item.fallback).toBe(FALLBACK); - expect(item.defaultValue).toBe(FALLBACK); + const item = storage.defineItem(`local:test`, { + [fallbackKey]: FALLBACK, }); - it('should return null when not provided', () => { - const item = storage.defineItem(`local:test`); + expect(item.fallback).toBe(FALLBACK); + expect(item.defaultValue).toBe(FALLBACK); + }); - expect(item.fallback).toBeNull(); - expect(item.defaultValue).toBeNull(); - }); - }, - ); + it('should return null when not provided', () => { + const item = storage.defineItem(`local:test`); + + expect(item.fallback).toBeNull(); + expect(item.defaultValue).toBeNull(); + }); + }); describe('init option', () => { it('should only call init once (per JS context) when calling getValue successively, avoiding race conditions', async () => { @@ -1464,6 +1479,7 @@ describe('Storage Utils', () => { expect(localGetSpy).toBeCalledTimes(1); expect(localGetSpy).toBeCalledWith(['item1$', 'item3$']); + expect(sessionGetSpy).toBeCalledTimes(1); expect(sessionGetSpy).toBeCalledWith(['item2$']); }); @@ -1541,6 +1557,7 @@ describe('Storage Utils', () => { expect(localGetSpy).toBeCalledTimes(1); expect(localGetSpy).toBeCalledWith(['one$', 'three$']); + expect(sessionGetSpy).toBeCalledTimes(1); expect(sessionGetSpy).toBeCalledWith(['two$']); @@ -1549,6 +1566,7 @@ describe('Storage Utils', () => { one$: { v: 1 }, three$: { v: 3 }, }); + expect(sessionSetSpy).toBeCalledTimes(1); expect(sessionSetSpy).toBeCalledWith({ two$: { v: 2 }, diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts index abbecdaa3..716199fca 100644 --- a/packages/storage/src/index.ts +++ b/packages/storage/src/index.ts @@ -17,6 +17,7 @@ function createStorage(): WxtStorage { sync: createDriver('sync'), managed: createDriver('managed'), }; + const getDriver = (area: StorageArea) => { const driver = drivers[area]; if (driver == null) { @@ -25,10 +26,12 @@ function createStorage(): WxtStorage { } return driver; }; + const resolveKey = (key: StorageItemKey) => { const deliminatorIndex = key.indexOf(':'); const driverArea = key.substring(0, deliminatorIndex) as StorageArea; const driverKey = key.substring(deliminatorIndex + 1); + if (driverKey == null) throw Error( `Storage key should be in the form of "area:key", but received "${key}"`, @@ -40,20 +43,24 @@ function createStorage(): WxtStorage { driver: getDriver(driverArea), }; }; + const getMetaKey = (key: string) => key + '$'; const mergeMeta = ( oldMeta: Record, newMeta: Record, ): Record => { const newFields = { ...oldMeta }; + Object.entries(newMeta).forEach(([key, value]) => { if (value == null) delete newFields[key]; else newFields[key] = value; }); + return newFields; }; const getValueOrFallback = (value: T | null | undefined, fallback: T) => value ?? fallback ?? null; + const getMetaValue = (properties: unknown): Record => typeof properties === 'object' && properties !== null && @@ -69,11 +76,13 @@ function createStorage(): WxtStorage { const res = await driver.getItem(driverKey); return getValueOrFallback(res, (opts?.fallback ?? opts?.defaultValue) as T); }; + const getMeta = async (driver: WxtStorageDriver, driverKey: string) => { const metaKey = getMetaKey(driverKey); const res = await driver.getItem>(metaKey); return getMetaValue(res); }; + const setItem = async ( driver: WxtStorageDriver, driverKey: string, @@ -81,6 +90,7 @@ function createStorage(): WxtStorage { ) => { await driver.setItem(driverKey, value ?? null); }; + const setMeta = async ( driver: WxtStorageDriver, driverKey: string, @@ -88,25 +98,30 @@ function createStorage(): WxtStorage { ) => { const metaKey = getMetaKey(driverKey); const existingFields = getMetaValue(await driver.getItem(metaKey)); + await driver.setItem(metaKey, mergeMeta(existingFields, properties ?? {})); }; + const removeItem = async ( driver: WxtStorageDriver, driverKey: string, opts: RemoveItemOptions | undefined, ) => { await driver.removeItem(driverKey); + if (opts?.removeMeta) { const metaKey = getMetaKey(driverKey); await driver.removeItem(metaKey); } }; + const removeMeta = async ( driver: WxtStorageDriver, driverKey: string, properties: string | string[] | undefined, ) => { const metaKey = getMetaKey(driverKey); + if (properties == null) { await driver.removeItem(metaKey); } else { @@ -115,6 +130,7 @@ function createStorage(): WxtStorage { await driver.setItem(metaKey, newFields); } }; + const watch = ( driver: WxtStorageDriver, driverKey: string, @@ -126,7 +142,7 @@ function createStorage(): WxtStorage { return { getItem: async (key, opts) => { const { driver, driverKey } = resolveKey(key); - return await getItem(driver, driverKey, opts); + return getItem(driver, driverKey, opts); }, getItems: async (keys) => { const areaToKeyMap = new Map(); @@ -139,6 +155,7 @@ function createStorage(): WxtStorage { keys.forEach((key) => { let keyStr: StorageItemKey; let opts: GetItemOptions | undefined; + if (typeof key === 'string') { // key: string keyStr = key; @@ -151,7 +168,9 @@ function createStorage(): WxtStorage { keyStr = key.key; opts = key.options; } + orderedKeys.push(keyStr); + const { driverArea, driverKey } = resolveKey(keyStr); const areaKeys = areaToKeyMap.get(driverArea) ?? []; areaToKeyMap.set(driverArea, areaKeys.concat(driverKey)); @@ -159,6 +178,7 @@ function createStorage(): WxtStorage { }); const resultsMap = new Map(); + await Promise.all( Array.from(areaToKeyMap.entries()).map(async ([driverArea, keys]) => { const driverResults = await drivers[driverArea].getItems(keys); @@ -187,6 +207,7 @@ function createStorage(): WxtStorage { const keys = args.map((arg) => { const key = typeof arg === 'string' ? arg : arg.key; const { driverArea, driverKey } = resolveKey(key); + return { key, driverArea, @@ -194,6 +215,7 @@ function createStorage(): WxtStorage { driverMetaKey: getMetaKey(driverKey), }; }); + const areaToDriverMetaKeysMap = keys.reduce< Partial> >((map, key) => { @@ -203,6 +225,7 @@ function createStorage(): WxtStorage { }, {}); const resultsMap: Record> = {}; + await Promise.all( Object.entries(areaToDriverMetaKeysMap).map(async ([area, keys]) => { const areaRes = await browser.storage[area as StorageArea].get( @@ -234,12 +257,14 @@ function createStorage(): WxtStorage { 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( Object.entries(areaToKeyValueMap).map(async ([driverArea, values]) => { const driver = getDriver(driverArea as StorageArea); @@ -262,6 +287,7 @@ function createStorage(): WxtStorage { const { driverArea, driverKey } = resolveKey( 'key' in item ? item.key : item.item.key, ); + areaToMetaUpdatesMap[driverArea] ??= []; areaToMetaUpdatesMap[driverArea]!.push({ key: driverKey, @@ -274,6 +300,7 @@ function createStorage(): WxtStorage { async ([storageArea, updates]) => { const driver = getDriver(storageArea as StorageArea); const metaKeys = updates.map(({ key }) => getMetaKey(key)); + const existingMetas = await driver.getItems(metaKeys); const existingMetaMap = Object.fromEntries( existingMetas.map(({ key, value }) => [key, getMetaValue(value)]), @@ -281,6 +308,7 @@ function createStorage(): WxtStorage { const metaUpdates = updates.map(({ key, properties }) => { const metaKey = getMetaKey(key); + return { key: metaKey, value: mergeMeta(existingMetaMap[metaKey] ?? {}, properties), @@ -302,6 +330,7 @@ function createStorage(): WxtStorage { keys.forEach((key) => { let keyStr: StorageItemKey; let opts: RemoveItemOptions | undefined; + if (typeof key === 'string') { // key: string keyStr = key; @@ -317,9 +346,12 @@ function createStorage(): WxtStorage { keyStr = key.key; opts = key.options; } + const { driverArea, driverKey } = resolveKey(keyStr); + areaToKeysMap[driverArea] ??= []; areaToKeysMap[driverArea].push(driverKey); + if (opts?.removeMeta) { areaToKeysMap[driverArea].push(getMetaKey(driverKey)); } @@ -343,10 +375,12 @@ function createStorage(): WxtStorage { snapshot: async (base, opts) => { const driver = getDriver(base); const data = await driver.snapshot(); + opts?.excludeKeys?.forEach((key) => { delete data[key]; delete data[getMetaKey(key)]; }); + return data; }, restoreSnapshot: async (base, data) => { @@ -362,10 +396,7 @@ function createStorage(): WxtStorage { driver.unwatch(); }); }, - defineItem: < - TValue, - TMetadata extends Record = Record, - >( + defineItem: ( key: StorageItemKey, opts?: WxtStorageItemOptions, ) => { @@ -377,6 +408,7 @@ function createStorage(): WxtStorage { onMigrationComplete, debug = false, } = opts ?? {}; + if (targetVersion < 1) { throw Error( 'Storage item version cannot be less than 1. Initial versions should be set to 1, not 0.', @@ -390,14 +422,17 @@ function createStorage(): WxtStorage { ]); const value = itemRes.value; const meta = getMetaValue(metaRes.value); + if (value == null) return; const currentVersion = (meta?.v as number | undefined) ?? 1; + if (currentVersion > targetVersion) { throw Error( `Version downgrade detected (v${currentVersion} -> v${targetVersion}) for "${key}"`, ); } + if (currentVersion === targetVersion) { return; } @@ -411,7 +446,9 @@ function createStorage(): WxtStorage { { length: targetVersion - currentVersion }, (_, i) => currentVersion + i + 1, ); + let migratedValue = value; + for (const migrateToVersion of migrationsToRun) { try { migratedValue = @@ -428,6 +465,7 @@ function createStorage(): WxtStorage { }); } } + await driver.setItems([ { key: driverKey, value: migratedValue }, { @@ -442,8 +480,10 @@ function createStorage(): WxtStorage { { migratedValue }, ); } + onMigrationComplete?.(migratedValue as TValue, targetVersion); }; + const migrationsDone = opts?.migrations == null ? Promise.resolve() @@ -466,6 +506,7 @@ function createStorage(): WxtStorage { const newValue = await opts.init(); await driver.setItem(driverKey, newValue); + return newValue; }); @@ -482,26 +523,26 @@ function createStorage(): WxtStorage { }, getValue: async () => { await migrationsDone; + if (opts?.init) { - return (await getOrInitValue()) as TValue; + return getOrInitValue() as TValue; } else { - return (await getItem(driver, driverKey, opts)) as TValue; + return getItem(driver, driverKey, opts) as TValue; } }, getMeta: async () => { await migrationsDone; - return (await getMeta( - driver, - driverKey, - )) as NullablePartial; + + return getMeta(driver, driverKey); }, setValue: async (value) => { await migrationsDone; - return await setItem(driver, driverKey, value); + return setItem(driver, driverKey, value); }, setMeta: async (properties) => { await migrationsDone; - return await setMeta( + + return setMeta( driver, driverKey, properties as Record, @@ -509,11 +550,13 @@ function createStorage(): WxtStorage { }, removeValue: async (opts) => { await migrationsDone; - return await removeItem(driver, driverKey, opts); + + return removeItem(driver, driverKey, opts); }, removeMeta: async (properties) => { await migrationsDone; - return await removeMeta(driver, driverKey, properties); + + return removeMeta(driver, driverKey, properties); }, watch: (cb: WatchCallback) => watch(driver, driverKey, (newValue, oldValue) => @@ -539,6 +582,7 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver { ].join('\n'), ); } + if (browser.storage == null) { throw Error( "You must add the 'storage' permission to your manifest to use 'wxt/storage'", @@ -546,18 +590,23 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver { } const area = browser.storage[storageArea]; - if (area == null) + + if (area == null) { throw Error(`"browser.storage.${storageArea}" is undefined`); + } + return area; }; const watchListeners = new Set<(changes: StorageAreaChanges) => void>(); return { getItem: async (key: string) => { const res = await getStorageArea().get>(key); + return (res[key] as T) ?? null; }, getItems: async (keys) => { const result = await getStorageArea().get(keys); + return keys.map((key) => ({ key, value: result[key] ?? null })); }, setItem: async (key, value) => { @@ -575,6 +624,7 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver { }, {}, ); + await getStorageArea().set(map); }, removeItem: async (key) => { @@ -587,7 +637,7 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver { await getStorageArea().clear(); }, snapshot: async () => { - return await getStorageArea().get(); + return getStorageArea().get(); }, restoreSnapshot: async (data) => { await getStorageArea().set(data); @@ -598,12 +648,17 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver { newValue?: T; oldValue?: T | null; } | null; + if (change == null) return; + if (dequal(change.newValue, change.oldValue)) return; + cb(change.newValue ?? null, change.oldValue ?? null); }; + getStorageArea().onChanged.addListener(listener); watchListeners.add(listener); + return () => { getStorageArea().onChanged.removeListener(listener); watchListeners.delete(listener); @@ -899,6 +954,7 @@ export type StorageArea = 'local' | 'session' | 'sync' | 'managed'; export type StorageItemKey = `${StorageArea}:${string}`; export interface GetItemOptions { + // TODO: MAYBE REMOVE IT BEFORE 1.0 RELEASE? /** * @deprecated Renamed to `fallback`, use it instead. */ From ee53d9ba1429e4c5fefba869c3a6ba3bcc0d4373 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sun, 28 Dec 2025 14:48:55 +0100 Subject: [PATCH 51/64] clean(packages/unocss): improve code readability --- packages/unocss/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/unocss/src/index.ts b/packages/unocss/src/index.ts index 5abb37da8..188bd21d0 100644 --- a/packages/unocss/src/index.ts +++ b/packages/unocss/src/index.ts @@ -16,10 +16,12 @@ export default defineWxtModule({ }, ); - if (!resolvedOptions.enabled) + if (!resolvedOptions.enabled) { return wxt.logger.warn(`\`[unocss]\` ${this.name} disabled`); + } const excludedEntrypoints = new Set(resolvedOptions.excludeEntrypoints); + if (wxt.config.debug) { wxt.logger.debug( `\`[unocss]\` Excluded entrypoints:`, From fcf2f362beacc6505fdc3530dc09913dba2b85fe Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sun, 28 Dec 2025 14:58:15 +0100 Subject: [PATCH 52/64] fix(packages/wxt): change @deprecated to @internal for resetBundleIncrement() --- packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts b/packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts index 266252e85..d88b53093 100644 --- a/packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts +++ b/packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts @@ -15,9 +15,8 @@ export function bundleAnalysis(config: ResolvedConfig): vite.Plugin { }) as vite.Plugin; } -// TODO: MAYBE REMOVE IT BEFORE 1.0.0? /** - * @deprecated FOR TESTING ONLY. + * @internal FOR TESTING ONLY. */ export function resetBundleIncrement() { increment = 0; From 5b36973f6a2eda00559827a60fad3272bad47729 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sun, 28 Dec 2025 15:48:12 +0100 Subject: [PATCH 53/64] fix(packages/wxt): remove unnecessary `// @ts-ignore` from auto-imports output --- packages/wxt/e2e/tests/auto-imports.test.ts | 9 --------- packages/wxt/src/builtin-modules/unimport.ts | 6 +++++- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/wxt/e2e/tests/auto-imports.test.ts b/packages/wxt/e2e/tests/auto-imports.test.ts index 6b20e2721..e3dec8d92 100644 --- a/packages/wxt/e2e/tests/auto-imports.test.ts +++ b/packages/wxt/e2e/tests/auto-imports.test.ts @@ -36,31 +36,22 @@ describe('Auto Imports', () => { } // for type re-export declare global { - // @ts-ignore export type { Browser } from 'wxt/browser' import('wxt/browser') - // @ts-ignore export type { StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage' import('wxt/utils/storage') - // @ts-ignore export type { WxtWindowEventMap } from 'wxt/utils/content-script-context' import('wxt/utils/content-script-context') - // @ts-ignore export type { IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe' import('wxt/utils/content-script-ui/iframe') - // @ts-ignore export type { IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated' import('wxt/utils/content-script-ui/integrated') - // @ts-ignore export type { ShadowRootContentScriptUi, ShadowRootContentScriptUiOptions } from 'wxt/utils/content-script-ui/shadow-root' import('wxt/utils/content-script-ui/shadow-root') - // @ts-ignore export type { ContentScriptUi, ContentScriptUiOptions, ContentScriptOverlayAlignment, ContentScriptAppendMode, ContentScriptInlinePositioningOptions, ContentScriptOverlayPositioningOptions, ContentScriptModalPositioningOptions, ContentScriptPositioningOptions, ContentScriptAnchoredOptions, AutoMountOptions, StopAutoMount, AutoMount } from 'wxt/utils/content-script-ui/types' import('wxt/utils/content-script-ui/types') - // @ts-ignore export type { WxtAppConfig } from 'wxt/utils/define-app-config' import('wxt/utils/define-app-config') - // @ts-ignore export type { ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script' import('wxt/utils/inject-script') } diff --git a/packages/wxt/src/builtin-modules/unimport.ts b/packages/wxt/src/builtin-modules/unimport.ts index c49bde02c..c22a1b800 100644 --- a/packages/wxt/src/builtin-modules/unimport.ts +++ b/packages/wxt/src/builtin-modules/unimport.ts @@ -75,7 +75,11 @@ async function getImportsDeclarationEntry( path: 'types/imports.d.ts', text: [ '// Generated by wxt', - await unimport.generateTypeDeclarations(), + (await unimport.generateTypeDeclarations()) + .replaceAll('// @ts-ignore', '') + .split('\n') + .filter((line) => line.trim() !== '') + .join('\n'), '', ].join('\n'), tsReference: true, From 7f6bb03bd571bb3dfbded2aa40454775d9a3a44a Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Sun, 28 Dec 2025 16:43:25 +0100 Subject: [PATCH 54/64] clean(packages/wxt/e2e): make code more readable and remove some unnecessary `await` --- packages/wxt/e2e/tests/modules.test.ts | 30 ++++++++++++------- packages/wxt/e2e/tests/npm-packages.test.ts | 14 +++++++-- .../wxt/e2e/tests/output-structure.test.ts | 7 ++--- packages/wxt/e2e/tests/zip.test.ts | 19 +++++++++--- packages/wxt/e2e/utils.ts | 3 +- 5 files changed, 49 insertions(+), 24 deletions(-) diff --git a/packages/wxt/e2e/tests/modules.test.ts b/packages/wxt/e2e/tests/modules.test.ts index 51e137cda..0aa821d53 100644 --- a/packages/wxt/e2e/tests/modules.test.ts +++ b/packages/wxt/e2e/tests/modules.test.ts @@ -7,7 +7,7 @@ import { normalizePath } from '../../src/core/utils/paths'; describe('Module Helpers', () => { describe('options', () => { it('should receive the options defined in wxt.config.ts based on the configKey field', async () => { - const options = { key: '123' }; + const options = { key: '123' } as const; const reportOptions = vi.fn(); vi.stubGlobal('reportOptions', reportOptions); const project = new TestProject(); @@ -56,6 +56,7 @@ describe('Module Helpers', () => { outputDir: project.resolvePath('.output/chrome-mv3'), skipped: false, }; + project.addFile( 'modules/test/injected.ts', `export default defineUnlistedScript(() => {})`, @@ -70,6 +71,7 @@ describe('Module Helpers', () => { }) `, ); + const config: InlineConfig = { browser: 'chrome', }; @@ -89,7 +91,6 @@ describe('Module Helpers', () => { 'entrypoints/background.ts', 'export default defineBackground(() => {})', ); - project.addFile('modules/test/public/module.txt'); const dir = project.resolvePath('modules/test/public'); project.addFile( @@ -130,7 +131,6 @@ describe('Module Helpers', () => { 'entrypoints/background.ts', 'export default defineBackground(() => {})', ); - project.addFile('public/user.txt', 'from-user'); project.addFile('modules/test/public/user.txt', 'from-module'); const dir = project.resolvePath('modules/test/public'); @@ -160,15 +160,17 @@ describe('Module Helpers', () => { describe('addWxtPlugin', () => { function addPluginModule(project: TestProject) { - const expectedText = 'Hello from plugin!'; + const EXPECTED_TEXT = 'Hello from plugin!'; + const pluginPath = project.addFile( 'modules/test/client-plugin.ts', ` export default defineWxtPlugin(() => { - console.log("${expectedText}") + console.log("${EXPECTED_TEXT}") }) `, ); + project.addFile( 'modules/test.ts', ` @@ -179,7 +181,8 @@ describe('Module Helpers', () => { }); `, ); - return expectedText; + + return EXPECTED_TEXT; } it('should include the plugin in the background', async () => { @@ -188,6 +191,7 @@ describe('Module Helpers', () => { 'entrypoints/background.ts', 'export default defineBackground(() => {})', ); + const expectedText = addPluginModule(project); await project.build(); @@ -205,6 +209,7 @@ describe('Module Helpers', () => { `, ); + const expectedText = addPluginModule(project); await project.build(); @@ -223,6 +228,7 @@ describe('Module Helpers', () => { }) `, ); + const expectedText = addPluginModule(project); await project.build(); @@ -232,10 +238,12 @@ describe('Module Helpers', () => { it('should include the plugin in unlisted scripts', async () => { const project = new TestProject(); + project.addFile( 'entrypoints/unlisted.ts', 'export default defineUnlistedScript(() => {})', ); + const expectedText = addPluginModule(project); await project.build(); @@ -246,7 +254,8 @@ describe('Module Helpers', () => { describe('imports', () => { it('should add auto-imports', async () => { - const expectedText = 'customImport!'; + const EXPECTED_TEXT = 'customImport!'; + const project = new TestProject(); project.addFile( 'entrypoints/background.ts', @@ -254,14 +263,12 @@ describe('Module Helpers', () => { customImport(); });`, ); - const utils = project.addFile( 'custom.ts', `export function customImport() { - console.log("${expectedText}") + console.log("${EXPECTED_TEXT}") }`, ); - project.addFile( 'modules/test.ts', `import { defineWxtModule } from 'wxt/modules'; @@ -275,11 +282,12 @@ describe('Module Helpers', () => { await project.build(); - await expect(project.serializeOutput()).resolves.toContain(expectedText); + await expect(project.serializeOutput()).resolves.toContain(EXPECTED_TEXT); }); it('should add preset', async () => { const project = new TestProject(); + project.addFile( 'entrypoints/background.ts', `export default defineBackground(() => { diff --git a/packages/wxt/e2e/tests/npm-packages.test.ts b/packages/wxt/e2e/tests/npm-packages.test.ts index f5d387d3f..02e5fc55c 100644 --- a/packages/wxt/e2e/tests/npm-packages.test.ts +++ b/packages/wxt/e2e/tests/npm-packages.test.ts @@ -17,8 +17,11 @@ test('Only one version of esbuild should be installed (each version is ~20mb of ]); const projects: NpmListProject[] = JSON.parse(stdout); const esbuildVersions = new Set(); + iterateDependencies(projects, (name, meta) => { - if (name === 'esbuild') esbuildVersions.add(meta.version); + if (name === 'esbuild') { + esbuildVersions.add(meta.version); + } }); expect([...esbuildVersions]).toHaveLength(1); @@ -31,10 +34,15 @@ function iterateDependencies( const recurse = (dependencies: Record) => { Object.entries(dependencies).forEach(([name, meta]) => { cb(name, meta); - if (meta.dependencies) recurse(meta.dependencies); + if (meta.dependencies) { + recurse(meta.dependencies); + } }); }; + projects.forEach((project) => { - if (project.dependencies) recurse(project.dependencies); + if (project.dependencies) { + recurse(project.dependencies); + } }); } diff --git a/packages/wxt/e2e/tests/output-structure.test.ts b/packages/wxt/e2e/tests/output-structure.test.ts index 94a76f401..9ddac9126 100644 --- a/packages/wxt/e2e/tests/output-structure.test.ts +++ b/packages/wxt/e2e/tests/output-structure.test.ts @@ -233,7 +233,6 @@ describe('Output Directory Structure', () => { color: #333; }`, ); - project.addFile( 'entrypoints/plain-two.content.css', `body { @@ -241,7 +240,6 @@ describe('Output Directory Structure', () => { color: #333; }`, ); - project.addFile( 'entrypoints/sass-one.scss', `$font-stack: Helvetica, sans-serif; @@ -252,7 +250,6 @@ describe('Output Directory Structure', () => { color: $primary-color; }`, ); - project.addFile( 'entrypoints/sass-two.content.scss', `$font-stack: Helvetica, sans-serif; @@ -338,7 +335,7 @@ describe('Output Directory Structure', () => { await project.build({ vite: () => ({ build: { - // Make output for snapshot readible + // Make output for the snapshot readable minify: false, }, }), @@ -411,7 +408,7 @@ describe('Output Directory Structure', () => { await project.build({ vite: () => ({ build: { - // Make output for snapshot readable + // Make output for the snapshot readable minify: false, }, }), diff --git a/packages/wxt/e2e/tests/zip.test.ts b/packages/wxt/e2e/tests/zip.test.ts index c370097bc..abc196f12 100644 --- a/packages/wxt/e2e/tests/zip.test.ts +++ b/packages/wxt/e2e/tests/zip.test.ts @@ -83,8 +83,8 @@ describe('Zipping', () => { 'entrypoints/background.ts', 'export default defineBackground(() => {});', ); - const artifactZip = '.output/test-1.0.0-firefox-development.zip'; - const sourcesZip = '.output/test-1.0.0-development-sources.zip'; + const ARTIFACT_ZIP_PATH = '.output/test-1.0.0-firefox-development.zip'; + const SOURCES_ZIP_PATH = '.output/test-1.0.0-development-sources.zip'; await project.zip({ browser: 'firefox', @@ -95,8 +95,8 @@ describe('Zipping', () => { }, }); - expect(await project.fileExists(artifactZip)).toBe(true); - expect(await project.fileExists(sourcesZip)).toBe(true); + expect(await project.fileExists(ARTIFACT_ZIP_PATH)).toBe(true); + expect(await project.fileExists(SOURCES_ZIP_PATH)).toBe(true); }); it('should not zip hidden files into sources by default', async () => { @@ -110,12 +110,14 @@ describe('Zipping', () => { ); project.addFile('.env'); project.addFile('.hidden-dir/file'); + const unzipDir = project.resolvePath('.output/test-1.0.0-sources'); const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip'); await project.zip({ browser: 'firefox', }); + await extract(sourcesZip, { dir: unzipDir }); expect(await project.fileExists(unzipDir, '.env')).toBe(false); expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(false); @@ -132,6 +134,7 @@ describe('Zipping', () => { ); project.addFile('.hidden-dir/file'); project.addFile('.hidden-dir/nested/file'); + const unzipDir = project.resolvePath('.output/test-1.0.0-sources'); const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip'); @@ -141,6 +144,7 @@ describe('Zipping', () => { includeSources: ['.hidden-dir'], }, }); + await extract(sourcesZip, { dir: unzipDir }); expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(false); expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file')).toBe( @@ -161,6 +165,7 @@ describe('Zipping', () => { project.addFile('.hidden-dir/file'); project.addFile('.hidden-dir/nested/file1'); project.addFile('.hidden-dir/nested/file2'); + const unzipDir = project.resolvePath('.output/test-1.0.0-sources'); const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip'); @@ -170,6 +175,7 @@ describe('Zipping', () => { includeSources: ['.env', '.hidden-dir/file', '.hidden-dir/nested/**'], }, }); + await extract(sourcesZip, { dir: unzipDir }); expect(await project.fileExists(unzipDir, '.env')).toBe(true); expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(true); @@ -202,12 +208,14 @@ describe('Zipping', () => { }); `, ); + const unzipDir = project.resolvePath('.output/test-1.0.0-sources'); const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip'); await project.zip({ browser: 'firefox', }); + await extract(sourcesZip, { dir: unzipDir }); expect( await project.fileExists(unzipDir, 'entrypoints/not-firefox.content.ts'), @@ -228,6 +236,7 @@ describe('Zipping', () => { 'entrypoints/background.ts', 'export default defineBackground(() => {});', ); + const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip'); await project.zip({ @@ -249,6 +258,7 @@ describe('Zipping', () => { 'entrypoints/background.ts', 'export default defineBackground(() => {});', ); + // TODO: TELL ME, IF YOU WANT TO DO CONST LIKE THIS UPPER_CASE, BECAUSE IT ISN'T REAL CONST, BUT LOOK LIKE CONST const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip'); await project.zip({ @@ -273,6 +283,7 @@ describe('Zipping', () => { 'entrypoints/background.ts', 'export default defineBackground(() => {});', ); + const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip'); await project.zip({ diff --git a/packages/wxt/e2e/utils.ts b/packages/wxt/e2e/utils.ts index ff34c9a3e..4c9e9d4fd 100644 --- a/packages/wxt/e2e/utils.ts +++ b/packages/wxt/e2e/utils.ts @@ -122,6 +122,7 @@ export class TestProject { await spawn('pnpm', ['--ignore-workspace', 'i', '--ignore-scripts'], { cwd: this.root, }); + await mkdir(resolve(this.root, 'public'), { recursive: true }).catch( () => {}, ); @@ -184,6 +185,6 @@ export class TestProject { async getOutputManifest( path: string = '.output/chrome-mv3/manifest.json', ): Promise { - return await fs.readJson(this.resolvePath(path)); + return fs.readJson(this.resolvePath(path)); } } From 00f06cac6de030197f2f0a9069c871c0b58a7223 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Mon, 29 Dec 2025 19:19:16 +0100 Subject: [PATCH 55/64] clean(packages/wxt/@types): make code more readable and fix const name --- packages/wxt/src/@types/modules.d.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/wxt/src/@types/modules.d.ts b/packages/wxt/src/@types/modules.d.ts index 99fbc79af..1bcfaac8e 100644 --- a/packages/wxt/src/@types/modules.d.ts +++ b/packages/wxt/src/@types/modules.d.ts @@ -1,20 +1,20 @@ // Custom TS definitions for non-TS packages declare module 'zip-dir' { - // Represents the options object for zipdir function + // Represents the options object for zipDir function interface ZipDirOptions { saveTo?: string; filter?: (path: string, stat: import('fs').Stats) => boolean; each?: (path: string) => void; } - function zipdir( + function zipDir( dirPath: string, options?: ZipDirOptions, callback?: (error: Error | null, buffer: Buffer) => void, ): Promise; - export = zipdir; + export = zipDir; } declare module 'web-ext-run' { @@ -39,13 +39,16 @@ declare module 'web-ext-run/util/logger' { write(packet: Packet, options: unknown): void; flushCapturedLogs(options: unknown): void; } + export interface Packet { name: string; msg: string; level: number; } + export class ConsoleStream implements IConsoleStream { constructor(options?: { verbose: false }); } + export const consoleStream: IConsoleStream; } From 1c94c5e3d0a8f195ca57b1eba67564966bbd1da3 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Mon, 29 Dec 2025 19:20:45 +0100 Subject: [PATCH 56/64] clean(packages/wxt/__tests__): make code more readable and add as const to objects --- packages/wxt/src/__tests__/modules.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/wxt/src/__tests__/modules.test.ts b/packages/wxt/src/__tests__/modules.test.ts index a9e62090c..cf07b9dc3 100644 --- a/packages/wxt/src/__tests__/modules.test.ts +++ b/packages/wxt/src/__tests__/modules.test.ts @@ -9,9 +9,10 @@ describe('Module Utilities', () => { const wxt = fakeWxt({ hooks: createHooks(), }); - const expected = { build: { sourcemap: true } }; - const userConfig = {}; - const moduleConfig = { build: { sourcemap: true } }; + + const expected = { build: { sourcemap: true } } as const; + const moduleConfig = { build: { sourcemap: true } } as const; + const userConfig = {} as const; wxt.config.vite = () => Promise.resolve(userConfig); addViteConfig(wxt, () => moduleConfig); @@ -25,9 +26,10 @@ describe('Module Utilities', () => { const wxt = fakeWxt({ hooks: createHooks(), }); - const expected = { build: { sourcemap: true, test: 2 } }; - const userConfig = { build: { sourcemap: true } }; - const moduleConfig = { build: { sourcemap: false, test: 2 } }; + + const expected = { build: { sourcemap: true, test: 2 } } as const; + const userConfig = { build: { sourcemap: true } } as const; + const moduleConfig = { build: { sourcemap: false, test: 2 } } as const; wxt.config.vite = () => userConfig; addViteConfig(wxt, () => moduleConfig); @@ -41,6 +43,7 @@ describe('Module Utilities', () => { describe('addImportPreset', () => { it('should add the import to the config', async () => { const PRESET = 'vue'; + const wxt = fakeWxt({ hooks: createHooks() }); addImportPreset(wxt, PRESET); @@ -53,6 +56,7 @@ describe('Module Utilities', () => { it('should not add duplicate presets', async () => { const PRESET = 'vue'; + const wxt = fakeWxt({ hooks: createHooks(), config: { From b05af620be201e8219f72e07c12aadd89f7b3617 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Mon, 29 Dec 2025 19:21:36 +0100 Subject: [PATCH 57/64] clean(packages/wxt/builtin-modules): make code more readable --- packages/wxt/src/builtin-modules/unimport.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/wxt/src/builtin-modules/unimport.ts b/packages/wxt/src/builtin-modules/unimport.ts index c22a1b800..21bdecf2d 100644 --- a/packages/wxt/src/builtin-modules/unimport.ts +++ b/packages/wxt/src/builtin-modules/unimport.ts @@ -13,6 +13,7 @@ export default defineWxtModule({ name: 'wxt:built-in:unimport', setup(wxt) { let unimport: Unimport; + const isEnabled = () => !wxt.config.imports.disabled; // Add user module imports to config @@ -49,7 +50,7 @@ export default defineWxtModule({ // Only create global types when user has enabled auto-imports entries.push(await getImportsDeclarationEntry(unimport)); - if (wxt.config.imports.eslintrc.enabled === false) return; + if (!wxt.config.imports.eslintrc.enabled) return; // Only generate ESLint config if that feature is enabled entries.push( @@ -91,6 +92,7 @@ async function getImportsModuleEntry( unimport: Unimport, ): Promise { const imports = await unimport.getImports(); + return { path: 'types/imports-module.d.ts', text: [ @@ -119,8 +121,11 @@ async function getEslintConfigEntry( return globals; }, {}); - if (version <= 8) return getEslint8ConfigEntry(options, globals); - else return getEslint9ConfigEntry(options, globals); + if (version <= 8) { + return getEslint8ConfigEntry(options, globals); + } else { + return getEslint9ConfigEntry(options, globals); + } } export function getEslint8ConfigEntry( From 49a55e173e0208c4f0e4b2f11ba13c1dd6ecae58 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Mon, 29 Dec 2025 19:22:11 +0100 Subject: [PATCH 58/64] clean(packages/wxt/cli): make code more readable --- packages/wxt/src/cli/cli-utils.ts | 6 +++++- packages/wxt/src/cli/commands.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/wxt/src/cli/cli-utils.ts b/packages/wxt/src/cli/cli-utils.ts index 3a772c434..fc2819d4c 100644 --- a/packages/wxt/src/cli/cli-utils.ts +++ b/packages/wxt/src/cli/cli-utils.ts @@ -21,11 +21,12 @@ export function wrapAction( return async (...args: any[]) => { // Enable consola's debug mode globally at the start of all commands when the `--debug` flag is passed const isDebug = !!args.find((arg) => arg?.debug); + const startTime = Date.now(); + if (isDebug) { consola.level = LogLevels.debug; } - const startTime = Date.now(); try { printHeader(); @@ -59,10 +60,12 @@ export function getArrayFromFlags( ): T[] | undefined { const array = toArray(flags[name]); const result = filterTruthy(array); + return result.length ? result : undefined; } const aliasCommandNames = new Set(); + /** * @param base Command to add this one to * @param name The command name to add @@ -87,6 +90,7 @@ export function createAliasedCommand( const args = process.argv.slice( process.argv.indexOf(aliasedCommand.name) + 1, ); + await spawn(bin, args, { stdio: 'inherit', }); diff --git a/packages/wxt/src/cli/commands.ts b/packages/wxt/src/cli/commands.ts index d9e8c82ab..357e013b8 100644 --- a/packages/wxt/src/cli/commands.ts +++ b/packages/wxt/src/cli/commands.ts @@ -32,6 +32,7 @@ cli const serverOptions: NonNullable< NonNullable[0]>['dev'] >['server'] = {}; + if (flags.host) serverOptions.host = flags.host; if (flags.port) serverOptions.port = parseInt(flags.port); @@ -49,6 +50,7 @@ cli : { server: serverOptions }, }); await server.start(); + return { isOngoing: true }; }), ); From 281241d4032fce32e95465bd3a44918f58bff6eb Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Mon, 29 Dec 2025 20:14:23 +0100 Subject: [PATCH 59/64] clean(packages/wxt/core): make code more readable, remove some unnecessary `await`, make closeBrowser function of ExtensionRunner optional --- packages/wxt/src/core/builders/vite/index.ts | 48 +++++++++++++++---- .../builders/vite/plugins/devHtmlPrerender.ts | 9 +++- .../core/builders/vite/plugins/download.ts | 3 +- .../vite/plugins/entrypointGroupGlobals.ts | 5 +- .../builders/vite/plugins/extensionApiMock.ts | 2 +- .../src/core/builders/vite/plugins/globals.ts | 2 + .../plugins/removeEntrypointMainFunction.ts | 2 + .../builders/vite/plugins/resolveAppConfig.ts | 3 +- .../vite/plugins/resolveVirtualModules.ts | 4 +- .../builders/vite/plugins/wxtPluginLoader.ts | 7 ++- packages/wxt/src/core/create-server.ts | 4 +- .../package-managers/__tests__/bun.test.ts | 2 + .../package-managers/__tests__/npm.test.ts | 6 ++- .../package-managers/__tests__/pnpm.test.ts | 3 ++ .../package-managers/__tests__/yarn.test.ts | 2 + packages/wxt/src/core/package-managers/bun.ts | 3 ++ .../wxt/src/core/package-managers/index.ts | 5 +- packages/wxt/src/core/package-managers/npm.ts | 4 ++ .../src/core/runners/__tests__/index.test.ts | 4 ++ packages/wxt/src/core/runners/index.ts | 1 - packages/wxt/src/core/runners/manual.ts | 4 -- packages/wxt/src/core/runners/safari.ts | 3 -- packages/wxt/src/core/runners/web-ext.ts | 3 +- packages/wxt/src/core/runners/wsl.ts | 3 -- .../src/core/utils/__tests__/manifest.test.ts | 32 ++++++++----- .../src/core/utils/__tests__/package.test.ts | 1 + .../core/utils/__tests__/validation.test.ts | 4 ++ .../__tests__/detect-dev-changes.test.ts | 7 +++ .../__tests__/find-entrypoints.test.ts | 38 +++++++-------- .../__tests__/group-entrypoints.test.ts | 14 ++++++ .../core/utils/building/build-entrypoints.ts | 8 ++++ .../core/utils/building/detect-dev-changes.ts | 17 ++++--- .../core/utils/building/find-entrypoints.ts | 43 ++++++++++++----- .../core/utils/building/group-entrypoints.ts | 3 ++ .../src/core/utils/building/internal-build.ts | 7 ++- .../src/core/utils/testing/fake-objects.ts | 6 +-- packages/wxt/src/types.ts | 2 +- 37 files changed, 220 insertions(+), 94 deletions(-) diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts index 78c071faf..4c934bada 100644 --- a/packages/wxt/src/core/builders/vite/index.ts +++ b/packages/wxt/src/core/builders/vite/index.ts @@ -55,10 +55,12 @@ export async function createViteBuilder( config.build.copyPublicDir = false; config.build.outDir = wxtConfig.outDir; config.build.emptyOutDir = false; + // Disable minification for the dev command if (config.build.minify == null && wxtConfig.command === 'serve') { config.build.minify = false; } + // Enable inline sourcemaps for the dev command (so content scripts have sourcemaps) if (config.build.sourcemap == null && wxtConfig.command === 'serve') { config.build.sourcemap = 'inline'; @@ -71,7 +73,6 @@ export async function createViteBuilder( // TODO: Remove once https://github.com/wxt-dev/wxt/pull/1411 is merged config.legacy ??= {}; - // @ts-ignore: Untyped option: config.legacy.skipWebSocketTokenCheck = true; const server = getWxtDevServer?.(); @@ -89,6 +90,7 @@ export async function createViteBuilder( wxtPlugins.wxtPluginLoader(wxtConfig), wxtPlugins.resolveAppConfig(wxtConfig), ); + if ( wxtConfig.analysis.enabled && // If included, vite-node entrypoint loader will increment the @@ -173,6 +175,7 @@ export async function createViteBuilder( const htmlEntrypoints = new Set( entrypoints.filter(isHtmlEntrypoint).map((e) => e.name), ); + return { mode: wxtConfig.mode, plugins: [wxtPlugins.entrypointGroupGlobals(entrypoints)], @@ -233,21 +236,22 @@ export async function createViteBuilder( baseConfig.optimizeDeps ??= {}; baseConfig.optimizeDeps.noDiscovery = true; baseConfig.optimizeDeps.include = []; + const envConfig: vite.InlineConfig = { plugins: paths.map((path) => wxtPlugins.removeEntrypointMainFunction(wxtConfig, path), ), }; const config = vite.mergeConfig(baseConfig, envConfig); + const server = await vite.createServer(config); await server.pluginContainer.buildStart({}); - const node = new ViteNodeServer( - // @ts-ignore: Some weird type error... - server, - ); + + const node = new ViteNodeServer(server); installSourcemapsSupport({ getSourceMap: (source) => node.getSourceMap(source), }); + const runner = new ViteNodeRunner({ root: server.config.root, base: server.config.base, @@ -261,11 +265,13 @@ export async function createViteBuilder( return node.resolveId(id, importer); }, }); + return { runner, server }; }; const requireDefaultExport = (path: string, mod: any) => { const relativePath = relative(wxtConfig.root, path); + if (mod?.default == null) { const defineFn = relativePath.includes('.content') ? 'defineContentScript' @@ -286,43 +292,55 @@ export async function createViteBuilder( const env = createExtensionEnvironment(); const { runner, server } = await createViteNodeImporter([path]); const res = await env.run(() => runner.executeFile(path)); + await server.close(); requireDefaultExport(path, res); + return res.default; }, async importEntrypoints(paths) { const env = createExtensionEnvironment(); const { runner, server } = await createViteNodeImporter(paths); + const res = await env.run(() => Promise.all( paths.map(async (path) => { const mod = await runner.executeFile(path); requireDefaultExport(path, mod); + return mod.default; }), ), ); + await server.close(); return res; }, async build(group) { let entryConfig; - if (Array.isArray(group)) entryConfig = getMultiPageConfig(group); - else if ( + + if (Array.isArray(group)) { + entryConfig = getMultiPageConfig(group); + } else if ( group.type === 'content-script-style' || group.type === 'unlisted-style' - ) + ) { entryConfig = getCssConfig(group); - else entryConfig = getLibModeConfig(group); + } else { + entryConfig = getLibModeConfig(group); + } const buildConfig = vite.mergeConfig(await getBaseConfig(), entryConfig); + await hooks.callHook( 'vite:build:extendConfig', toArray(group), buildConfig, ); + const result = await vite.build(buildConfig); const chunks = getBuildOutputChunks(result); + return { entrypoints: group, chunks: await moveHtmlFiles(wxtConfig, group, chunks), @@ -339,6 +357,7 @@ export async function createViteBuilder( }; const baseConfig = await getBaseConfig(); const finalConfig = vite.mergeConfig(baseConfig, serverConfig); + await hooks.callHook('vite:devServer:extendConfig', finalConfig); const viteServer = await vite.createServer(finalConfig); @@ -375,7 +394,9 @@ function getBuildOutputChunks( result: Awaited>, ): BuildStepOutput['chunks'] { if ('on' in result) throw Error('wxt does not support vite watch mode.'); + if (Array.isArray(result)) return result.flatMap(({ output }) => output); + return result.output; } @@ -385,6 +406,7 @@ function getBuildOutputChunks( */ function getRollupEntry(entrypoint: Entrypoint): string { let virtualEntrypointType: VirtualEntrypointType | undefined; + switch (entrypoint.type) { case 'background': case 'unlisted-script': @@ -400,8 +422,10 @@ function getRollupEntry(entrypoint: Entrypoint): string { if (virtualEntrypointType) { const moduleId: VirtualModuleId = `virtual:wxt-${virtualEntrypointType}-entrypoint`; + return `${moduleId}?${entrypoint.inputPath}`; } + return entrypoint.inputPath; } @@ -426,12 +450,15 @@ async function moveHtmlFiles( const entryMap = group.reduce>((map, entry) => { const a = normalizePath(relative(config.root, entry.inputPath)); map[a] = entry; + return map; }, {}); const movedChunks = await Promise.all( chunks.map(async (chunk) => { - if (!chunk.fileName.endsWith('.html')) return chunk; + if (!chunk.fileName.endsWith('.html')) { + return chunk; + } const entry = entryMap[chunk.fileName]; const oldBundlePath = chunk.fileName; @@ -442,6 +469,7 @@ async function moveHtmlFiles( ); const oldAbsPath = join(config.outDir, oldBundlePath); const newAbsPath = join(config.outDir, newBundlePath); + await fs.ensureDir(dirname(newAbsPath)); await fs.move(oldAbsPath, newAbsPath, { overwrite: true }); diff --git a/packages/wxt/src/core/builders/vite/plugins/devHtmlPrerender.ts b/packages/wxt/src/core/builders/vite/plugins/devHtmlPrerender.ts index 28e641fbd..86c3100fc 100644 --- a/packages/wxt/src/core/builders/vite/plugins/devHtmlPrerender.ts +++ b/packages/wxt/src/core/builders/vite/plugins/devHtmlPrerender.ts @@ -24,7 +24,7 @@ export function devHtmlPrerender( ); const VIRTUAL_INLINE_SCRIPT = 'virtual:wxt-inline-script'; - const RESOLVED_VIRTUAL_INLINE_SCRIPT = '\0' + VIRTUAL_INLINE_SCRIPT; + const RESOLVED_VIRTUAL_INLINE_SCRIPT = `\0${VIRTUAL_INLINE_SCRIPT}`; return [ { @@ -60,9 +60,11 @@ export function devHtmlPrerender( const reloader = document.createElement('script'); reloader.src = HTML_RELOAD_ID; reloader.type = 'module'; + document.head.appendChild(reloader); const newHtml = document.toString(); + config.logger.debug('transform ' + id); config.logger.debug('Old HTML:\n' + code); config.logger.debug('New HTML:\n' + newHtml); @@ -77,6 +79,7 @@ export function devHtmlPrerender( const originalUrl = `${server.origin}${ctx.path}`; const name = getEntrypointName(config.entrypointsDir, ctx.filename); const url = `${server.origin}/${name}.html`; + const serverHtml = await server.transformHtml(url, html, originalUrl); const { document } = parseHTML(serverHtml); @@ -112,6 +115,7 @@ export function devHtmlPrerender( config.logger.debug('transformIndexHtml ' + ctx.filename); config.logger.debug('Old HTML:\n' + html); config.logger.debug('New HTML:\n' + newHtml); + return newHtml; }, }, @@ -121,7 +125,7 @@ export function devHtmlPrerender( resolveId(id) { // Resolve inline scripts if (id.startsWith(VIRTUAL_INLINE_SCRIPT)) { - return '\0' + id; + return `\0${id}`; } // Ignore chunks during HTML file pre-rendering @@ -195,6 +199,7 @@ export function pointToDevServer( let path = normalizePath(resolvedAbsolutePath); // Add "/" to start of windows paths ("D:/some/path" -> "/D:/some/path") if (!path.startsWith('/')) path = '/' + path; + element.setAttribute(attr, `${server.origin}/@fs${path}`); } else { // Inside the project, use relative path diff --git a/packages/wxt/src/core/builders/vite/plugins/download.ts b/packages/wxt/src/core/builders/vite/plugins/download.ts index 1046041d2..de42301bd 100644 --- a/packages/wxt/src/core/builders/vite/plugins/download.ts +++ b/packages/wxt/src/core/builders/vite/plugins/download.ts @@ -20,7 +20,8 @@ export function download(config: ResolvedConfig): Plugin { // Load file from network or cache const url = id.replace('\0url:', ''); - return await fetchCached(url, config); + + return fetchCached(url, config); }, }; } diff --git a/packages/wxt/src/core/builders/vite/plugins/entrypointGroupGlobals.ts b/packages/wxt/src/core/builders/vite/plugins/entrypointGroupGlobals.ts index b203fe1ad..e98859b0c 100644 --- a/packages/wxt/src/core/builders/vite/plugins/entrypointGroupGlobals.ts +++ b/packages/wxt/src/core/builders/vite/plugins/entrypointGroupGlobals.ts @@ -17,9 +17,8 @@ export function entrypointGroupGlobals( for (const global of getEntrypointGlobals(name)) { define[`import.meta.env.${global.name}`] = JSON.stringify(global.value); } - return { - define, - }; + + return { define }; }, }; } diff --git a/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts b/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts index 71ecf884c..36bf6491e 100644 --- a/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts +++ b/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts @@ -7,7 +7,7 @@ import { ResolvedConfig } from '../../../../types'; */ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption { const VIRTUAL_SETUP_MODULE = 'virtual:wxt-setup'; - const RESOLVED_VIRTUAL_SETUP_MODULE = '\0' + VIRTUAL_SETUP_MODULE; + const RESOLVED_VIRTUAL_SETUP_MODULE = `\0${VIRTUAL_SETUP_MODULE}`; return { name: 'wxt:extension-api-mock', diff --git a/packages/wxt/src/core/builders/vite/plugins/globals.ts b/packages/wxt/src/core/builders/vite/plugins/globals.ts index 5d59e24e7..964fe2f32 100644 --- a/packages/wxt/src/core/builders/vite/plugins/globals.ts +++ b/packages/wxt/src/core/builders/vite/plugins/globals.ts @@ -7,9 +7,11 @@ export function globals(config: ResolvedConfig): vite.PluginOption { name: 'wxt:globals', config() { const define: vite.InlineConfig['define'] = {}; + for (const global of getGlobals(config)) { define[`import.meta.env.${global.name}`] = JSON.stringify(global.value); } + return { define, }; diff --git a/packages/wxt/src/core/builders/vite/plugins/removeEntrypointMainFunction.ts b/packages/wxt/src/core/builders/vite/plugins/removeEntrypointMainFunction.ts index 91cc0b995..a5131d59c 100644 --- a/packages/wxt/src/core/builders/vite/plugins/removeEntrypointMainFunction.ts +++ b/packages/wxt/src/core/builders/vite/plugins/removeEntrypointMainFunction.ts @@ -19,9 +19,11 @@ export function removeEntrypointMainFunction( handler(code, id) { if (id === absPath) { const newCode = removeMainFunctionCode(code); + config.logger.debug('vite-node transformed entrypoint', path); config.logger.debug(`Original:\n---\n${code}\n---`); config.logger.debug(`Transformed:\n---\n${newCode.code}\n---`); + return newCode; } }, diff --git a/packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts b/packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts index 0fca3e6b6..e713db43a 100644 --- a/packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts +++ b/packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts @@ -8,7 +8,8 @@ import { ResolvedConfig } from '../../../../types'; */ export function resolveAppConfig(config: ResolvedConfig): vite.Plugin { const VIRTUAL_MODULE_ID = 'virtual:app-config'; - const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; + const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`; + const appConfigFile = resolve(config.srcDir, 'app.config.ts'); return { diff --git a/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts b/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts index 193ec07d8..b748ea8d7 100644 --- a/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts +++ b/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts @@ -14,7 +14,7 @@ import { resolve } from 'path'; export function resolveVirtualModules(config: ResolvedConfig): Plugin[] { return virtualModuleNames.map((name) => { const VIRTUAL_ID: `${VirtualModuleId}?` = `virtual:wxt-${name}?`; - const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_ID; + const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_ID}`; return { name: `wxt:resolve-virtual-${name}`, @@ -27,6 +27,7 @@ export function resolveVirtualModules(config: ResolvedConfig): Plugin[] { const inputPath = normalizePath( id.substring(index + VIRTUAL_ID.length), ); + return RESOLVED_VIRTUAL_ID + inputPath; }, async load(id) { @@ -37,6 +38,7 @@ export function resolveVirtualModules(config: ResolvedConfig): Plugin[] { resolve(config.wxtModuleDir, `dist/virtual/${name}.mjs`), 'utf-8', ); + return TEMPLATE.replace(`virtual:user-${name}`, inputPath); }, }; diff --git a/packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts b/packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts index e3231e2cd..0e0e82193 100644 --- a/packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts +++ b/packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts @@ -8,9 +8,9 @@ import { ResolvedConfig } from '../../../../types'; */ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { const VIRTUAL_MODULE_ID = 'virtual:wxt-plugins'; - const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; + const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`; const VIRTUAL_HTML_MODULE_ID = 'virtual:wxt-html-plugins'; - const RESOLVED_VIRTUAL_HTML_MODULE_ID = '\0' + VIRTUAL_HTML_MODULE_ID; + const RESOLVED_VIRTUAL_HTML_MODULE_ID = `\0${VIRTUAL_HTML_MODULE_ID}`; return { name: 'wxt:plugin-loader', @@ -27,9 +27,11 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { `import initPlugin${i} from '${normalizePath(plugin)}';`, ) .join('\n'); + const initCalls = config.plugins .map((_, i) => ` initPlugin${i}();`) .join('\n'); + return `${imports}\n\nexport function initPlugins() {\n${initCalls}\n}`; } if (id === RESOLVED_VIRTUAL_HTML_MODULE_ID) { @@ -66,6 +68,7 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { } document.head.prepend(script); + return document.toString(); }, }, diff --git a/packages/wxt/src/core/create-server.ts b/packages/wxt/src/core/create-server.ts index fb0d95919..557ed5a95 100644 --- a/packages/wxt/src/core/create-server.ts +++ b/packages/wxt/src/core/create-server.ts @@ -126,7 +126,7 @@ async function createServerInternal(): Promise { async stop() { wasStopped = true; keyboardShortcuts.stop(); - await runner.closeBrowser(); + await runner.closeBrowser?.(); await builderServer.close(); await wxt.hooks.callHook('server:closed', wxt, server); @@ -150,7 +150,7 @@ async function createServerInternal(): Promise { server.ws.send('wxt:reload-extension'); }, async restartBrowser() { - await runner.closeBrowser(); + await runner.closeBrowser?.(); keyboardShortcuts.stop(); await wxt.reloadConfig(); runner = await createExtensionRunner(); diff --git a/packages/wxt/src/core/package-managers/__tests__/bun.test.ts b/packages/wxt/src/core/package-managers/__tests__/bun.test.ts index 4a351e533..bdc3d2b24 100644 --- a/packages/wxt/src/core/package-managers/__tests__/bun.test.ts +++ b/packages/wxt/src/core/package-managers/__tests__/bun.test.ts @@ -10,6 +10,7 @@ describe.skipIf(() => process.platform === 'win32')( it('should list direct dependencies', async () => { const actual = await bun.listDependencies({ cwd }); + expect(actual).toEqual([ { name: 'flatten', version: '1.0.3' }, { name: 'mime-types', version: '2.1.35' }, @@ -18,6 +19,7 @@ describe.skipIf(() => process.platform === 'win32')( it('should list all dependencies', async () => { const actual = await bun.listDependencies({ cwd, all: true }); + expect(actual).toEqual([ { name: 'flatten', version: '1.0.3' }, { name: 'mime-db', version: '1.52.0' }, diff --git a/packages/wxt/src/core/package-managers/__tests__/npm.test.ts b/packages/wxt/src/core/package-managers/__tests__/npm.test.ts index 024cde5ef..0310932e6 100644 --- a/packages/wxt/src/core/package-managers/__tests__/npm.test.ts +++ b/packages/wxt/src/core/package-managers/__tests__/npm.test.ts @@ -7,6 +7,7 @@ import { pathExists } from 'fs-extra'; describe('NPM Package Management Utils', () => { describe('listDependencies', () => { const cwd = path.resolve(__dirname, 'fixtures/simple-npm-project'); + beforeAll(async () => { // NPM needs the modules installed for 'npm ls' to work await spawn('npm', ['i'], { cwd }); @@ -14,6 +15,7 @@ describe('NPM Package Management Utils', () => { it('should list direct dependencies', async () => { const actual = await npm.listDependencies({ cwd }); + expect(actual).toEqual([ { name: 'flatten', version: '1.0.3' }, { name: 'mime-types', version: '2.1.35' }, @@ -22,6 +24,7 @@ describe('NPM Package Management Utils', () => { it('should list all dependencies', async () => { const actual = await npm.listDependencies({ cwd, all: true }); + expect(actual).toEqual([ { name: 'flatten', version: '1.0.3' }, { name: 'mime-types', version: '2.1.35' }, @@ -35,9 +38,10 @@ describe('NPM Package Management Utils', () => { it('should download the dependency as a tarball', async () => { const ID = 'mime-db@1.52.0'; + const downloadDir = path.resolve(cwd, 'dist'); - const expected = path.resolve(downloadDir, 'mime-db-1.52.0.tgz'); + const expected = path.resolve(downloadDir, 'mime-db-1.52.0.tgz'); const actual = await npm.downloadDependency(ID, downloadDir); expect(actual).toEqual(expected); diff --git a/packages/wxt/src/core/package-managers/__tests__/pnpm.test.ts b/packages/wxt/src/core/package-managers/__tests__/pnpm.test.ts index 20b6312c8..a0d6a7bfb 100644 --- a/packages/wxt/src/core/package-managers/__tests__/pnpm.test.ts +++ b/packages/wxt/src/core/package-managers/__tests__/pnpm.test.ts @@ -8,6 +8,7 @@ process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true'; describe('PNPM Package Management Utils', () => { describe('listDependencies', () => { const cwd = path.resolve(__dirname, 'fixtures/simple-pnpm-project'); + beforeAll(async () => { // PNPM needs the modules installed, or 'pnpm ls' will return a blank list. await spawn('pnpm', ['i', '--ignore-workspace'], { cwd }); @@ -15,6 +16,7 @@ describe('PNPM Package Management Utils', () => { it('should list direct dependencies', async () => { const actual = await pnpm.listDependencies({ cwd }); + expect(actual).toEqual([ { name: 'flatten', version: '1.0.3' }, { name: 'mime-types', version: '2.1.35' }, @@ -23,6 +25,7 @@ describe('PNPM Package Management Utils', () => { it('should list all dependencies', async () => { const actual = await pnpm.listDependencies({ cwd, all: true }); + expect(actual).toEqual([ { name: 'flatten', version: '1.0.3' }, { name: 'mime-types', version: '2.1.35' }, diff --git a/packages/wxt/src/core/package-managers/__tests__/yarn.test.ts b/packages/wxt/src/core/package-managers/__tests__/yarn.test.ts index 40415b7df..a356dab1b 100644 --- a/packages/wxt/src/core/package-managers/__tests__/yarn.test.ts +++ b/packages/wxt/src/core/package-managers/__tests__/yarn.test.ts @@ -8,6 +8,7 @@ describe('Yarn Package Management Utils', () => { it('should list direct dependencies', async () => { const actual = await yarn.listDependencies({ cwd }); + expect(actual).toEqual([ { name: 'mime-db', version: '1.52.0' }, { name: 'flatten', version: '1.0.3' }, @@ -17,6 +18,7 @@ describe('Yarn Package Management Utils', () => { it('should list all dependencies', async () => { const actual = await yarn.listDependencies({ cwd, all: true }); + expect(actual).toEqual([ { name: 'mime-db', version: '1.52.0' }, { name: 'flatten', version: '1.0.3' }, diff --git a/packages/wxt/src/core/package-managers/bun.ts b/packages/wxt/src/core/package-managers/bun.ts index e06882d11..6adb68072 100644 --- a/packages/wxt/src/core/package-managers/bun.ts +++ b/packages/wxt/src/core/package-managers/bun.ts @@ -9,10 +9,13 @@ export const bun: WxtPackageManagerImpl = { }, async listDependencies(options) { const args = ['pm', 'ls']; + if (options?.all) { args.push('--all'); } + const res = await spawn('bun', args, { cwd: options?.cwd }); + return dedupeDependencies( res.stdout .split('\n') diff --git a/packages/wxt/src/core/package-managers/index.ts b/packages/wxt/src/core/package-managers/index.ts index 0c270808c..3175004c4 100644 --- a/packages/wxt/src/core/package-managers/index.ts +++ b/packages/wxt/src/core/package-managers/index.ts @@ -25,7 +25,10 @@ export async function createWxtPackageManager( // Use requirePm to prevent throwing errors before the package manager utils are used. const requirePm = (cb: (pm: PackageManager) => T) => { - if (pm == null) throw Error('Could not detect package manager'); + if (pm == null) { + throw Error('Could not detect package manager'); + } + return cb(pm); }; diff --git a/packages/wxt/src/core/package-managers/npm.ts b/packages/wxt/src/core/package-managers/npm.ts index 68ba93055..0e473f345 100644 --- a/packages/wxt/src/core/package-managers/npm.ts +++ b/packages/wxt/src/core/package-managers/npm.ts @@ -8,9 +8,11 @@ export const npm: WxtPackageManagerImpl = { overridesKey: 'overrides', async downloadDependency(id, downloadDir) { await ensureDir(downloadDir); + const res = await spawn('npm', ['pack', id, '--json'], { cwd: downloadDir, }); + const packed: PackedDependency[] = JSON.parse(res.stdout); return path.resolve(downloadDir, packed[0].filename); @@ -48,6 +50,7 @@ export function flattenNpmListOutput(projects: NpmListProject[]): Dependency[] { name, version: meta.version, }); + if (meta.dependencies) queue.push(meta.dependencies); if (meta.devDependencies) queue.push(meta.devDependencies); }); @@ -61,6 +64,7 @@ export function dedupeDependencies(dependencies: Dependency[]): Dependency[] { return dependencies.filter((dep) => { const HASH = `${dep.name}@${dep.version}`; + if (hashes.has(HASH)) { return false; } else { diff --git a/packages/wxt/src/core/runners/__tests__/index.test.ts b/packages/wxt/src/core/runners/__tests__/index.test.ts index 7b46e11bf..599a3b695 100644 --- a/packages/wxt/src/core/runners/__tests__/index.test.ts +++ b/packages/wxt/src/core/runners/__tests__/index.test.ts @@ -31,6 +31,7 @@ describe('createExtensionRunner', () => { browser: 'safari', }, }); + const safariRunner = mock(); createSafariRunnerMock.mockReturnValue(safariRunner); @@ -44,6 +45,7 @@ describe('createExtensionRunner', () => { browser: 'chrome', }, }); + const wslRunner = mock(); createWslRunnerMock.mockReturnValue(wslRunner); @@ -52,6 +54,7 @@ describe('createExtensionRunner', () => { it('should return a manual runner when `runner.disabled` is true', async () => { isWslMock.mockResolvedValueOnce(false); + setFakeWxt({ config: { browser: 'chrome', @@ -62,6 +65,7 @@ describe('createExtensionRunner', () => { }, }, }); + const manualRunner = mock(); createManualRunnerMock.mockReturnValue(manualRunner); diff --git a/packages/wxt/src/core/runners/index.ts b/packages/wxt/src/core/runners/index.ts index 65c4523f6..968c2686b 100644 --- a/packages/wxt/src/core/runners/index.ts +++ b/packages/wxt/src/core/runners/index.ts @@ -8,7 +8,6 @@ import { wxt } from '../wxt'; export async function createExtensionRunner(): Promise { if (wxt.config.browser === 'safari') return createSafariRunner(); - if (await isWsl()) return createWslRunner(); if (wxt.config.runnerConfig.config?.disabled) return createManualRunner(); diff --git a/packages/wxt/src/core/runners/manual.ts b/packages/wxt/src/core/runners/manual.ts index fb5773096..2d6e58825 100644 --- a/packages/wxt/src/core/runners/manual.ts +++ b/packages/wxt/src/core/runners/manual.ts @@ -15,9 +15,5 @@ export function createManualRunner(): ExtensionRunner { )}" as an unpacked extension manually`, ); }, - async closeBrowser() { - // TODO: THIS ISN'T USED ANYWHERE, MAYBE REMOVE? - // noop - }, }; } diff --git a/packages/wxt/src/core/runners/safari.ts b/packages/wxt/src/core/runners/safari.ts index f3d27c559..a0f77955c 100644 --- a/packages/wxt/src/core/runners/safari.ts +++ b/packages/wxt/src/core/runners/safari.ts @@ -15,8 +15,5 @@ export function createSafariRunner(): ExtensionRunner { )}" as an unpacked extension manually`, ); }, - async closeBrowser() { - // noop - }, }; } diff --git a/packages/wxt/src/core/runners/web-ext.ts b/packages/wxt/src/core/runners/web-ext.ts index f7efcdbe9..1d21012ee 100644 --- a/packages/wxt/src/core/runners/web-ext.ts +++ b/packages/wxt/src/core/runners/web-ext.ts @@ -77,6 +77,7 @@ export function createWebExtRunner(): ExtensionRunner { // Don't call `process.exit(0)` after starting web-ext shouldExitProgram: false, }; + wxt.logger.debug('web-ext config:', finalConfig); wxt.logger.debug('web-ext options:', options); @@ -88,7 +89,7 @@ export function createWebExtRunner(): ExtensionRunner { }, async closeBrowser() { - return await runner?.exit(); + return runner?.exit(); }, }; } diff --git a/packages/wxt/src/core/runners/wsl.ts b/packages/wxt/src/core/runners/wsl.ts index 4fc0b126f..34c7d0bb3 100644 --- a/packages/wxt/src/core/runners/wsl.ts +++ b/packages/wxt/src/core/runners/wsl.ts @@ -15,8 +15,5 @@ export function createWslRunner(): ExtensionRunner { )}" as an unpacked extension manually`, ); }, - async closeBrowser() { - // noop - }, }; } diff --git a/packages/wxt/src/core/utils/__tests__/manifest.test.ts b/packages/wxt/src/core/utils/__tests__/manifest.test.ts index 216761b0d..37a3dc5e8 100644 --- a/packages/wxt/src/core/utils/__tests__/manifest.test.ts +++ b/packages/wxt/src/core/utils/__tests__/manifest.test.ts @@ -34,6 +34,7 @@ describe('Manifest Utils', () => { describe('generateManifest', () => { describe('popup', () => { type ActionType = 'browser_action' | 'page_action'; + const popupEntrypoint = (type?: ActionType) => fakePopupEntrypoint({ options: { @@ -234,7 +235,7 @@ describe('Manifest Utils', () => { open_in_tab: false, chrome_style: true, page: 'options.html', - }; + } as const; const { manifest: actual } = await generateManifest( [options], @@ -346,7 +347,7 @@ describe('Manifest Utils', () => { const expected = { persistent: true, scripts: ['background.js'], - }; + } as const; const { manifest: actual } = await generateManifest( [background], @@ -439,11 +440,12 @@ describe('Manifest Utils', () => { { type: 'asset', fileName: 'logo-48.png' }, ], }); + const expected = { 16: 'logo-16.png', 32: 'logo-32.png', 48: 'logo-48.png', - }; + } as const; setFakeWxt({ config: { @@ -544,6 +546,7 @@ describe('Manifest Utils', () => { }; const entrypoints = [cs1, cs2, cs3, cs4, cs5]; + setFakeWxt({ config: { command: 'build', @@ -1141,7 +1144,7 @@ describe('Manifest Utils', () => { async (browser) => { const VERSION = '1.0.0'; const VERSION_NAME = '1.0.0-alpha1'; - const entrypoints: Entrypoint[] = []; + const entrypoints: Entrypoint[] = [] as const; const buildOutput = fakeBuildOutput(); setFakeWxt({ @@ -1169,7 +1172,7 @@ describe('Manifest Utils', () => { async (browser) => { const VERSION = '1.0.0'; const VERSION_NAME = '1.0.0-alpha1'; - const entrypoints: Entrypoint[] = []; + const entrypoints: Entrypoint[] = [] as const; const buildOutput = fakeBuildOutput(); setFakeWxt({ @@ -1196,7 +1199,7 @@ describe('Manifest Utils', () => { 'should not include the version_name if it is equal to version', async (browser) => { const VERSION = '1.0.0'; - const entrypoints: Entrypoint[] = []; + const entrypoints: Entrypoint[] = [] as const; const buildOutput = fakeBuildOutput(); setFakeWxt({ @@ -1220,7 +1223,7 @@ describe('Manifest Utils', () => { ); it('should log a warning if the version could not be detected', async () => { - const entrypoints: Entrypoint[] = []; + const entrypoints: Entrypoint[] = [] as const; const buildOutput = fakeBuildOutput(); setFakeWxt({ @@ -1390,6 +1393,9 @@ describe('Manifest Utils', () => { }); describe('Stripping keys', () => { + // TODO: This is really necessary? + // TODO: This seems to be the same, as it would be undefined, + // TODO: that's mean we can remove all of this objects, because it's undefined by default const mv2Manifest = { page_action: {}, browser_action: {}, @@ -1409,15 +1415,18 @@ describe('Manifest Utils', () => { system_indicator: {}, user_scripts: {}, }; + const mv3Manifest = { action: {}, export: {}, optional_host_permissions: {}, side_panel: {}, }; + const hostPermissionsManifest = { host_permissions: {}, }; + const manifest: any = { ...mv2Manifest, ...mv3Manifest, @@ -1575,7 +1584,7 @@ describe('Manifest Utils', () => { }); const output = fakeBuildOutput(); - const entrypoints: Entrypoint[] = []; + const entrypoints: Entrypoint[] = [] as const; const { manifest: actual } = await generateManifest( entrypoints, @@ -1595,13 +1604,14 @@ describe('Manifest Utils', () => { }); it('should convert MV3 CSP object to MV2 CSP string with localhost for MV2', async () => { - const entrypoints: Entrypoint[] = []; - const buildOutput = fakeBuildOutput(); const INPUT_CSP = "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"; const EXPECTED_CSP = "script-src 'self' 'wasm-unsafe-eval' http://localhost:3000; object-src 'self';"; + const entrypoints: Entrypoint[] = []; + const buildOutput = fakeBuildOutput(); + // Setup WXT for Firefox and serve command setFakeWxt({ config: { @@ -1655,8 +1665,8 @@ describe('Manifest Utils', () => { describe('manifest_version', () => { it('should ignore and log a warning when someone sets `manifest_version` inside the manifest', async () => { - const buildOutput = fakeBuildOutput(); const EXPECTED_VERSION = 2; + const buildOutput = fakeBuildOutput(); setFakeWxt({ logger: mock(), diff --git a/packages/wxt/src/core/utils/__tests__/package.test.ts b/packages/wxt/src/core/utils/__tests__/package.test.ts index 97d14675f..a76b1506d 100644 --- a/packages/wxt/src/core/utils/__tests__/package.test.ts +++ b/packages/wxt/src/core/utils/__tests__/package.test.ts @@ -22,6 +22,7 @@ describe('Package JSON Utils', () => { it("should return an empty object when /package.json doesn't exist", async () => { const ROOT = '/some/path/that/does/not/exist'; const logger = mock(); + setFakeWxt({ config: { root: ROOT, logger }, logger, diff --git a/packages/wxt/src/core/utils/__tests__/validation.test.ts b/packages/wxt/src/core/utils/__tests__/validation.test.ts index 24f6fed71..8247f8ef3 100644 --- a/packages/wxt/src/core/utils/__tests__/validation.test.ts +++ b/packages/wxt/src/core/utils/__tests__/validation.test.ts @@ -25,6 +25,7 @@ describe('Validation Utils', () => { it('should return an error when exclude is not an array', () => { const entrypoint = fakeGenericEntrypoint({ options: { + // TODO: TS ISN'T ENOUGH HERE TO PREVENT THIS? // @ts-expect-error exclude: 0, }, @@ -50,6 +51,7 @@ describe('Validation Utils', () => { it('should return an error when include is not an array', () => { const entrypoint = fakeGenericEntrypoint({ options: { + // TODO: TS ISN'T ENOUGH HERE TO PREVENT THIS? // @ts-expect-error include: 0, }, @@ -76,6 +78,7 @@ describe('Validation Utils', () => { const entrypoint = fakeContentScriptEntrypoint({ options: { registration: 'manifest', + // TODO: TS ISN'T ENOUGH HERE TO PREVENT THIS? // @ts-expect-error matches: null, }, @@ -103,6 +106,7 @@ describe('Validation Utils', () => { const entrypoint = fakeContentScriptEntrypoint({ options: { registration: 'runtime', + // TODO: TS ISN'T ENOUGH HERE TO PREVENT THIS? // @ts-expect-error matches: null, }, diff --git a/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts b/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts index 9be848265..bc046a19e 100644 --- a/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts +++ b/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts @@ -188,6 +188,7 @@ describe('Detect Dev Changes', () => { }), ], }; + const step2: BuildStepOutput = { entrypoints: background, chunks: [ @@ -202,6 +203,7 @@ describe('Detect Dev Changes', () => { publicAssets: [], steps: [step1, step2], }; + const expected: DevModeChange = { type: 'extension-reload', cachedOutput: { @@ -224,9 +226,11 @@ describe('Detect Dev Changes', () => { const htmlPage1 = fakePopupEntrypoint({ inputPath: CHANGED_PATH, }); + const htmlPage2 = fakeOptionsEntrypoint({ inputPath: '/root/page2.html', }); + const htmlPage3 = fakeGenericEntrypoint({ type: 'sandbox', inputPath: '/root/page3.html', @@ -240,6 +244,7 @@ describe('Detect Dev Changes', () => { }), ], }; + const step2: BuildStepOutput = { entrypoints: [htmlPage3], chunks: [ @@ -254,6 +259,7 @@ describe('Detect Dev Changes', () => { publicAssets: [], steps: [step1, step2], }; + const expected: DevModeChange = { type: 'html-reload', cachedOutput: { @@ -371,6 +377,7 @@ describe('Detect Dev Changes', () => { publicAssets: [], steps: [step1, step2, step3], }; + const expected: DevModeChange = { type: 'content-script-reload', cachedOutput: { diff --git a/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts b/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts index 05d0937f8..80f77483b 100644 --- a/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts +++ b/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts @@ -13,11 +13,8 @@ import { resolve } from 'path'; import { findEntrypoints } from '../find-entrypoints'; import fs from 'fs-extra'; import glob from 'fast-glob'; -import { - fakeResolvedConfig, - setFakeWxt, -} from '../../../utils/testing/fake-objects'; -import { unnormalizePath } from '../../../utils/paths'; +import { fakeResolvedConfig, setFakeWxt } from '../../testing/fake-objects'; +import { unnormalizePath } from '../../paths'; import { wxt } from '../../../wxt'; vi.mock('fast-glob'); @@ -48,7 +45,7 @@ describe('findEntrypoints', () => { [ 'popup.html', ` - + Default Title @@ -70,7 +67,7 @@ describe('findEntrypoints', () => { [ 'popup/index.html', ` - + Title @@ -104,7 +101,7 @@ describe('findEntrypoints', () => { [ 'options.html', ` - + Default Title @@ -122,7 +119,7 @@ describe('findEntrypoints', () => { [ 'options/index.html', ` - + Title @@ -210,6 +207,7 @@ describe('findEntrypoints', () => { const options: ContentScriptEntrypoint['options'] = { matches: [''], }; + globMock.mockResolvedValueOnce([path]); importEntrypointsMock.mockResolvedValue([options]); @@ -248,6 +246,7 @@ describe('findEntrypoints', () => { const options = { type: 'module', } satisfies BackgroundEntrypointOptions; + globMock.mockResolvedValueOnce([path]); importEntrypointsMock.mockResolvedValue([options]); @@ -263,7 +262,7 @@ describe('findEntrypoints', () => { [ 'sidepanel.html', ` - + Default Title @@ -286,7 +285,7 @@ describe('findEntrypoints', () => { ], [ 'sidepanel/index.html', - ``, + ``, { type: 'sidepanel', name: 'sidepanel', @@ -298,7 +297,7 @@ describe('findEntrypoints', () => { ], [ 'named.sidepanel.html', - ``, + ``, { type: 'sidepanel', name: 'named', @@ -310,7 +309,7 @@ describe('findEntrypoints', () => { ], [ 'named.sidepanel/index.html', - ``, + ``, { type: 'sidepanel', name: 'named', @@ -411,6 +410,7 @@ describe('findEntrypoints', () => { outputDir: config.outDir, skipped: false, }; + const options = {} satisfies BaseEntrypointOptions; globMock.mockResolvedValueOnce([path]); importEntrypointsMock.mockResolvedValue([options]); @@ -756,7 +756,7 @@ describe('findEntrypoints', () => { it("should mark the popup as skipped when include doesn't contain the target browser", async () => { globMock.mockResolvedValueOnce(['popup.html']); readFileMock.mockResolvedValueOnce( - ` + ` { globMock.mockResolvedValueOnce(['options.html']); readFileMock.mockResolvedValueOnce( - ` + ` { globMock.mockResolvedValueOnce(['unlisted.html']); readFileMock.mockResolvedValueOnce( - ` + ` @@ -872,7 +872,7 @@ describe('findEntrypoints', () => { it('should mark the options page as skipped when exclude contains the target browser', async () => { globMock.mockResolvedValueOnce(['options.html']); readFileMock.mockResolvedValueOnce( - ` + ` @@ -892,7 +892,7 @@ describe('findEntrypoints', () => { it('should mark unlisted pages as skipped when exclude contains the target browser', async () => { globMock.mockResolvedValueOnce(['unlisted.html']); readFileMock.mockResolvedValueOnce( - ` + ` diff --git a/packages/wxt/src/core/utils/building/__tests__/group-entrypoints.test.ts b/packages/wxt/src/core/utils/building/__tests__/group-entrypoints.test.ts index bb21a6fd6..d2757bf51 100644 --- a/packages/wxt/src/core/utils/building/__tests__/group-entrypoints.test.ts +++ b/packages/wxt/src/core/utils/building/__tests__/group-entrypoints.test.ts @@ -15,6 +15,7 @@ const background: Entrypoint = { options: {}, skipped: false, }; + const contentScript: Entrypoint = { type: 'content-script', name: 'overlay', @@ -25,6 +26,7 @@ const contentScript: Entrypoint = { }, skipped: false, }; + const unlistedScript: Entrypoint = { type: 'unlisted-script', name: 'injected', @@ -33,6 +35,7 @@ const unlistedScript: Entrypoint = { options: {}, skipped: false, }; + const popup: Entrypoint = { type: 'popup', name: 'popup', @@ -41,6 +44,7 @@ const popup: Entrypoint = { options: {}, skipped: false, }; + const unlistedPage: Entrypoint = { type: 'unlisted-page', name: 'onboarding', @@ -49,6 +53,7 @@ const unlistedPage: Entrypoint = { options: {}, skipped: false, }; + const options: Entrypoint = { type: 'options', name: 'options', @@ -57,6 +62,7 @@ const options: Entrypoint = { options: {}, skipped: false, }; + const sandbox1: Entrypoint = { type: 'sandbox', name: 'sandbox', @@ -65,6 +71,7 @@ const sandbox1: Entrypoint = { options: {}, skipped: false, }; + const sandbox2: Entrypoint = { type: 'sandbox', name: 'sandbox2', @@ -73,6 +80,7 @@ const sandbox2: Entrypoint = { options: {}, skipped: false, }; + const unlistedStyle: Entrypoint = { type: 'unlisted-style', name: 'injected', @@ -81,6 +89,7 @@ const unlistedStyle: Entrypoint = { options: {}, skipped: false, }; + const contentScriptStyle: Entrypoint = { type: 'content-script-style', name: 'injected', @@ -98,6 +107,7 @@ describe('groupEntrypoints', () => { unlistedScript, popup, ]; + const expected = [contentScript, background, unlistedScript, [popup]]; const actual = groupEntrypoints(entrypoints); @@ -111,6 +121,7 @@ describe('groupEntrypoints', () => { contentScriptStyle, popup, ]; + const expected = [unlistedStyle, contentScriptStyle, [popup]]; const actual = groupEntrypoints(entrypoints); @@ -126,6 +137,7 @@ describe('groupEntrypoints', () => { options, sandbox1, ]; + const expected = [[popup, unlistedPage, options], background, [sandbox1]]; const actual = groupEntrypoints(entrypoints); @@ -140,6 +152,7 @@ describe('groupEntrypoints', () => { sandbox2, contentScript, ]; + const expected = [[sandbox1, sandbox2], [popup], contentScript]; const actual = groupEntrypoints(entrypoints); @@ -176,6 +189,7 @@ describe('groupEntrypoints', () => { }, skipped: false, }); + const popup = fakePopupEntrypoint({ skipped: true, }); diff --git a/packages/wxt/src/core/utils/building/build-entrypoints.ts b/packages/wxt/src/core/utils/building/build-entrypoints.ts index 4edd2aff5..87ea90235 100644 --- a/packages/wxt/src/core/utils/building/build-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/build-entrypoints.ts @@ -17,12 +17,14 @@ export async function buildEntrypoints( spinner: Ora, ): Promise> { const steps: BuildStepOutput[] = []; + for (let i = 0; i < groups.length; i++) { const group = groups[i]; const groupNames = toArray(group).map((e) => e.name); const groupNameColored = groupNames.join(pc.dim(', ')); spinner.text = pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNameColored}`; + try { steps.push(await wxt.builder.build(group)); } catch (err) { @@ -31,6 +33,7 @@ export async function buildEntrypoints( throw Error(`Failed to build ${groupNames.join(', ')}`, { cause: err }); } } + const publicAssets = await copyPublicDirectory(); return { publicAssets, steps }; @@ -41,19 +44,24 @@ async function copyPublicDirectory(): Promise { absoluteSrc: resolve(wxt.config.publicDir, file), relativeDest: file, })); + await wxt.hooks.callHook('build:publicAssets', wxt, files); + if (files.length === 0) return []; const publicAssets: BuildOutput['publicAssets'] = []; + for (const file of files) { const absoluteDest = resolve(wxt.config.outDir, file.relativeDest); await fs.ensureDir(dirname(absoluteDest)); + if ('absoluteSrc' in file) { await fs.copyFile(file.absoluteSrc, absoluteDest); } else { await fs.writeFile(absoluteDest, file.contents, 'utf8'); } + publicAssets.push({ type: 'asset', fileName: file.relativeDest, diff --git a/packages/wxt/src/core/utils/building/detect-dev-changes.ts b/packages/wxt/src/core/utils/building/detect-dev-changes.ts index 1fe00653e..174e4be5d 100644 --- a/packages/wxt/src/core/utils/building/detect-dev-changes.ts +++ b/packages/wxt/src/core/utils/building/detect-dev-changes.ts @@ -4,8 +4,8 @@ import { EntrypointGroup, OutputFile, } from '../../../types'; -import { every, some } from '../../utils/arrays'; -import { normalizePath } from '../../utils/paths'; +import { every, some } from '../arrays'; +import { normalizePath } from '../paths'; import { wxt } from '../../wxt'; /** @@ -40,17 +40,20 @@ export function detectDevChanges( changedFiles, (file) => file === wxt.config.userConfigMetadata.configFile, ); + if (isConfigChange) return { type: 'full-restart' }; const isWxtModuleChange = some(changedFiles, (file) => file.startsWith(wxt.config.modulesDir), ); + if (isWxtModuleChange) return { type: 'full-restart' }; const isRunnerChange = some( changedFiles, (file) => file === wxt.config.runnerConfig.configFile, ); + if (isRunnerChange) return { type: 'browser-restart' }; const changedSteps = new Set( @@ -58,10 +61,12 @@ export function detectDevChanges( findEffectedSteps(changedFile, currentOutput), ), ); + if (changedSteps.size === 0) { const hasPublicChange = some(changedFiles, (file) => file.startsWith(wxt.config.publicDir), ); + if (hasPublicChange) { return { type: 'extension-reload', @@ -78,6 +83,7 @@ export function detectDevChanges( steps: [], publicAssets: [...currentOutput.publicAssets], }; + const changedOutput: BuildOutput = { manifest: currentOutput.manifest, steps: [], @@ -95,6 +101,7 @@ export function detectDevChanges( const isOnlyHtmlChanges = changedFiles.length > 0 && every(changedFiles, (file) => file.endsWith('.html')); + if (isOnlyHtmlChanges) { return { type: 'html-reload', @@ -109,6 +116,7 @@ export function detectDevChanges( changedOutput.steps.flatMap((step) => step.entrypoints), (entry) => entry.type === 'content-script', ); + if (isOnlyContentScripts) { return { type: 'content-script-reload', @@ -158,6 +166,7 @@ function findEffectedSteps( for (const step of currentOutput.steps) { const effectedChunk = step.chunks.find((chunk) => isChunkEffected(chunk)); + if (effectedChunk) changes.push(step); } @@ -207,10 +216,6 @@ interface ExtensionReload extends RebuildChange { type: 'extension-reload'; } -// interface BrowserRestart extends RebuildChange { -// type: 'browser-restart'; -// } - interface ContentScriptReload extends RebuildChange { type: 'content-script-reload'; changedSteps: BuildStepOutput[]; diff --git a/packages/wxt/src/core/utils/building/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts index 3f6e4bcd6..d73718e86 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -5,11 +5,11 @@ import { Entrypoint, EntrypointInfo, GenericEntrypoint, + IsolatedWorldContentScriptEntrypointOptions, + MainWorldContentScriptEntrypointOptions, OptionsEntrypoint, PopupEntrypoint, SidepanelEntrypoint, - MainWorldContentScriptEntrypointOptions, - IsolatedWorldContentScriptEntrypointOptions, } from '../../../types'; import fs from 'fs-extra'; import { minimatch } from 'minimatch'; @@ -21,9 +21,9 @@ import { isHtmlEntrypoint, isJsEntrypoint, resolvePerBrowserOptions, -} from '../../utils/entrypoints'; -import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../../utils/constants'; -import { CSS_EXTENSIONS_PATTERN } from '../../utils/paths'; +} from '../entrypoints'; +import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../constants'; +import { CSS_EXTENSIONS_PATTERN } from '../paths'; import pc from 'picocolors'; import { wxt } from '../../wxt'; import { camelCase } from 'scule'; @@ -34,6 +34,7 @@ import { camelCase } from 'scule'; export async function findEntrypoints(): Promise { // Make sure required TSConfig file exists to load dependencies await fs.mkdir(wxt.config.wxtDir, { recursive: true }); + try { await fs.writeJson( resolve(wxt.config.wxtDir, 'tsconfig.json'), @@ -53,33 +54,38 @@ export async function findEntrypoints(): Promise { relativePaths.sort(); const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP); + const entrypointInfos: EntrypointInfo[] = relativePaths .reduce((results, relativePath) => { const inputPath = resolve(wxt.config.entrypointsDir, relativePath); const name = getEntrypointName(wxt.config.entrypointsDir, inputPath); + const matchingGlob = pathGlobs.find((glob) => minimatch(relativePath, glob), ); + if (matchingGlob) { const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob]; results.push({ name, inputPath, type }); } + return results; }, []) .filter(({ name, inputPath }, _, entrypointInfos) => { // Remove /index.* if /index.html exists if (inputPath.endsWith('.html')) return true; + const isIndexFile = /index\..+$/.test(inputPath); + if (!isIndexFile) return true; const hasIndexHtml = entrypointInfos.some( (entry) => entry.name === name && entry.inputPath.endsWith('index.html'), ); - if (hasIndexHtml) return false; - return true; + return !hasIndexHtml; }); await wxt.hooks.callHook('entrypoints:found', wxt, entrypointInfos); @@ -90,11 +96,13 @@ export async function findEntrypoints(): Promise { // Import entrypoints to get their config let hasBackground = false; + const entrypointOptions = await importEntrypoints(entrypointInfos); const entrypointsWithoutSkipped: Entrypoint[] = await Promise.all( entrypointInfos.map(async (info): Promise => { const { type } = info; const options = entrypointOptions[info.inputPath] ?? {}; + switch (type) { case 'popup': return await getPopupEntrypoint(info, options); @@ -157,9 +165,11 @@ export async function findEntrypoints(): Promise { await wxt.hooks.callHook('entrypoints:resolved', wxt, entrypoints); wxt.logger.debug('All entrypoints:', entrypoints); + const skippedEntrypointNames = entrypoints .filter((item) => item.skipped) .map((item) => item.name); + if (skippedEntrypointNames.length) { wxt.logger.warn( [ @@ -184,8 +194,7 @@ async function importEntrypoints(infos: EntrypointInfo[]) { await Promise.all([ // HTML ...htmlInfos.map(async (info) => { - const res = await importHtmlEntrypoint(info); - resMap[info.inputPath] = res; + resMap[info.inputPath] = await importHtmlEntrypoint(info); }), // JS (async () => { @@ -210,6 +219,7 @@ async function importHtmlEntrypoint( const { document } = parseHTML(content); const metaTags = document.querySelectorAll('meta'); + const res: Record = { title: document.querySelector('title')?.textContent || undefined, }; @@ -219,6 +229,7 @@ async function importHtmlEntrypoint( if (!name.startsWith('manifest.')) return; const key = camelCase(name.slice(9)); + try { res[key] = JSON5.parse(tag.content); } catch { @@ -238,6 +249,7 @@ function preventDuplicateEntrypointNames(files: EntrypointInfo[]) { }, {}, ); + const errorLines = Object.entries(namesToPaths).reduce( (lines, [name, absolutePaths]) => { if (absolutePaths.length > 1) { @@ -246,12 +258,15 @@ function preventDuplicateEntrypointNames(files: EntrypointInfo[]) { lines.push(` - ${relative(wxt.config.root, absolutePath)}`); }); } + return lines; }, [], ); + if (errorLines.length > 0) { const errorContent = errorLines.join('\n'); + throw Error( `Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.\n\n${errorContent}`, ); @@ -268,7 +283,7 @@ async function getPopupEntrypoint( info: EntrypointInfo, options: Record, ): Promise { - const stictOptions: PopupEntrypoint['options'] = resolvePerBrowserOptions( + const strictOptions: PopupEntrypoint['options'] = resolvePerBrowserOptions( { browserStyle: options.browserStyle, exclude: options.exclude, @@ -279,13 +294,14 @@ async function getPopupEntrypoint( }, wxt.config.browser, ); - if (stictOptions.mv2Key && stictOptions.mv2Key !== 'page_action') - stictOptions.mv2Key = 'browser_action'; + + if (strictOptions.mv2Key && strictOptions.mv2Key !== 'page_action') + strictOptions.mv2Key = 'browser_action'; return { type: 'popup', name: 'popup', - options: stictOptions, + options: strictOptions, inputPath: info.inputPath, outputDir: wxt.config.outDir, }; @@ -433,6 +449,7 @@ function isEntrypointSkipped(entry: Omit): boolean { if (exclude?.length && !include?.length) { return exclude.includes(wxt.config.browser); } + if (include?.length && !exclude?.length) { return !include.includes(wxt.config.browser); } diff --git a/packages/wxt/src/core/utils/building/group-entrypoints.ts b/packages/wxt/src/core/utils/building/group-entrypoints.ts index 6381bbd7e..04d7b707d 100644 --- a/packages/wxt/src/core/utils/building/group-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/group-entrypoints.ts @@ -14,13 +14,16 @@ export function groupEntrypoints(entrypoints: Entrypoint[]): EntrypointGroup[] { if (entry.skipped) continue; let group = ENTRY_TYPE_TO_GROUP_MAP[entry.type]; + if (entry.type === 'background' && entry.options.type === 'module') { group = 'esm'; } + if (group === 'individual') { groups.push(entry); } else { let groupIndex = groupIndexMap[group]; + if (groupIndex == null) { groupIndex = groups.push([]) - 1; groupIndexMap[group] = groupIndex; diff --git a/packages/wxt/src/core/utils/building/internal-build.ts b/packages/wxt/src/core/utils/building/internal-build.ts index 8fb88eee4..48d581b22 100644 --- a/packages/wxt/src/core/utils/building/internal-build.ts +++ b/packages/wxt/src/core/utils/building/internal-build.ts @@ -40,6 +40,7 @@ export async function internalBuild(): Promise { `${wxt.builder.name} ${wxt.builder.version}`, )}`, ); + const startTime = Date.now(); // Cleanup @@ -50,10 +51,10 @@ export async function internalBuild(): Promise { wxt.logger.debug('Detected entrypoints:', entrypoints); const validationResults = validateEntrypoints(entrypoints); + if (validationResults.errorCount + validationResults.warningCount > 0) { printValidationResults(validationResults); - } - if (validationResults.errorCount > 0) { + } else if (validationResults.errorCount > 0) { throw new ValidationError(`Entrypoint validation failed`, { cause: validationResults, }); @@ -78,6 +79,7 @@ export async function internalBuild(): Promise { if (wxt.config.analysis.enabled) { await combineAnalysisStats(); + const statsPath = relative(wxt.config.root, wxt.config.analysis.outputFile); wxt.logger.info( @@ -90,6 +92,7 @@ export async function internalBuild(): Promise { } else { wxt.logger.info(`Opening ${pc.yellow(statsPath)} in browser...`); const { default: open } = await import('open'); + await open(wxt.config.analysis.outputFile); } } diff --git a/packages/wxt/src/core/utils/testing/fake-objects.ts b/packages/wxt/src/core/utils/testing/fake-objects.ts index 923113d3f..f4edbf89f 100644 --- a/packages/wxt/src/core/utils/testing/fake-objects.ts +++ b/packages/wxt/src/core/utils/testing/fake-objects.ts @@ -25,7 +25,7 @@ import { } from '../../../types'; import { mock } from 'vitest-mock-extended'; import { vi } from 'vitest'; -import { setWxtForTesting } from '../../../core/wxt'; +import { setWxtForTesting } from '../../wxt'; import type { Browser } from '@wxt-dev/browser'; faker.seed(import.meta.test.SEED); @@ -204,10 +204,6 @@ export const fakeOutputAsset = fakeObjectCreator(() => ({ fileName: fakeFileName(), })); -export function fakeOutputFile(): OutputFile { - return faker.helpers.arrayElement([fakeOutputAsset(), fakeOutputChunk()]); -} - export const fakeManifest = fakeObjectCreator(() => ({ manifest_version: faker.helpers.arrayElement([2, 3]), name: faker.string.alphanumeric(), diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index df4d042d0..58007947a 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -1407,7 +1407,7 @@ export interface FsCache { export interface ExtensionRunner { openBrowser(): Promise; - closeBrowser(): Promise; + closeBrowser?(): Promise; /** Whether or not this runner actually opens the browser. */ canOpen?(): boolean; } From 6ecb908984cdeb0598c3f4df1cb3cc14d85dd0b9 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Mon, 29 Dec 2025 20:45:31 +0100 Subject: [PATCH 60/64] clean(packages/wxt/utils): make code more readable and add questions --- .../__tests__/content-script-context.test.ts | 10 +++++-- .../wxt/src/utils/content-script-context.ts | 6 ++++ .../content-script-ui/__tests__/index.test.ts | 28 +++++++++++-------- .../wxt/src/utils/content-script-ui/iframe.ts | 2 ++ .../utils/content-script-ui/shadow-root.ts | 2 ++ .../wxt/src/utils/content-script-ui/shared.ts | 3 ++ packages/wxt/src/utils/define-background.ts | 1 + .../wxt/src/utils/define-unlisted-script.ts | 1 + .../utils/internal/dev-server-websocket.ts | 1 + .../src/utils/internal/location-watcher.ts | 1 + 10 files changed, 40 insertions(+), 15 deletions(-) diff --git a/packages/wxt/src/utils/__tests__/content-script-context.test.ts b/packages/wxt/src/utils/__tests__/content-script-context.test.ts index 9993ebcb7..b724a0fb4 100644 --- a/packages/wxt/src/utils/__tests__/content-script-context.test.ts +++ b/packages/wxt/src/utils/__tests__/content-script-context.test.ts @@ -24,8 +24,9 @@ describe('Content Script Context', () => { const onInvalidated = vi.fn(); ctx.onInvalidated(onInvalidated); - // @ts-ignore + // @ts-expect-error -- deleting runtime.id to simulate disconnection delete fakeBrowser.runtime.id; + const isValid = ctx.isValid; expect(onInvalidated).toBeCalled(); @@ -40,7 +41,7 @@ describe('Content Script Context', () => { ctx.onInvalidated(onInvalidated); - // Wait for events to run before next tick next tick + // Wait for events to run before next tick await waitForEventsToFire(); // Create a new context after first is initialized, and wait for it to initialize @@ -57,7 +58,7 @@ describe('Content Script Context', () => { ctx.onInvalidated(onInvalidated); - // Wait for events to run before next tick next tick + // Wait for events to run before next tick await waitForEventsToFire(); // Create a new context after first is initialized, and wait for it to initialize @@ -70,10 +71,12 @@ describe('Content Script Context', () => { describe('addEventListener', () => { const context = new ContentScriptContext('test'); + it('should infer types correctly for the window target', () => { context.addEventListener(window, 'DOMContentLoaded', (_) => {}); context.addEventListener(window, 'orientationchange', (_) => {}); context.addEventListener(window, 'wxt:locationchange', (_) => {}); + // TODO: IT SEEMS IT ISN'T IN 'WINDOW`, MISSING TYPE OR IT SHOULDN'T BE HERE? // @ts-expect-error context.addEventListener(window, 'visibilitychange', (_) => {}); }); @@ -85,6 +88,7 @@ describe('Content Script Context', () => { it('should infer types correctly for HTML element targets', () => { const button = document.createElement('button'); + context.addEventListener(button, 'click', (_) => {}); context.addEventListener(button, 'mouseover', (_) => {}); }); diff --git a/packages/wxt/src/utils/content-script-context.ts b/packages/wxt/src/utils/content-script-context.ts index bd2de28dd..7bcc9daa4 100644 --- a/packages/wxt/src/utils/content-script-context.ts +++ b/packages/wxt/src/utils/content-script-context.ts @@ -74,6 +74,7 @@ export class ContentScriptContext implements AbortController { if (browser.runtime.id == null) { this.notifyInvalidated(); // Sets `signal.aborted` to true } + return this.signal.aborted; } @@ -96,6 +97,7 @@ export class ContentScriptContext implements AbortController { */ onInvalidated(cb: () => void): () => void { this.signal.addEventListener('abort', cb); + return () => this.signal.removeEventListener('abort', cb); } @@ -142,6 +144,7 @@ export class ContentScriptContext implements AbortController { return id; } + // TODO: IT SEEMS UNUSED, IT'S SHARED VIA API FOR DEVS? /** * Wrapper around `window.requestAnimationFrame` that automatically cancels the request when * invalidated. @@ -237,6 +240,7 @@ export class ContentScriptContext implements AbortController { */ notifyInvalidated() { this.abort('Content script context invalidated'); + logger.debug( `Content script "${this.contentScriptName}" context invalidated`, ); @@ -257,8 +261,10 @@ export class ContentScriptContext implements AbortController { verifyScriptStartedEvent(event: MessageEvent) { const isScriptStartedEvent = event.data?.type === ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE; + const isSameContentScript = event.data?.contentScriptName === this.contentScriptName; + const isNotDuplicate = !this.receivedMessageIds.has(event.data?.messageId); return isScriptStartedEvent && isSameContentScript && isNotDuplicate; diff --git a/packages/wxt/src/utils/content-script-ui/__tests__/index.test.ts b/packages/wxt/src/utils/content-script-ui/__tests__/index.test.ts index 95751a79b..b50c7460f 100644 --- a/packages/wxt/src/utils/content-script-ui/__tests__/index.test.ts +++ b/packages/wxt/src/utils/content-script-ui/__tests__/index.test.ts @@ -457,17 +457,17 @@ describe('Content Script UIs', () => { describe('mounted value', () => { describe('integrated', () => { it('should set the mounted value based on the onMounted return value', () => { - const expected = Symbol(); + const EXPECTED = Symbol(); const ui = createIntegratedUi(new ContentScriptContext('test'), { position: 'inline', - onMount: () => expected, + onMount: () => EXPECTED, }); expect(ui.mounted).toBeUndefined(); ui.mount(); - expect(ui.mounted).toBe(expected); + expect(ui.mounted).toBe(EXPECTED); ui.remove(); expect(ui.mounted).toBeUndefined(); @@ -476,18 +476,18 @@ describe('Content Script UIs', () => { describe('iframe', () => { it('should set the mounted value based on the onMounted return value', async () => { - const expected = Symbol(); + const EXPECTED = Symbol(); const ui = createIframeUi(new ContentScriptContext('test'), { page: '', position: 'inline', - onMount: () => expected, + onMount: () => EXPECTED, }); expect(ui.mounted).toBeUndefined(); ui.mount(); - expect(ui.mounted).toBe(expected); + expect(ui.mounted).toBe(EXPECTED); ui.remove(); expect(ui.mounted).toBeUndefined(); @@ -496,18 +496,18 @@ describe('Content Script UIs', () => { describe('shadow-root', () => { it('should set the mounted value based on the onMounted return value', async () => { - const expected = Symbol(); + const EXPECTED = Symbol(); const ui = await createShadowRootUi(new ContentScriptContext('test'), { name: 'test-component', position: 'inline', - onMount: () => expected, + onMount: () => EXPECTED, }); expect(ui.mounted).toBeUndefined(); ui.mount(); - expect(ui.mounted).toBe(expected); + expect(ui.mounted).toBe(EXPECTED); ui.remove(); expect(ui.mounted).toBeUndefined(); @@ -577,6 +577,8 @@ describe('Content Script UIs', () => { }); it('should mount when an anchor is dynamically added and unmount when an anchor is removed', async () => { + let dynamicEl; + const onRemove = vi.fn(); ui = await createUiFunction(ctx, { @@ -588,7 +590,6 @@ describe('Content Script UIs', () => { name: 'test-component', }); - let dynamicEl; ui.autoMount(); await runMicrotasks(); @@ -599,6 +600,7 @@ describe('Content Script UIs', () => { dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID }); await runMicrotasks(); + await expect .poll(() => document.querySelector(uiSelector)) .not.toBeNull(); @@ -613,6 +615,7 @@ describe('Content Script UIs', () => { describe('options', () => { it('should auto-mount only once mount and remove when the `once` option is true', async () => { + let dynamicEl; const onRemove = vi.fn(); ui = await createUiFunction(ctx, { @@ -623,7 +626,7 @@ describe('Content Script UIs', () => { page: name === 'iframe' ? '/page.html' : undefined, name: 'test-component', }); - let dynamicEl; + ui.autoMount({ once: true }); await runMicrotasks(); @@ -634,6 +637,7 @@ describe('Content Script UIs', () => { dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID }); await runMicrotasks(); + await expect .poll(() => document.querySelector(uiSelector)) .not.toBeNull(); @@ -688,6 +692,7 @@ describe('Content Script UIs', () => { describe('StopAutoMount', () => { it('should stop auto-mounting and remove ui when `ui.remove` is called', async () => { + let dynamicEl; const onRemove = vi.fn(); ui = await createUiFunction(ctx, { @@ -699,7 +704,6 @@ describe('Content Script UIs', () => { name: 'test-component', }); - let dynamicEl; ui.autoMount(); await runMicrotasks(); diff --git a/packages/wxt/src/utils/content-script-ui/iframe.ts b/packages/wxt/src/utils/content-script-ui/iframe.ts index 109c55bc7..447e69f28 100644 --- a/packages/wxt/src/utils/content-script-ui/iframe.ts +++ b/packages/wxt/src/utils/content-script-ui/iframe.ts @@ -15,6 +15,7 @@ export function createIframeUi( ): IframeContentScriptUi { const wrapper = document.createElement('div'); const iframe = document.createElement('iframe'); + // TODO: MAYBE REDEFINE IT OR IMPORT IN SOME WAY? // @ts-expect-error: getURL is defined per-project, but not inside the package iframe.src = browser.runtime.getURL(options.page); wrapper.appendChild(iframe); @@ -66,6 +67,7 @@ export type IframeContentScriptUiOptions = * The path to the HTML page that will be shown in the iframe. This string is passed into * `browser.runtime.getURL`. */ + // TODO: MAYBE REDEFINE IT OR IMPORT IN SOME WAY? // @ts-expect-error: HtmlPublicPath is generated per-project page: import('wxt/browser').HtmlPublicPath; /** diff --git a/packages/wxt/src/utils/content-script-ui/shadow-root.ts b/packages/wxt/src/utils/content-script-ui/shadow-root.ts index 047fbacc4..114c3fd22 100644 --- a/packages/wxt/src/utils/content-script-ui/shadow-root.ts +++ b/packages/wxt/src/utils/content-script-ui/shadow-root.ts @@ -122,10 +122,12 @@ export async function createShadowRootUi( */ async function loadCss(): Promise { const url = browser.runtime + // TODO: MAYBE REDEFINE IT OR IMPORT IN SOME WAY? // @ts-expect-error: getURL is defined per-project, but not inside the package .getURL(`/content-scripts/${import.meta.env.ENTRYPOINT}.css`); try { const res = await fetch(url); + return res.text(); } catch (err) { logger.warn( diff --git a/packages/wxt/src/utils/content-script-ui/shared.ts b/packages/wxt/src/utils/content-script-ui/shared.ts index 6f38476c3..1a671b26d 100644 --- a/packages/wxt/src/utils/content-script-ui/shared.ts +++ b/packages/wxt/src/utils/content-script-ui/shared.ts @@ -74,6 +74,7 @@ export function getAnchor( XPathResult.FIRST_ORDERED_NODE_TYPE, null, ); + return (result.singleNodeValue as Element) ?? undefined; } else { // If the string is a CSS selector, query the document and return the element @@ -204,6 +205,7 @@ function autoMountUi( uiCallbacks.mount(); } else { uiCallbacks.unmount(); + if (options.once) { uiCallbacks.stopAutoMount(); } @@ -221,6 +223,7 @@ function autoMountUi( } } + // TODO: MISSING AWAIT observeElement(resolvedAnchor); return { stopAutoMount: _stopAutoMount }; diff --git a/packages/wxt/src/utils/define-background.ts b/packages/wxt/src/utils/define-background.ts index f1d6a7225..243ad4350 100644 --- a/packages/wxt/src/utils/define-background.ts +++ b/packages/wxt/src/utils/define-background.ts @@ -11,5 +11,6 @@ export function defineBackground( arg: (() => void) | BackgroundDefinition, ): BackgroundDefinition { if (arg == null || typeof arg === 'function') return { main: arg }; + return arg; } diff --git a/packages/wxt/src/utils/define-unlisted-script.ts b/packages/wxt/src/utils/define-unlisted-script.ts index e3e43ca38..49bc75acf 100644 --- a/packages/wxt/src/utils/define-unlisted-script.ts +++ b/packages/wxt/src/utils/define-unlisted-script.ts @@ -13,5 +13,6 @@ export function defineUnlistedScript( arg: (() => void) | UnlistedScriptDefinition, ): UnlistedScriptDefinition { if (arg == null || typeof arg === 'function') return { main: arg }; + return arg; } diff --git a/packages/wxt/src/utils/internal/dev-server-websocket.ts b/packages/wxt/src/utils/internal/dev-server-websocket.ts index 1f63f6fd4..a25205bc8 100644 --- a/packages/wxt/src/utils/internal/dev-server-websocket.ts +++ b/packages/wxt/src/utils/internal/dev-server-websocket.ts @@ -38,6 +38,7 @@ export function getDevServerWebSocket(): WxtWebSocket { if (ws == null) { const serverUrl = __DEV_SERVER_ORIGIN__; + logger.debug('Connecting to dev server @', serverUrl); ws = new WebSocket(serverUrl, 'vite-hmr') as WxtWebSocket; diff --git a/packages/wxt/src/utils/internal/location-watcher.ts b/packages/wxt/src/utils/internal/location-watcher.ts index 085844e8d..923414010 100644 --- a/packages/wxt/src/utils/internal/location-watcher.ts +++ b/packages/wxt/src/utils/internal/location-watcher.ts @@ -18,6 +18,7 @@ export function createLocationWatcher(ctx: ContentScriptContext) { if (interval != null) return; oldUrl = new URL(location.href); + interval = ctx.setInterval(() => { let newUrl = new URL(location.href); if (newUrl.href !== oldUrl.href) { From ee94b1df964c3790136e64fe999434ee49345686 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Mon, 29 Dec 2025 20:47:50 +0100 Subject: [PATCH 61/64] clean(packages/wxt/utils): make code more readable, add question and remove unnecessary `await` --- packages/wxt/src/virtual/background-entrypoint.ts | 1 + .../wxt/src/virtual/content-script-isolated-world-entrypoint.ts | 2 +- packages/wxt/src/virtual/utils/reload-content-scripts.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/wxt/src/virtual/background-entrypoint.ts b/packages/wxt/src/virtual/background-entrypoint.ts index 75d8f637c..00a9d91e8 100644 --- a/packages/wxt/src/virtual/background-entrypoint.ts +++ b/packages/wxt/src/virtual/background-entrypoint.ts @@ -42,6 +42,7 @@ let result; try { initPlugins(); result = definition.main(); + // TODO: WHAT'S THE REASON FOR THIS CHECK? // @ts-expect-error: Res shouldn't be a promise, but we're checking it anyway if (result instanceof Promise) { console.warn( diff --git a/packages/wxt/src/virtual/content-script-isolated-world-entrypoint.ts b/packages/wxt/src/virtual/content-script-isolated-world-entrypoint.ts index 4bd388331..4e6c7c95f 100644 --- a/packages/wxt/src/virtual/content-script-isolated-world-entrypoint.ts +++ b/packages/wxt/src/virtual/content-script-isolated-world-entrypoint.ts @@ -9,7 +9,7 @@ const result = (async () => { const { main, ...options } = definition; const ctx = new ContentScriptContext(import.meta.env.ENTRYPOINT, options); - return await main(ctx); + return main(ctx); } catch (err) { logger.error( `The content script "${import.meta.env.ENTRYPOINT}" crashed on startup!`, diff --git a/packages/wxt/src/virtual/utils/reload-content-scripts.ts b/packages/wxt/src/virtual/utils/reload-content-scripts.ts index eb331d1f7..83e927035 100644 --- a/packages/wxt/src/virtual/utils/reload-content-scripts.ts +++ b/packages/wxt/src/virtual/utils/reload-content-scripts.ts @@ -76,6 +76,7 @@ export async function reloadRuntimeContentScriptMv3( const matches = registered.filter((cs) => { const hasJs = contentScript.js?.find((js) => cs.js?.includes(js)); const hasCss = contentScript.css?.find((css) => cs.css?.includes(css)); + return hasJs || hasCss; }); From 97c4780cf9f266013575e5418fa38497ce84ba9c Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Mon, 29 Dec 2025 20:49:56 +0100 Subject: [PATCH 62/64] clean(packages/wxt/src): make code more readable, add question --- packages/wxt/src/modules.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/wxt/src/modules.ts b/packages/wxt/src/modules.ts index c1bd87d1e..3dee2b0fa 100644 --- a/packages/wxt/src/modules.ts +++ b/packages/wxt/src/modules.ts @@ -74,10 +74,12 @@ export function addEntrypoint(wxt: Wxt, entrypoint: Entrypoint): void { export function addPublicAssets(wxt: Wxt, dir: string): void { wxt.hooks.hook('build:publicAssets', async (wxt, files) => { const moreFiles = await glob('**/*', { cwd: dir }); + if (moreFiles.length === 0) { wxt.logger.warn('No files to copy in', dir); return; } + moreFiles.forEach((file) => { files.unshift({ absoluteSrc: resolve(dir, file), relativeDest: file }); }); @@ -181,6 +183,7 @@ export function addImportPreset( }); } +// TODO: IT SEEMS UNUSED, IT'S AVAILABLE VIA API TO DEVS? /** * Adds an import alias to the project's TSConfig paths and bundler. Path can * be absolute or relative to the project's root directory. @@ -218,6 +221,7 @@ export function addAlias(wxt: Wxt, alias: string, path: string) { ); return; } + wxt.config.alias[alias] = target; }); } From e84bda0bc5a20943bb80f1c96253de6740681416 Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Mon, 29 Dec 2025 21:00:14 +0100 Subject: [PATCH 63/64] clean(packages/wxt): make code more readable --- packages/wxt/tsdown.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/wxt/tsdown.config.ts b/packages/wxt/tsdown.config.ts index 635ef0746..2ea0550c4 100644 --- a/packages/wxt/tsdown.config.ts +++ b/packages/wxt/tsdown.config.ts @@ -55,8 +55,10 @@ async function replaceVars( vars: Record, ): Promise { let text = await readFile(file, 'utf8'); + Object.entries(vars).forEach(([name, value]) => { text = text.replaceAll(`{{${name}}}`, value); }); + await writeFile(file, text, 'utf8'); } From ff884954b902a05b548c43d6c20e55fac7876f3a Mon Sep 17 00:00:00 2001 From: PatrykKuniczak Date: Mon, 29 Dec 2025 21:00:34 +0100 Subject: [PATCH 64/64] fix(packages/wxt): make type for __ENTRYPOINT__ --- packages/wxt/globals.d.ts | 3 +++ packages/wxt/vitest.globalSetup.ts | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 packages/wxt/globals.d.ts diff --git a/packages/wxt/globals.d.ts b/packages/wxt/globals.d.ts new file mode 100644 index 000000000..dcf2642c5 --- /dev/null +++ b/packages/wxt/globals.d.ts @@ -0,0 +1,3 @@ +declare module globalThis { + var __ENTRYPOINT__: string; +} diff --git a/packages/wxt/vitest.globalSetup.ts b/packages/wxt/vitest.globalSetup.ts index 3da3b5ff5..d4a92be7f 100644 --- a/packages/wxt/vitest.globalSetup.ts +++ b/packages/wxt/vitest.globalSetup.ts @@ -9,7 +9,6 @@ export async function setup() { setupHappened = true; - // @ts-expect-error globalThis.__ENTRYPOINT__ = 'test'; const E2E_DIST_PATH = './e2e/dist/';