1
+ import fsp from 'node:fs/promises'
1
2
import { createUnplugin } from 'unplugin'
2
3
import MagicString from 'magic-string'
3
4
import type { SourceMapInput } from 'rollup'
4
5
import type { Node } from 'estree-walker'
5
- import { walk } from 'estree-walker'
6
+ import { asyncWalk } from 'estree-walker'
6
7
import type { Literal , ObjectExpression , Property , SimpleCallExpression } from 'estree'
7
8
import type { InferInput } from 'valibot'
9
+ import { hasProtocol , parseURL , joinURL } from 'ufo'
10
+ import { hash as ohash } from 'ohash'
11
+ import { join } from 'pathe'
12
+ import { colors } from 'consola/utils'
13
+ import { useNuxt } from '@nuxt/kit'
14
+ import { logger } from '../logger'
15
+ import { storage } from '../assets'
8
16
import { isJS , isVue } from './util'
9
17
import type { RegistryScript } from '#nuxt-scripts'
10
18
11
19
export interface AssetBundlerTransformerOptions {
12
- resolveScript : ( src : string ) => string
13
20
moduleDetected ?: ( module : string ) => void
14
21
defaultBundle ?: boolean
22
+ assetsBaseURL ?: string
15
23
scripts ?: Required < RegistryScript > [ ]
24
+ fallbackOnSrcOnBundleFail ?: boolean
25
+ renderedScript ?: Map < string , {
26
+ content : Buffer
27
+ /**
28
+ * in kb
29
+ */
30
+ size : number
31
+ encoding ?: string
32
+ src : string
33
+ filename ?: string
34
+ } | Error >
16
35
}
17
36
18
- export function NuxtScriptBundleTransformer ( options : AssetBundlerTransformerOptions ) {
37
+ function normalizeScriptData ( src : string , assetsBaseURL : string = '/_scripts' ) : { url : string , filename ?: string } {
38
+ if ( hasProtocol ( src , { acceptRelative : true } ) ) {
39
+ src = src . replace ( / ^ \/ \/ / , 'https://' )
40
+ const url = parseURL ( src )
41
+ const file = [
42
+ `${ ohash ( url ) } .js` , // force an extension
43
+ ] . filter ( Boolean ) . join ( '-' )
44
+ return { url : joinURL ( assetsBaseURL , file ) , filename : file }
45
+ }
46
+ return { url : src }
47
+ }
48
+ async function downloadScript ( opts : {
49
+ src : string ,
50
+ url : string ,
51
+ filename ?: string
52
+ } , renderedScript : NonNullable < AssetBundlerTransformerOptions [ 'renderedScript' ] > ) {
53
+ const { src, url, filename } = opts
54
+ if ( src === url || ! filename ) {
55
+ return
56
+ }
57
+ const scriptContent = renderedScript . get ( src )
58
+ let res : Buffer | undefined = scriptContent instanceof Error ? undefined : scriptContent ?. content
59
+ if ( ! res ) {
60
+ // Use storage to cache the font data between builds
61
+ if ( await storage . hasItem ( `data:scripts:${ filename } ` ) ) {
62
+ const res = await storage . getItemRaw < Buffer > ( `data:scripts:${ filename } ` )
63
+ renderedScript . set ( url , {
64
+ content : res ! ,
65
+ size : res ! . length / 1024 ,
66
+ encoding : 'utf-8' ,
67
+ src,
68
+ filename,
69
+ } )
70
+
71
+ return
72
+ }
73
+ let encoding
74
+ let size = 0
75
+ res = await fetch ( src ) . then ( ( r ) => {
76
+ if ( ! r . ok ) {
77
+ throw new Error ( `Failed to fetch ${ src } ` )
78
+ }
79
+ encoding = r . headers . get ( 'content-encoding' )
80
+ const contentLength = r . headers . get ( 'content-length' )
81
+ size = contentLength ? Number ( contentLength ) / 1024 : 0
82
+
83
+ return r . arrayBuffer ( )
84
+ } ) . then ( r => Buffer . from ( r ) )
85
+
86
+ storage . setItemRaw ( `data:scripts:${ filename } ` , res )
87
+ renderedScript . set ( url , {
88
+ content : res ! ,
89
+ size,
90
+ encoding,
91
+ src,
92
+ filename,
93
+ } )
94
+ }
95
+ }
96
+
97
+ export function NuxtScriptBundleTransformer ( options : AssetBundlerTransformerOptions = {
98
+ renderedScript : new Map ( )
99
+ } ) {
100
+ const nuxt = useNuxt ( )
101
+ const { renderedScript = new Map ( ) } = options
102
+ const cacheDir = join ( nuxt . options . buildDir , 'cache' , 'scripts' )
103
+
104
+ // done after all transformation is done
105
+ // copy all scripts to build
106
+ nuxt . hooks . hook ( 'build:done' , async ( ) => {
107
+ logger . log ( '[nuxt:scripts:bundler-transformer] Bundling scripts...' )
108
+ await fsp . rm ( cacheDir , { recursive : true , force : true } )
109
+ await fsp . mkdir ( cacheDir , { recursive : true } )
110
+ await Promise . all ( [ ...renderedScript ] . map ( async ( [ url , content ] ) => {
111
+ if ( content instanceof Error || ! content . filename )
112
+ return
113
+ await fsp . writeFile ( join ( nuxt . options . buildDir , 'cache' , 'scripts' , content . filename ) , content . content )
114
+ logger . log ( colors . gray ( ` ├─ ${ url } → ${ joinURL ( content . src ) } (${ content . size . toFixed ( 2 ) } kB ${ content . encoding } )` ) )
115
+ } ) )
116
+ } )
117
+
19
118
return createUnplugin ( ( ) => {
20
119
return {
21
120
name : 'nuxt:scripts:bundler-transformer' ,
@@ -30,8 +129,8 @@ export function NuxtScriptBundleTransformer(options: AssetBundlerTransformerOpti
30
129
31
130
const ast = this . parse ( code )
32
131
const s = new MagicString ( code )
33
- walk ( ast as Node , {
34
- enter ( _node ) {
132
+ await asyncWalk ( ast as Node , {
133
+ async enter ( _node ) {
35
134
// @ts -expect-error untyped
36
135
const calleeName = ( _node as SimpleCallExpression ) . callee ?. name
37
136
if ( ! calleeName )
@@ -138,15 +237,28 @@ export function NuxtScriptBundleTransformer(options: AssetBundlerTransformerOpti
138
237
} )
139
238
canBundle = bundleOption ? bundleOption . value . value : canBundle
140
239
if ( canBundle ) {
141
- const newSrc = options . resolveScript ( src )
142
- if ( src === newSrc ) {
240
+ let { url, filename } = normalizeScriptData ( src , options . assetsBaseURL )
241
+ try {
242
+ await downloadScript ( { src, url, filename } , renderedScript )
243
+ }
244
+ catch ( e ) {
245
+ if ( options . fallbackOnSrcOnBundleFail ) {
246
+ logger . warn ( `[Nuxt Scripts: Bundle Transformer] Failed to bundle ${ src } . Fallback to remote loading.` )
247
+ url = src
248
+ }
249
+ else {
250
+ throw e
251
+ }
252
+ }
253
+
254
+ if ( src === url ) {
143
255
if ( src && src . startsWith ( '/' ) )
144
256
console . warn ( `[Nuxt Scripts: Bundle Transformer] Relative scripts are already bundled. Skipping bundling for \`${ src } \`.` )
145
257
else
146
258
console . warn ( `[Nuxt Scripts: Bundle Transformer] Failed to bundle ${ src } .` )
147
259
}
148
260
if ( scriptSrcNode ) {
149
- s . overwrite ( scriptSrcNode . start , scriptSrcNode . end , `'${ newSrc } '` )
261
+ s . overwrite ( scriptSrcNode . start , scriptSrcNode . end , `'${ url } '` )
150
262
}
151
263
else {
152
264
const optionsNode = node . arguments [ 0 ] as ObjectExpression
@@ -163,14 +275,14 @@ export function NuxtScriptBundleTransformer(options: AssetBundlerTransformerOpti
163
275
( p : any ) => p . key ?. name === 'src' || p . key ?. value === 'src' ,
164
276
)
165
277
if ( srcProperty )
166
- s . overwrite ( srcProperty . value . start , srcProperty . value . end , `'${ newSrc } '` )
278
+ s . overwrite ( srcProperty . value . start , srcProperty . value . end , `'${ url } '` )
167
279
else
168
- s . appendRight ( scriptInput . end , `, src: '${ newSrc } '` )
280
+ s . appendRight ( scriptInput . end , `, src: '${ url } '` )
169
281
}
170
282
}
171
283
else {
172
284
// @ts -expect-error untyped
173
- s . appendRight ( node . arguments [ 0 ] . start + 1 , ` scriptInput: { src: '${ newSrc } ' }, ` )
285
+ s . appendRight ( node . arguments [ 0 ] . start + 1 , ` scriptInput: { src: '${ url } ' }, ` )
174
286
}
175
287
}
176
288
}
0 commit comments