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/calm-geckos-drop.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 0 additions & 6 deletions docs/api/define-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -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> | unknown`
- **Required:** No
- **Description:** React Router loader function for data fetching

## Return Type

```typescript
Expand Down
19 changes: 0 additions & 19 deletions docs/api/define-resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -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> | unknown`
- **Required:** No
- **Description:** React Router loader function

## Return Type

```typescript
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion docs/concepts/file-based-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ type AppShellPageProps = {
breadcrumbTitle?: string | ((segment: string) => string);
};
guards?: Guard[];
loader?: LoaderHandler;
};
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/fs-routes/converter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const redirectGuard = async () => ({
type: "redirect" as const,
to: "/dashboard",
});
const explicitLoader = async () => ({ ok: true });

const createMockPage = (
path: string,
Expand Down Expand Up @@ -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();
});
});

// ============================================
Expand Down
10 changes: 2 additions & 8 deletions packages/core/src/fs-routes/converter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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[] = [];
Expand Down
9 changes: 2 additions & 7 deletions packages/core/src/fs-routes/types.ts
Original file line number Diff line number Diff line change
@@ -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";

// ============================================
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 0 additions & 6 deletions packages/vite-plugin/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { s } from "./validator";
* DashboardPage.appShellPageProps = {
* meta: { title: "Dashboard", icon: <DashboardIcon /> },
* guards: [authGuard],
* loader: async () => ({ data: "..." }),
* } satisfies AppShellPageProps;
* ```
*/
Expand Down Expand Up @@ -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(),
});
24 changes: 20 additions & 4 deletions packages/vite-plugin/src/validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,14 @@ describe("validateAppShellPageProps", () => {
})
.optional(),
guards: s.array(s.any()).optional(),
loader: s.any().optional(),
});

it("validates valid appShellPageProps", () => {
const sourceFile = createSourceFile(`
const Page = () => <div>Hello</div>;
Page.appShellPageProps = {
meta: { title: "Test", icon: null },
guards: [],
loader: async () => ({})
guards: []
};
export default Page;
`);
Expand All @@ -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 = () => <div>Hello</div>;
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", () => {
Expand Down
1 change: 0 additions & 1 deletion packages/vite-plugin/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ function createBuilder<T extends Schema>(schema: T): SchemaBuilder<T> {
* icon: s.any(),
* }).optional(),
* guards: s.array(s.any()).optional(),
* loader: s.any().optional(),
* });
* ```
*/
Expand Down
Loading