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
8 changes: 6 additions & 2 deletions packages/script/src/runtime/components/ScriptCarbonAds.vue
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ defineSlots<{
}>()

const attrId = `_carbonads_js`
const carbonadsEl = ref<HTMLElement | null>(import.meta.client ? document.getElementById(attrId) : null)
const carbonadsEl = ref<HTMLElement | null>(null)
// syncs to useScript status
const status = ref('awaitingLoad')
let scriptEl: HTMLScriptElement | undefined
Expand DownExpand Up@@ -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)
}

Expand All@@ -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 <head> rather than removing it
document.head.appendChild(scriptEl)
scriptEl = undefined
}
})
Expand Down
33 changes: 33 additions & 0 deletions test/nuxt-runtime/script-component-lifecycle.nuxt.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -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()
Expand Down
Loading