diff --git a/.changeset/crisp-days-reply.md b/.changeset/crisp-days-reply.md new file mode 100644 index 000000000..87b357291 --- /dev/null +++ b/.changeset/crisp-days-reply.md @@ -0,0 +1,6 @@ +--- +"@solidjs/start": patch +--- + +Fix Vite preview for non-HTML responses and projects whose server build uses an `.mjs` +entry. diff --git a/packages/start/src/config/dev-server.spec.ts b/packages/start/src/config/dev-server.spec.ts new file mode 100644 index 000000000..33aa05af5 --- /dev/null +++ b/packages/start/src/config/dev-server.spec.ts @@ -0,0 +1,66 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +import { isHtmlResponse, resolvePreviewServerEntry } from "./dev-server.ts"; + +const temporaryDirectories: string[] = []; + +function createServerEntry(extension: "js" | "mjs") { + const root = mkdtempSync(join(tmpdir(), "solid-start-preview-")); + const serverDirectory = join(root, "dist/server"); + const serverEntry = join(serverDirectory, `entry-server.${extension}`); + + temporaryDirectories.push(root); + mkdirSync(serverDirectory, { recursive: true }); + writeFileSync(serverEntry, "export default {};"); + + return { root, serverEntry }; +} + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) { + rmSync(directory, { recursive: true }); + } +}); + +describe("resolvePreviewServerEntry", () => { + it.each(["js", "mjs"] as const)("finds the generated .%s entry", extension => { + const { root, serverEntry } = createServerEntry(extension); + + expect(resolvePreviewServerEntry(root)).toBe(serverEntry); + }); + + it("throws when the server entry has not been built", () => { + const root = mkdtempSync(join(tmpdir(), "solid-start-preview-")); + temporaryDirectories.push(root); + + expect(() => resolvePreviewServerEntry(root)).toThrow( + `Could not find the SolidStart server entry in ${join(root, "dist/server")}`, + ); + }); +}); + +describe("isHtmlResponse", () => { + it("recognizes HTML responses with content type parameters", () => { + const response = new Response(null, { + headers: { "content-type": "text/html; charset=utf-8" }, + }); + + expect(isHtmlResponse(response)).toBe(true); + }); + + it.each(["video/mp4", "application/json"])( + "does not identify %s responses as HTML", + contentType => { + const response = new Response(null, { headers: { "content-type": contentType } }); + + expect(isHtmlResponse(response)).toBe(false); + }, + ); + + it("does not identify a response without a content type as HTML", () => { + expect(isHtmlResponse(new Response())).toBe(false); + }); +}); diff --git a/packages/start/src/config/dev-server.ts b/packages/start/src/config/dev-server.ts index 9188e1c1b..e1d4aa188 100644 --- a/packages/start/src/config/dev-server.ts +++ b/packages/start/src/config/dev-server.ts @@ -1,3 +1,4 @@ +import { existsSync } from "node:fs"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { NodeRequest, sendNodeResponse } from "srvx/node"; @@ -14,16 +15,19 @@ export function devServer(): Array { { name: "solid-start-dev-server", configurePreviewServer(server) { + const serverEntryUrl = pathToFileURL(resolvePreviewServerEntry(server.config.root)).href; + return () => { server.middlewares.use(async (req, res) => { - res.setHeader("content-encoding", "identity"); const webReq = new NodeRequest({ req, res }); const def: { default: { fetch: (req: Request) => Promise }; - } = await import( - pathToFileURL(join(process.cwd(), "dist/server/entry-server.js")).href - ); - sendNodeResponse(res, await def.default.fetch(webReq)); + } = await import(serverEntryUrl); + const webRes = await def.default.fetch(webReq); + if (isHtmlResponse(webRes)) { + res.setHeader("content-encoding", "identity"); + } + sendNodeResponse(res, webRes); }); }; }, @@ -121,6 +125,23 @@ export function devServer(): Array { ]; } +export function resolvePreviewServerEntry(root: string): string { + const serverDirectory = join(root, "dist/server"); + const serverEntry = ["js", "mjs"] + .map(extension => join(serverDirectory, `entry-server.${extension}`)) + .find(existsSync); + + if (!serverEntry) { + throw new Error(`Could not find the SolidStart server entry in ${serverDirectory}`); + } + + return serverEntry; +} + +export function isHtmlResponse(response: Response): boolean { + return response.headers.get("content-type")?.startsWith("text/html") ?? false; +} + /** * Removes Vite internal middleware *