diff --git a/packages/script/src/runtime/components/ScriptCarbonAds.vue b/packages/script/src/runtime/components/ScriptCarbonAds.vue index 820e8a09..549ded31 100644 --- a/packages/script/src/runtime/components/ScriptCarbonAds.vue +++ b/packages/script/src/runtime/components/ScriptCarbonAds.vue @@ -26,7 +26,7 @@ defineSlots<{ }>() const attrId = `_carbonads_js` -const carbonadsEl = ref(import.meta.client ? document.getElementById(attrId) : null) +const carbonadsEl = ref(null) // syncs to useScript status const status = ref('awaitingLoad') let scriptEl: HTMLScriptElement | undefined @@ -56,6 +56,8 @@ function loadCarbon() { emit('ready', script) } } + // carbon.js resolves #_carbonads_js by id, so only one may exist at a time + document.getElementById(attrId)?.remove() carbonadsEl.value.appendChild(script) } @@ -82,7 +84,9 @@ onBeforeUnmount(() => { if (scriptEl) { scriptEl.onload = null scriptEl.onerror = null - scriptEl.remove() + // a pending carbon.js ad callback reads #_carbonads_js.src unguarded, so park + // the already-executed script in rather than removing it + document.head.appendChild(scriptEl) scriptEl = undefined } }) diff --git a/test/nuxt-runtime/script-component-lifecycle.nuxt.test.ts b/test/nuxt-runtime/script-component-lifecycle.nuxt.test.ts index 7485acc3..1ed4119c 100644 --- a/test/nuxt-runtime/script-component-lifecycle.nuxt.test.ts +++ b/test/nuxt-runtime/script-component-lifecycle.nuxt.test.ts @@ -33,6 +33,8 @@ describe('script component lifecycle', () => { afterEach(() => { for (const wrapper of wrappers.splice(0)) wrapper.unmount() + for (const el of document.querySelectorAll('#_carbonads_js')) + el.remove() if (lemonSqueezyDescriptor) Object.defineProperty(window, 'LemonSqueezy', lemonSqueezyDescriptor) else @@ -51,6 +53,37 @@ describe('script component lifecycle', () => { expect(removeRoot).not.toHaveBeenCalled() }) + // carbon.js reads `document.getElementById('_carbonads_js').src` from its own + // async ad callback, unguarded, so the id must outlive the component. + it('keeps the carbon script resolvable by id after unmount', () => { + const wrapper = mount(ScriptCarbonAds, { + attachTo: document.body, + props: { serve: 'CW7DTKJL', placement: 'unlighthousedev', format: 'cover' }, + }) + wrappers.push(wrapper) + + wrapper.unmount() + + const readServe = () => new URL(document.getElementById('_carbonads_js')!.getAttribute('src')!, 'https://x.test').searchParams.get('serve') + expect(readServe()).toBe('CW7DTKJL') + }) + + it('replaces the parked script when a new instance mounts', () => { + const first = mount(ScriptCarbonAds, { + attachTo: document.body, + props: { serve: 'CW7DTKJL', placement: 'first', format: 'cover' }, + }) + first.unmount() + const second = mount(ScriptCarbonAds, { + attachTo: document.body, + props: { serve: 'CW7DTKJL', placement: 'second', format: 'cover' }, + }) + wrappers.push(second) + + expect(document.querySelectorAll('#_carbonads_js')).toHaveLength(1) + expect(document.getElementById('_carbonads_js')!.getAttribute('src')).toContain('placement=second') + }) + it('fans Lemon Squeezy events out to every live component', () => { const firstEvent = vi.fn() const secondEvent = vi.fn()