diff --git a/.changeset/calm-geckos-drop.md b/.changeset/calm-geckos-drop.md new file mode 100644 index 00000000..a39b8ef1 --- /dev/null +++ b/.changeset/calm-geckos-drop.md @@ -0,0 +1,6 @@ +--- +"@tailor-platform/app-shell": patch +"@tailor-platform/app-shell-vite-plugin": patch +--- + +Remove `loader` from file-based page definitions (`Page.appShellPageProps`). This removes an accidentally exposed incomplete API and makes `guards` the single source of page-level route behavior in file-based routing. diff --git a/docs/api/define-module.md b/docs/api/define-module.md index 7996bf10..1265c813 100644 --- a/docs/api/define-module.md +++ b/docs/api/define-module.md @@ -85,12 +85,6 @@ See [Guards Overview](./guards/overview.md) for details. - **Required:** No - **Description:** Error boundary component for this module and its child resources. Use `useRouteError` hook to access error details. -### `loader` - -- **Type:** `(args: LoaderFunctionArgs) => Promise | unknown` -- **Required:** No -- **Description:** React Router loader function for data fetching - ## Return Type ```typescript diff --git a/docs/api/define-resource.md b/docs/api/define-resource.md index a4829339..31ab7d57 100644 --- a/docs/api/define-resource.md +++ b/docs/api/define-resource.md @@ -86,12 +86,6 @@ See [Guards Overview](./guards/overview.md) for details. - **Required:** No - **Description:** Error boundary component for this resource -### `loader` - -- **Type:** `(args: LoaderFunctionArgs) => Promise | unknown` -- **Required:** No -- **Description:** React Router loader function - ## Return Type ```typescript @@ -184,19 +178,6 @@ const adminSettingsResource = defineResource({ }); ``` -### Resource with Loader - -```typescript -const productResource = defineResource({ - path: ":id", - component: ProductPage, - loader: async ({ params }) => { - const product = await fetch(`/api/products/${params.id}`); - return product.json(); - }, -}); -``` - ### Resource with Error Boundary ```typescript diff --git a/docs/concepts/file-based-routing.md b/docs/concepts/file-based-routing.md index 4b88991d..ef00ced7 100644 --- a/docs/concepts/file-based-routing.md +++ b/docs/concepts/file-based-routing.md @@ -133,7 +133,6 @@ type AppShellPageProps = { breadcrumbTitle?: string | ((segment: string) => string); }; guards?: Guard[]; - loader?: LoaderHandler; }; ``` diff --git a/packages/core/skills/app-shell-patterns/references/fundamental/components.md b/packages/core/skills/app-shell-patterns/references/fundamental/components.md index 8b5e935c..673d3de6 100644 --- a/packages/core/skills/app-shell-patterns/references/fundamental/components.md +++ b/packages/core/skills/app-shell-patterns/references/fundamental/components.md @@ -605,7 +605,7 @@ Types for authoring guard functions used by `WithGuard` and `appShellPageProps.g ### `RouteParams`, `PageComponent`, `PageMeta`, `AppShellPageProps`, `AppShellRegister`, `ContextData` (types) -Types for declaring page props (`appShellPageProps = { meta, guards, loader }`), reading typed route params, and registering route types globally. See `project-setup.md` for usage. +Types for declaring page props (`appShellPageProps = { meta, guards }`), reading typed route params, and registering route types globally. See `project-setup.md` for usage. --- diff --git a/packages/core/skills/app-shell-patterns/references/fundamental/graphql.md b/packages/core/skills/app-shell-patterns/references/fundamental/graphql.md index c889c42d..9096953f 100644 --- a/packages/core/skills/app-shell-patterns/references/fundamental/graphql.md +++ b/packages/core/skills/app-shell-patterns/references/fundamental/graphql.md @@ -191,7 +191,7 @@ Every page component: }; ``` -3. Uses `appShellPageProps.guards` for permission gates and `appShellPageProps.loader` for route loaders. See [project-setup.md](project-setup.md). +3. Uses `appShellPageProps.guards` for permission gates. See [project-setup.md](project-setup.md). ## Quick reference diff --git a/packages/core/src/fs-routes/converter.test.tsx b/packages/core/src/fs-routes/converter.test.tsx index 1956d4f3..88a7a8fe 100644 --- a/packages/core/src/fs-routes/converter.test.tsx +++ b/packages/core/src/fs-routes/converter.test.tsx @@ -18,6 +18,7 @@ const redirectGuard = async () => ({ type: "redirect" as const, to: "/dashboard", }); +const explicitLoader = async () => ({ ok: true }); const createMockPage = ( path: string, @@ -270,6 +271,32 @@ describe("convertPagesToModules", () => { expect(modules[0].guards).toHaveLength(0); expect(modules[0].loader).toBeUndefined(); }); + + it("ignores appShellPageProps.loader on modules", () => { + const pages = [ + createMockPage("/dashboard", { + meta: { title: "Dashboard" }, + // @ts-expect-error breaking change: page loaders are no longer supported + loader: explicitLoader, + }), + ]; + const modules = convertPagesToModules(pages); + + expect(modules[0].loader).toBeUndefined(); + }); + + it("ignores appShellPageProps.loader on resources", () => { + const pages = [ + createMockPage("/dashboard"), + createMockPage("/dashboard/orders", { + // @ts-expect-error breaking change: page loaders are no longer supported + loader: explicitLoader, + }), + ]; + const modules = convertPagesToModules(pages); + + expect(modules[0].resources[0].loader).toBeUndefined(); + }); }); // ============================================ diff --git a/packages/core/src/fs-routes/converter.tsx b/packages/core/src/fs-routes/converter.tsx index 2555f582..88fb8a67 100644 --- a/packages/core/src/fs-routes/converter.tsx +++ b/packages/core/src/fs-routes/converter.tsx @@ -79,10 +79,7 @@ function nodeToResource(node: PageNode): Resource { const title = getTitle(Component, node.path); const icon = Component?.appShellPageProps?.meta?.icon; const breadcrumbTitle = Component?.appShellPageProps?.meta?.breadcrumbTitle; - const explicitLoader = Component?.appShellPageProps?.loader; - const loader = - explicitLoader ?? - (node.guards && node.guards.length > 0 ? withGuardsLoader(node.guards) : undefined); + const loader = node.guards && node.guards.length > 0 ? withGuardsLoader(node.guards) : undefined; // Recursively convert children to subResources const subResources: Resource[] = []; @@ -116,10 +113,7 @@ function nodeToModule(node: PageNode): Module { const title = getTitle(Component, node.path); const icon = Component?.appShellPageProps?.meta?.icon; const breadcrumbTitle = Component?.appShellPageProps?.meta?.breadcrumbTitle; - const explicitLoader = Component?.appShellPageProps?.loader; - const loader = - explicitLoader ?? - (node.guards && node.guards.length > 0 ? withGuardsLoader(node.guards) : undefined); + const loader = node.guards && node.guards.length > 0 ? withGuardsLoader(node.guards) : undefined; // Convert children to resources const resources: Resource[] = []; diff --git a/packages/core/src/fs-routes/types.ts b/packages/core/src/fs-routes/types.ts index 754cdf03..a179e3a4 100644 --- a/packages/core/src/fs-routes/types.ts +++ b/packages/core/src/fs-routes/types.ts @@ -1,5 +1,5 @@ import type { FC, ReactNode } from "react"; -import type { Guard, LoaderHandler } from "@/resource"; +import type { Guard } from "@/resource"; import type { LocalizedString } from "@/lib/i18n"; // ============================================ @@ -52,18 +52,13 @@ export type AppShellPageProps = { * ``` */ guards?: Guard[]; - - /** - * Loader function to fetch data before rendering the page. - */ - loader?: LoaderHandler; }; /** * A React component that can be used as a page in file-based routing. * * The component can optionally have an `appShellPageProps` static field - * for configuring navigation metadata, guards, and loaders. + * for configuring navigation metadata and guards. */ export type PageComponent = FC & { appShellPageProps?: AppShellPageProps; diff --git a/packages/vite-plugin/src/schema.ts b/packages/vite-plugin/src/schema.ts index b546eeb1..e3f757a6 100644 --- a/packages/vite-plugin/src/schema.ts +++ b/packages/vite-plugin/src/schema.ts @@ -12,7 +12,6 @@ import { s } from "./validator"; * DashboardPage.appShellPageProps = { * meta: { title: "Dashboard", icon: }, * guards: [authGuard], - * loader: async () => ({ data: "..." }), * } satisfies AppShellPageProps; * ``` */ @@ -43,9 +42,4 @@ export const appShellPagePropsSchema = s.object({ * Guards are executed in order. Each page evaluates only its own guards (no inheritance from parent pages). */ guards: s.array(s.any()).optional(), - - /** - * Loader function to fetch data before rendering the page. - */ - loader: s.any().optional(), }); diff --git a/packages/vite-plugin/src/validator.test.ts b/packages/vite-plugin/src/validator.test.ts index cf12e21e..1a42db23 100644 --- a/packages/vite-plugin/src/validator.test.ts +++ b/packages/vite-plugin/src/validator.test.ts @@ -140,7 +140,6 @@ describe("validateAppShellPageProps", () => { }) .optional(), guards: s.array(s.any()).optional(), - loader: s.any().optional(), }); it("validates valid appShellPageProps", () => { @@ -148,8 +147,7 @@ describe("validateAppShellPageProps", () => { const Page = () =>
Hello
; Page.appShellPageProps = { meta: { title: "Test", icon: null }, - guards: [], - loader: async () => ({}) + guards: [] }; export default Page; `); @@ -175,7 +173,25 @@ describe("validateAppShellPageProps", () => { expect(warnings).toHaveLength(1); expect(warnings[0].key).toBe("unknownKey"); expect(warnings[0].message).toContain("Unknown key"); - expect(warnings[0].validKeys).toEqual(["meta", "guards", "loader"]); + expect(warnings[0].validKeys).toEqual(["meta", "guards"]); + }); + + it("reports loader as an unknown top-level key", () => { + const sourceFile = createSourceFile(` + const Page = () =>
Hello
; + Page.appShellPageProps = { + meta: { title: "Test" }, + loader: async () => ({}) + }; + export default Page; + `); + const node = findAppShellPagePropsNode(sourceFile); + const warnings = validateAppShellPageProps(node!, testSchema, "test.tsx"); + + expect(warnings).toHaveLength(1); + expect(warnings[0].key).toBe("loader"); + expect(warnings[0].message).toContain("Unknown key"); + expect(warnings[0].validKeys).toEqual(["meta", "guards"]); }); it("reports unknown nested keys in meta", () => { diff --git a/packages/vite-plugin/src/validator.ts b/packages/vite-plugin/src/validator.ts index a248418a..be942d55 100644 --- a/packages/vite-plugin/src/validator.ts +++ b/packages/vite-plugin/src/validator.ts @@ -52,7 +52,6 @@ function createBuilder(schema: T): SchemaBuilder { * icon: s.any(), * }).optional(), * guards: s.array(s.any()).optional(), - * loader: s.any().optional(), * }); * ``` */