Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/wxt/e2e/tests/modules.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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',
Expand Down
100 changes: 100 additions & 0 deletions packages/wxt/e2e/tests/output-structure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
14 changes: 12 additions & 2 deletions packages/wxt/src/core/builders/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' ||
Expand All @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions packages/wxt/src/core/builders/vite/plugins/iifeAnonymous.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
},
};
}
1 change: 1 addition & 0 deletions packages/wxt/src/core/builders/vite/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from './removeEntrypointMainFunction';
export * from './wxtPluginLoader';
export * from './resolveAppConfig';
export * from './iifeFooter';
export * from './iifeAnonymous';
3 changes: 2 additions & 1 deletion packages/wxt/src/core/utils/building/find-entrypoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
SidepanelEntrypoint,
MainWorldContentScriptEntrypointOptions,
IsolatedWorldContentScriptEntrypointOptions,
UnlistedScriptEntrypoint,
} from '../../../types';
import fs from 'fs-extra';
import { minimatch } from 'minimatch';
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/wxt/src/core/utils/testing/fake-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Wxt,
SidepanelEntrypoint,
BaseEntrypoint,
UnlistedScriptEntrypoint,
} from '../../../types';
import { mock } from 'vitest-mock-extended';
import { vi } from 'vitest';
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -186,7 +187,6 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
'newtab',
'devtools',
'unlisted-page',
'unlisted-script',
]),
inputPath: fakeFile('src'),
name: faker.string.alpha(),
Expand Down
32 changes: 29 additions & 3 deletions packages/wxt/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -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>;
Expand Down Expand Up @@ -786,6 +811,7 @@ export interface SidepanelEntrypoint extends BaseEntrypoint {
export type Entrypoint =
| GenericEntrypoint
| BackgroundEntrypoint
| UnlistedScriptEntrypoint
| ContentScriptEntrypoint
| PopupEntrypoint
| OptionsEntrypoint
Expand Down Expand Up @@ -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.
*
Expand Down
Loading