@@ -2,6 +2,7 @@ import * as fs from 'fs-extra';
22import * as path from 'path' ;
33import { marked } from 'marked' ;
44import hljs from 'highlight.js' ;
5+ import { createHash } from 'crypto' ;
56import type { KnowledgeConfig , NavigationItem } from './config.js' ;
67import { resolveThemesDir } from './config.js' ;
78import { SearchIndexGenerator } from './search.js' ;
@@ -33,12 +34,17 @@ interface TemplateData {
3334 currentPath : string ;
3435}
3536
37+ interface AssetMapping {
38+ [ originalPath : string ] : string ;
39+ }
40+
3641export class DocumentationGenerator {
3742 private pages : DocumentPage [ ] = [ ] ;
3843 private navigation : NavigationItem [ ] = [ ] ;
3944 private searchIndex : SearchIndexGenerator ;
4045 private markdownProcessor : MarkdownProcessor ;
4146 private resolvedThemesDir : string ;
47+ private assetMapping : AssetMapping = { } ;
4248
4349 constructor ( private config : KnowledgeConfig ) {
4450 this . setupMarked ( ) ;
@@ -72,15 +78,15 @@ export class DocumentationGenerator {
7278 // Gerar índice de busca
7379 await this . generateSearchIndex ( ) ;
7480
75- // Gerar páginas HTML
81+ // Copiar assets com cache busting
82+ await this . copyAssetsWithCacheBusting ( ) ;
83+
84+ // Gerar páginas HTML (após copiar assets para ter o mapping)
7685 await this . generatePages ( ) ;
7786
7887 // Copiar arquivos markdown para download
7988 await this . copyMarkdownFiles ( ) ;
8089
81- // Copiar assets
82- await this . copyAssets ( ) ;
83-
8490 console . log ( `✅ Documentation generated successfully in ${ this . config . outputDir } ` ) ;
8591 }
8692
@@ -327,7 +333,7 @@ export class DocumentationGenerator {
327333 }
328334
329335 private renderTemplate ( template : string , page : DocumentPage ) : string {
330- return template
336+ let renderedTemplate = template
331337 . replace ( / \{ \{ t i t l e \} \} / g, page . title )
332338 . replace ( / \{ \{ c o n t e n t \} \} / g, page . content )
333339 . replace ( / \{ \{ s i t e \. t i t l e \} \} / g, this . config . site . title )
@@ -337,6 +343,11 @@ export class DocumentationGenerator {
337343 . replace ( / \{ \{ n a v i g a t i o n \} \} / g, this . renderNavigation ( ) )
338344 . replace ( / \{ \{ b a s e U r l \} \} / g, this . config . site . baseUrl )
339345 . replace ( / \{ \{ m a r k d o w n U r l \} \} / g, this . config . site . baseUrl + page . markdownUrl ) ;
346+
347+ // Aplicar cache busting nos assets
348+ renderedTemplate = this . applyAssetCacheBusting ( renderedTemplate ) ;
349+
350+ return renderedTemplate ;
340351 }
341352
342353 private renderNavigation ( ) : string {
@@ -403,13 +414,25 @@ export class DocumentationGenerator {
403414</html>` ;
404415 }
405416
406- private async copyAssets ( ) : Promise < void > {
417+ private async copyAssetsWithCacheBusting ( ) : Promise < void > {
407418 const themeAssetsDir = path . join ( this . resolvedThemesDir , this . config . theme , 'assets' ) ;
408419 const outputAssetsDir = path . join ( this . config . outputDir , 'assets' ) ;
409420
410421 try {
411- await fs . copy ( themeAssetsDir , outputAssetsDir ) ;
412- console . log ( `✅ Assets copied from: ${ themeAssetsDir } ` ) ;
422+ // Verificar se o diretório de assets do tema existe
423+ if ( ! await fs . pathExists ( themeAssetsDir ) ) {
424+ console . warn ( `Theme assets directory not found: ${ themeAssetsDir } ` ) ;
425+ await this . createDefaultAssets ( ) ;
426+ return ;
427+ }
428+
429+ // Criar diretório de saída
430+ await fs . ensureDir ( outputAssetsDir ) ;
431+
432+ // Processar assets com cache busting
433+ await this . processAssetsRecursively ( themeAssetsDir , outputAssetsDir , '' ) ;
434+
435+ console . log ( `✅ Assets copied with cache busting from: ${ themeAssetsDir } ` ) ;
413436 } catch ( err ) {
414437 console . warn ( `Could not copy theme assets from ${ themeAssetsDir } ` ) ;
415438 console . warn ( 'Error:' , err instanceof Error ? err . message : err ) ;
@@ -418,6 +441,86 @@ export class DocumentationGenerator {
418441 }
419442 }
420443
444+ private async processAssetsRecursively ( sourceDir : string , outputDir : string , relativePath : string ) : Promise < void > {
445+ const entries = await fs . readdir ( sourceDir , { withFileTypes : true } ) ;
446+
447+ for ( const entry of entries ) {
448+ const sourcePath = path . join ( sourceDir , entry . name ) ;
449+ const currentRelativePath = relativePath ? path . join ( relativePath , entry . name ) : entry . name ;
450+
451+ if ( entry . isDirectory ( ) ) {
452+ // Criar subdiretório e processar recursivamente
453+ const outputSubDir = path . join ( outputDir , entry . name ) ;
454+ await fs . ensureDir ( outputSubDir ) ;
455+ await this . processAssetsRecursively ( sourcePath , outputSubDir , currentRelativePath ) ;
456+ } else if ( entry . isFile ( ) ) {
457+ // Verificar se é um arquivo que precisa de cache busting
458+ if ( this . shouldApplyCacheBusting ( entry . name ) ) {
459+ await this . copyAssetWithHash ( sourcePath , outputDir , entry . name , currentRelativePath ) ;
460+ } else {
461+ // Copiar arquivo normalmente
462+ const outputPath = path . join ( outputDir , entry . name ) ;
463+ await fs . copy ( sourcePath , outputPath ) ;
464+ }
465+ }
466+ }
467+ }
468+
469+ private shouldApplyCacheBusting ( filename : string ) : boolean {
470+ const extensions = [ '.css' , '.js' , '.png' , '.jpg' , '.jpeg' , '.gif' , '.svg' , '.woff' , '.woff2' , '.ttf' , '.eot' ] ;
471+ return extensions . some ( ext => filename . toLowerCase ( ) . endsWith ( ext ) ) ;
472+ }
473+
474+ private async copyAssetWithHash ( sourcePath : string , outputDir : string , filename : string , relativePath : string ) : Promise < void > {
475+ // Ler conteúdo do arquivo
476+ const content = await fs . readFile ( sourcePath ) ;
477+
478+ // Gerar hash do conteúdo
479+ const hash = createHash ( 'md5' ) . update ( content ) . digest ( 'hex' ) . substring ( 0 , 8 ) ;
480+
481+ // Gerar novo nome com hash
482+ const parsedPath = path . parse ( filename ) ;
483+ const hashedFilename = `${ parsedPath . name } .${ hash } ${ parsedPath . ext } ` ;
484+
485+ // Normalizar caminhos
486+ const normalizedRelativePath = relativePath . replace ( / \\ / g, '/' ) ;
487+ const hashedRelativePath = path . join ( path . dirname ( normalizedRelativePath ) , hashedFilename ) . replace ( / \\ / g, '/' ) ;
488+
489+ // Salvar mapping para substituição posterior
490+ const originalAssetPath = `assets/${ normalizedRelativePath } ` ;
491+ const hashedAssetPath = `assets/${ hashedRelativePath } ` ;
492+ this . assetMapping [ originalAssetPath ] = hashedAssetPath ;
493+
494+ // Copiar arquivo com novo nome
495+ const outputPath = path . join ( outputDir , hashedFilename ) ;
496+ await fs . writeFile ( outputPath , content ) ;
497+
498+ console . log ( `📦 Asset with cache busting: ${ originalAssetPath } → ${ hashedAssetPath } ` ) ;
499+ }
500+
501+ private applyAssetCacheBusting ( template : string ) : string {
502+ let result = template ;
503+
504+ // Substituir referências de assets pelos nomes com hash
505+ for ( const [ originalPath , hashedPath ] of Object . entries ( this . assetMapping ) ) {
506+ // Substituir em href e src attributes
507+ const patterns = [
508+ new RegExp ( `href="([^"]*?)${ originalPath . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) } "` , 'g' ) ,
509+ new RegExp ( `src="([^"]*?)${ originalPath . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) } "` , 'g' ) ,
510+ new RegExp ( `href='([^']*?)${ originalPath . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) } '` , 'g' ) ,
511+ new RegExp ( `src='([^']*?)${ originalPath . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) } '` , 'g' )
512+ ] ;
513+
514+ patterns . forEach ( pattern => {
515+ result = result . replace ( pattern , ( match , prefix ) => {
516+ return match . replace ( originalPath , hashedPath ) ;
517+ } ) ;
518+ } ) ;
519+ }
520+
521+ return result ;
522+ }
523+
421524 private async createDefaultAssets ( ) : Promise < void > {
422525 const assetsDir = path . join ( this . config . outputDir , 'assets' ) ;
423526 await fs . ensureDir ( path . join ( assetsDir , 'css' ) ) ;
0 commit comments