@@ -18,13 +18,25 @@ import { bundleStorage } from '../assets'
1818import { isJS , isVue } from './util'
1919import type { RegistryScript } from '#nuxt-scripts/types'
2020
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+
2132export interface AssetBundlerTransformerOptions {
2233 moduleDetected ?: ( module : string ) => void
23- defaultBundle ?: boolean
34+ defaultBundle ?: boolean | 'force'
2435 assetsBaseURL ?: string
2536 scripts ?: Required < RegistryScript > [ ]
2637 fallbackOnSrcOnBundleFail ?: boolean
2738 fetchOptions ?: FetchOptions
39+ cacheMaxAge ?: number
2840 renderedScript ?: Map < string , {
2941 content : Buffer
3042 /**
@@ -56,8 +68,9 @@ async function downloadScript(opts: {
5668 src : string
5769 url : string
5870 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
6174 if ( src === url || ! filename ) {
6275 return
6376 }
@@ -66,8 +79,11 @@ async function downloadScript(opts: {
6679 let res : Buffer | undefined = scriptContent instanceof Error ? undefined : scriptContent ?. content
6780 if ( ! res ) {
6881 // 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 )
7187 renderedScript . set ( url , {
7288 content : res ! ,
7389 size : res ! . length / 1024 ,
@@ -91,6 +107,12 @@ async function downloadScript(opts: {
91107 } )
92108
93109 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+ } )
94116 size = size || res ! . length / 1024
95117 logger . info ( `Downloading script ${ colors . gray ( `${ src } → ${ filename } (${ size . toFixed ( 2 ) } kB ${ encoding } )` ) } ` )
96118 renderedScript . set ( url , {
@@ -214,10 +236,37 @@ export function NuxtScriptBundleTransformer(options: AssetBundlerTransformerOpti
214236 }
215237 }
216238
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+
217265 if ( scriptSrcNode || src ) {
218266 src = src || ( typeof scriptSrcNode ?. value === 'string' ? scriptSrcNode ?. value : false )
219267 if ( src ) {
220- let canBundle = ! ! options . defaultBundle
268+ let canBundle = options . defaultBundle === true || options . defaultBundle === 'force'
269+ let forceDownload = options . defaultBundle === 'force'
221270 // useScript
222271 if ( node . arguments [ 1 ] ?. type === 'ObjectExpression' ) {
223272 const scriptOptionsArg = node . arguments [ 1 ] as ObjectExpression & { start : number , end : number }
@@ -227,7 +276,8 @@ export function NuxtScriptBundleTransformer(options: AssetBundlerTransformerOpti
227276 ) as Property & { start : number , end : number } | undefined
228277 if ( bundleProperty && bundleProperty . value . type === 'Literal' ) {
229278 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' ) {
231281 canBundle = false
232282 return
233283 }
@@ -242,23 +292,28 @@ export function NuxtScriptBundleTransformer(options: AssetBundlerTransformerOpti
242292 s . remove ( bundleProperty . start , nextProperty ? nextProperty . start : bundleProperty . end )
243293 }
244294 canBundle = true
295+ forceDownload = bundleValue === 'force'
245296 }
246297 }
247298 // @ts -expect-error untyped
248299 const scriptOptions = node . arguments [ 0 ] . properties ?. find (
249300 ( p : any ) => ( p . key ?. name === 'scriptOptions' ) ,
250301 ) 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
252303 // @ts -expect-error untyped
253304 const bundleOption = scriptOptions ?. value . properties ?. find ( ( prop ) => {
254305 return prop . type === 'Property' && prop . key ?. name === 'bundle' && prop . value . type === 'Literal'
255306 } )
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+ }
257312 if ( canBundle ) {
258313 const { url : _url , filename } = normalizeScriptData ( src , options . assetsBaseURL )
259314 let url = _url
260315 try {
261- await downloadScript ( { src, url, filename } , renderedScript , options . fetchOptions )
316+ await downloadScript ( { src, url, filename, forceDownload } , renderedScript , options . fetchOptions , options . cacheMaxAge )
262317 }
263318 catch ( e ) {
264319 if ( options . fallbackOnSrcOnBundleFail ) {
0 commit comments