1
1
import type {
2
- AsAsyncFunctionValues ,
3
2
Head ,
3
+ } from '@unhead/schema'
4
+ import type {
5
+ EventHandlerOptions ,
4
6
ScriptInstance ,
5
7
UseFunctionType ,
8
+ UseScriptContext ,
6
9
UseScriptInput ,
7
10
UseScriptOptions ,
8
11
UseScriptResolvedInput ,
9
- } from '@unhead/schema'
12
+ WarmupStrategy ,
13
+ } from './types'
10
14
import { hashCode , ScriptNetworkEvents } from '@unhead/shared'
11
- import { useUnhead } from '../context'
12
-
13
- export type UseScriptContext < T extends Record < symbol | string , any > > =
14
- ( Promise < T > & ScriptInstance < T > )
15
- & AsAsyncFunctionValues < T >
16
- & {
17
- /**
18
- * @deprecated Use top-level functions instead.
19
- */
20
- $script : Promise < T > & ScriptInstance < T >
21
- }
22
-
23
- const ScriptProxyTarget = Symbol ( 'ScriptProxyTarget' )
24
- function scriptProxy ( ) { }
25
- scriptProxy [ ScriptProxyTarget ] = true
15
+ import { useUnhead } from 'unhead'
16
+ import { createForwardingProxy , createNoopedRecordingProxy , replayProxyRecordings } from './proxy'
26
17
27
18
export function resolveScriptKey ( input : UseScriptResolvedInput ) {
28
19
return input . key || hashCode ( input . src || ( typeof input . innerHTML === 'string' ? input . innerHTML : '' ) )
29
20
}
30
21
22
+ const PreconnectServerModes = [ 'preconnect' , 'dns-prefetch' ]
23
+
31
24
/**
32
25
* Load third-party scripts with SSR support and a proxied API.
33
26
*
34
27
* @see https://unhead.unjs.io/usage/composables/use-script
35
28
*/
36
- export function useScript < T extends Record < symbol | string , any > = Record < symbol | string , any > , U = Record < symbol | string , any > > ( _input : UseScriptInput , _options ?: UseScriptOptions < T > ) : UseScriptContext < UseFunctionType < UseScriptOptions < T > , T > > {
29
+ export function useScript < T extends Record < symbol | string , any > = Record < symbol | string , any > > ( _input : UseScriptInput , _options ?: UseScriptOptions < T > ) : UseScriptContext < UseFunctionType < UseScriptOptions < T > , T > > {
37
30
const input : UseScriptResolvedInput = typeof _input === 'string' ? { src : _input } : _input
38
31
const options = _options || { }
39
32
const head = options . head || useUnhead ( )
@@ -60,7 +53,15 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
60
53
} )
61
54
62
55
const _cbs : ScriptInstance < T > [ '_cbs' ] = { loaded : [ ] , error : [ ] }
63
- const _registerCb = ( key : 'loaded' | 'error' , cb : any ) => {
56
+ const _uniqueCbs : Set < string > = new Set < string > ( )
57
+ const _registerCb = ( key : 'loaded' | 'error' , cb : any , options ?: EventHandlerOptions ) => {
58
+ if ( options ?. key ) {
59
+ const key = `${ options ?. key } :${ options . key } `
60
+ if ( _uniqueCbs . has ( key ) ) {
61
+ return
62
+ }
63
+ _uniqueCbs . add ( key )
64
+ }
64
65
if ( _cbs [ key ] ) {
65
66
const i : number = _cbs [ key ] . push ( cb )
66
67
return ( ) => _cbs [ key ] ?. splice ( i - 1 , 1 )
@@ -98,15 +99,18 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
98
99
} )
99
100
} )
100
101
101
- const script = Object . assign ( loadPromise , < Partial < UseScriptContext < T > > > {
102
+ const script = {
103
+ _loadPromise : loadPromise ,
102
104
instance : ( ! head . ssr && options ?. use ?.( ) ) || null ,
103
105
proxy : null ,
104
106
id,
105
107
status : 'awaitingLoad' ,
108
+
106
109
remove ( ) {
107
110
// cancel any pending triggers as we've started loading
108
111
script . _triggerAbortController ?. abort ( )
109
112
script . _triggerPromises = [ ] // clear any pending promises
113
+ script . _warmupEl ?. dispose ( )
110
114
if ( script . entry ) {
111
115
script . entry . dispose ( )
112
116
script . entry = undefined
@@ -116,6 +120,31 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
116
120
}
117
121
return false
118
122
} ,
123
+ warmup ( rel : WarmupStrategy ) {
124
+ const { src } = input
125
+ const isCrossOrigin = ! src . startsWith ( '/' ) || src . startsWith ( '//' )
126
+ const isPreconnect = rel && PreconnectServerModes . includes ( rel )
127
+ let href = src
128
+ if ( ! rel || ( isPreconnect && ! isCrossOrigin ) ) {
129
+ return
130
+ }
131
+ if ( isPreconnect ) {
132
+ const $url = new URL ( src )
133
+ href = `${ $url . protocol } //${ $url . host } `
134
+ }
135
+ const link : Required < Head > [ 'link' ] [ 0 ] = {
136
+ href,
137
+ rel,
138
+ crossorigin : input . crossorigin || isCrossOrigin ? 'anonymous' : undefined ,
139
+ referrerpolicy : input . referrerpolicy || isCrossOrigin ? 'no-referrer' : undefined ,
140
+ fetchpriority : input . fetchpriority || 'low' ,
141
+ integrity : input . integrity ,
142
+ as : rel === 'preload' ? 'script' : undefined ,
143
+ }
144
+ // @ts -expect-error untyped
145
+ script . _warmupEl = head . push ( { link : [ link ] } , { head, tagPriority : 'high' } )
146
+ return script . _warmupEl
147
+ } ,
119
148
load ( cb ?: ( ) => void | Promise < void > ) {
120
149
// cancel any pending triggers as we've started loading
121
150
script . _triggerAbortController ?. abort ( )
@@ -140,11 +169,11 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
140
169
_registerCb ( 'loaded' , cb )
141
170
return loadPromise
142
171
} ,
143
- onLoaded ( cb : ( instance : T ) => void | Promise < void > ) {
144
- return _registerCb ( 'loaded' , cb )
172
+ onLoaded ( cb : ( instance : T ) => void | Promise < void > , options ?: EventHandlerOptions ) {
173
+ return _registerCb ( 'loaded' , cb , options )
145
174
} ,
146
- onError ( cb : ( err ?: Error ) => void | Promise < void > ) {
147
- return _registerCb ( 'error' , cb )
175
+ onError ( cb : ( err ?: Error ) => void | Promise < void > , options ?: EventHandlerOptions ) {
176
+ return _registerCb ( 'error' , cb , options )
148
177
} ,
149
178
setupTriggerHandler ( trigger : UseScriptOptions [ 'trigger' ] ) {
150
179
if ( script . status !== 'awaitingLoad' ) {
@@ -187,7 +216,7 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
187
216
}
188
217
} ,
189
218
_cbs,
190
- } ) as UseScriptContext < T >
219
+ } as any as UseScriptContext < T >
191
220
// script is ready
192
221
loadPromise
193
222
. then ( ( api ) => {
@@ -204,62 +233,22 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
204
233
const hookCtx = { script }
205
234
206
235
script . setupTriggerHandler ( options . trigger )
207
- // support deprecated behavior
208
- script . $script = script
209
- const proxyChain = ( instance : any , accessor ?: string | symbol , accessors ?: ( string | symbol ) [ ] ) => {
210
- return new Proxy ( ( ! accessor ? instance : instance ?. [ accessor ] ) || scriptProxy , {
211
- get ( _ , k , r ) {
212
- head . hooks . callHook ( 'script:instance-fn' , { script, fn : k , exists : k in _ } )
213
- if ( ! accessor ) {
214
- const stub = options . stub ?.( { script, fn : k } )
215
- if ( stub )
216
- return stub
217
- }
218
- if ( _ && k in _ && typeof _ [ k ] !== 'undefined' ) {
219
- return Reflect . get ( _ , k , r )
220
- }
221
- if ( k === Symbol . iterator ) {
222
- return [ ] [ Symbol . iterator ]
223
- }
224
- return proxyChain ( accessor ? instance ?. [ accessor ] : instance , k , accessors || [ k ] )
225
- } ,
226
- async apply ( _ , _this , args ) {
227
- // we are faking, just return, avoid promise handles
228
- if ( head . ssr && _ [ ScriptProxyTarget ] )
229
- return
230
- let instance : any
231
- const access = ( fn ?: T ) => {
232
- instance = fn || instance
233
- for ( let i = 0 ; i < ( accessors || [ ] ) . length ; i ++ ) {
234
- const k = ( accessors || [ ] ) [ i ]
235
- fn = fn ?. [ k ]
236
- }
237
- return fn
238
- }
239
- let fn = access ( script . instance )
240
- if ( ! fn ) {
241
- fn = await ( new Promise < T | undefined > ( ( resolve ) => {
242
- script . onLoaded ( ( api ) => {
243
- resolve ( access ( api ) )
244
- } )
245
- } ) )
246
- }
247
- return typeof fn === 'function' ? Reflect . apply ( fn , instance , args ) : fn
248
- } ,
236
+ if ( options . use ) {
237
+ const { proxy, stack } = createNoopedRecordingProxy < T > ( options . use ( ) || { } as T )
238
+ script . proxy = proxy
239
+ script . onLoaded ( ( instance ) => {
240
+ replayProxyRecordings ( instance , stack )
241
+ // just forward everything with the same behavior
242
+ script . proxy = createForwardingProxy ( instance )
249
243
} )
250
244
}
251
- script . proxy = proxyChain ( script . instance )
252
- // remove in v2, just return the script
253
- const res = new Proxy ( script , {
254
- get ( _ , k ) {
255
- // _ keys are reserved for internal overrides
256
- const target = ( k in script || String ( k ) [ 0 ] === '_' ) ? script : script . proxy
257
- if ( k === 'then' || k === 'catch' ) {
258
- return script [ k ] . bind ( script )
259
- }
260
- return Reflect . get ( target , k , target )
261
- } ,
262
- } )
263
- head . _scripts = Object . assign ( head . _scripts || { } , { [ id ] : res } )
264
- return res
245
+ // need to make sure it's not already registered
246
+ if ( ! options . warmupStrategy && ( typeof options . trigger === 'undefined' || options . trigger === 'client' ) ) {
247
+ options . warmupStrategy = 'preload'
248
+ }
249
+ if ( options . warmupStrategy ) {
250
+ script . warmup ( options . warmupStrategy )
251
+ }
252
+ head . _scripts = Object . assign ( head . _scripts || { } , { [ id ] : script } )
253
+ return script
265
254
}
0 commit comments