Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/crisp-days-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@solidjs/start": patch
---

Fix Vite preview for non-HTML responses and projects whose server build uses an `.mjs`
entry.
66 changes: 66 additions & 0 deletions packages/start/src/config/dev-server.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
31 changes: 26 additions & 5 deletions packages/start/src/config/dev-server.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -14,16 +15,19 @@ export function devServer(): Array<PluginOption> {
{
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<Response> };
} = 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);
});
};
},
Expand Down Expand Up @@ -121,6 +125,23 @@ export function devServer(): Array<PluginOption> {
];
}

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
*
Expand Down
Loading