Skip to content

Commit 7551135

Browse files
committed
feat(generator): implement asset copying with cache busting
- Introduced a new method to copy assets with cache busting by generating hashed filenames for specific file types. - Updated the documentation generator to apply these hashed asset paths in the rendered HTML templates. - Enhanced error handling for asset copying, including checks for the existence of theme asset directories and creating default assets if necessary.
1 parent 5a43b34 commit 7551135

File tree

2 files changed

+291
-8
lines changed

2 files changed

+291
-8
lines changed

scripts/test-cache-busting.sh

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/bin/bash
2+
3+
echo "🧪 Testing Knowledge Cache Busting"
4+
echo "=================================="
5+
6+
# Função para extrair hash de um arquivo
7+
extract_hash() {
8+
local file=$1
9+
echo $(basename "$file" | sed 's/.*\.\([a-f0-9]\{8\}\)\..*/\1/')
10+
}
11+
12+
# Função para verificar se arquivo existe
13+
check_file() {
14+
local file=$1
15+
if [ -f "$file" ]; then
16+
echo "✅ Found: $(basename "$file")"
17+
return 0
18+
else
19+
echo "❌ Missing: $(basename "$file")"
20+
return 1
21+
fi
22+
}
23+
24+
echo ""
25+
echo "1. Building documentation..."
26+
bun run build > /dev/null 2>&1
27+
28+
if [ $? -eq 0 ]; then
29+
echo "✅ Build successful"
30+
else
31+
echo "❌ Build failed"
32+
exit 1
33+
fi
34+
35+
echo ""
36+
echo "2. Checking generated assets with cache busting..."
37+
38+
# Verificar CSS files
39+
echo ""
40+
echo "CSS Files:"
41+
for css_file in dist/assets/css/*.css; do
42+
if [ -f "$css_file" ]; then
43+
hash=$(extract_hash "$css_file")
44+
echo " 📄 $(basename "$css_file") (hash: $hash)"
45+
fi
46+
done
47+
48+
# Verificar JS files
49+
echo ""
50+
echo "JavaScript Files:"
51+
for js_file in dist/assets/js/*.js; do
52+
if [ -f "$js_file" ]; then
53+
hash=$(extract_hash "$js_file")
54+
echo " 📄 $(basename "$js_file") (hash: $hash)"
55+
fi
56+
done
57+
58+
echo ""
59+
echo "3. Checking HTML references..."
60+
61+
# Verificar se o HTML contém as referências corretas
62+
html_file="dist/index.html"
63+
if [ -f "$html_file" ]; then
64+
echo ""
65+
echo "CSS references in HTML:"
66+
grep -o 'href="[^"]*\.css"' "$html_file" | sed 's/href="//;s/"//' | while read css_ref; do
67+
echo " 🔗 $css_ref"
68+
done
69+
70+
echo ""
71+
echo "JavaScript references in HTML:"
72+
grep -o 'src="[^"]*\.js"' "$html_file" | sed 's/src="//;s/"//' | while read js_ref; do
73+
echo " 🔗 $js_ref"
74+
done
75+
else
76+
echo "❌ HTML file not found"
77+
fi
78+
79+
echo ""
80+
echo "4. Testing cache busting behavior..."
81+
82+
# Salvar hashes atuais
83+
echo "Current asset hashes:" > /tmp/knowledge_hashes_before.txt
84+
find dist/assets -name "*.css" -o -name "*.js" | while read file; do
85+
hash=$(extract_hash "$file")
86+
echo "$(basename "$file" | sed 's/\.[a-f0-9]\{8\}\./ /') $hash" >> /tmp/knowledge_hashes_before.txt
87+
done
88+
89+
# Modificar um arquivo CSS para testar mudança de hash
90+
echo ""
91+
echo "5. Modifying CSS file to test hash change..."
92+
css_source="themes/default/assets/css/style.css"
93+
if [ -f "$css_source" ]; then
94+
# Fazer backup
95+
cp "$css_source" "$css_source.backup"
96+
97+
# Adicionar comentário para mudar o hash
98+
echo "/* Cache busting test - $(date) */" >> "$css_source"
99+
100+
# Rebuild
101+
echo "Rebuilding with modified CSS..."
102+
bun run build > /dev/null 2>&1
103+
104+
# Verificar se o hash mudou
105+
echo ""
106+
echo "New asset hashes:"
107+
find dist/assets -name "*.css" -o -name "*.js" | while read file; do
108+
hash=$(extract_hash "$file")
109+
filename=$(basename "$file" | sed 's/\.[a-f0-9]\{8\}\./ /')
110+
echo " $filename $hash"
111+
done
112+
113+
# Restaurar arquivo original
114+
mv "$css_source.backup" "$css_source"
115+
116+
echo ""
117+
echo "✅ CSS file restored to original state"
118+
else
119+
echo "❌ CSS source file not found"
120+
fi
121+
122+
echo ""
123+
echo "6. Starting test server..."
124+
bun run serve --port 8084 --no-open > /dev/null 2>&1 &
125+
SERVER_PID=$!
126+
127+
sleep 2
128+
129+
echo ""
130+
echo "7. Testing asset loading..."
131+
132+
# Testar carregamento de assets
133+
test_asset() {
134+
local asset_path=$1
135+
local status=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8084$asset_path")
136+
if [ "$status" = "200" ]; then
137+
echo "$asset_path (HTTP $status)"
138+
else
139+
echo "$asset_path (HTTP $status)"
140+
fi
141+
}
142+
143+
# Extrair paths dos assets do HTML
144+
if [ -f "dist/index.html" ]; then
145+
grep -o 'href="[^"]*\.css"' dist/index.html | sed 's/href="//;s/"//' | while read css_path; do
146+
test_asset "$css_path"
147+
done
148+
149+
grep -o 'src="[^"]*\.js"' dist/index.html | sed 's/src="//;s/"//' | while read js_path; do
150+
test_asset "$js_path"
151+
done
152+
fi
153+
154+
echo ""
155+
echo "8. Testing main page..."
156+
MAIN_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8084/)
157+
if [ "$MAIN_STATUS" = "200" ]; then
158+
echo "✅ Main page loads correctly (HTTP $MAIN_STATUS)"
159+
else
160+
echo "❌ Main page failed to load (HTTP $MAIN_STATUS)"
161+
fi
162+
163+
echo ""
164+
echo "=================================="
165+
echo "🎉 Cache Busting Test Complete!"
166+
echo ""
167+
echo "📋 Summary:"
168+
echo "- Assets are generated with content-based hashes"
169+
echo "- HTML references are automatically updated"
170+
echo "- Server serves assets correctly"
171+
echo "- Hash changes when content changes"
172+
echo ""
173+
echo "🌐 Test server running at: http://localhost:8084"
174+
echo "🛑 To stop server: kill $SERVER_PID"
175+
echo ""
176+
echo "💡 Benefits:"
177+
echo "- Browsers will always load the latest version"
178+
echo "- No more 'hard refresh' needed after updates"
179+
echo "- Optimal caching for unchanged files"
180+
echo "- Automatic cache invalidation for changed files"

src/generator.ts

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as fs from 'fs-extra';
22
import * as path from 'path';
33
import { marked } from 'marked';
44
import hljs from 'highlight.js';
5+
import { createHash } from 'crypto';
56
import type { KnowledgeConfig, NavigationItem } from './config.js';
67
import { resolveThemesDir } from './config.js';
78
import { SearchIndexGenerator } from './search.js';
@@ -33,12 +34,17 @@ interface TemplateData {
3334
currentPath: string;
3435
}
3536

37+
interface AssetMapping {
38+
[originalPath: string]: string;
39+
}
40+
3641
export 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(/\{\{title\}\}/g, page.title)
332338
.replace(/\{\{content\}\}/g, page.content)
333339
.replace(/\{\{site\.title\}\}/g, this.config.site.title)
@@ -337,6 +343,11 @@ export class DocumentationGenerator {
337343
.replace(/\{\{navigation\}\}/g, this.renderNavigation())
338344
.replace(/\{\{baseUrl\}\}/g, this.config.site.baseUrl)
339345
.replace(/\{\{markdownUrl\}\}/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

Comments
 (0)