Skip to content

Commit 9660405

Browse files
committed
fix(scripts): attempt resolving api before loading
1 parent 3bfe2d1 commit 9660405

File tree

5 files changed

+77
-17
lines changed

5 files changed

+77
-17
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<script setup lang="ts">
2+
import { useScript } from '@unhead/vue'
3+
4+
const { dataLayer, $script } = useScript<{ dataLayer: any[] }>({
5+
src: 'https://www.googletagmanager.com/gtm.js?id=GTM-MNJD4B',
6+
}, {
7+
stub({ fn }) {
8+
return fn === 'dataLayer' && typeof window === 'undefined' ? [] : undefined
9+
},
10+
beforeInit() {
11+
if (typeof window !== 'undefined') {
12+
window.dataLayer = window.dataLayer || []
13+
window.dataLayer.push({'gtm.start': new Date().getTime(), 'event': 'gtm.js'})
14+
}
15+
},
16+
use() {
17+
return {
18+
dataLayer: window.dataLayer
19+
}
20+
},
21+
trigger: typeof window !== 'undefined' ? window.requestIdleCallback : 'manual'
22+
})
23+
24+
dataLayer.push({
25+
event: 'page_view',
26+
page_path: '/stripe',
27+
})
28+
29+
$script.then((res) => {
30+
console.log('ready!', res)
31+
})
32+
33+
useHead({
34+
title: $script.status,
35+
})
36+
</script>
37+
38+
<template>
39+
<div>
40+
<h1>gtm</h1>
41+
<div>
42+
script status: {{ $script.status }}
43+
</div>
44+
<div>
45+
data layer:
46+
<div v-for="data in dataLayer">
47+
{{ data }}
48+
</div>
49+
</div>
50+
</div>
51+
</template>
52+
53+
<style scoped>
54+
h1,
55+
a {
56+
color: green;
57+
}
58+
</style>

examples/vite-ssr-vue/src/pages/stripe.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useScript } from '@unhead/vue'
44
const { elements, $script } = useScript<{ elements: (() => void) }>({
55
src: 'https://js.stripe.com/v3/',
66
}, {
7-
use: () => window.Stripe('pk_test_TYooMQauvdEDq54NiTphI7jx'),
7+
use: () => window.Stripe?.('pk_test_TYooMQauvdEDq54NiTphI7jx'),
88
trigger: typeof window !== 'undefined' ? window.requestIdleCallback : 'manual'
99
})
1010

packages/schema/src/hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,5 @@ export interface HeadHooks {
5555
'ssr:rendered': (ctx: SSRRenderContext) => HookResult
5656

5757
'script:updated': (ctx: { script: ScriptInstance<any> }) => HookResult
58-
'script:instance-fn': (ctx: { script: ScriptInstance<any>, fn: string | symbol, args: any }) => HookResult
58+
'script:instance-fn': (ctx: { script: ScriptInstance<any>, fn: string | symbol, args: any, exists: boolean }) => HookResult
5959
}

packages/unhead/src/composables/useScript.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function useScript<T>(_input: UseScriptInput, _options?: UseScriptOptions
2727
const key = `use-script.${id}`
2828
if (head._scripts?.[id])
2929
return head._scripts[id]
30-
30+
options.beforeInit?.()
3131
const syncStatus = (s: ScriptInstance<T>['status']) => {
3232
script.status = s
3333
head.hooks.callHook(`script:updated`, hookCtx)
@@ -44,9 +44,11 @@ export function useScript<T>(_input: UseScriptInput, _options?: UseScriptOptions
4444
const loadPromise = new Promise<T>((resolve, reject) => {
4545
const cleanUp = head.hooks.hook('script:updated', ({ script }: { script: ScriptInstance<T> }) => {
4646
if (script.id === id && (script.status === 'loaded' || script.status === 'error')) {
47-
if (script.status === 'loaded')
48-
resolve(options.use?.() as T)
49-
else if (script.status === 'error')
47+
if (script.status === 'loaded') {
48+
const api = options.use?.()
49+
api && resolve(api)
50+
}
51+
else if (script.status === 'error') {
5052
reject(new Error(`Failed to load script: ${input.src}`))
5153
cleanUp()
5254
}
@@ -105,16 +107,19 @@ export function useScript<T>(_input: UseScriptInput, _options?: UseScriptOptions
105107
// $script is stubbed by abstraction layers
106108
if (fn === '$script')
107109
return $script
108-
return (...args: any[]) => {
109-
const hookCtx = { script, fn, args }
110+
const attempt = (args?: any[]) => {
111+
if (head.ssr)
112+
return
113+
const api = options.use?.()
114+
const exists = !!(api && fn in api)
115+
const hookCtx = { script, fn, args, exists }
110116
// we can't await this, mainly used for debugging
111117
head.hooks.callHook('script:instance-fn', hookCtx)
112-
// third party scripts only run on client-side, mock the function
113-
if (head.ssr || !options.use)
114-
return
115-
// @ts-expect-error untyped
116-
return script.status === 'loaded' ? options.use()[fn]?.(...args) : loadPromise.then(api => api[fn]?.(...args))
118+
return exists && api[fn]
117119
}
120+
// api may already be loaded
121+
// for instance GTM will already have dataLayer available, we can expose it directly
122+
return attempt() || ((...args: any[]) => loadPromise.then(() => attempt(args)(...args)))
118123
},
119124
}) as any as T & { $script: ScriptInstance<T> }
120125
// 4. Providing a unique context for the script

test/unhead/dom/useScript.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('dom useScript', () => {
1313
expect(await useDelayedSerializedDom()).toMatchInlineSnapshot(`
1414
"<!DOCTYPE html><html><head>
1515
16-
<script data-onload="" data-onerror="" defer="" fetchpriority="low" crossorigin="anonymous" referrerpolicy="no-referrer" src="https://cdn.example.com/script.js" data-hid="438d65b"></script></head>
16+
<script data-onload="" data-onerror="" defer="" fetchpriority="low" crossorigin="anonymous" referrerpolicy="no-referrer" src="https://cdn.example.com/script.js" data-hid="c5c65b0"></script></head>
1717
<body>
1818
1919
<div>
@@ -26,19 +26,16 @@ describe('dom useScript', () => {
2626
`)
2727

2828
let calledFn
29-
let calledFnArgs
3029
const hookPromise = new Promise<void>((resolve) => {
3130
head.hooks.hook('script:instance-fn', ({ script, fn, args }) => {
3231
if (script.id === instance.$script.id) {
3332
calledFn = fn
34-
calledFnArgs = args
3533
resolve()
3634
}
3735
})
3836
})
3937
instance.test('hello-world')
4038
await hookPromise
4139
expect(calledFn).toBe('test')
42-
expect(calledFnArgs).toEqual(['hello-world'])
4340
})
4441
})

0 commit comments

Comments
 (0)