Rust library to generate static websites

fix: make it more reliable

+82 -57
+8
Cargo.lock
··· 1662 1662 checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" 1663 1663 1664 1664 [[package]] 1665 + name = "fixtures-hot-reload" 1666 + version = "0.1.0" 1667 + dependencies = [ 1668 + "maud", 1669 + "maudit", 1670 + ] 1671 + 1672 + [[package]] 1665 1673 name = "fixtures-prefetch-prerender" 1666 1674 version = "0.1.0" 1667 1675 dependencies = [
+1 -1
Cargo.toml
··· 1 1 [workspace] 2 - members = ["crates/*", "benchmarks/*", "examples/*", "website", "xtask", "e2e/fixtures/prefetch-prerender"] 2 + members = ["crates/*", "benchmarks/*", "examples/*", "website", "xtask", "e2e/fixtures/*"] 3 3 resolver = "3" 4 4 5 5 [workspace.dependencies]
+1
e2e/fixtures/hot-reload/data.txt
··· 1 + Test data
+68 -54
e2e/tests/hot-reload.spec.ts
··· 12 12 13 13 test.describe.configure({ mode: "serial" }); 14 14 15 + /** 16 + * Wait for dev server to complete a build/rerun by polling logs 17 + */ 18 + async function waitForBuildComplete(devServer: any, timeoutMs = 20000): Promise<string[]> { 19 + const startTime = Date.now(); 20 + 21 + while (Date.now() - startTime < timeoutMs) { 22 + const logs = devServer.getLogs(100); 23 + const logsText = logs.join("\n").toLowerCase(); 24 + 25 + // Look for completion messages 26 + if (logsText.includes("finished") || 27 + logsText.includes("rerun finished") || 28 + logsText.includes("build finished")) { 29 + return logs; 30 + } 31 + 32 + // Wait 100ms before checking again 33 + await new Promise(resolve => setTimeout(resolve, 100)); 34 + } 35 + 36 + throw new Error(`Build did not complete within ${timeoutMs}ms`); 37 + } 38 + 15 39 test.describe("Hot Reload", () => { 40 + // Increase timeout for these tests since they involve compilation 41 + test.setTimeout(60000); 42 + 16 43 const fixturePath = resolve(__dirname, "..", "fixtures", "hot-reload"); 17 44 const indexPath = resolve(fixturePath, "src", "pages", "index.rs"); 18 45 const mainPath = resolve(fixturePath, "src", "main.rs"); 46 + const dataPath = resolve(fixturePath, "data.txt"); 19 47 let originalIndexContent: string; 20 48 let originalMainContent: string; 49 + let originalDataContent: string; 21 50 22 51 test.beforeAll(async () => { 23 52 // Save original content 24 53 originalIndexContent = readFileSync(indexPath, "utf-8"); 25 54 originalMainContent = readFileSync(mainPath, "utf-8"); 55 + originalDataContent = readFileSync(dataPath, "utf-8"); 56 + 57 + // Ensure files are in original state 58 + writeFileSync(indexPath, originalIndexContent, "utf-8"); 59 + writeFileSync(mainPath, originalMainContent, "utf-8"); 60 + writeFileSync(dataPath, originalDataContent, "utf-8"); 26 61 }); 27 62 28 - test.afterEach(async () => { 63 + test.afterEach(async ({ devServer }) => { 29 64 // Restore original content after each test 30 65 writeFileSync(indexPath, originalIndexContent, "utf-8"); 31 66 writeFileSync(mainPath, originalMainContent, "utf-8"); 32 - // Wait a bit for the rebuild 33 - await new Promise((resolve) => setTimeout(resolve, 2000)); 67 + writeFileSync(dataPath, originalDataContent, "utf-8"); 68 + 69 + // Only wait for build if devServer is available (startup might have failed) 70 + if (devServer) { 71 + try { 72 + devServer.clearLogs(); 73 + await waitForBuildComplete(devServer); 74 + } catch (error) { 75 + console.warn("Failed to wait for build completion in afterEach:", error); 76 + } 77 + } 34 78 }); 35 79 36 80 test.afterAll(async () => { 37 81 // Restore original content 38 82 writeFileSync(indexPath, originalIndexContent, "utf-8"); 39 83 writeFileSync(mainPath, originalMainContent, "utf-8"); 84 + writeFileSync(dataPath, originalDataContent, "utf-8"); 40 85 }); 41 86 42 87 test("should recompile when Rust code changes (dependencies)", async ({ page, devServer }) => { ··· 55 100 ); 56 101 writeFileSync(mainPath, modifiedMain, "utf-8"); 57 102 58 - // Wait for rebuild to complete - look for "finished" in logs 59 - await new Promise((resolve) => { 60 - const checkInterval = setInterval(() => { 61 - const logs = devServer.getLogs(50).join("\n"); 62 - if (logs.includes("finished") || logs.includes("Rebuild")) { 63 - clearInterval(checkInterval); 64 - resolve(null); 65 - } 66 - }, 100); 67 - 68 - // Timeout after 15 seconds 69 - setTimeout(() => { 70 - clearInterval(checkInterval); 71 - resolve(null); 72 - }, 15000); 73 - }); 103 + // Wait for rebuild to complete 104 + const logs = await waitForBuildComplete(devServer, 20000); 105 + const logsText = logs.join("\n"); 74 106 75 107 // Check logs to verify it actually recompiled (ran cargo) 76 - const logs = devServer.getLogs(50).join("\n"); 77 - expect(logs).toContain("rebuilding"); 78 - expect(logs).not.toContain("Rerunning binary"); 79 - expect(logs).not.toContain("rerunning binary"); 108 + expect(logsText).toContain("rebuilding"); 109 + // Make sure it didn't just rerun the binary 110 + expect(logsText.toLowerCase()).not.toContain("rerunning binary"); 80 111 }); 81 112 82 - test("should rerun without recompile when template changes (non-dependencies)", async ({ 83 - page, 84 - devServer 113 + test("should rerun without recompile when non-dependency files change", async ({ 114 + page, 115 + devServer, 85 116 }) => { 86 117 await page.goto(devServer.url); 87 118 88 119 // Verify initial content 89 120 await expect(page.locator("#title")).toHaveText("Original Title"); 90 121 91 - // Prepare to wait for actual reload 92 - const currentUrl = page.url(); 93 - 94 122 // Clear logs to track what happens after this point 95 123 devServer.clearLogs(); 96 124 97 - // Modify the template in index.rs - this should NOT require recompilation 98 - // since it's just the HTML template, not the actual Rust code structure 99 - const modifiedContent = originalIndexContent.replace( 100 - 'h1 id="title" { "Original Title" }', 101 - 'h1 id="title" { "Template Updated" }', 102 - ); 103 - writeFileSync(indexPath, modifiedContent, "utf-8"); 125 + // Modify data.txt - this file is NOT in the .d dependencies 126 + // So it should trigger a rerun without recompilation 127 + writeFileSync(dataPath, "Modified data", "utf-8"); 104 128 105 - // Wait for the page to reload 106 - await page.waitForURL(currentUrl, { timeout: 15000 }); 107 - 108 - // Verify the updated content 109 - await expect(page.locator("#title")).toHaveText("Template Updated", { timeout: 15000 }); 110 - 111 - // Give logs time to be captured 112 - await new Promise((resolve) => setTimeout(resolve, 1000)); 129 + // Wait for build/rerun to complete 130 + const logs = await waitForBuildComplete(devServer, 20000); 131 + const logsText = logs.join("\n"); 113 132 114 - // Check logs to verify it did NOT recompile 115 - const logs = devServer.getLogs(50).join("\n"); 116 - 117 - // Should see "rerunning binary" or similar message 118 - const hasRerunMessage = logs.toLowerCase().includes("rerunning") || 119 - logs.toLowerCase().includes("rerun"); 133 + // Should see "rerunning binary" message (case insensitive) 134 + const hasRerunMessage = logsText.toLowerCase().includes("rerunning binary"); 120 135 expect(hasRerunMessage).toBe(true); 121 - 122 - // Should NOT see cargo compilation messages 123 - expect(logs).not.toContain("Compiling"); 124 - expect(logs.toLowerCase()).not.toContain("rebuilding"); 136 + 137 + // Should NOT see "rebuilding" message 138 + expect(logsText.toLowerCase()).not.toContain("rebuilding"); 125 139 }); 126 140 127 141 test("should show updated content after file changes", async ({ page, devServer }) => {
+4 -2
e2e/tests/test-utils.ts
··· 64 64 65 65 const outputPromise = new Promise<number>((resolve, reject) => { 66 66 const timeout = setTimeout(() => { 67 - reject(new Error("Dev server did not start within 60 seconds")); 68 - }, 60000); 67 + console.error("[test-utils] Dev server startup timeout. Recent logs:"); 68 + console.error(capturedLogs.slice(-20).join("\n")); 69 + reject(new Error("Dev server did not start within 120 seconds")); 70 + }, 120000); // Increased to 120 seconds for CI 69 71 70 72 childProcess.stdout?.on("data", (data: Buffer) => { 71 73 const output = data.toString();