Skip to content

Commit d6fc1d0

Browse files
bartlomiejuclaude
andauthored
fix: normalize Windows paths in generated snapshot and server entry (#3727)
## Summary - When building on Windows, `path.relative()` produces backslash separators (`\`) that get hardcoded into the generated `snapshot.js` and `server.js` files - If the bundle is then deployed to a Linux server (e.g. Windows CI → Linux prod), those paths break at runtime - Fix: add a `toPosix()` helper that normalizes all paths written into generated files to use forward slashes, applied in three places: - `root` path passed to `generateServerEntry()` - `filePath` in `prepareStaticFile()` (from `path.relative()`) - All path options inside `generateServerEntry()` (root, serverEntry, snapshotSpecifier) Closes #3513 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6dd2c97 commit d6fc1d0

2 files changed

Lines changed: 76 additions & 13 deletions

File tree

packages/fresh/src/dev/dev_build_cache.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ import { contentType as getStdContentType } from "@std/media-types/content-type"
1717

1818
const WINDOWS_SEPARATOR = pathWin32.SEPARATOR;
1919

20+
/** Normalize a path to use forward slashes so that generated files
21+
* are portable across operating systems (e.g. build on Windows,
22+
* deploy on Linux). */
23+
function toPosix(p: string): string {
24+
return p.replaceAll(WINDOWS_SEPARATOR, "/");
25+
}
26+
2027
export interface MemoryFile {
2128
hash: string | null;
2229
contentType: string;
@@ -378,7 +385,7 @@ export class DiskBuildCache<State> implements DevBuildCache<State> {
378385
await Deno.writeTextFile(
379386
path.join(outDir, "server.js"),
380387
generateServerEntry({
381-
root: appPath,
388+
root: toPosix(appPath),
382389
serverEntry: pathToSpec(outDir, this.#config.serverEntry),
383390
snapshotSpecifier: "./snapshot.js",
384391
}),
@@ -550,9 +557,11 @@ export async function prepareStaticFile(
550557
return {
551558
name: encodedPathname,
552559
hash,
553-
filePath: path.isAbsolute(item.filePath)
554-
? path.relative(outDir, item.filePath)
555-
: item.filePath,
560+
filePath: toPosix(
561+
path.isAbsolute(item.filePath)
562+
? path.relative(outDir, item.filePath)
563+
: item.filePath,
564+
),
556565
contentType: getContentType(item.filePath),
557566
};
558567
}
@@ -564,22 +573,24 @@ export function generateServerEntry(
564573
snapshotSpecifier: string;
565574
},
566575
): string {
567-
let rootPath = `path.join(import.meta.dirname, ${
568-
JSON.stringify(options.root)
569-
})`;
570-
if (path.isAbsolute(options.root)) {
576+
const root = toPosix(options.root);
577+
const serverEntry = toPosix(options.serverEntry);
578+
const snapshotSpecifier = toPosix(options.snapshotSpecifier);
579+
580+
let rootPath = `path.join(import.meta.dirname, ${JSON.stringify(root)})`;
581+
if (path.isAbsolute(root)) {
571582
// deno-lint-ignore no-console
572583
console.warn(
573-
`WARN: using absolute root path in snapshot: "${options.root}"`,
584+
`WARN: using absolute root path in snapshot: "${root}"`,
574585
);
575586

576-
rootPath = JSON.stringify(options.root);
587+
rootPath = JSON.stringify(root);
577588
}
578589

579590
return `${EDIT_WARNING}
580591
import { setBuildCache, ProdBuildCache, path } from "fresh/internal";
581-
import * as snapshot from "${options.snapshotSpecifier}";
582-
import { app } from "${options.serverEntry}";
592+
import * as snapshot from "${snapshotSpecifier}";
593+
import { app } from "${serverEntry}";
583594
584595
const root = ${rootPath};
585596
setBuildCache(app, new ProdBuildCache(root, snapshot), "production");

packages/fresh/src/dev/dev_build_cache_test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { expect } from "@std/expect";
2-
import { MemoryBuildCache } from "./dev_build_cache.ts";
2+
import {
3+
generateServerEntry,
4+
MemoryBuildCache,
5+
prepareStaticFile,
6+
} from "./dev_build_cache.ts";
37
import { FileTransformer } from "./file_transformer.ts";
48
import { createFakeFs, withTmpDir } from "../test_utils.ts";
59
import type { ResolvedBuildConfig } from "./builder.ts";
@@ -45,3 +49,51 @@ Deno.test({
4549
await expect(noThrown4).resolves.toBe(null);
4650
},
4751
});
52+
53+
Deno.test("generateServerEntry - normalizes Windows paths to forward slashes", () => {
54+
const output = generateServerEntry({
55+
root: "..\\..\\myapp",
56+
serverEntry: ".\\src\\main.ts",
57+
snapshotSpecifier: ".\\snapshot.js",
58+
});
59+
60+
// No backslashes should appear in the generated code
61+
expect(output).not.toContain("\\");
62+
expect(output).toContain(`from "./snapshot.js"`);
63+
expect(output).toContain(`from "./src/main.ts"`);
64+
expect(output).toContain(`"../../myapp"`);
65+
});
66+
67+
Deno.test("generateServerEntry - forward slashes pass through unchanged", () => {
68+
const output = generateServerEntry({
69+
root: "../../myapp",
70+
serverEntry: "./src/main.ts",
71+
snapshotSpecifier: "./snapshot.js",
72+
});
73+
74+
expect(output).not.toContain("\\");
75+
expect(output).toContain(`from "./snapshot.js"`);
76+
expect(output).toContain(`from "./src/main.ts"`);
77+
expect(output).toContain(`"../../myapp"`);
78+
});
79+
80+
Deno.test({
81+
name: "prepareStaticFile - normalizes Windows filePath to forward slashes",
82+
fn: async () => {
83+
await using _tmp = await withTmpDir();
84+
const tmp = _tmp.dir;
85+
86+
// Create a test file
87+
const filePath = `${tmp}/assets/style.css`;
88+
await Deno.mkdir(`${tmp}/assets`, { recursive: true });
89+
await Deno.writeTextFile(filePath, "body {}");
90+
91+
const result = await prepareStaticFile(
92+
{ filePath, hash: null, pathname: "/assets/style.css" },
93+
tmp,
94+
);
95+
96+
expect(result.filePath).not.toContain("\\");
97+
expect(result.filePath).toEqual("assets/style.css");
98+
},
99+
});

0 commit comments

Comments
 (0)