feat: Add globalName entrypoint option. (#2017)

This commit is contained in:
Jonathan Viney
2026-02-23 10:05:38 +13:00
committed by Aaron
parent 68dd3fc027
commit c6fdc5a5d5
8 changed files with 165 additions and 10 deletions
+2 -2
View File
@@ -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',
@@ -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);
});
});
});
+12 -2
View File
@@ -109,7 +109,7 @@ export async function createViteBuilder(
const plugins: NonNullable<vite.UserConfig['plugins']> = [
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 {
@@ -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');
}
}
},
};
}
@@ -14,3 +14,4 @@ export * from './removeEntrypointMainFunction';
export * from './wxtPluginLoader';
export * from './resolveAppConfig';
export * from './iifeFooter';
export * from './iifeAnonymous';
@@ -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<string, any>,
): Promise<GenericEntrypoint> {
): Promise<UnlistedScriptEntrypoint> {
return {
type: 'unlisted-script',
name,
@@ -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<BackgroundEntrypoint>(
);
export const fakeUnlistedScriptEntrypoint =
fakeObjectCreator<GenericEntrypoint>(() => ({
fakeObjectCreator<UnlistedScriptEntrypoint>(() => ({
type: 'unlisted-script',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
@@ -186,7 +187,6 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
'newtab',
'devtools',
'unlisted-page',
'unlisted-script',
]),
inputPath: fakeFile('src'),
name: faker.string.alpha(),
+29 -3
View File
@@ -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 <script> tag.
*
* Available options:
* - `true`: automatically generate a name for the IIFE based on the entrypoint name
* - `false`: Output the IIFE without a variable name, making it anonymous. This is the safest option
* to avoid conflicts with existing variables on the page. This will become the default in a future version of WXT.
* - `string`: Use the provided string as the global variable name.
* - `function`: A function that receives the entrypoint and returns a string to use as the variable name.
*
* @default true
*/
globalName?: string | boolean | ((entrypoint: Entrypoint) => string);
}
export interface BaseContentScriptEntrypointOptions extends BaseScriptEntrypointOptions {
matches?: PerBrowserOption<NonNullable<ManifestContentScript['matches']>>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
@@ -749,12 +770,16 @@ export interface GenericEntrypoint extends BaseEntrypoint {
| 'newtab'
| 'devtools'
| 'unlisted-page'
| 'unlisted-script'
| 'unlisted-style'
| 'content-script-style';
options: ResolvedPerBrowserOptions<BaseEntrypointOptions>;
}
export interface UnlistedScriptEntrypoint extends BaseEntrypoint {
type: 'unlisted-script';
options: ResolvedPerBrowserOptions<BaseScriptEntrypointOptions>;
}
export interface BackgroundEntrypoint extends BaseEntrypoint {
type: 'background';
options: ResolvedPerBrowserOptions<BackgroundEntrypointOptions>;
@@ -786,6 +811,7 @@ export interface SidepanelEntrypoint extends BaseEntrypoint {
export type Entrypoint =
| GenericEntrypoint
| BackgroundEntrypoint
| UnlistedScriptEntrypoint
| ContentScriptEntrypoint
| PopupEntrypoint
| OptionsEntrypoint
@@ -835,7 +861,7 @@ export interface BackgroundDefinition extends BackgroundEntrypointOptions {
main(): void;
}
export interface UnlistedScriptDefinition extends BaseEntrypointOptions {
export interface UnlistedScriptDefinition extends BaseScriptEntrypointOptions {
/**
* Main function executed when the unlisted script is ran.
*