@@ -40,6 +40,7 @@ pub fn embed_asset(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
4040struct EmbedAsset {
4141 asset_file : AssetFile ,
4242 should_compress : ShouldCompress ,
43+ cache_busted : IsCacheBusted ,
4344}
4445
4546struct AssetFile ( LitStr ) ;
@@ -48,37 +49,51 @@ impl Parse for EmbedAsset {
4849 fn parse ( input : ParseStream ) -> syn:: Result < Self > {
4950 let asset_file: AssetFile = input. parse ( ) ?;
5051
51- // Default to no compression
52+ // Default to no compression, no cache-busting
5253 let mut maybe_should_compress = None ;
54+ let mut maybe_is_cache_busted = None ;
5355
5456 while !input. is_empty ( ) {
5557 input. parse :: < Token ! [ , ] > ( ) ?;
5658 let key: Ident = input. parse ( ) ?;
5759 input. parse :: < Token ! [ =] > ( ) ?;
5860
59- if matches ! ( key. to_string( ) . as_str( ) , "compress" ) {
60- let value = input. parse ( ) ?;
61- maybe_should_compress = Some ( value) ;
62- } else {
63- return Err ( syn:: Error :: new (
61+ match key. to_string ( ) . as_str ( ) {
62+ "compress" => {
63+ let value = input. parse ( ) ?;
64+ maybe_should_compress = Some ( value) ;
65+ }
66+ "cache_bust" => {
67+ let value = input. parse ( ) ?;
68+ maybe_is_cache_busted = Some ( value) ;
69+ }
70+ _ => {
71+ return Err ( syn:: Error :: new (
6472 key. span ( ) ,
6573 format ! (
66- "Unknown key in `embed_asset!` macro. Expected `compress` but got {key}"
74+ "Unknown key in `embed_asset!` macro. Expected `compress` or `cache_bust` but got {key}"
6775 ) ,
6876 ) ) ;
77+ }
6978 }
7079 }
71-
7280 let should_compress = maybe_should_compress. unwrap_or_else ( || {
7381 ShouldCompress ( LitBool {
7482 value : false ,
7583 span : Span :: call_site ( ) ,
7684 } )
7785 } ) ;
86+ let cache_busted = maybe_is_cache_busted. unwrap_or_else ( || {
87+ IsCacheBusted ( LitBool {
88+ value : false ,
89+ span : Span :: call_site ( ) ,
90+ } )
91+ } ) ;
7892
7993 Ok ( Self {
8094 asset_file,
8195 should_compress,
96+ cache_busted,
8297 } )
8398 }
8499}
@@ -120,8 +135,9 @@ impl ToTokens for EmbedAsset {
120135 fn to_tokens ( & self , tokens : & mut TokenStream ) {
121136 let AssetFile ( asset_file) = & self . asset_file ;
122137 let ShouldCompress ( should_compress) = & self . should_compress ;
138+ let IsCacheBusted ( cache_busted) = & self . cache_busted ;
123139
124- let result = generate_static_handler ( asset_file, should_compress) ;
140+ let result = generate_static_handler ( asset_file, should_compress, cache_busted ) ;
125141
126142 match result {
127143 Ok ( value) => {
@@ -142,6 +158,7 @@ struct EmbedAssets {
142158 validated_ignore_dirs : IgnoreDirs ,
143159 should_compress : ShouldCompress ,
144160 should_strip_html_ext : ShouldStripHtmlExt ,
161+ cache_busted_paths : CacheBustedPaths ,
145162}
146163
147164impl Parse for EmbedAssets {
@@ -152,6 +169,7 @@ impl Parse for EmbedAssets {
152169 let mut maybe_should_compress = None ;
153170 let mut maybe_ignore_dirs = None ;
154171 let mut maybe_should_strip_html_ext = None ;
172+ let mut maybe_cache_busted_paths = None ;
155173
156174 while !input. is_empty ( ) {
157175 input. parse :: < Token ! [ , ] > ( ) ?;
@@ -171,10 +189,14 @@ impl Parse for EmbedAssets {
171189 let value = input. parse ( ) ?;
172190 maybe_should_strip_html_ext = Some ( value) ;
173191 }
192+ "cache_busted_paths" => {
193+ let value = input. parse ( ) ?;
194+ maybe_cache_busted_paths = Some ( value) ;
195+ }
174196 _ => {
175197 return Err ( syn:: Error :: new (
176198 key. span ( ) ,
177- "Unknown key in embed_assets! macro. Expected `compress`, `ignore_dirs`, or `strip_html_ext `" ,
199+ "Unknown key in embed_assets! macro. Expected `compress`, `ignore_dirs`, `strip_html_ext`, or `cache_busted_paths `" ,
178200 ) ) ;
179201 }
180202 }
@@ -197,11 +219,17 @@ impl Parse for EmbedAssets {
197219 let ignore_dirs_with_span = maybe_ignore_dirs. unwrap_or ( IgnoreDirsWithSpan ( vec ! [ ] ) ) ;
198220 let validated_ignore_dirs = validate_ignore_dirs ( ignore_dirs_with_span, & assets_dir. 0 ) ?;
199221
222+ let maybe_cache_busted_paths =
223+ maybe_cache_busted_paths. unwrap_or ( CacheBustedPathsWithSpan ( vec ! [ ] ) ) ;
224+ let cache_busted_paths =
225+ validate_cache_busted_paths ( maybe_cache_busted_paths, & assets_dir. 0 ) ?;
226+
200227 Ok ( Self {
201228 assets_dir,
202229 validated_ignore_dirs,
203230 should_compress,
204231 should_strip_html_ext,
232+ cache_busted_paths,
205233 } )
206234 }
207235}
@@ -212,12 +240,14 @@ impl ToTokens for EmbedAssets {
212240 let ignore_dirs = & self . validated_ignore_dirs ;
213241 let ShouldCompress ( should_compress) = & self . should_compress ;
214242 let ShouldStripHtmlExt ( should_strip_html_ext) = & self . should_strip_html_ext ;
243+ let cache_busted_paths = & self . cache_busted_paths ;
215244
216245 let result = generate_static_routes (
217246 assets_dir,
218247 ignore_dirs,
219248 should_compress,
220249 should_strip_html_ext,
250+ cache_busted_paths,
221251 ) ;
222252
223253 match result {
@@ -278,20 +308,7 @@ struct IgnoreDirsWithSpan(Vec<(PathBuf, Span)>);
278308
279309impl Parse for IgnoreDirsWithSpan {
280310 fn parse ( input : ParseStream ) -> syn:: Result < Self > {
281- let inner_content;
282- bracketed ! ( inner_content in input) ;
283-
284- let mut dirs = Vec :: new ( ) ;
285- while !inner_content. is_empty ( ) {
286- let directory_span = inner_content. span ( ) ;
287- let directory_str = inner_content. parse :: < LitStr > ( ) ?;
288- let path = PathBuf :: from ( directory_str. value ( ) ) ;
289- dirs. push ( ( path, directory_span) ) ;
290-
291- if !inner_content. is_empty ( ) {
292- inner_content. parse :: < Token ! [ , ] > ( ) ?;
293- }
294- }
311+ let dirs = parse_dirs ( input) ?;
295312
296313 Ok ( IgnoreDirsWithSpan ( dirs) )
297314 }
@@ -351,11 +368,94 @@ impl Parse for ShouldStripHtmlExt {
351368 }
352369}
353370
371+ struct IsCacheBusted ( LitBool ) ;
372+
373+ impl Parse for IsCacheBusted {
374+ fn parse ( input : ParseStream ) -> syn:: Result < Self > {
375+ let lit = input. parse ( ) ?;
376+ Ok ( IsCacheBusted ( lit) )
377+ }
378+ }
379+
380+ struct CacheBustedPaths {
381+ dirs : Vec < PathBuf > ,
382+ files : Vec < PathBuf > ,
383+ }
384+ struct CacheBustedPathsWithSpan ( Vec < ( PathBuf , Span ) > ) ;
385+
386+ impl Parse for CacheBustedPathsWithSpan {
387+ fn parse ( input : ParseStream ) -> syn:: Result < Self > {
388+ let dirs = parse_dirs ( input) ?;
389+ Ok ( CacheBustedPathsWithSpan ( dirs) )
390+ }
391+ }
392+
393+ fn validate_cache_busted_paths (
394+ tuples : CacheBustedPathsWithSpan ,
395+ assets_dir : & LitStr ,
396+ ) -> syn:: Result < CacheBustedPaths > {
397+ let mut valid_dirs = Vec :: new ( ) ;
398+ let mut valid_files = Vec :: new ( ) ;
399+ for ( dir, span) in tuples. 0 {
400+ let full_path = PathBuf :: from ( assets_dir. value ( ) ) . join ( & dir) ;
401+ match fs:: metadata ( & full_path) {
402+ Ok ( meta) => {
403+ if meta. is_dir ( ) {
404+ valid_dirs. push ( full_path) ;
405+ } else {
406+ valid_files. push ( full_path) ;
407+ }
408+ }
409+ Err ( e) if matches ! ( e. kind( ) , std:: io:: ErrorKind :: NotFound ) => {
410+ return Err ( syn:: Error :: new (
411+ span,
412+ "The specified directory for cache busting does not exist" ,
413+ ) )
414+ }
415+ Err ( e) => {
416+ return Err ( syn:: Error :: new (
417+ span,
418+ format ! (
419+ "Error reading path {}: {}" ,
420+ dir. to_string_lossy( ) ,
421+ DisplayFullError ( & e)
422+ ) ,
423+ ) )
424+ }
425+ }
426+ }
427+ Ok ( CacheBustedPaths {
428+ dirs : valid_dirs,
429+ files : valid_files,
430+ } )
431+ }
432+
433+ /// Helper function for turning an array of strs representing paths into
434+ /// a `Vec` containing tuples of each `PathBuf` and its `Span` in the `ParseStream`
435+ fn parse_dirs ( input : ParseStream ) -> syn:: Result < Vec < ( PathBuf , Span ) > > {
436+ let inner_content;
437+ bracketed ! ( inner_content in input) ;
438+
439+ let mut dirs = Vec :: new ( ) ;
440+ while !inner_content. is_empty ( ) {
441+ let directory_span = inner_content. span ( ) ;
442+ let directory_str = inner_content. parse :: < LitStr > ( ) ?;
443+ let path = PathBuf :: from ( directory_str. value ( ) ) ;
444+ dirs. push ( ( path, directory_span) ) ;
445+
446+ if !inner_content. is_empty ( ) {
447+ inner_content. parse :: < Token ! [ , ] > ( ) ?;
448+ }
449+ }
450+ Ok ( dirs)
451+ }
452+
354453fn generate_static_routes (
355454 assets_dir : & LitStr ,
356455 ignore_dirs : & IgnoreDirs ,
357456 should_compress : & LitBool ,
358457 should_strip_html_ext : & LitBool ,
458+ cache_busted_paths : & CacheBustedPaths ,
359459) -> Result < TokenStream , error:: Error > {
360460 let assets_dir_abs = Path :: new ( & assets_dir. value ( ) )
361461 . canonicalize ( )
@@ -368,6 +468,19 @@ fn generate_static_routes(
368468 . iter ( )
369469 . map ( |d| d. canonicalize ( ) . map_err ( Error :: CannotCanonicalizeIgnoreDir ) )
370470 . collect :: < Result < Vec < _ > , _ > > ( ) ?;
471+ let canon_cache_busted_dirs = cache_busted_paths
472+ . dirs
473+ . iter ( )
474+ . map ( |d| {
475+ d. canonicalize ( )
476+ . map_err ( Error :: CannotCanonicalizeCacheBustedDir )
477+ } )
478+ . collect :: < Result < Vec < _ > , _ > > ( ) ?;
479+ let canon_cache_busted_files = cache_busted_paths
480+ . files
481+ . iter ( )
482+ . map ( |file| file. canonicalize ( ) . map_err ( Error :: CannotCanonicalizeFile ) )
483+ . collect :: < Result < Vec < _ > , _ > > ( ) ?;
371484
372485 let mut routes = Vec :: new ( ) ;
373486 for entry in glob ( & format ! ( "{assets_dir_abs_str}/**/*" ) ) . map_err ( Error :: Pattern ) ? {
@@ -385,6 +498,15 @@ fn generate_static_routes(
385498 continue ;
386499 }
387500
501+ let mut is_entry_cache_busted = false ;
502+ if canon_cache_busted_dirs
503+ . iter ( )
504+ . any ( |dir| entry. starts_with ( dir) )
505+ || canon_cache_busted_files. contains ( & entry)
506+ {
507+ is_entry_cache_busted = true ;
508+ }
509+
388510 let entry = entry
389511 . canonicalize ( )
390512 . map_err ( Error :: CannotCanonicalizeFile ) ?;
@@ -396,11 +518,13 @@ fn generate_static_routes(
396518 lit_byte_str_contents,
397519 maybe_gzip,
398520 maybe_zstd,
521+ cache_busted,
399522 } = EmbeddedFileInfo :: from_path (
400523 & entry,
401524 Some ( assets_dir_abs_str) ,
402525 should_compress,
403526 should_strip_html_ext,
527+ is_entry_cache_busted,
404528 ) ?;
405529
406530 routes. push ( quote ! {
@@ -413,10 +537,11 @@ fn generate_static_routes(
413537 // Poor man's `tracked_path`
414538 // https://github.com/rust-lang/rust/issues/99515
415539 const _: & [ u8 ] = include_bytes!( #entry_str) ;
416- #lit_byte_str_contents
540+ #lit_byte_str_contents
417541 } ,
418542 #maybe_gzip,
419543 #maybe_zstd,
544+ #cache_busted
420545 ) ;
421546 } ) ;
422547 }
@@ -434,6 +559,7 @@ fn generate_static_routes(
434559fn generate_static_handler (
435560 asset_file : & LitStr ,
436561 should_compress : & LitBool ,
562+ cache_busted : & LitBool ,
437563) -> Result < TokenStream , error:: Error > {
438564 let asset_file_abs = Path :: new ( & asset_file. value ( ) )
439565 . canonicalize ( )
@@ -447,6 +573,7 @@ fn generate_static_handler(
447573 lit_byte_str_contents,
448574 maybe_gzip,
449575 maybe_zstd,
576+ cache_busted,
450577 } = EmbeddedFileInfo :: from_path (
451578 & asset_file_abs,
452579 None ,
@@ -455,6 +582,7 @@ fn generate_static_handler(
455582 value : false ,
456583 span : Span :: call_site ( ) ,
457584 } ,
585+ cache_busted. value ( ) ,
458586 ) ?;
459587
460588 let route = quote ! {
@@ -469,6 +597,7 @@ fn generate_static_handler(
469597 } ,
470598 #maybe_gzip,
471599 #maybe_zstd,
600+ #cache_busted
472601 )
473602 } ;
474603
@@ -496,6 +625,7 @@ struct EmbeddedFileInfo<'a> {
496625 lit_byte_str_contents : LitByteStr ,
497626 maybe_gzip : OptionBytesSlice ,
498627 maybe_zstd : OptionBytesSlice ,
628+ cache_busted : bool ,
499629}
500630
501631impl < ' a > EmbeddedFileInfo < ' a > {
@@ -504,6 +634,7 @@ impl<'a> EmbeddedFileInfo<'a> {
504634 assets_dir_abs_str : Option < & str > ,
505635 should_compress : & LitBool ,
506636 should_strip_html_ext : & LitBool ,
637+ cache_busted : bool ,
507638 ) -> Result < Self , Error > {
508639 let contents = fs:: read ( pathbuf) . map_err ( Error :: CannotReadEntryContents ) ?;
509640
@@ -548,6 +679,7 @@ impl<'a> EmbeddedFileInfo<'a> {
548679 lit_byte_str_contents,
549680 maybe_gzip,
550681 maybe_zstd,
682+ cache_busted,
551683 } )
552684 }
553685}
0 commit comments