1
1
import { ScriptNetworkEvents , hashCode } from '@unhead/shared'
2
2
import type {
3
+ AsAsyncFunctionValues ,
3
4
Head ,
4
5
ScriptInstance ,
5
6
UseScriptInput ,
@@ -8,20 +9,37 @@ import type {
8
9
} from '@unhead/schema'
9
10
import { getActiveHead } from './useActiveHead'
10
11
12
+ export type UseScriptContext < T extends Record < symbol | string , any > > =
13
+ ( Promise < T > & ScriptInstance < T > )
14
+ & AsAsyncFunctionValues < T >
15
+ & {
16
+ /**
17
+ * @deprecated Use top-level functions instead.
18
+ */
19
+ $script : Promise < T > & ScriptInstance < T >
20
+ }
21
+
22
+ const ScriptProxyTarget = Symbol ( 'ScriptProxyTarget' )
23
+ function sharedTarget ( ) { }
24
+ sharedTarget [ ScriptProxyTarget ] = true
25
+
26
+ export function resolveScriptKey ( input : UseScriptResolvedInput ) {
27
+ return input . key || hashCode ( input . src || ( typeof input . innerHTML === 'string' ? input . innerHTML : '' ) )
28
+ }
29
+
11
30
/**
12
31
* Load third-party scripts with SSR support and a proxied API.
13
32
*
14
- * @experimental
15
33
* @see https://unhead.unjs.io/usage/composables/use-script
16
34
*/
17
- export function useScript < T extends Record < symbol | string , any > > ( _input : UseScriptInput , _options ?: UseScriptOptions < T > ) : T & { $script : Promise < T > & ScriptInstance < T > } {
35
+ export function useScript < T extends Record < symbol | string , any > > ( _input : UseScriptInput , _options ?: UseScriptOptions < T > ) : UseScriptContext < T > {
18
36
const input : UseScriptResolvedInput = typeof _input === 'string' ? { src : _input } : _input
19
37
const options = _options || { }
20
38
const head = options . head || getActiveHead ( )
21
39
if ( ! head )
22
40
throw new Error ( 'Missing Unhead context.' )
23
41
24
- const id = input . key || hashCode ( input . src || ( typeof input . innerHTML === 'string' ? input . innerHTML : '' ) )
42
+ const id = resolveScriptKey ( input )
25
43
if ( head . _scripts ?. [ id ] )
26
44
return head . _scripts [ id ]
27
45
options . beforeInit ?.( )
@@ -39,8 +57,10 @@ export function useScript<T extends Record<symbol | string, any>>(_input: UseScr
39
57
}
40
58
} )
41
59
42
- const proxy = { instance : ( ! head . ssr && options ?. use ?.( ) ) || { } } as { instance : T , $script : ScriptInstance < T > }
43
60
const loadPromise = new Promise < T > ( ( resolve , reject ) => {
61
+ // promise never resolves
62
+ if ( head . ssr )
63
+ return
44
64
const emit = ( api : T ) => requestAnimationFrame ( ( ) => resolve ( api ) )
45
65
const _ = head . hooks . hook ( 'script:updated' , ( { script } ) => {
46
66
if ( script . id === id && ( script . status === 'loaded' || script . status === 'error' ) ) {
@@ -60,8 +80,10 @@ export function useScript<T extends Record<symbol | string, any>>(_input: UseScr
60
80
_ ( )
61
81
}
62
82
} )
63
- } ) . then ( api => ( proxy . instance = api ) )
64
- const script : ScriptInstance < T > = {
83
+ } )
84
+ const script = Object . assign ( loadPromise , {
85
+ instance : ( ! head . ssr && options ?. use ?.( ) ) || null ,
86
+ proxy : null ,
65
87
id,
66
88
status : 'awaitingLoad' ,
67
89
remove ( ) {
@@ -92,7 +114,8 @@ export function useScript<T extends Record<symbol | string, any>>(_input: UseScr
92
114
}
93
115
return loadPromise
94
116
} ,
95
- }
117
+ } ) as any as UseScriptContext < T >
118
+ loadPromise . then ( api => ( script . instance = api ) )
96
119
const hookCtx = { script }
97
120
if ( ( trigger === 'client' && ! head . ssr ) || ( trigger === 'server' && head . ssr ) )
98
121
script . load ( )
@@ -101,31 +124,54 @@ export function useScript<T extends Record<symbol | string, any>>(_input: UseScr
101
124
else if ( typeof trigger === 'function' )
102
125
trigger ( async ( ) => script . load ( ) )
103
126
104
- // 3. Proxy the script API
105
- proxy . $script = Object . assign ( loadPromise , script )
106
- const instance = new Proxy < { instance : T } > ( proxy , {
107
- get ( { instance : _ } , k ) {
108
- const stub = options . stub ?.( { script : proxy . $script , fn : k } )
109
- if ( stub )
110
- return stub
111
- if ( k === '$script' )
112
- return proxy . $script
113
- const exists = _ && k in _ && _ [ k ] !== undefined
114
- head . hooks . callHook ( 'script:instance-fn' , { script, fn : k , exists } )
115
- return exists
116
- ? Reflect . get ( _ , k )
117
- : ( ...args : any [ ] ) => loadPromise . then ( ( api ) => {
118
- const _k = Reflect . get ( api , k )
119
- return typeof _k === 'function'
120
- ? Reflect . apply ( api [ k ] , api , args )
121
- : _k
122
- } )
127
+ // support deprecated behavior
128
+ script . $script = script
129
+ const proxyChain = ( instance : any , accessor ?: string | symbol , accessors ?: ( string | symbol ) [ ] ) => {
130
+ return new Proxy ( ( ! accessor ? instance : instance ?. [ accessor ] ) || sharedTarget , {
131
+ get ( _ , k , r ) {
132
+ head . hooks . callHook ( 'script:instance-fn' , { script, fn : k , exists : k in _ } )
133
+ if ( ! accessor ) {
134
+ const stub = options . stub ?.( { script, fn : k } )
135
+ if ( stub )
136
+ return stub
137
+ }
138
+ if ( _ && k in _ ) {
139
+ return Reflect . get ( _ , k , r )
140
+ }
141
+ if ( k === Symbol . iterator ) {
142
+ return [ ] [ Symbol . iterator ]
143
+ }
144
+ return proxyChain ( accessor ? instance ?. [ accessor ] : instance , k , accessors || [ k ] )
145
+ } ,
146
+ async apply ( _ , _this , args ) {
147
+ // we are faking, just return, avoid promise handles
148
+ if ( head . ssr && _ [ ScriptProxyTarget ] )
149
+ return
150
+ let instance : any
151
+ const access = ( fn ?: T ) => {
152
+ instance = fn || instance
153
+ for ( let i = 0 ; i < ( accessors || [ ] ) . length ; i ++ ) {
154
+ const k = ( accessors || [ ] ) [ i ]
155
+ fn = fn ?. [ k ]
156
+ }
157
+ return fn
158
+ }
159
+ const fn = access ( script . instance ) || access ( await loadPromise )
160
+ return typeof fn === 'function' ? Reflect . apply ( fn , instance , args ) : fn
161
+ } ,
162
+ } )
163
+ }
164
+ script . proxy = proxyChain ( script . instance )
165
+ // remove in v2, just return the script
166
+ const res = new Proxy ( script , {
167
+ get ( _ , k ) {
168
+ const target = k in script ? script : script . proxy
169
+ if ( k === 'then' || k === 'catch' ) {
170
+ return script [ k ] . bind ( script )
171
+ }
172
+ return Reflect . get ( target , k , target )
123
173
} ,
124
- } ) as any as T & { $script : ScriptInstance < T > & Promise < T > }
125
- // 4. Providing a unique context for the script
126
- head . _scripts = Object . assign (
127
- head . _scripts || { } ,
128
- { [ id ] : instance } ,
129
- )
130
- return instance
174
+ } )
175
+ head . _scripts = Object . assign ( head . _scripts || { } , { [ id ] : res } )
176
+ return res
131
177
}
0 commit comments