diff --git a/.changeset/spa-page-api-route.md b/.changeset/spa-page-api-route.md new file mode 100644 index 000000000..774a9b043 --- /dev/null +++ b/.changeset/spa-page-api-route.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": patch +--- + +Serve pages in SPA mode (`ssr: false`) when a route file exports both a page and an API handler, such as a `[...404].tsx` with a `GET`. The server treated those routes as API-only, so a `GET` that returned nothing gave a 404 for every URL the route matched. diff --git a/packages/start/src/router/routes.spec.ts b/packages/start/src/router/routes.spec.ts new file mode 100644 index 000000000..851c80401 --- /dev/null +++ b/packages/start/src/router/routes.spec.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from "vitest"; + +const GET = { import: () => Promise.resolve({}) }; + +// What the server file router produces for `routes/[...404].tsx` exporting +// both a page and `GET`, with and without the page component. +vi.mock("vinxi/routes", () => ({ + default: [ + { + path: "/*404", + page: true, + $component: { src: "routes/[...404].tsx", import: () => Promise.resolve({}) }, + $GET: GET + }, + { path: "/spa/*404", page: true, $GET: GET }, + { path: "/api/*rest", $GET: GET } + ] +})); + +const { matchAPIRoute } = await import("./routes"); + +describe("matchAPIRoute", () => { + it("marks a route with a page and an API handler as a page", () => { + expect(matchAPIRoute("/missing", "GET")?.isPage).toBe(true); + }); + + it("marks it as a page in SPA builds, where the server routes have no page components", () => { + expect(matchAPIRoute("/spa/missing", "GET")?.isPage).toBe(true); + }); + + it("does not mark an API-only route as a page", () => { + expect(matchAPIRoute("/api/missing", "GET")?.isPage).toBe(false); + }); +}); diff --git a/packages/start/src/router/routes.ts b/packages/start/src/router/routes.ts index fe569b31a..cfa88f44a 100644 --- a/packages/start/src/router/routes.ts +++ b/packages/start/src/router/routes.ts @@ -69,8 +69,9 @@ export function matchAPIRoute(path: string, method: string) { if (handler === undefined) return; - // Check if this is a page route - const isPage = route.page === true && route.$component !== undefined; + // Check if this is a page route. SPA builds leave page components out of + // the server routes, so this can't depend on `$component`. + const isPage = route.page === true; // Return comprehensive route information return { diff --git a/packages/start/test/vinxi-routes.stub.ts b/packages/start/test/vinxi-routes.stub.ts new file mode 100644 index 000000000..d6d1738de --- /dev/null +++ b/packages/start/test/vinxi-routes.stub.ts @@ -0,0 +1 @@ +export default []; diff --git a/packages/start/vitest.config.ts b/packages/start/vitest.config.ts index 8cac8fda7..e49ffe63c 100644 --- a/packages/start/vitest.config.ts +++ b/packages/start/vitest.config.ts @@ -1,6 +1,14 @@ +import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; export default defineConfig({ + resolve: { + alias: { + // vinxi serves its route manifest only inside an app build. Specs that + // need it mock this stub with `vi.mock("vinxi/routes")`. + "vinxi/routes": fileURLToPath(new URL("./test/vinxi-routes.stub.ts", import.meta.url)) + } + }, test: { globals: true, environment: "node",