@@ -18,13 +18,25 @@ import { bundleStorage } from '../assets'
18
18
import { isJS , isVue } from './util'
19
19
import type { RegistryScript } from '#nuxt-scripts/types'
20
20
21
+ const SEVEN_DAYS_IN_MS = 7 * 24 * 60 * 60 * 1000
22
+
23
+ export async function isCacheExpired ( storage : any , filename : string , cacheMaxAge : number = SEVEN_DAYS_IN_MS ) : Promise < boolean > {
24
+ const metaKey = `bundle-meta:${ filename } `
25
+ const meta = await storage . getItem ( metaKey )
26
+ if ( ! meta || ! meta . timestamp ) {
27
+ return true // No metadata means expired/invalid cache
28
+ }
29
+ return Date . now ( ) - meta . timestamp > cacheMaxAge
30
+ }
31
+
21
32
export interface AssetBundlerTransformerOptions {
22
33
moduleDetected ?: ( module : string ) => void
23
- defaultBundle ?: boolean
34
+ defaultBundle ?: boolean | 'force'
24
35
assetsBaseURL ?: string
25
36
scripts ?: Required < RegistryScript > [ ]
26
37
fallbackOnSrcOnBundleFail ?: boolean
27
38
fetchOptions ?: FetchOptions
39
+ cacheMaxAge ?: number
28
40
renderedScript ?: Map < string , {
29
41
content : Buffer
30
42
/**
@@ -56,8 +68,9 @@ async function downloadScript(opts: {
56
68
src : string
57
69
url : string
58
70
filename ?: string
59
- } , renderedScript : NonNullable < AssetBundlerTransformerOptions [ 'renderedScript' ] > , fetchOptions ?: FetchOptions ) {
60
- const { src, url, filename } = opts
71
+ forceDownload ?: boolean
72
+ } , renderedScript : NonNullable < AssetBundlerTransformerOptions [ 'renderedScript' ] > , fetchOptions ?: FetchOptions , cacheMaxAge ?: number ) {
73
+ const { src, url, filename, forceDownload } = opts
61
74
if ( src === url || ! filename ) {
62
75
return
63
76
}
@@ -66,8 +79,11 @@ async function downloadScript(opts: {
66
79
let res : Buffer | undefined = scriptContent instanceof Error ? undefined : scriptContent ?. content
67
80
if ( ! res ) {
68
81
// Use storage to cache the font data between builds
69
- if ( await storage . hasItem ( `bundle:${ filename } ` ) ) {
70
- const res = await storage . getItemRaw < Buffer > ( `bundle:${ filename } ` )
82
+ const cacheKey = `bundle:${ filename } `
83
+ const shouldUseCache = ! forceDownload && await storage . hasItem ( cacheKey ) && ! ( await isCacheExpired ( storage , filename , cacheMaxAge ) )
84
+
85
+ if ( shouldUseCache ) {
86
+ const res = await storage . getItemRaw < Buffer > ( cacheKey )
71
87
renderedScript . set ( url , {
72
88
content : res ! ,
73
89
size : res ! . length / 1024 ,
@@ -91,6 +107,12 @@ async function downloadScript(opts: {
91
107
} )
92
108
93
109
await storage . setItemRaw ( `bundle:${ filename } ` , res )
110
+ // Save metadata with timestamp for cache expiration
111
+ await storage . setItem ( `bundle-meta:${ filename } ` , {
112
+ timestamp : Date . now ( ) ,
113
+ src,
114
+ filename,
115
+ } )
94
116
size = size || res ! . length / 1024
95
117
logger . info ( `Downloading script ${ colors . gray ( `${ src } → ${ filename } (${ size . toFixed ( 2 ) } kB ${ encoding } )` ) } ` )
96
118
renderedScript . set ( url , {
@@ -214,10 +236,37 @@ export function NuxtScriptBundleTransformer(options: AssetBundlerTransformerOpti
214
236
}
215
237
}
216
238
239
+ // Check for dynamic src with bundle option - warn user and replace with 'unsupported'
240
+ if ( ! scriptSrcNode && ! src ) {
241
+ // This is a dynamic src case, check if bundle option is specified
242
+ const hasBundleOption = node . arguments [ 1 ] ?. type === 'ObjectExpression'
243
+ && ( node . arguments [ 1 ] as ObjectExpression ) . properties . some (
244
+ ( p : any ) => ( p . key ?. name === 'bundle' || p . key ?. value === 'bundle' ) && p . type === 'Property' ,
245
+ )
246
+
247
+ if ( hasBundleOption ) {
248
+ const scriptOptionsArg = node . arguments [ 1 ] as ObjectExpression & { start : number , end : number }
249
+ const bundleProperty = scriptOptionsArg . properties . find (
250
+ ( p : any ) => ( p . key ?. name === 'bundle' || p . key ?. value === 'bundle' ) && p . type === 'Property' ,
251
+ ) as Property & { start : number , end : number } | undefined
252
+
253
+ if ( bundleProperty && bundleProperty . value . type === 'Literal' ) {
254
+ const bundleValue = bundleProperty . value . value
255
+ if ( bundleValue === true || bundleValue === 'force' || String ( bundleValue ) === 'true' ) {
256
+ // Replace bundle value with 'unsupported' - runtime will handle the warning
257
+ const valueNode = bundleProperty . value as any
258
+ s . overwrite ( valueNode . start , valueNode . end , `'unsupported'` )
259
+ }
260
+ }
261
+ }
262
+ return
263
+ }
264
+
217
265
if ( scriptSrcNode || src ) {
218
266
src = src || ( typeof scriptSrcNode ?. value === 'string' ? scriptSrcNode ?. value : false )
219
267
if ( src ) {
220
- let canBundle = ! ! options . defaultBundle
268
+ let canBundle = options . defaultBundle === true || options . defaultBundle === 'force'
269
+ let forceDownload = options . defaultBundle === 'force'
221
270
// useScript
222
271
if ( node . arguments [ 1 ] ?. type === 'ObjectExpression' ) {
223
272
const scriptOptionsArg = node . arguments [ 1 ] as ObjectExpression & { start : number , end : number }
@@ -227,7 +276,8 @@ export function NuxtScriptBundleTransformer(options: AssetBundlerTransformerOpti
227
276
) as Property & { start : number , end : number } | undefined
228
277
if ( bundleProperty && bundleProperty . value . type === 'Literal' ) {
229
278
const value = bundleProperty . value as Literal
230
- if ( String ( value . value ) !== 'true' ) {
279
+ const bundleValue = value . value
280
+ if ( bundleValue !== true && bundleValue !== 'force' && String ( bundleValue ) !== 'true' ) {
231
281
canBundle = false
232
282
return
233
283
}
@@ -242,23 +292,28 @@ export function NuxtScriptBundleTransformer(options: AssetBundlerTransformerOpti
242
292
s . remove ( bundleProperty . start , nextProperty ? nextProperty . start : bundleProperty . end )
243
293
}
244
294
canBundle = true
295
+ forceDownload = bundleValue === 'force'
245
296
}
246
297
}
247
298
// @ts -expect-error untyped
248
299
const scriptOptions = node . arguments [ 0 ] . properties ?. find (
249
300
( p : any ) => ( p . key ?. name === 'scriptOptions' ) ,
250
301
) as Property | undefined
251
- // we need to check if scriptOptions contains bundle: true, if it exists
302
+ // we need to check if scriptOptions contains bundle: true/false/'force' , if it exists
252
303
// @ts -expect-error untyped
253
304
const bundleOption = scriptOptions ?. value . properties ?. find ( ( prop ) => {
254
305
return prop . type === 'Property' && prop . key ?. name === 'bundle' && prop . value . type === 'Literal'
255
306
} )
256
- canBundle = bundleOption ? bundleOption . value . value : canBundle
307
+ if ( bundleOption ) {
308
+ const bundleValue = bundleOption . value . value
309
+ canBundle = bundleValue === true || bundleValue === 'force' || String ( bundleValue ) === 'true'
310
+ forceDownload = bundleValue === 'force'
311
+ }
257
312
if ( canBundle ) {
258
313
const { url : _url , filename } = normalizeScriptData ( src , options . assetsBaseURL )
259
314
let url = _url
260
315
try {
261
- await downloadScript ( { src, url, filename } , renderedScript , options . fetchOptions )
316
+ await downloadScript ( { src, url, filename, forceDownload } , renderedScript , options . fetchOptions , options . cacheMaxAge )
262
317
}
263
318
catch ( e ) {
264
319
if ( options . fallbackOnSrcOnBundleFail ) {
0 commit comments