diff --git a/packages/wxt/e2e/tests/modules.test.ts b/packages/wxt/e2e/tests/modules.test.ts index 4d6af641e..af3ee0cf6 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 { TestProject } from '../utils'; -import type { GenericEntrypoint, InlineConfig } from '../../src'; +import type { InlineConfig, UnlistedScriptEntrypoint } from '../../src'; import { readFile } from 'fs-extra'; import { normalizePath } from '../../src'; @@ -49,7 +49,7 @@ describe('Module Helpers', () => { 'export default defineBackground(() => {})', ); - const entrypoint: GenericEntrypoint = { + const entrypoint: UnlistedScriptEntrypoint = { type: 'unlisted-script', inputPath: project.resolvePath('modules/test/injected.ts'), name: 'injected', diff --git a/packages/wxt/e2e/tests/output-structure.test.ts b/packages/wxt/e2e/tests/output-structure.test.ts index 3b0dc9339..2a691a9fe 100644 --- a/packages/wxt/e2e/tests/output-structure.test.ts +++ b/packages/wxt/e2e/tests/output-structure.test.ts @@ -459,4 +459,104 @@ describe('Output Directory Structure', () => { " `); }); + + describe('globalName option', () => { + it('generates an IIFE with a default name', async () => { + const project = new TestProject(); + project.addFile( + 'entrypoints/content.js', + `export default defineContentScript({ + matches: ["*://*/*"], + main() {}, + })`, + ); + + await project.build({ vite: () => ({ build: { minify: false } }) }); + + const output = await project.serializeFile( + '.output/chrome-mv3/content-scripts/content.js', + ); + expect(output).toMatch(/^var content\s?=[\s\S]*^content;$/gm); + }); + + it('generates an IIFE with a specific name', async () => { + const project = new TestProject(); + project.addFile( + 'entrypoints/content.js', + `export default defineContentScript({ + globalName: "MyContentScript", + matches: ["*://*/*"], + main() {}, + })`, + ); + + await project.build({ vite: () => ({ build: { minify: false } }) }); + + const output = await project.serializeFile( + '.output/chrome-mv3/content-scripts/content.js', + ); + expect(output).toMatch( + /^var MyContentScript =[\s\S]*^MyContentScript;$/gm, + ); + }); + + it('generates an IIFE with a specific name provided by a function', async () => { + const project = new TestProject(); + project.addFile( + 'entrypoints/content.js', + `export default defineContentScript({ + globalName: () => "MyContentScript", + matches: ["*://*/*"], + main() {}, + })`, + ); + + await project.build({ vite: () => ({ build: { minify: false } }) }); + + const output = await project.serializeFile( + '.output/chrome-mv3/content-scripts/content.js', + ); + expect(output).toMatch( + /^var MyContentScript =[\s\S]*^MyContentScript;$/gm, + ); + }); + + it('generates an anonymous IIFE when not minified', async () => { + const project = new TestProject(); + project.addFile( + 'entrypoints/content.js', + `export default defineContentScript({ + globalName: false, + matches: ["*://*/*"], + main() {}, + })`, + ); + + await project.build({ vite: () => ({ build: { minify: false } }) }); + + const output = await project.serializeFile( + '.output/chrome-mv3/content-scripts/content.js', + ); + expect(output).toMatch(/^\(function\(\) {[\s\S]*^}\)\(\);$/gm); + }); + + it('generates an anonymous IIFE when minified', async () => { + const project = new TestProject(); + project.addFile( + 'entrypoints/content.js', + `export default defineContentScript({ + globalName: false, + matches: ["*://*/*"], + main() {}, + })`, + ); + + await project.build({ vite: () => ({ build: { minify: true } }) }); + + const output = await project.serializeFile( + '.output/chrome-mv3/content-scripts/content.js', + ); + expect(output).toMatch(/^\(function\(\){[\s\S]*}\)\(\);$/gm); + }); + }); }); diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts index 4cbbc07df..986a99bd3 100644 --- a/packages/wxt/src/core/builders/vite/index.ts +++ b/packages/wxt/src/core/builders/vite/index.ts @@ -109,7 +109,7 @@ export async function createViteBuilder( const plugins: NonNullable = [ wxtPlugins.entrypointGroupGlobals(entrypoint), ]; - const iifeReturnValueName = safeVarName(entrypoint.name); + let iifeReturnValueName = safeVarName(entrypoint.name); if ( entrypoint.type === 'content-script-style' || @@ -122,7 +122,17 @@ export async function createViteBuilder( entrypoint.type === 'content-script' || entrypoint.type === 'unlisted-script' ) { - plugins.push(wxtPlugins.iifeFooter(iifeReturnValueName)); + if (typeof entrypoint.options.globalName === 'string') { + iifeReturnValueName = entrypoint.options.globalName; + } else if (typeof entrypoint.options.globalName === 'function') { + iifeReturnValueName = entrypoint.options.globalName(entrypoint); + } + + if (entrypoint.options.globalName === false) { + plugins.push(wxtPlugins.iifeAnonymous(iifeReturnValueName)); + } else { + plugins.push(wxtPlugins.iifeFooter(iifeReturnValueName)); + } } return { diff --git a/packages/wxt/src/core/builders/vite/plugins/iifeAnonymous.ts b/packages/wxt/src/core/builders/vite/plugins/iifeAnonymous.ts new file mode 100644 index 000000000..fe4e0cc28 --- /dev/null +++ b/packages/wxt/src/core/builders/vite/plugins/iifeAnonymous.ts @@ -0,0 +1,17 @@ +import type { Plugin } from 'vite'; + +export function iifeAnonymous(iifeReturnValueName: string): Plugin { + return { + name: 'wxt:iife-anonymous', + generateBundle(_, bundle) { + for (const chunk of Object.values(bundle)) { + if (chunk.type === 'chunk' && chunk.isEntry) { + const namedIIFEPrefix = new RegExp( + `^var ${iifeReturnValueName}\\s*=\\s*(\\(function)`, + ); + chunk.code = chunk.code.replace(namedIIFEPrefix, '$1'); + } + } + }, + }; +} diff --git a/packages/wxt/src/core/builders/vite/plugins/index.ts b/packages/wxt/src/core/builders/vite/plugins/index.ts index 27ea24c82..703b3bb04 100644 --- a/packages/wxt/src/core/builders/vite/plugins/index.ts +++ b/packages/wxt/src/core/builders/vite/plugins/index.ts @@ -14,3 +14,4 @@ export * from './removeEntrypointMainFunction'; export * from './wxtPluginLoader'; export * from './resolveAppConfig'; export * from './iifeFooter'; +export * from './iifeAnonymous'; diff --git a/packages/wxt/src/core/utils/building/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts index f6992fe0d..49c72c54b 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -10,6 +10,7 @@ import { SidepanelEntrypoint, MainWorldContentScriptEntrypointOptions, IsolatedWorldContentScriptEntrypointOptions, + UnlistedScriptEntrypoint, } from '../../../types'; import fs from 'fs-extra'; import { minimatch } from 'minimatch'; @@ -319,7 +320,7 @@ async function getUnlistedPageEntrypoint( async function getUnlistedScriptEntrypoint( { inputPath, name }: EntrypointInfo, options: Record, -): Promise { +): Promise { return { type: 'unlisted-script', name, diff --git a/packages/wxt/src/core/utils/testing/fake-objects.ts b/packages/wxt/src/core/utils/testing/fake-objects.ts index 62b2154d2..c21e03412 100644 --- a/packages/wxt/src/core/utils/testing/fake-objects.ts +++ b/packages/wxt/src/core/utils/testing/fake-objects.ts @@ -21,6 +21,7 @@ import { Wxt, SidepanelEntrypoint, BaseEntrypoint, + UnlistedScriptEntrypoint, } from '../../../types'; import { mock } from 'vitest-mock-extended'; import { vi } from 'vitest'; @@ -99,7 +100,7 @@ export const fakeBackgroundEntrypoint = fakeObjectCreator( ); export const fakeUnlistedScriptEntrypoint = - fakeObjectCreator(() => ({ + fakeObjectCreator(() => ({ type: 'unlisted-script', inputPath: fakeFile('src'), name: faker.string.alpha(), @@ -186,7 +187,6 @@ export const fakeGenericEntrypoint = fakeObjectCreator( 'newtab', 'devtools', 'unlisted-page', - 'unlisted-script', ]), inputPath: fakeFile('src'), name: faker.string.alpha(), diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index 04b5306e4..65a4c8e8d 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -566,7 +566,28 @@ export interface BackgroundEntrypointOptions extends BaseEntrypointOptions { type?: PerBrowserOption<'module'>; } -export interface BaseContentScriptEntrypointOptions extends BaseEntrypointOptions { +export interface BaseScriptEntrypointOptions extends BaseEntrypointOptions { + /** + * The variable name for the IIFE in the output bundle. + * + * This option is relevant for scripts inserted into the page context where the default IIFE + * variable name may conflict with an existing variable on the target page. This applies to content + * scripts with world=MAIN, and others, such as unlisted scripts, that could be dynamically injected + * into the page with a