diff --git a/.changeset/fix-fs-routes-guard-loader-chain.md b/.changeset/fix-fs-routes-guard-loader-chain.md new file mode 100644 index 00000000..430f7a6c --- /dev/null +++ b/.changeset/fix-fs-routes-guard-loader-chain.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/app-shell": patch +--- + +Fixed file-based routing (vite plugin) not chaining guards with user-provided loaders. When a page module defines both guards and a `loader` export, guards are now evaluated first via `withGuardsLoader`, and the user loader runs only after guards pass. Previously, guards could overwrite the user loader. diff --git a/packages/core/src/fs-routes/converter.test.tsx b/packages/core/src/fs-routes/converter.test.tsx index 1956d4f3..58169dde 100644 --- a/packages/core/src/fs-routes/converter.test.tsx +++ b/packages/core/src/fs-routes/converter.test.tsx @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; import { convertPagesToModules, validateExclusiveRouteConfig } from "./converter"; import { createContentRoutes } from "@/routing/routes"; import type { PageEntry, PageComponent } from "./types"; @@ -102,6 +102,40 @@ describe("convertPagesToModules", () => { expect(modules[0].resources[0].guards).not.toContain(parentGuard); }); + it("runs guards before module loaders when both are defined", async () => { + const loader = vi.fn().mockResolvedValue({ data: "test" }); + + const pages = [ + createMockPage("/dashboard", { + guards: [async () => ({ type: "redirect" as const, to: "/login" })], + loader, + }), + ]; + const modules = convertPagesToModules(pages); + + const result = await modules[0].loader!({} as never); + expect(result).toBeInstanceOf(Response); + expect((result as Response).status).toBe(302); + expect((result as Response).headers.get("Location")).toBe("/login"); + expect(loader).not.toHaveBeenCalled(); + }); + + it("runs guards before resource loaders when both are defined", async () => { + const guard = vi.fn().mockResolvedValue({ type: "pass" as const }); + const loader = vi.fn().mockResolvedValue({ items: [1, 2, 3] }); + + const pages = [ + createMockPage("/dashboard"), + createMockPage("/dashboard/orders", { guards: [guard], loader }), + ]; + const modules = convertPagesToModules(pages); + + const result = await modules[0].resources[0].loader!({} as never); + expect(guard).toHaveBeenCalled(); + expect(loader).toHaveBeenCalled(); + expect(result).toEqual({ items: [1, 2, 3] }); + }); + it("handles multiple top-level modules", () => { const pages = [ createMockPage("/dashboard"), diff --git a/packages/core/src/fs-routes/converter.tsx b/packages/core/src/fs-routes/converter.tsx index 2555f582..2e6b2268 100644 --- a/packages/core/src/fs-routes/converter.tsx +++ b/packages/core/src/fs-routes/converter.tsx @@ -81,8 +81,7 @@ function nodeToResource(node: PageNode): Resource { const breadcrumbTitle = Component?.appShellPageProps?.meta?.breadcrumbTitle; const explicitLoader = Component?.appShellPageProps?.loader; const loader = - explicitLoader ?? - (node.guards && node.guards.length > 0 ? withGuardsLoader(node.guards) : undefined); + node.guards.length > 0 ? withGuardsLoader(node.guards, explicitLoader) : explicitLoader; // Recursively convert children to subResources const subResources: Resource[] = []; @@ -118,8 +117,7 @@ function nodeToModule(node: PageNode): Module { const breadcrumbTitle = Component?.appShellPageProps?.meta?.breadcrumbTitle; const explicitLoader = Component?.appShellPageProps?.loader; const loader = - explicitLoader ?? - (node.guards && node.guards.length > 0 ? withGuardsLoader(node.guards) : undefined); + node.guards.length > 0 ? withGuardsLoader(node.guards, explicitLoader) : explicitLoader; // Convert children to resources const resources: Resource[] = [];