···1515 fs,
1616 path::{Path, PathBuf},
1717};
1818-use tokio::{
1919- signal,
2020- sync::mpsc::channel,
2121- task::JoinHandle,
2222-};
1818+use tokio::{signal, sync::mpsc::channel, task::JoinHandle};
2319use tracing::{error, info};
24202521use crate::dev::build::BuildManager;
···107103108104 match result {
109105 Ok(events) => {
110110- info!(name: "watch", "Received {} events: {:?}", events.len(), events);
111106 // TODO: Handle rescan events, I don't fully understand the implication of them yet
112107 // some issues:
113108 // - https://github.com/notify-rs/notify/issues/434
···176171 .filter(|e| should_rebuild_for_event(e))
177172 .flat_map(|e| e.paths.iter().cloned())
178173 .collect();
174174+175175+ // Expand directory paths to include files inside them
176176+ // This is needed because folder renames only report the folder, not contents
177177+ changed_paths = expand_directory_paths(changed_paths);
179178180179 // Deduplicate paths
181180 changed_paths.sort();
···300299 }
301300302301 true
302302+}
303303+304304+/// Expand directory paths to include all files within them recursively.
305305+/// This is needed because file watcher events for folder renames only include
306306+/// the folder path, not the files inside.
307307+fn expand_directory_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
308308+ let mut expanded = Vec::new();
309309+310310+ for path in paths {
311311+ if path.is_dir() {
312312+ // Recursively collect all files in the directory
313313+ collect_files_recursive(&path, &mut expanded);
314314+ // Also keep the directory itself for cases where we need to know a dir changed
315315+ expanded.push(path);
316316+ } else {
317317+ expanded.push(path);
318318+ }
319319+ }
320320+321321+ expanded
322322+}
323323+324324+/// Recursively collect all files in a directory.
325325+fn collect_files_recursive(dir: &Path, files: &mut Vec<PathBuf>) {
326326+ if let Ok(entries) = fs::read_dir(dir) {
327327+ for entry in entries.filter_map(|e| e.ok()) {
328328+ let path = entry.path();
329329+ if path.is_dir() {
330330+ collect_files_recursive(&path, files);
331331+ } else if path.is_file() {
332332+ files.push(path);
333333+ }
334334+ }
335335+ }
303336}
304337305338async fn shutdown_signal() {
+34-12
crates/maudit-cli/src/dev/build.rs
···101101102102 // Log that we're doing an incremental build
103103 info!(name: "build", "Incremental build: {} files changed", changed_paths.len());
104104- info!(name: "build", "Changed files: {:?}", changed_paths);
104104+ debug!(name: "build", "Changed files: {:?}", changed_paths);
105105 info!(name: "build", "Rerunning binary without recompilation...");
106106107107 self.state
···172172 self.internal_build(false).await
173173 }
174174175175- async fn internal_build(&self, is_initial: bool) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
175175+ async fn internal_build(
176176+ &self,
177177+ is_initial: bool,
178178+ ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
176179 // Cancel any existing build immediately
177180 let cancel = CancellationToken::new();
178181 {
···276279 let stderr_bytes = stderr_task.await.unwrap_or_default();
277280278281 let duration = build_start_time.elapsed();
279279- let formatted_elapsed_time = format_elapsed_time(
280280- duration,
281281- &FormatElapsedTimeOptions::default_dev(),
282282- );
282282+ let formatted_elapsed_time =
283283+ format_elapsed_time(duration, &FormatElapsedTimeOptions::default_dev());
283284284285 if status.success() {
285285- let build_type = if is_initial { "Initial build" } else { "Rebuild" };
286286+ let build_type = if is_initial {
287287+ "Initial build"
288288+ } else {
289289+ "Rebuild"
290290+ };
286291 info!(name: "build", "{} finished {}", build_type, formatted_elapsed_time);
287287- self.state.status_manager.update(StatusType::Success, "Build finished successfully").await;
292292+ self.state
293293+ .status_manager
294294+ .update(StatusType::Success, "Build finished successfully")
295295+ .await;
288296289297 self.update_dependency_tracker().await;
290298···294302 // Raw stderr sometimes has something to say whenever cargo fails
295303 println!("{}", stderr_str);
296304297297- let build_type = if is_initial { "Initial build" } else { "Rebuild" };
305305+ let build_type = if is_initial {
306306+ "Initial build"
307307+ } else {
308308+ "Rebuild"
309309+ };
298310 error!(name: "build", "{} failed with errors {}", build_type, formatted_elapsed_time);
299311300312 if is_initial {
301313 error!(name: "build", "Initial build needs to succeed before we can start the dev server");
302302- self.state.status_manager.update(StatusType::Error, "Initial build failed - fix errors and save to retry").await;
314314+ self.state
315315+ .status_manager
316316+ .update(
317317+ StatusType::Error,
318318+ "Initial build failed - fix errors and save to retry",
319319+ )
320320+ .await;
303321 } else {
304304- self.state.status_manager.update(StatusType::Error, &rendered_messages.join("\n")).await;
322322+ self.state
323323+ .status_manager
324324+ .update(StatusType::Error, &rendered_messages.join("\n"))
325325+ .await;
305326 }
306327307328 Ok(false)
···341362 }
342363 }
343364344344- fn get_binary_name_from_cargo_toml() -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
365365+ fn get_binary_name_from_cargo_toml() -> Result<String, Box<dyn std::error::Error + Send + Sync>>
366366+ {
345367 let cargo_toml_path = PathBuf::from("Cargo.toml");
346368 if !cargo_toml_path.exists() {
347369 return Err("Cargo.toml not found in current directory".into());
+7-7
crates/maudit-cli/src/dev/server.rs
···346346 ws.on_upgrade(move |socket| handle_socket(socket, addr, state.status_manager))
347347}
348348349349-async fn handle_socket(
350350- socket: WebSocket,
351351- who: SocketAddr,
352352- status_manager: StatusManager,
353353-) {
349349+async fn handle_socket(socket: WebSocket, who: SocketAddr, status_manager: StatusManager) {
354350 let (mut sender, mut receiver) = socket.split();
355351356352 // Send current persistent status to new connection if there is one
···424420 async fn test_status_manager_update_error_persists() {
425421 let manager = StatusManager::new();
426422427427- manager.update(StatusType::Error, "Something went wrong").await;
423423+ manager
424424+ .update(StatusType::Error, "Something went wrong")
425425+ .await;
428426429427 let status = manager.get_current().await;
430428 assert!(status.is_some());
···521519 let manager2 = manager1.clone();
522520523521 // Update via one clone
524524- manager1.update(StatusType::Error, "Error from clone 1").await;
522522+ manager1
523523+ .update(StatusType::Error, "Error from clone 1")
524524+ .await;
525525526526 // Should be visible via the other clone
527527 let status = manager2.get_current().await;
···800800 let asset_path = PathBuf::from(original_file);
801801 if let Ok(canonical_asset) = asset_path.canonicalize() {
802802 for route in &all_bundler_routes {
803803- build_state.track_asset(
804804- canonical_asset.clone(),
805805- route.clone(),
806806- );
803803+ build_state
804804+ .track_asset(canonical_asset.clone(), route.clone());
807805 }
808806 asset_count += 1;
809807 }
+3
e2e/README.md
···1313## Running Tests
14141515The tests will automatically:
1616+16171. Build the prefetch.js bundle (via `cargo xtask build-maudit-js`)
17182. Start the Maudit dev server on the test fixture site
18193. Run the tests
···4647## Features Tested
47484849### Basic Prefetch
5050+4951- Creating link elements with `rel="prefetch"`
5052- Preventing duplicate prefetches
5153- Skipping current page prefetch
5254- Blocking cross-origin prefetches
53555456### Prerendering (Chromium only)
5757+5558- Creating `<script type="speculationrules">` elements
5659- Different eagerness levels (immediate, eager, moderate, conservative)
5760- Fallback to link prefetch on non-Chromium browsers
···1717 */
1818async function waitForBuildComplete(devServer: any, timeoutMs = 20000): Promise<string[]> {
1919 const startTime = Date.now();
2020-2020+2121 while (Date.now() - startTime < timeoutMs) {
2222 const logs = devServer.getLogs(100);
2323 const logsText = logs.join("\n").toLowerCase();
2424-2424+2525 // Look for completion messages
2626- if (logsText.includes("finished") ||
2727- logsText.includes("rerun finished") ||
2828- logsText.includes("build finished")) {
2626+ if (
2727+ logsText.includes("finished") ||
2828+ logsText.includes("rerun finished") ||
2929+ logsText.includes("build finished")
3030+ ) {
2931 return logs;
3032 }
3131-3333+3234 // Wait 100ms before checking again
3333- await new Promise(resolve => setTimeout(resolve, 100));
3535+ await new Promise((resolve) => setTimeout(resolve, 100));
3436 }
3535-3737+3638 throw new Error(`Build did not complete within ${timeoutMs}ms`);
3739}
3840···6567 writeFileSync(indexPath, originalIndexContent, "utf-8");
6668 writeFileSync(mainPath, originalMainContent, "utf-8");
6769 writeFileSync(dataPath, originalDataContent, "utf-8");
6868-7070+6971 // Only wait for build if devServer is available (startup might have failed)
7072 if (devServer) {
7173 try {
+156-77
e2e/tests/incremental-build.spec.ts
···11import { expect } from "@playwright/test";
22import { createTestWithFixture } from "./test-utils";
33-import { readFileSync, writeFileSync, copyFileSync } from "node:fs";
33+import { readFileSync, writeFileSync, mkdirSync, renameSync, rmSync, existsSync } from "node:fs";
44import { resolve, dirname } from "node:path";
55import { fileURLToPath } from "node:url";
66···1919 */
2020async function waitForBuildComplete(devServer: any, timeoutMs = 30000): Promise<string[]> {
2121 const startTime = Date.now();
2222-2222+2323 // Phase 1: Wait for build to start
2424 while (Date.now() - startTime < timeoutMs) {
2525 const logs = devServer.getLogs(200);
2626 const logsText = logs.join("\n").toLowerCase();
2727-2828- if (logsText.includes("rerunning") ||
2929- logsText.includes("rebuilding") ||
3030- logsText.includes("files changed")) {
2727+2828+ if (
2929+ logsText.includes("rerunning") ||
3030+ logsText.includes("rebuilding") ||
3131+ logsText.includes("files changed")
3232+ ) {
3133 break;
3234 }
3333-3434- await new Promise(resolve => setTimeout(resolve, 50));
3535+3636+ await new Promise((resolve) => setTimeout(resolve, 50));
3537 }
3636-3838+3739 // Phase 2: Wait for build to finish
3840 while (Date.now() - startTime < timeoutMs) {
3941 const logs = devServer.getLogs(200);
4042 const logsText = logs.join("\n").toLowerCase();
4141-4242- if (logsText.includes("finished") ||
4343- logsText.includes("rerun finished") ||
4444- logsText.includes("build finished")) {
4343+4444+ if (
4545+ logsText.includes("finished") ||
4646+ logsText.includes("rerun finished") ||
4747+ logsText.includes("build finished")
4848+ ) {
4549 // Wait for filesystem to fully sync
4646- await new Promise(resolve => setTimeout(resolve, 500));
5050+ await new Promise((resolve) => setTimeout(resolve, 500));
4751 return devServer.getLogs(200);
4852 }
4949-5050- await new Promise(resolve => setTimeout(resolve, 100));
5353+5454+ await new Promise((resolve) => setTimeout(resolve, 100));
5155 }
5252-5656+5357 throw new Error(`Build did not complete within ${timeoutMs}ms`);
5458}
5559···8791 */
8892async function setupIncrementalState(
8993 devServer: any,
9090- triggerChange: (suffix: string) => Promise<string[]>
9494+ triggerChange: (suffix: string) => Promise<string[]>,
9195): Promise<void> {
9296 // First change triggers a full build (no previous state)
9397 await triggerChange("init");
9494- await new Promise(resolve => setTimeout(resolve, 500));
9595-9898+ await new Promise((resolve) => setTimeout(resolve, 500));
9999+96100 // Second change should be incremental (state now exists)
97101 const logs = await triggerChange("setup");
98102 expect(isIncrementalBuild(logs)).toBe(true);
9999- await new Promise(resolve => setTimeout(resolve, 500));
103103+ await new Promise((resolve) => setTimeout(resolve, 500));
100104}
101105102106/**
···114118 test.setTimeout(180000);
115119116120 const fixturePath = resolve(__dirname, "..", "fixtures", "incremental-build");
117117-121121+118122 // Asset paths
119123 const assets = {
120124 blogCss: resolve(fixturePath, "src", "assets", "blog.css"),
···126130 teamPng: resolve(fixturePath, "src", "assets", "team.png"),
127131 bgPng: resolve(fixturePath, "src", "assets", "bg.png"),
128132 };
129129-133133+130134 // Output HTML paths
131135 const htmlPaths = {
132136 index: resolve(fixturePath, "dist", "index.html"),
133137 about: resolve(fixturePath, "dist", "about", "index.html"),
134138 blog: resolve(fixturePath, "dist", "blog", "index.html"),
135139 };
136136-140140+137141 // Original content storage
138142 const originals: Record<string, string | Buffer> = {};
139143···166170 // ============================================================
167171 test("CSS file change rebuilds only routes using it", async ({ devServer }) => {
168172 let testCounter = 0;
169169-173173+170174 async function triggerChange(suffix: string) {
171175 testCounter++;
172176 devServer.clearLogs();
173177 writeFileSync(assets.blogCss, originals.blogCss + `\n/* test-${testCounter}-${suffix} */`);
174178 return await waitForBuildComplete(devServer, 30000);
175179 }
176176-180180+177181 await setupIncrementalState(devServer, triggerChange);
178182179183 // Record build IDs before
···181185 expect(before.index).not.toBeNull();
182186 expect(before.about).not.toBeNull();
183187 expect(before.blog).not.toBeNull();
184184-185185- await new Promise(resolve => setTimeout(resolve, 500));
186186-188188+189189+ await new Promise((resolve) => setTimeout(resolve, 500));
190190+187191 // Trigger the change
188192 const logs = await triggerChange("final");
189189-193193+190194 // Verify incremental build with 1 route
191195 expect(isIncrementalBuild(logs)).toBe(true);
192196 expect(getAffectedRouteCount(logs)).toBe(1);
193193-197197+194198 // Verify only blog was rebuilt
195199 const after = recordBuildIds(htmlPaths);
196200 expect(after.index).toBe(before.index);
···203207 // ============================================================
204208 test("transitive JS dependency change rebuilds affected routes", async ({ devServer }) => {
205209 let testCounter = 0;
206206-210210+207211 async function triggerChange(suffix: string) {
208212 testCounter++;
209213 devServer.clearLogs();
210214 writeFileSync(assets.utilsJs, originals.utilsJs + `\n// test-${testCounter}-${suffix}`);
211215 return await waitForBuildComplete(devServer, 30000);
212216 }
213213-217217+214218 await setupIncrementalState(devServer, triggerChange);
215219216220 const before = recordBuildIds(htmlPaths);
217221 expect(before.index).not.toBeNull();
218218-219219- await new Promise(resolve => setTimeout(resolve, 500));
220220-222222+223223+ await new Promise((resolve) => setTimeout(resolve, 500));
224224+221225 const logs = await triggerChange("final");
222222-226226+223227 // Verify incremental build with 1 route
224228 expect(isIncrementalBuild(logs)).toBe(true);
225229 expect(getAffectedRouteCount(logs)).toBe(1);
226226-230230+227231 // Only index should be rebuilt (uses main.js which imports utils.js)
228232 const after = recordBuildIds(htmlPaths);
229233 expect(after.about).toBe(before.about);
···236240 // ============================================================
237241 test("direct JS entry point change rebuilds only routes using it", async ({ devServer }) => {
238242 let testCounter = 0;
239239-243243+240244 async function triggerChange(suffix: string) {
241245 testCounter++;
242246 devServer.clearLogs();
243247 writeFileSync(assets.aboutJs, originals.aboutJs + `\n// test-${testCounter}-${suffix}`);
244248 return await waitForBuildComplete(devServer, 30000);
245249 }
246246-250250+247251 await setupIncrementalState(devServer, triggerChange);
248252249253 const before = recordBuildIds(htmlPaths);
250254 expect(before.about).not.toBeNull();
251251-252252- await new Promise(resolve => setTimeout(resolve, 500));
253253-255255+256256+ await new Promise((resolve) => setTimeout(resolve, 500));
257257+254258 const logs = await triggerChange("final");
255255-259259+256260 // Verify incremental build with 1 route
257261 expect(isIncrementalBuild(logs)).toBe(true);
258262 expect(getAffectedRouteCount(logs)).toBe(1);
259259-263263+260264 // Only about should be rebuilt
261265 const after = recordBuildIds(htmlPaths);
262266 expect(after.index).toBe(before.index);
···269273 // ============================================================
270274 test("shared asset change rebuilds all routes using it", async ({ devServer }) => {
271275 let testCounter = 0;
272272-276276+273277 async function triggerChange(suffix: string) {
274278 testCounter++;
275279 devServer.clearLogs();
276276- writeFileSync(assets.stylesCss, originals.stylesCss + `\n/* test-${testCounter}-${suffix} */`);
280280+ writeFileSync(
281281+ assets.stylesCss,
282282+ originals.stylesCss + `\n/* test-${testCounter}-${suffix} */`,
283283+ );
277284 return await waitForBuildComplete(devServer, 30000);
278285 }
279279-286286+280287 await setupIncrementalState(devServer, triggerChange);
281288282289 const before = recordBuildIds(htmlPaths);
283290 expect(before.index).not.toBeNull();
284291 expect(before.about).not.toBeNull();
285285-286286- await new Promise(resolve => setTimeout(resolve, 500));
287287-292292+293293+ await new Promise((resolve) => setTimeout(resolve, 500));
294294+288295 const logs = await triggerChange("final");
289289-296296+290297 // Verify incremental build with 2 routes (/ and /about both use styles.css)
291298 expect(isIncrementalBuild(logs)).toBe(true);
292299 expect(getAffectedRouteCount(logs)).toBe(2);
293293-300300+294301 // Index and about should be rebuilt, blog should not
295302 const after = recordBuildIds(htmlPaths);
296303 expect(after.blog).toBe(before.blog);
···303310 // ============================================================
304311 test("image change rebuilds only routes using it", async ({ devServer }) => {
305312 let testCounter = 0;
306306-313313+307314 async function triggerChange(suffix: string) {
308315 testCounter++;
309316 devServer.clearLogs();
···311318 // This simulates modifying an image file
312319 const modified = Buffer.concat([
313320 originals.logoPng as Buffer,
314314- Buffer.from(`<!-- test-${testCounter}-${suffix} -->`)
321321+ Buffer.from(`<!-- test-${testCounter}-${suffix} -->`),
315322 ]);
316323 writeFileSync(assets.logoPng, modified);
317324 return await waitForBuildComplete(devServer, 30000);
318325 }
319319-326326+320327 await setupIncrementalState(devServer, triggerChange);
321328322329 const before = recordBuildIds(htmlPaths);
323330 expect(before.index).not.toBeNull();
324324-325325- await new Promise(resolve => setTimeout(resolve, 500));
326326-331331+332332+ await new Promise((resolve) => setTimeout(resolve, 500));
333333+327334 const logs = await triggerChange("final");
328328-335335+329336 // Verify incremental build with 1 route
330337 expect(isIncrementalBuild(logs)).toBe(true);
331338 expect(getAffectedRouteCount(logs)).toBe(1);
332332-339339+333340 // Only index should be rebuilt (uses logo.png)
334341 const after = recordBuildIds(htmlPaths);
335342 expect(after.about).toBe(before.about);
···342349 // ============================================================
343350 test("multiple file changes rebuild union of affected routes", async ({ devServer }) => {
344351 let testCounter = 0;
345345-352352+346353 async function triggerChange(suffix: string) {
347354 testCounter++;
348355 devServer.clearLogs();
···351358 writeFileSync(assets.aboutJs, originals.aboutJs + `\n// test-${testCounter}-${suffix}`);
352359 return await waitForBuildComplete(devServer, 30000);
353360 }
354354-361361+355362 await setupIncrementalState(devServer, triggerChange);
356363357364 const before = recordBuildIds(htmlPaths);
358365 expect(before.about).not.toBeNull();
359366 expect(before.blog).not.toBeNull();
360360-361361- await new Promise(resolve => setTimeout(resolve, 500));
362362-367367+368368+ await new Promise((resolve) => setTimeout(resolve, 500));
369369+363370 const logs = await triggerChange("final");
364364-371371+365372 // Verify incremental build with 2 routes (/about and /blog)
366373 expect(isIncrementalBuild(logs)).toBe(true);
367374 expect(getAffectedRouteCount(logs)).toBe(2);
368368-375375+369376 // About and blog should be rebuilt, index should not
370377 const after = recordBuildIds(htmlPaths);
371378 expect(after.index).toBe(before.index);
···376383 // ============================================================
377384 // TEST 7: CSS url() asset dependency (bg.png via blog.css → /blog)
378385 // ============================================================
379379- test("CSS url() asset change triggers rebundling and rebuilds affected routes", async ({ devServer }) => {
386386+ test("CSS url() asset change triggers rebundling and rebuilds affected routes", async ({
387387+ devServer,
388388+ }) => {
380389 let testCounter = 0;
381381-390390+382391 async function triggerChange(suffix: string) {
383392 testCounter++;
384393 devServer.clearLogs();
···386395 // Changing it should trigger rebundling and rebuild /blog
387396 const modified = Buffer.concat([
388397 originals.bgPng as Buffer,
389389- Buffer.from(`<!-- test-${testCounter}-${suffix} -->`)
398398+ Buffer.from(`<!-- test-${testCounter}-${suffix} -->`),
390399 ]);
391400 writeFileSync(assets.bgPng, modified);
392401 return await waitForBuildComplete(devServer, 30000);
393402 }
394394-403403+395404 await setupIncrementalState(devServer, triggerChange);
396405397406 const before = recordBuildIds(htmlPaths);
398407 expect(before.blog).not.toBeNull();
399399-400400- await new Promise(resolve => setTimeout(resolve, 500));
401401-408408+409409+ await new Promise((resolve) => setTimeout(resolve, 500));
410410+402411 const logs = await triggerChange("final");
403403-412412+404413 // Verify incremental build triggered
405414 expect(isIncrementalBuild(logs)).toBe(true);
406406-415415+407416 // Blog should be rebuilt (uses blog.css which references bg.png via url())
408417 // The bundler should have been re-run to update the hashed asset reference
409418 const after = recordBuildIds(htmlPaths);
410419 expect(after.blog).not.toBe(before.blog);
420420+ });
421421+422422+ // ============================================================
423423+ // TEST 8: Folder rename handling
424424+ // ============================================================
425425+ test("folder rename triggers rebuild with correct changed paths", async ({ devServer }) => {
426426+ // Create a test folder structure that we can rename
427427+ const testFolder = resolve(fixturePath, "src", "assets", "test-icons");
428428+ const renamedFolder = resolve(fixturePath, "src", "assets", "test-icons-renamed");
429429+ const testFile = resolve(testFolder, "test-icon.css");
430430+431431+ // Clean up any leftover folders from previous test runs
432432+ if (existsSync(testFolder)) {
433433+ rmSync(testFolder, { recursive: true });
434434+ }
435435+ if (existsSync(renamedFolder)) {
436436+ rmSync(renamedFolder, { recursive: true });
437437+ }
438438+439439+ try {
440440+ // Step 1: Create the test folder and file
441441+ mkdirSync(testFolder, { recursive: true });
442442+ writeFileSync(testFile, "/* test icon styles */\n.icon { width: 16px; }");
443443+444444+ // Wait for initial detection
445445+ devServer.clearLogs();
446446+ await new Promise((resolve) => setTimeout(resolve, 1000));
447447+448448+ // Step 2: Rename the folder
449449+ devServer.clearLogs();
450450+ renameSync(testFolder, renamedFolder);
451451+452452+ // Wait for the file watcher to detect the rename and process it
453453+ const startTime = Date.now();
454454+ const timeoutMs = 10000;
455455+ let logsAfterRename: string[] = [];
456456+457457+ while (Date.now() - startTime < timeoutMs) {
458458+ logsAfterRename = devServer.getLogs(100);
459459+ const logsText = logsAfterRename.join("\n");
460460+461461+ // Wait for the build to complete (indicates paths were processed)
462462+ if (logsText.includes("rerun finished") || logsText.includes("Build completed")) {
463463+ break;
464464+ }
465465+466466+ await new Promise((resolve) => setTimeout(resolve, 100));
467467+ }
468468+469469+ // Log what we received for debugging
470470+ console.log("Logs after folder rename:", logsAfterRename.slice(-20));
471471+472472+ const logsText = logsAfterRename.join("\n");
473473+474474+ // Verify that events were received
475475+ expect(logsText).toContain("Received");
476476+477477+ // The key assertion: the changed files should include the FILE inside the folder,
478478+ // not just the folder path itself
479479+ // After expanding directory paths, we should see the CSS file
480480+ expect(logsText).toContain("test-icon.css");
481481+ } finally {
482482+ // Cleanup: remove test folders
483483+ if (existsSync(testFolder)) {
484484+ rmSync(testFolder, { recursive: true });
485485+ }
486486+ if (existsSync(renamedFolder)) {
487487+ rmSync(renamedFolder, { recursive: true });
488488+ }
489489+ }
411490 });
412491});
+2-2
e2e/tests/test-utils.ts
···188188 let hash = 0;
189189 for (let i = 0; i < fixtureName.length; i++) {
190190 const char = fixtureName.charCodeAt(i);
191191- hash = ((hash << 5) - hash) + char;
191191+ hash = (hash << 5) - hash + char;
192192 hash = hash & hash; // Convert to 32bit integer
193193 }
194194 // Use modulo to keep the offset reasonable (0-99)
···237237 if (!server) {
238238 // Calculate port based on fixture name hash + worker index to avoid collisions
239239 const fixtureOffset = getFixturePortOffset(fixtureName);
240240- const preferredPort = basePort + (workerIndex * 100) + fixtureOffset;
240240+ const preferredPort = basePort + workerIndex * 100 + fixtureOffset;
241241 const port = findAvailablePort(preferredPort);
242242243243 server = await startDevServer({
+1-1
website/content/docs/content.md
···214214215215```markdown
216216---
217217-title: {{ enhance title="Super Title" /}}
217217+title: { { enhance title="Super Title" / } }
218218---
219219220220Here's an image with a caption:
+1-1
website/content/docs/prefetching.md
···49495050Note that prerendering, unlike prefetching, may require rethinking how the JavaScript on your pages works, as it'll run JavaScript from pages that the user hasn't visited yet. For example, this might result in analytics reporting incorrect page views.
51515252-## Possible risks
5252+## Possible risks
53535454Prefetching pages in static websites is typically always safe. In more traditional apps, an issue can arise if your pages cause side effects to happen on the server. For instance, if you were to prefetch `/logout`, your user might get disconnected on hover, or worse as soon as the log out link appear in the viewport. In modern times, it is typically not recommended to have links cause such side effects anyway, reducing the risk of this happening.
5555
+1-1
website/content/news/2026-in-the-cursed-lands.md
···70707171### Shortcodes
72727373-Embedding a YouTube video typically means copying a long, ugly iframe tag and configuring several attributes to ensure proper rendering. It'd be nice to have something friendlier, a code that would be short, you will.
7373+Embedding a YouTube video typically means copying a long, ugly iframe tag and configuring several attributes to ensure proper rendering. It'd be nice to have something friendlier, a code that would be short, you will.
74747575```md
7676Here's my cool video: