@usegraft/sdk-react-router 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +40 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anderson Joseph
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# @usegraft/sdk-react-router
|
|
2
|
+
|
|
3
|
+
> React Router v7 adapter: typed content reads and loader/action mounts over the same handlers every other surface serves.
|
|
4
|
+
|
|
5
|
+
Part of [Graft](https://github.com/AndersonDesign1/graft), a CMS built so an AI agent is the primary operator. For framework mode, which is where React Router runs loaders and actions on a server.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @usegraft/sdk-react-router
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Read content
|
|
14
|
+
|
|
15
|
+
Build the handle once in a `.server.ts` module, then import it from loaders and actions.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// app/lib/graft.server.ts
|
|
19
|
+
import { createDb } from "@usegraft/db";
|
|
20
|
+
import { createGraft } from "@usegraft/sdk-react-router";
|
|
21
|
+
import { collections } from "../../graft.config";
|
|
22
|
+
|
|
23
|
+
export const graft = createGraft({ db: createDb(process.env.DATABASE_URL!).db, collections });
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// app/routes/docs.$slug.tsx
|
|
28
|
+
import { graft } from "../lib/graft.server";
|
|
29
|
+
import type { Route } from "./+types/docs.$slug";
|
|
30
|
+
|
|
31
|
+
export async function loader({ params }: Route.LoaderArgs) {
|
|
32
|
+
return { doc: await graft.getContent("docs", params.slug) };
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Return types come from your `defineCollection` schemas, so a renamed field is a build error rather than a runtime `undefined`.
|
|
37
|
+
|
|
38
|
+
`loader` and `action` run on the server and React Router strips them from the browser bundle. The `.server.ts` name is belt and braces on top of that: it turns a stray client import into a build error rather than a database URL in a bundle.
|
|
39
|
+
|
|
40
|
+
There is no request-level memo here, because `React.cache` covers React Server Components and framework mode is not one. Reads go straight to the index, which is the right default: a loader runs once per request and makes a handful of reads.
|
|
41
|
+
|
|
42
|
+
To read content in the browser instead — a search box, an editor preview — use [`@usegraft/sdk-react`](https://www.npmjs.com/package/@usegraft/sdk-react), which reads the same content over HTTP and needs no database handle.
|
|
43
|
+
|
|
44
|
+
## Read with no database
|
|
45
|
+
|
|
46
|
+
Pass `index` instead of `db` and the same surface reads the SQLite artifact `graft compile` writes.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { openStaticIndex } from "@usegraft/db";
|
|
50
|
+
|
|
51
|
+
export const graft = createGraft({
|
|
52
|
+
index: await openStaticIndex(".graft/index.db"),
|
|
53
|
+
collections,
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Mount the runtime
|
|
58
|
+
|
|
59
|
+
`graftRoute` binds a Graft handler to a resource route — a route module with no default export, whose loader and action return raw Responses — so typed functions and MCP are served from your own app rather than a separate process.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// app/routes/api.fn.$name.ts
|
|
63
|
+
import { createFunctionsHandler } from "@usegraft/core";
|
|
64
|
+
import { graftRoute } from "@usegraft/sdk-react-router";
|
|
65
|
+
|
|
66
|
+
const handler = createFunctionsHandler({ db, collections, functions });
|
|
67
|
+
|
|
68
|
+
export const action = graftRoute(handler); // POST
|
|
69
|
+
export const loader = graftRoute(handler); // GET, which 405s with Allow and a fix
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// app/routes/api.mcp.ts
|
|
74
|
+
import { createGraftMcpHandler } from "@usegraft/mcp";
|
|
75
|
+
import { graftRoute } from "@usegraft/sdk-react-router";
|
|
76
|
+
|
|
77
|
+
export const action = graftRoute(createGraftMcpHandler({ db, collections, functions, actor }));
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Register them the way you register any route:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// app/routes.ts
|
|
84
|
+
import { route, type RouteConfig } from "@react-router/dev/routes";
|
|
85
|
+
|
|
86
|
+
export default [
|
|
87
|
+
route("api/fn/:name", "routes/api.fn.$name.ts"),
|
|
88
|
+
route("api/mcp", "routes/api.mcp.ts"),
|
|
89
|
+
] satisfies RouteConfig;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
React Router splits a route by method into two exports rather than naming the method, so one handler is mounted twice. Exporting `loader` is not ceremony: it is what makes a GET to a function endpoint answer with Graft's 405 and its `Allow` header, instead of React Router's own "no loader" error, which teaches the caller nothing.
|
|
93
|
+
|
|
94
|
+
The parameter is typed structurally as `{ request: Request }`, so this package needs no `react-router` dependency and every `LoaderFunctionArgs`, `ActionFunctionArgs` and generated `Route.LoaderArgs` satisfies it.
|
|
95
|
+
|
|
96
|
+
## MDX
|
|
97
|
+
|
|
98
|
+
Bodies come back as authored source. Render them with your own pipeline. The `MdxBody` in [`@usegraft/sdk-next`](https://www.npmjs.com/package/@usegraft/sdk-next) evaluates MDX in a Server Component, which framework mode does not have, so it is not re-exported here.
|
|
99
|
+
|
|
100
|
+
## Cache invalidation
|
|
101
|
+
|
|
102
|
+
React Router has no tag-based data cache, so the [`@usegraft/sdk-core`](https://www.npmjs.com/package/@usegraft/sdk-core) tag contract maps onto HTTP. Stamp `tagsFor(...)` into a CDN surrogate-key header from the route's `headers` export, and purge `tagsForChanges(branch, changeSet)` from your compile webhook.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
export function headers() {
|
|
106
|
+
return { "Cache-Tag": tagsFor("main", "docs", slug).join(",") };
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
MIT. [Repository](https://github.com/AndersonDesign1/graft) · [Changelog](https://github.com/AndersonDesign1/graft/blob/main/packages/sdk-react-router/CHANGELOG.md)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { AnyCollection, ReadOptions, Document, ListOptions, SearchOptions, SearchHit, GraftClient, ClientOptions } from '@usegraft/sdk-core';
|
|
2
|
+
export { AnyCollection, ChangeSet, ClientOptions, Document, GraftClient, ListOptions, ReadOptions, SearchHit, SearchOptions, TAG_NAMESPACE, collectionTag, createClient, documentTag, tagsFor, tagsForChanges, toDocument } from '@usegraft/sdk-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React Router v7 (framework mode) adapter over @usegraft/sdk-core — the same
|
|
6
|
+
* `getContent` / `listContent` / `searchContent` surface as
|
|
7
|
+
* @usegraft/sdk-next, so a schema types every read identically across
|
|
8
|
+
* frameworks.
|
|
9
|
+
*
|
|
10
|
+
* No request-level memo here (sdk-next's React.cache has no React Router
|
|
11
|
+
* equivalent): reads go straight to the index from `loader` and `action`,
|
|
12
|
+
* which is the right default — a route makes a handful of reads and its loader
|
|
13
|
+
* runs once per request.
|
|
14
|
+
*
|
|
15
|
+
* Server-only by nature (it holds a database handle). `loader` and `action`
|
|
16
|
+
* run on the server and React Router strips them from the browser bundle, but
|
|
17
|
+
* build the handle in a `.server.ts` module anyway: that turns a stray client
|
|
18
|
+
* import into a build error instead of a database URL in a bundle.
|
|
19
|
+
*
|
|
20
|
+
* Caching: React Router has no tag-based data cache, so the Phase 4 tag
|
|
21
|
+
* contract maps onto HTTP — stamp `tagsFor(...)` into a CDN surrogate-key
|
|
22
|
+
* header (`Cache-Tag` / `Surrogate-Key`) from the route's `headers` export,
|
|
23
|
+
* and purge `tagsForChanges(branch, changeSet)` from your compile webhook.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface Graft<TCollections extends Record<string, AnyCollection>> {
|
|
27
|
+
/** Typed getDocument. */
|
|
28
|
+
getContent<K extends keyof TCollections & string>(collection: K, slug: string, options?: ReadOptions): Promise<Document<TCollections[K]> | null>;
|
|
29
|
+
/** Typed listDocuments. */
|
|
30
|
+
listContent<K extends keyof TCollections & string>(collection: K, options?: ListOptions): Promise<Document<TCollections[K]>[]>;
|
|
31
|
+
/** Typed searchDocuments (full-text, best-ranked first). */
|
|
32
|
+
searchContent<K extends keyof TCollections & string>(collection: K, query: string, options?: SearchOptions): Promise<SearchHit<TCollections[K]>[]>;
|
|
33
|
+
/** The underlying sdk-core client, for anything the helpers don't cover. */
|
|
34
|
+
client: GraftClient<TCollections>;
|
|
35
|
+
}
|
|
36
|
+
declare function createGraft<TCollections extends Record<string, AnyCollection>>(options: ClientOptions<TCollections>): Graft<TCollections>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Mounting the Graft runtime in React Router v7 — a resource route (a route
|
|
40
|
+
* module with no default export) returns raw Responses, and its `loader` and
|
|
41
|
+
* `action` are already Web-standard, so the "adapter" is one property access:
|
|
42
|
+
* React Router hands them a `{ request, params, context }` object, the
|
|
43
|
+
* handlers want its `request`.
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* // app/routes/api.fn.$name.ts
|
|
47
|
+
* import { createFunctionsHandler } from "@usegraft/core";
|
|
48
|
+
* import { graftRoute } from "@usegraft/sdk-react-router";
|
|
49
|
+
* const handler = createFunctionsHandler({ … });
|
|
50
|
+
* export const action = graftRoute(handler); // POST
|
|
51
|
+
* export const loader = graftRoute(handler); // GET → 405s with Allow + fix
|
|
52
|
+
*
|
|
53
|
+
* // app/routes/api.mcp.ts
|
|
54
|
+
* import { createGraftMcpHandler } from "@usegraft/mcp";
|
|
55
|
+
* export const action = graftRoute(createGraftMcpHandler({ … }));
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* React Router splits a route by method into two exports rather than naming
|
|
59
|
+
* the method, so one handler is mounted twice: `action` takes POST and the
|
|
60
|
+
* other mutating verbs, `loader` takes GET. Mounting `loader` is not
|
|
61
|
+
* ceremony — it is what makes a GET to a function endpoint answer with Graft's
|
|
62
|
+
* 405 and its `Allow` header instead of React Router's own "no loader" error,
|
|
63
|
+
* which teaches the caller nothing.
|
|
64
|
+
*
|
|
65
|
+
* Typed structurally (`{ request: Request }`) so this package needs no
|
|
66
|
+
* react-router dependency — every LoaderFunctionArgs and ActionFunctionArgs
|
|
67
|
+
* satisfies it, including the per-route `Route.LoaderArgs` types React Router
|
|
68
|
+
* generates.
|
|
69
|
+
*/
|
|
70
|
+
type FetchHandler = (request: Request) => Promise<Response>;
|
|
71
|
+
/** A React Router loader or action (or anything args-shaped) over a Graft handler. */
|
|
72
|
+
declare function graftRoute(handler: FetchHandler): (args: {
|
|
73
|
+
request: Request;
|
|
74
|
+
}) => Promise<Response>;
|
|
75
|
+
|
|
76
|
+
export { type FetchHandler, type Graft, createGraft, graftRoute };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/graft.ts
|
|
2
|
+
import {
|
|
3
|
+
createClient
|
|
4
|
+
} from "@usegraft/sdk-core";
|
|
5
|
+
function createGraft(options) {
|
|
6
|
+
const client = createClient(options);
|
|
7
|
+
return {
|
|
8
|
+
client,
|
|
9
|
+
getContent: (collection, slug, opts) => client.getDocument(collection, slug, opts),
|
|
10
|
+
listContent: (collection, opts) => client.listDocuments(collection, opts),
|
|
11
|
+
searchContent: (collection, query, opts) => client.searchDocuments(collection, query, opts)
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/routes.ts
|
|
16
|
+
function graftRoute(handler) {
|
|
17
|
+
return (args) => handler(args.request);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
import {
|
|
22
|
+
createClient as createClient2,
|
|
23
|
+
toDocument,
|
|
24
|
+
collectionTag,
|
|
25
|
+
documentTag,
|
|
26
|
+
tagsFor,
|
|
27
|
+
tagsForChanges,
|
|
28
|
+
TAG_NAMESPACE
|
|
29
|
+
} from "@usegraft/sdk-core";
|
|
30
|
+
export {
|
|
31
|
+
TAG_NAMESPACE,
|
|
32
|
+
collectionTag,
|
|
33
|
+
createClient2 as createClient,
|
|
34
|
+
createGraft,
|
|
35
|
+
documentTag,
|
|
36
|
+
graftRoute,
|
|
37
|
+
tagsFor,
|
|
38
|
+
tagsForChanges,
|
|
39
|
+
toDocument
|
|
40
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usegraft/sdk-react-router",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "React Router v7 adapter: typed content reads and loader/action mounts over Graft's stateless handlers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"ai",
|
|
8
|
+
"cms",
|
|
9
|
+
"graft",
|
|
10
|
+
"headless-cms",
|
|
11
|
+
"mcp",
|
|
12
|
+
"react",
|
|
13
|
+
"react-router",
|
|
14
|
+
"remix",
|
|
15
|
+
"typescript"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/AndersonDesign1/graft#readme",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/AndersonDesign1/graft.git",
|
|
22
|
+
"directory": "packages/sdk-react-router"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@usegraft/sdk-core": "0.2.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@usegraft/core": "0.2.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=22.16"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
51
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "vitest run --passWithNoTests"
|
|
54
|
+
}
|
|
55
|
+
}
|