diff --git a/packages/wxt-demo/src/entrypoints/background.ts b/packages/wxt-demo/src/entrypoints/background.ts index 096261015..d63cb2120 100644 --- a/packages/wxt-demo/src/entrypoints/background.ts +++ b/packages/wxt-demo/src/entrypoints/background.ts @@ -1,5 +1,6 @@ export default defineBackground({ // type: 'module', + globalName: false, main() { console.log(browser.runtime.id); diff --git a/packages/wxt/e2e/tests/output-structure.test.ts b/packages/wxt/e2e/tests/output-structure.test.ts index 0db84f041..49cd40d43 100644 --- a/packages/wxt/e2e/tests/output-structure.test.ts +++ b/packages/wxt/e2e/tests/output-structure.test.ts @@ -459,4 +459,377 @@ 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(); + + expect( + await project.serializeFile( + '.output/chrome-mv3/content-scripts/content.js', + ), + ).toMatchInlineSnapshot(` + ".output/chrome-mv3/content-scripts/content.js + ---------------------------------------- + var content=(function(){"use strict";function E(e){return e}const h={matches:["*://*/*"],main(){}};function s(e,...t){}const u={debug:(...e)=>s(console.debug,...e),log:(...e)=>s(console.log,...e),warn:(...e)=>s(console.warn,...e),error:(...e)=>s(console.error,...e)},l=globalThis.browser?.runtime?.id?globalThis.browser:globalThis.chrome;var g=class d extends Event{static EVENT_NAME=a("wxt:locationchange");constructor(t,n){super(d.EVENT_NAME,{}),this.newUrl=t,this.oldUrl=n}};function a(e){return\`\${l?.runtime?.id}:content:\${e}\`}function v(e){let t,n;return{run(){t==null&&(n=new URL(location.href),t=e.setInterval(()=>{let r=new URL(location.href);r.href!==n.href&&(window.dispatchEvent(new g(r,n)),n=r)},1e3))}}}var m=class c{static SCRIPT_STARTED_MESSAGE_TYPE=a("wxt:content-script-started");isTopFrame=window.self===window.top;abortController;locationWatcher=v(this);receivedMessageIds=new Set;constructor(t,n){this.contentScriptName=t,this.options=n,this.abortController=new AbortController,this.isTopFrame?(this.listenForNewerScripts({ignoreFirstEvent:!0}),this.stopOldScripts()):this.listenForNewerScripts()}get signal(){return this.abortController.signal}abort(t){return this.abortController.abort(t)}get isInvalid(){return l.runtime.id==null&&this.notifyInvalidated(),this.signal.aborted}get isValid(){return!this.isInvalid}onInvalidated(t){return this.signal.addEventListener("abort",t),()=>this.signal.removeEventListener("abort",t)}block(){return new Promise(()=>{})}setInterval(t,n){const r=setInterval(()=>{this.isValid&&t()},n);return this.onInvalidated(()=>clearInterval(r)),r}setTimeout(t,n){const r=setTimeout(()=>{this.isValid&&t()},n);return this.onInvalidated(()=>clearTimeout(r)),r}requestAnimationFrame(t){const n=requestAnimationFrame((...r)=>{this.isValid&&t(...r)});return this.onInvalidated(()=>cancelAnimationFrame(n)),n}requestIdleCallback(t,n){const r=requestIdleCallback((...i)=>{this.signal.aborted||t(...i)},n);return this.onInvalidated(()=>cancelIdleCallback(r)),r}addEventListener(t,n,r,i){n==="wxt:locationchange"&&this.isValid&&this.locationWatcher.run(),t.addEventListener?.(n.startsWith("wxt:")?a(n):n,r,{...i,signal:this.signal})}notifyInvalidated(){this.abort("Content script context invalidated"),u.debug(\`Content script "\${this.contentScriptName}" context invalidated\`)}stopOldScripts(){window.postMessage({type:c.SCRIPT_STARTED_MESSAGE_TYPE,contentScriptName:this.contentScriptName,messageId:Math.random().toString(36).slice(2)},"*")}verifyScriptStartedEvent(t){const n=t.data?.type===c.SCRIPT_STARTED_MESSAGE_TYPE,r=t.data?.contentScriptName===this.contentScriptName,i=!this.receivedMessageIds.has(t.data?.messageId);return n&&r&&i}listenForNewerScripts(t){let n=!0;const r=i=>{if(this.verifyScriptStartedEvent(i)){this.receivedMessageIds.add(i.data.messageId);const S=n;if(n=!1,S&&t?.ignoreFirstEvent)return;this.notifyInvalidated()}};addEventListener("message",r),this.onInvalidated(()=>removeEventListener("message",r))}};function b(){}function o(e,...t){}const p={debug:(...e)=>o(console.debug,...e),log:(...e)=>o(console.log,...e),warn:(...e)=>o(console.warn,...e),error:(...e)=>o(console.error,...e)};var w=(async()=>{try{const{main:e,...t}=h;return await e(new m("content",t))}catch(e){throw p.error('The content script "content" crashed on startup!',e),e}})();return w})(); + content;" + `); + }); + + 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(); + + expect( + await project.serializeFile( + '.output/chrome-mv3/content-scripts/content.js', + ), + ).toMatchInlineSnapshot(` + ".output/chrome-mv3/content-scripts/content.js + ---------------------------------------- + var MyContentScript=(function(){"use strict";function E(e){return e}const h={globalName:"MyContentScript",matches:["*://*/*"],main(){}};function s(e,...t){}const u={debug:(...e)=>s(console.debug,...e),log:(...e)=>s(console.log,...e),warn:(...e)=>s(console.warn,...e),error:(...e)=>s(console.error,...e)},l=globalThis.browser?.runtime?.id?globalThis.browser:globalThis.chrome;var g=class d extends Event{static EVENT_NAME=a("wxt:locationchange");constructor(t,n){super(d.EVENT_NAME,{}),this.newUrl=t,this.oldUrl=n}};function a(e){return\`\${l?.runtime?.id}:content:\${e}\`}function v(e){let t,n;return{run(){t==null&&(n=new URL(location.href),t=e.setInterval(()=>{let r=new URL(location.href);r.href!==n.href&&(window.dispatchEvent(new g(r,n)),n=r)},1e3))}}}var p=class c{static SCRIPT_STARTED_MESSAGE_TYPE=a("wxt:content-script-started");isTopFrame=window.self===window.top;abortController;locationWatcher=v(this);receivedMessageIds=new Set;constructor(t,n){this.contentScriptName=t,this.options=n,this.abortController=new AbortController,this.isTopFrame?(this.listenForNewerScripts({ignoreFirstEvent:!0}),this.stopOldScripts()):this.listenForNewerScripts()}get signal(){return this.abortController.signal}abort(t){return this.abortController.abort(t)}get isInvalid(){return l.runtime.id==null&&this.notifyInvalidated(),this.signal.aborted}get isValid(){return!this.isInvalid}onInvalidated(t){return this.signal.addEventListener("abort",t),()=>this.signal.removeEventListener("abort",t)}block(){return new Promise(()=>{})}setInterval(t,n){const r=setInterval(()=>{this.isValid&&t()},n);return this.onInvalidated(()=>clearInterval(r)),r}setTimeout(t,n){const r=setTimeout(()=>{this.isValid&&t()},n);return this.onInvalidated(()=>clearTimeout(r)),r}requestAnimationFrame(t){const n=requestAnimationFrame((...r)=>{this.isValid&&t(...r)});return this.onInvalidated(()=>cancelAnimationFrame(n)),n}requestIdleCallback(t,n){const r=requestIdleCallback((...i)=>{this.signal.aborted||t(...i)},n);return this.onInvalidated(()=>cancelIdleCallback(r)),r}addEventListener(t,n,r,i){n==="wxt:locationchange"&&this.isValid&&this.locationWatcher.run(),t.addEventListener?.(n.startsWith("wxt:")?a(n):n,r,{...i,signal:this.signal})}notifyInvalidated(){this.abort("Content script context invalidated"),u.debug(\`Content script "\${this.contentScriptName}" context invalidated\`)}stopOldScripts(){window.postMessage({type:c.SCRIPT_STARTED_MESSAGE_TYPE,contentScriptName:this.contentScriptName,messageId:Math.random().toString(36).slice(2)},"*")}verifyScriptStartedEvent(t){const n=t.data?.type===c.SCRIPT_STARTED_MESSAGE_TYPE,r=t.data?.contentScriptName===this.contentScriptName,i=!this.receivedMessageIds.has(t.data?.messageId);return n&&r&&i}listenForNewerScripts(t){let n=!0;const r=i=>{if(this.verifyScriptStartedEvent(i)){this.receivedMessageIds.add(i.data.messageId);const w=n;if(n=!1,w&&t?.ignoreFirstEvent)return;this.notifyInvalidated()}};addEventListener("message",r),this.onInvalidated(()=>removeEventListener("message",r))}};function b(){}function o(e,...t){}const m={debug:(...e)=>o(console.debug,...e),log:(...e)=>o(console.log,...e),warn:(...e)=>o(console.warn,...e),error:(...e)=>o(console.error,...e)};var S=(async()=>{try{const{main:e,...t}=h;return await e(new p("content",t))}catch(e){throw m.error('The content script "content" crashed on startup!',e),e}})();return S})(); + MyContentScript;" + `); + }); + + it('generates an IIFE with a variable name returned from a provided function', async () => { + const project = new TestProject(); + project.addFile( + 'entrypoints/content.js', + `export default defineContentScript({ + globalName: (entrypoint) => entrypoint.name + "_dynamic", + matches: ["*://*/*"], + main() {}, + })`, + ); + + await project.build(); + + expect( + await project.serializeFile( + '.output/chrome-mv3/content-scripts/content.js', + ), + ).toMatchInlineSnapshot(` + ".output/chrome-mv3/content-scripts/content.js + ---------------------------------------- + var content_dynamic=(function(){"use strict";function E(e){return e}const h={globalName:e=>e.name+"_dynamic",matches:["*://*/*"],main(){}};function s(e,...t){}const u={debug:(...e)=>s(console.debug,...e),log:(...e)=>s(console.log,...e),warn:(...e)=>s(console.warn,...e),error:(...e)=>s(console.error,...e)},l=globalThis.browser?.runtime?.id?globalThis.browser:globalThis.chrome;var g=class d extends Event{static EVENT_NAME=a("wxt:locationchange");constructor(t,n){super(d.EVENT_NAME,{}),this.newUrl=t,this.oldUrl=n}};function a(e){return\`\${l?.runtime?.id}:content:\${e}\`}function v(e){let t,n;return{run(){t==null&&(n=new URL(location.href),t=e.setInterval(()=>{let r=new URL(location.href);r.href!==n.href&&(window.dispatchEvent(new g(r,n)),n=r)},1e3))}}}var m=class c{static SCRIPT_STARTED_MESSAGE_TYPE=a("wxt:content-script-started");isTopFrame=window.self===window.top;abortController;locationWatcher=v(this);receivedMessageIds=new Set;constructor(t,n){this.contentScriptName=t,this.options=n,this.abortController=new AbortController,this.isTopFrame?(this.listenForNewerScripts({ignoreFirstEvent:!0}),this.stopOldScripts()):this.listenForNewerScripts()}get signal(){return this.abortController.signal}abort(t){return this.abortController.abort(t)}get isInvalid(){return l.runtime.id==null&&this.notifyInvalidated(),this.signal.aborted}get isValid(){return!this.isInvalid}onInvalidated(t){return this.signal.addEventListener("abort",t),()=>this.signal.removeEventListener("abort",t)}block(){return new Promise(()=>{})}setInterval(t,n){const r=setInterval(()=>{this.isValid&&t()},n);return this.onInvalidated(()=>clearInterval(r)),r}setTimeout(t,n){const r=setTimeout(()=>{this.isValid&&t()},n);return this.onInvalidated(()=>clearTimeout(r)),r}requestAnimationFrame(t){const n=requestAnimationFrame((...r)=>{this.isValid&&t(...r)});return this.onInvalidated(()=>cancelAnimationFrame(n)),n}requestIdleCallback(t,n){const r=requestIdleCallback((...i)=>{this.signal.aborted||t(...i)},n);return this.onInvalidated(()=>cancelIdleCallback(r)),r}addEventListener(t,n,r,i){n==="wxt:locationchange"&&this.isValid&&this.locationWatcher.run(),t.addEventListener?.(n.startsWith("wxt:")?a(n):n,r,{...i,signal:this.signal})}notifyInvalidated(){this.abort("Content script context invalidated"),u.debug(\`Content script "\${this.contentScriptName}" context invalidated\`)}stopOldScripts(){window.postMessage({type:c.SCRIPT_STARTED_MESSAGE_TYPE,contentScriptName:this.contentScriptName,messageId:Math.random().toString(36).slice(2)},"*")}verifyScriptStartedEvent(t){const n=t.data?.type===c.SCRIPT_STARTED_MESSAGE_TYPE,r=t.data?.contentScriptName===this.contentScriptName,i=!this.receivedMessageIds.has(t.data?.messageId);return n&&r&&i}listenForNewerScripts(t){let n=!0;const r=i=>{if(this.verifyScriptStartedEvent(i)){this.receivedMessageIds.add(i.data.messageId);const S=n;if(n=!1,S&&t?.ignoreFirstEvent)return;this.notifyInvalidated()}};addEventListener("message",r),this.onInvalidated(()=>removeEventListener("message",r))}};function b(){}function o(e,...t){}const p={debug:(...e)=>o(console.debug,...e),log:(...e)=>o(console.log,...e),warn:(...e)=>o(console.warn,...e),error:(...e)=>o(console.error,...e)};var w=(async()=>{try{const{main:e,...t}=h;return await e(new m("content",t))}catch(e){throw p.error('The content script "content" crashed on startup!',e),e}})();return w})(); + content_dynamic;" + `); + }); + + 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 } }) }); + + expect( + await project.serializeFile( + '.output/chrome-mv3/content-scripts/content.js', + ), + ).toMatchInlineSnapshot(` + ".output/chrome-mv3/content-scripts/content.js + ---------------------------------------- + (function() { + "use strict"; + function defineContentScript(definition2) { + return definition2; + } + const definition = defineContentScript({ + globalName: false, + matches: ["*://*/*"], + main() { + } + }); + function print$1(method, ...args) { + return; + } + const logger$1 = { + debug: (...args) => print$1(console.debug, ...args), + log: (...args) => print$1(console.log, ...args), + warn: (...args) => print$1(console.warn, ...args), + error: (...args) => print$1(console.error, ...args) + }; + const browser$1 = globalThis.browser?.runtime?.id ? globalThis.browser : globalThis.chrome; + const browser = browser$1; + var WxtLocationChangeEvent = class WxtLocationChangeEvent2 extends Event { + static EVENT_NAME = getUniqueEventName("wxt:locationchange"); + constructor(newUrl, oldUrl) { + super(WxtLocationChangeEvent2.EVENT_NAME, {}); + this.newUrl = newUrl; + this.oldUrl = oldUrl; + } + }; + function getUniqueEventName(eventName) { + return \`\${browser?.runtime?.id}:\${"content"}:\${eventName}\`; + } + function createLocationWatcher(ctx) { + let interval; + let oldUrl; + return { run() { + if (interval != null) return; + oldUrl = new URL(location.href); + interval = ctx.setInterval(() => { + let newUrl = new URL(location.href); + if (newUrl.href !== oldUrl.href) { + window.dispatchEvent(new WxtLocationChangeEvent(newUrl, oldUrl)); + oldUrl = newUrl; + } + }, 1e3); + } }; + } + var ContentScriptContext = class ContentScriptContext2 { + static SCRIPT_STARTED_MESSAGE_TYPE = getUniqueEventName("wxt:content-script-started"); + isTopFrame = window.self === window.top; + abortController; + locationWatcher = createLocationWatcher(this); + receivedMessageIds = /* @__PURE__ */ new Set(); + constructor(contentScriptName, options) { + this.contentScriptName = contentScriptName; + this.options = options; + this.abortController = new AbortController(); + if (this.isTopFrame) { + this.listenForNewerScripts({ ignoreFirstEvent: true }); + this.stopOldScripts(); + } else this.listenForNewerScripts(); + } + get signal() { + return this.abortController.signal; + } + abort(reason) { + return this.abortController.abort(reason); + } + get isInvalid() { + if (browser.runtime.id == null) this.notifyInvalidated(); + return this.signal.aborted; + } + get isValid() { + return !this.isInvalid; + } + /** + * Add a listener that is called when the content script's context is invalidated. + * + * @returns A function to remove the listener. + * + * @example + * browser.runtime.onMessage.addListener(cb); + * const removeInvalidatedListener = ctx.onInvalidated(() => { + * browser.runtime.onMessage.removeListener(cb); + * }) + * // ... + * removeInvalidatedListener(); + */ + onInvalidated(cb) { + this.signal.addEventListener("abort", cb); + return () => this.signal.removeEventListener("abort", cb); + } + /** + * Return a promise that never resolves. Useful if you have an async function that shouldn't run + * after the context is expired. + * + * @example + * const getValueFromStorage = async () => { + * if (ctx.isInvalid) return ctx.block(); + * + * // ... + * } + */ + block() { + return new Promise(() => { + }); + } + /** + * Wrapper around \`window.setInterval\` that automatically clears the interval when invalidated. + * + * Intervals can be cleared by calling the normal \`clearInterval\` function. + */ + setInterval(handler, timeout) { + const id = setInterval(() => { + if (this.isValid) handler(); + }, timeout); + this.onInvalidated(() => clearInterval(id)); + return id; + } + /** + * Wrapper around \`window.setTimeout\` that automatically clears the interval when invalidated. + * + * Timeouts can be cleared by calling the normal \`setTimeout\` function. + */ + setTimeout(handler, timeout) { + const id = setTimeout(() => { + if (this.isValid) handler(); + }, timeout); + this.onInvalidated(() => clearTimeout(id)); + return id; + } + /** + * Wrapper around \`window.requestAnimationFrame\` that automatically cancels the request when + * invalidated. + * + * Callbacks can be canceled by calling the normal \`cancelAnimationFrame\` function. + */ + requestAnimationFrame(callback) { + const id = requestAnimationFrame((...args) => { + if (this.isValid) callback(...args); + }); + this.onInvalidated(() => cancelAnimationFrame(id)); + return id; + } + /** + * Wrapper around \`window.requestIdleCallback\` that automatically cancels the request when + * invalidated. + * + * Callbacks can be canceled by calling the normal \`cancelIdleCallback\` function. + */ + requestIdleCallback(callback, options) { + const id = requestIdleCallback((...args) => { + if (!this.signal.aborted) callback(...args); + }, options); + this.onInvalidated(() => cancelIdleCallback(id)); + return id; + } + addEventListener(target, type, handler, options) { + if (type === "wxt:locationchange") { + if (this.isValid) this.locationWatcher.run(); + } + target.addEventListener?.(type.startsWith("wxt:") ? getUniqueEventName(type) : type, handler, { + ...options, + signal: this.signal + }); + } + /** + * @internal + * Abort the abort controller and execute all \`onInvalidated\` listeners. + */ + notifyInvalidated() { + this.abort("Content script context invalidated"); + logger$1.debug(\`Content script "\${this.contentScriptName}" context invalidated\`); + } + stopOldScripts() { + window.postMessage({ + type: ContentScriptContext2.SCRIPT_STARTED_MESSAGE_TYPE, + contentScriptName: this.contentScriptName, + messageId: Math.random().toString(36).slice(2) + }, "*"); + } + verifyScriptStartedEvent(event) { + const isScriptStartedEvent = event.data?.type === ContentScriptContext2.SCRIPT_STARTED_MESSAGE_TYPE; + const isSameContentScript = event.data?.contentScriptName === this.contentScriptName; + const isNotDuplicate = !this.receivedMessageIds.has(event.data?.messageId); + return isScriptStartedEvent && isSameContentScript && isNotDuplicate; + } + listenForNewerScripts(options) { + let isFirst = true; + const cb = (event) => { + if (this.verifyScriptStartedEvent(event)) { + this.receivedMessageIds.add(event.data.messageId); + const wasFirst = isFirst; + isFirst = false; + if (wasFirst && options?.ignoreFirstEvent) return; + this.notifyInvalidated(); + } + }; + addEventListener("message", cb); + this.onInvalidated(() => removeEventListener("message", cb)); + } + }; + function initPlugins() { + } + function print(method, ...args) { + return; + } + const logger = { + debug: (...args) => print(console.debug, ...args), + log: (...args) => print(console.log, ...args), + warn: (...args) => print(console.warn, ...args), + error: (...args) => print(console.error, ...args) + }; + const result = (async () => { + try { + initPlugins(); + const { main, ...options } = definition; + return await main(new ContentScriptContext("content", options)); + } catch (err) { + logger.error(\`The content script "\${"content"}" crashed on startup!\`, err); + throw err; + } + })(); + var content_script_isolated_world_entrypoint_default = result; + return content_script_isolated_world_entrypoint_default; + })(); + " + `); + }); + + 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(); + + expect( + await project.serializeFile( + '.output/chrome-mv3/content-scripts/content.js', + ), + ).toMatchInlineSnapshot(` + ".output/chrome-mv3/content-scripts/content.js + ---------------------------------------- + (function(){"use strict";function f(e){return e}const h={globalName:!1,matches:["*://*/*"],main(){}};function s(e,...t){}const u={debug:(...e)=>s(console.debug,...e),log:(...e)=>s(console.log,...e),warn:(...e)=>s(console.warn,...e),error:(...e)=>s(console.error,...e)},c=globalThis.browser?.runtime?.id?globalThis.browser:globalThis.chrome;var g=class d extends Event{static EVENT_NAME=a("wxt:locationchange");constructor(t,n){super(d.EVENT_NAME,{}),this.newUrl=t,this.oldUrl=n}};function a(e){return\`\${c?.runtime?.id}:content:\${e}\`}function v(e){let t,n;return{run(){t==null&&(n=new URL(location.href),t=e.setInterval(()=>{let r=new URL(location.href);r.href!==n.href&&(window.dispatchEvent(new g(r,n)),n=r)},1e3))}}}var m=class l{static SCRIPT_STARTED_MESSAGE_TYPE=a("wxt:content-script-started");isTopFrame=window.self===window.top;abortController;locationWatcher=v(this);receivedMessageIds=new Set;constructor(t,n){this.contentScriptName=t,this.options=n,this.abortController=new AbortController,this.isTopFrame?(this.listenForNewerScripts({ignoreFirstEvent:!0}),this.stopOldScripts()):this.listenForNewerScripts()}get signal(){return this.abortController.signal}abort(t){return this.abortController.abort(t)}get isInvalid(){return c.runtime.id==null&&this.notifyInvalidated(),this.signal.aborted}get isValid(){return!this.isInvalid}onInvalidated(t){return this.signal.addEventListener("abort",t),()=>this.signal.removeEventListener("abort",t)}block(){return new Promise(()=>{})}setInterval(t,n){const r=setInterval(()=>{this.isValid&&t()},n);return this.onInvalidated(()=>clearInterval(r)),r}setTimeout(t,n){const r=setTimeout(()=>{this.isValid&&t()},n);return this.onInvalidated(()=>clearTimeout(r)),r}requestAnimationFrame(t){const n=requestAnimationFrame((...r)=>{this.isValid&&t(...r)});return this.onInvalidated(()=>cancelAnimationFrame(n)),n}requestIdleCallback(t,n){const r=requestIdleCallback((...i)=>{this.signal.aborted||t(...i)},n);return this.onInvalidated(()=>cancelIdleCallback(r)),r}addEventListener(t,n,r,i){n==="wxt:locationchange"&&this.isValid&&this.locationWatcher.run(),t.addEventListener?.(n.startsWith("wxt:")?a(n):n,r,{...i,signal:this.signal})}notifyInvalidated(){this.abort("Content script context invalidated"),u.debug(\`Content script "\${this.contentScriptName}" context invalidated\`)}stopOldScripts(){window.postMessage({type:l.SCRIPT_STARTED_MESSAGE_TYPE,contentScriptName:this.contentScriptName,messageId:Math.random().toString(36).slice(2)},"*")}verifyScriptStartedEvent(t){const n=t.data?.type===l.SCRIPT_STARTED_MESSAGE_TYPE,r=t.data?.contentScriptName===this.contentScriptName,i=!this.receivedMessageIds.has(t.data?.messageId);return n&&r&&i}listenForNewerScripts(t){let n=!0;const r=i=>{if(this.verifyScriptStartedEvent(i)){this.receivedMessageIds.add(i.data.messageId);const S=n;if(n=!1,S&&t?.ignoreFirstEvent)return;this.notifyInvalidated()}};addEventListener("message",r),this.onInvalidated(()=>removeEventListener("message",r))}};function b(){}function o(e,...t){}const p={debug:(...e)=>o(console.debug,...e),log:(...e)=>o(console.log,...e),warn:(...e)=>o(console.warn,...e),error:(...e)=>o(console.error,...e)};var w=(async()=>{try{const{main:e,...t}=h;return await e(new m("content",t))}catch(e){throw p.error('The content script "content" crashed on startup!',e),e}})();return w})(); + " + `); + }); + + it('can be specified on unlisted scripts', async () => { + const project = new TestProject(); + project.addFile( + 'entrypoints/unlisted.js', + `export default defineUnlistedScript({ + globalName: false, + main() {} + })`, + ); + + await project.build(); + + expect(await project.serializeFile('.output/chrome-mv3/unlisted.js')) + .toMatchInlineSnapshot(` + ".output/chrome-mv3/unlisted.js + ---------------------------------------- + (function(){"use strict";function i(t){return t==null||typeof t=="function"?{main:t}:t}const o=i({globalName:!1,main(){}});function l(){}function n(t,...r){}const e={debug:(...t)=>n(console.debug,...t),log:(...t)=>n(console.log,...t),warn:(...t)=>n(console.warn,...t),error:(...t)=>n(console.error,...t)};var s=(()=>{try{}catch(r){throw e.error('Failed to initialize plugins for "unlisted"',r),r}let t;try{t=o.main(),t instanceof Promise&&(t=t.catch(r=>{throw e.error('The unlisted script "unlisted" crashed on startup!',r),r}))}catch(r){throw e.error('The unlisted script "unlisted" crashed on startup!',r),r}return t})();return s})(); + " + `); + }); + }); }); diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts index c798749da..edd8d179b 100644 --- a/packages/wxt/src/core/builders/vite/index.ts +++ b/packages/wxt/src/core/builders/vite/index.ts @@ -109,7 +109,6 @@ export async function createViteBuilder( const plugins: NonNullable = [ wxtPlugins.entrypointGroupGlobals(entrypoint), ]; - const iifeReturnValueName = safeVarName(entrypoint.name); if ( entrypoint.type === 'content-script-style' || @@ -118,11 +117,24 @@ export async function createViteBuilder( plugins.push(wxtPlugins.cssEntrypoints(entrypoint, wxtConfig)); } + let iifeReturnValueName: string; + if (typeof entrypoint.options.globalName === 'string') { + iifeReturnValueName = entrypoint.options.globalName; + } else if (typeof entrypoint.options.globalName === 'function') { + iifeReturnValueName = entrypoint.options.globalName(entrypoint); + } else { + iifeReturnValueName = safeVarName(entrypoint.name); + } + if ( entrypoint.type === 'content-script' || entrypoint.type === 'unlisted-script' ) { - plugins.push(wxtPlugins.iifeFooter(iifeReturnValueName)); + 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..e1103f9a2 --- /dev/null +++ b/packages/wxt/src/core/builders/vite/plugins/iifeAnonymous.ts @@ -0,0 +1,21 @@ +import type { Plugin } from 'vite'; + +/** + * Remove the `var name = ` prefix from the generated code so it becomes an anonymous IIFE. + * Handle both minified and non-minified cases. + */ +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 3f6e4bcd6..a10c845a3 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -342,6 +342,7 @@ async function getUnlistedScriptEntrypoint( { include: options.include, exclude: options.exclude, + globalName: options.globalName, }, wxt.config.browser, ), diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index 284af1d78..48d5910ec 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -551,6 +551,25 @@ export interface BaseEntrypointOptions { * @default undefined */ exclude?: TargetBrowser[]; + /** + * The variable name for the IIFE in the JS output bundle. + * + * This option is for content scripts with world=MAIN, and unlisted scripts. + * It's relevant for scripts that are 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