@exposify/next 0.0.0-stage → 0.0.2
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 +328 -2
- package/dist/chunk-D5TQF5DY.js +53 -0
- package/dist/config.cjs +2046 -0
- package/dist/config.d.cts +191 -0
- package/dist/config.d.ts +191 -0
- package/dist/config.js +1978 -0
- package/dist/exposify-next.js +2651 -0
- package/dist/fields-olvyFnxj.d.cts +23 -0
- package/dist/fields-olvyFnxj.d.ts +23 -0
- package/dist/index.cjs +1179 -0
- package/dist/index.d.cts +201 -0
- package/dist/index.d.ts +201 -0
- package/dist/index.js +1186 -0
- package/package.json +58 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Exposify
|
|
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
CHANGED
|
@@ -1,3 +1,329 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @exposify/next
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Next.js integration for [Exposify](../../README.md): your route handlers,
|
|
4
|
+
pages and Server Actions become MCP tools that AI agents can call, at `/mcp` on
|
|
5
|
+
your own app.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @exposify/next
|
|
9
|
+
npx exposify-next setup # pick the endpoints, get the route file to create
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Or by hand, in two files:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// next.config.ts
|
|
16
|
+
import { withExposify } from "@exposify/next/config";
|
|
17
|
+
|
|
18
|
+
export default withExposify(nextConfig);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// app/mcp/[[...transport]]/route.ts
|
|
23
|
+
import { aiReady } from "@exposify/next";
|
|
24
|
+
import { exposify } from "../../../exposify.generated";
|
|
25
|
+
|
|
26
|
+
const handler = aiReady(exposify, { expose: ["POST /api/orders"] });
|
|
27
|
+
|
|
28
|
+
export { handler as GET, handler as POST, handler as DELETE };
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`withExposify()` writes `exposify.generated.ts` whenever `next dev` or `next
|
|
32
|
+
build` starts, and keeps it current in dev as route files change. It holds
|
|
33
|
+
the route table, your Server Actions, your docs (`README.md`, `docs/**/*.md`) for `search_docs`,
|
|
34
|
+
and `exposify.descriptions.json`'s texts, so nothing is read from disk at
|
|
35
|
+
runtime. Commit it or ignore it; `npx exposify-next generate` writes it too.
|
|
36
|
+
|
|
37
|
+
## Which routes become tools
|
|
38
|
+
|
|
39
|
+
Every method an `app/**/route.ts` exports: reads (`GET`) are exposed by
|
|
40
|
+
default, writes need an opt-in, as with every Exposify adapter.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { describe, exposed, hidden } from "@exposify/next";
|
|
44
|
+
import { z } from "zod";
|
|
45
|
+
|
|
46
|
+
export async function GET() {} // exposed (a read)
|
|
47
|
+
export const POST = exposed(async (req) => {}); // a write, opted in
|
|
48
|
+
export const DELETE = hidden(async () => {}); // never a tool
|
|
49
|
+
|
|
50
|
+
export const PATCH = describe(async (req) => {}, {
|
|
51
|
+
name: "update_order",
|
|
52
|
+
description: "Change an order's quantity",
|
|
53
|
+
input: { id: z.string(), body: z.object({ qty: z.number().int().min(1) }) },
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or in config: `expose: ["POST /api/orders"]`, `exclude: ["/api/admin/**"]`.
|
|
58
|
+
Paths are what Next serves: route groups like `(shop)` drop out, `[id]`
|
|
59
|
+
becomes `:id`, `[...slug]` becomes `:slug*` and `[[...slug]]` becomes
|
|
60
|
+
`:slug*?` (the tool takes `"a/b/c"`).
|
|
61
|
+
|
|
62
|
+
**Pages Router** (`pages/api/*`): one handler serves every method, so nothing
|
|
63
|
+
proves which methods exist. A Pages route becomes a tool only for the
|
|
64
|
+
methods you list, reads included: `expose: ["GET /api/stats"]`. `setup`
|
|
65
|
+
reads your handler (`if (req.method === …)`, `switch (req.method)`,
|
|
66
|
+
next-connect's `.get()`/`.post()`) and suggests them.
|
|
67
|
+
|
|
68
|
+
## How tool calls run
|
|
69
|
+
|
|
70
|
+
A tool call is an HTTP request to your own app's origin, with the agent's
|
|
71
|
+
`Authorization` and `Cookie` headers. So `proxy.ts` (middleware), and each
|
|
72
|
+
route's own checks, run exactly as for a browser: a signed-out agent gets
|
|
73
|
+
your 401, and a redirect to your login page comes back as an error naming
|
|
74
|
+
it. The MCP client's address and host are forwarded as
|
|
75
|
+
`x-forwarded-for`/`x-forwarded-host`.
|
|
76
|
+
|
|
77
|
+
- `baseUrl` (or `EXPOSIFY_BASE_URL`): where tool calls go, when the request's
|
|
78
|
+
own origin isn't reachable from the server.
|
|
79
|
+
- **Vercel Deployment Protection** blocks those requests on protected
|
|
80
|
+
previews: set `VERCEL_AUTOMATION_BYPASS_SECRET` (Vercel's "Protection
|
|
81
|
+
Bypass for Automation") and it's sent as `x-vercel-protection-bypass`.
|
|
82
|
+
- `basePath` and `trailingSlash` from `next.config` are applied.
|
|
83
|
+
- `rateLimit` / `rateWindowMs`: MCP requests per client address per window
|
|
84
|
+
(default 60 per 60 s), in memory and per instance.
|
|
85
|
+
- `oauth`, `auth`, `docsAuth` and `openapi` work as in `@exposify/express`.
|
|
86
|
+
With `oauth`, the protected-resource metadata is served at
|
|
87
|
+
`/mcp/.well-known/oauth-protected-resource`.
|
|
88
|
+
|
|
89
|
+
The MCP route runs on the Node.js runtime. Routes with `export const runtime
|
|
90
|
+
= "edge"` are tools too, but their `exposed()`/`describe()` wrappers aren't
|
|
91
|
+
read (their modules aren't loaded into the Node route): use `expose`/`exclude`
|
|
92
|
+
for those.
|
|
93
|
+
|
|
94
|
+
## Each user sees the tools they can use
|
|
95
|
+
|
|
96
|
+
Tools behind your own checks are listed only for people who pass them. The
|
|
97
|
+
generator notes which of your functions a page or Server Action calls that
|
|
98
|
+
stop a request with `notFound()`, `redirect()`, `forbidden()` or
|
|
99
|
+
`unauthorized()` (say `requireAdmin()` and `requireUser()`), directly or
|
|
100
|
+
through another one. When an agent lists tools, the MCP route opens one page
|
|
101
|
+
per check as that agent's user (a page without params that runs it), and
|
|
102
|
+
leaves out every tool behind a check they don't pass. So a customer's
|
|
103
|
+
assistant doesn't see `view_admin` or `save_product`; an admin's does.
|
|
104
|
+
|
|
105
|
+
- One request per check per user, cached for a minute.
|
|
106
|
+
- A check with no page to try it on, or a probe that errors, leaves its
|
|
107
|
+
tools listed. Your checks still run on every call either way: this only
|
|
108
|
+
changes what's listed.
|
|
109
|
+
- Checks written inline in a page (`if (!user) redirect("/login")`) aren't
|
|
110
|
+
shared functions, so those pages stay listed.
|
|
111
|
+
|
|
112
|
+
## AI assistants sign in with your own login
|
|
113
|
+
|
|
114
|
+
With `EXPOSIFY_KEY` set (the hosted broker), an AI assistant that connects
|
|
115
|
+
signs in with your app's own login, and you write no code for it:
|
|
116
|
+
|
|
117
|
+
1. The generator finds your login: a Server Action (or a `POST` route
|
|
118
|
+
handler) that takes a password and something saying who, and sets a
|
|
119
|
+
session cookie or returns a token. Sign-up, sign-out and password-reset
|
|
120
|
+
actions are never picked.
|
|
121
|
+
2. When Claude connects, the person clicks Allow on Exposify's page and lands
|
|
122
|
+
on a sign-in form this MCP route serves at `/mcp/login`, on your domain.
|
|
123
|
+
The password goes to your app only.
|
|
124
|
+
3. The form runs your login exactly as your own form does (a real Server
|
|
125
|
+
Action on the page that uses it), with no browser cookies, so it's a
|
|
126
|
+
session of its own. Its cookies go to the broker for this connection, and
|
|
127
|
+
every tool call after that runs as that person.
|
|
128
|
+
4. Cookies your app sets on later calls are kept. A 401, or a redirect to
|
|
129
|
+
your login page, ends the connection, and the assistant asks to connect
|
|
130
|
+
again.
|
|
131
|
+
|
|
132
|
+
The MCP route tells Exposify about the login (its field names) and its
|
|
133
|
+
address before answering its first request, so there's nothing to add in
|
|
134
|
+
the dashboard; it shows there with an off switch. `inspect` says which login
|
|
135
|
+
was found. Name another one with `aiReady(exposify, { login: "signIn" })`
|
|
136
|
+
(an action's export name) or `{ login: { path: "/api/login", fields:
|
|
137
|
+
["email", "password"] } }`, or turn it off with `login: false`.
|
|
138
|
+
|
|
139
|
+
**No login found, and no sign-in method in the dashboard?** Then agents use
|
|
140
|
+
your app signed out: `/mcp` doesn't ask them to sign in, and your own checks
|
|
141
|
+
decide what they can do. The startup log says which it is
|
|
142
|
+
(`[agentready] How agents sign in: ...`), and so does the dashboard's "How
|
|
143
|
+
agents sign in" page. Adding a sign-in method there takes effect within a
|
|
144
|
+
minute.
|
|
145
|
+
|
|
146
|
+
Not supported yet: logins in more than one step (SMS codes, email links),
|
|
147
|
+
and logins that need a CSRF token from your own form page.
|
|
148
|
+
|
|
149
|
+
## Pages
|
|
150
|
+
|
|
151
|
+
Every App Router page is a read tool too, so an app whose server components
|
|
152
|
+
read their data directly (a database client, an ORM, `fetch`, `"use cache"`
|
|
153
|
+
functions), with no API route in between, is readable by agents with no code
|
|
154
|
+
changes. `/shop/[category]` becomes `view_shop_category { category }`, `/`
|
|
155
|
+
becomes `view_home`.
|
|
156
|
+
|
|
157
|
+
A call requests the page the way a browser does, with the agent's
|
|
158
|
+
`Authorization` and `Cookie`, so your own checks run: `proxy.ts`, a login
|
|
159
|
+
check in a layout or the page, `notFound()`, `redirect()`. It returns:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{ "url": "/product/celeste", "title": "Celeste | Shop", "text": "# Celeste\n\n$348.00\n…", "data": [{ "component": "AddToCart", "props": { "product": { … } } }] }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- `text` is the page's main content (`<main>`, or the body without its
|
|
166
|
+
header, nav, footer and aside) as compact text: headings, lists, tables,
|
|
167
|
+
links (a card wrapped in a link ends with `→ /its/url`), images' alt text
|
|
168
|
+
and form controls. Scripts, styles and hidden elements are left out.
|
|
169
|
+
- `data` is what the page was rendered with: the props your server
|
|
170
|
+
components pass to client components, read from the payload Next already
|
|
171
|
+
puts in the page. A page that renders its list in server components only
|
|
172
|
+
has no such props, and its `text` carries the list.
|
|
173
|
+
- A `redirect()` or `notFound()` comes back as the error a route's would
|
|
174
|
+
(`redirected to /login`, a 404), also when the page streams and Next has
|
|
175
|
+
already answered 200 (Suspense, `cacheComponents`).
|
|
176
|
+
- Each `[param]` is an argument; a catch-all takes `"a/b/c"`. Search params
|
|
177
|
+
the page reads (`searchParams.q`, `(await searchParams).page`, a
|
|
178
|
+
destructure, `useSearchParams().get("tab")` in a client component it
|
|
179
|
+
renders) are optional arguments.
|
|
180
|
+
- The description is the page's static `metadata` (`description`, else
|
|
181
|
+
`title`), the descriptions file (`GET /path`, which `describe` writes), or
|
|
182
|
+
a derived one.
|
|
183
|
+
|
|
184
|
+
What's left out, and how to change it:
|
|
185
|
+
|
|
186
|
+
- A page that's **just a form** posting to a Server Action (a sign-in page,
|
|
187
|
+
say) isn't a tool: the action is. List `"GET /login"` in `expose` (setup's
|
|
188
|
+
checklist does) to read it anyway.
|
|
189
|
+
- `exclude` globs apply to pages, as to routes: `exclude: ["/admin/**"]`.
|
|
190
|
+
- `pages: false` turns page tools off. By path, `false` hides one and an
|
|
191
|
+
object renames or describes one:
|
|
192
|
+
`pages: { "/": { name: "storefront", description: "…" }, "/debug": false }`.
|
|
193
|
+
|
|
194
|
+
Data a page fetches in the browser after it loads (`useEffect`, SWR) isn't in
|
|
195
|
+
its HTML, so its tool doesn't see it. A page tool is a full render: cheap for
|
|
196
|
+
cached pages, a normal request otherwise.
|
|
197
|
+
|
|
198
|
+
## Server Actions
|
|
199
|
+
|
|
200
|
+
The exported functions of every `"use server"` file are listed in the
|
|
201
|
+
generated manifest, with the input each one takes, worked out from its code:
|
|
202
|
+
the fields it reads off its FormData (`formData.get("email")`, through
|
|
203
|
+
same-file helpers like `field("email")` or `parseProduct(formData)`, and
|
|
204
|
+
names built over a constant list, like `` `stock_${size}` `` for each of
|
|
205
|
+
`SIZES`), and their types (`Number(…)` is a number, `=== "on"` a checkbox,
|
|
206
|
+
`JSON.parse(…)` JSON, `STATUSES.includes(…)` one of `STATUSES`). Constant
|
|
207
|
+
lists are followed into the files they're imported from, through your
|
|
208
|
+
tsconfig `paths`.
|
|
209
|
+
|
|
210
|
+
Then the forms that submit to each action (`<form action={…}>`,
|
|
211
|
+
`onSubmit={…}` around it, `<button formAction={…}>`, through
|
|
212
|
+
`useActionState` or `.bind(null, id)`, or an inline wrapper that calls it)
|
|
213
|
+
add what only the form says: which fields are `required`, `min`/`max`,
|
|
214
|
+
`minLength`/`maxLength`, `pattern`, `<select>` and radio options, and which
|
|
215
|
+
of the action's parameters are `.bind` arguments or `useActionState`'s
|
|
216
|
+
previous state. Wrapper components (`<Field name="email" />` rendering
|
|
217
|
+
`<input required {...props} />`) are followed to the input they render. A
|
|
218
|
+
field required by every form that has it is required in the tool; hidden
|
|
219
|
+
inputs count as required unless they're rendered conditionally.
|
|
220
|
+
|
|
221
|
+
Actions are writes, so none is a tool until you opt it in:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// app/mcp/[[...transport]]/route.ts
|
|
225
|
+
const handler = aiReady(exposify, {
|
|
226
|
+
actions: {
|
|
227
|
+
placeOrder: { description: "Place an order" }, // input worked out from its code
|
|
228
|
+
// Or written out, which wins: form field names, and a parameter before the FormData (a `.bind` argument) by its name.
|
|
229
|
+
updateOrderStatus: { input: { orderId: z.string(), status: z.enum(["paid", "shipped"]) } },
|
|
230
|
+
clearCart: true,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Actions fill in defaults for missing fields instead of rejecting them, so a
|
|
236
|
+
wrong schema would let an agent save wrong data. An action whose input
|
|
237
|
+
can't all be worked out stays hidden, with a warning naming what (a field
|
|
238
|
+
name computed at runtime, the form handed to a function from a package, a
|
|
239
|
+
parameter with no type), until you write its `input`; so does one that reads
|
|
240
|
+
the whole form (`Object.fromEntries(formData)`) with no form found to say
|
|
241
|
+
which fields it gets, and one that takes a file, which agents can't send
|
|
242
|
+
yet. Fields no form requires are optional; the tool's description tells the
|
|
243
|
+
agent a field left out is as if left blank.
|
|
244
|
+
|
|
245
|
+
Or in the action's own file: `export const clearCart = describe(async () => { … }, { description: "…" })`
|
|
246
|
+
(`exposed()` and `hidden()` work too). Tools are named after the export,
|
|
247
|
+
snake_cased (`place_order`), with the file's name added where two files
|
|
248
|
+
export the same name.
|
|
249
|
+
|
|
250
|
+
A call is sent to your app the way a browser's form sends it: a Server Action
|
|
251
|
+
request (`Next-Action`) to a page that uses the action, which the code
|
|
252
|
+
generator finds by following imports from your pages and layouts. Next runs
|
|
253
|
+
it as a real Server Action, so everything works as for a browser:
|
|
254
|
+
`updateTag()`, `refresh()`, `revalidatePath()`, `redirect()`, and
|
|
255
|
+
`cookies()`/`headers()`, which are the agent's. Nothing in your code changes,
|
|
256
|
+
and nothing needs configuring. An action no page uses has no such page: the
|
|
257
|
+
MCP route (it must be the `[[...transport]]` catch-all) calls it in-process
|
|
258
|
+
at `/mcp/actions/<tool>` instead, where Next treats it as part of a route
|
|
259
|
+
handler and refuses `updateTag()` and `refresh()`; the startup log names
|
|
260
|
+
those actions. Arguments become the FormData a browser would post: `true` is a
|
|
261
|
+
checked checkbox (`"on"`), `false` is left out, arrays are repeated fields,
|
|
262
|
+
objects are JSON, and a field the action `JSON.parse`s is sent whole as JSON. A useActionState action's previous state is `undefined`.
|
|
263
|
+
The result is the action's return value as JSON; a `redirect()` comes back
|
|
264
|
+
as `{ ok: true, redirectedTo }`, since for an action it's usually success
|
|
265
|
+
(saved, then off to the list page). `notFound()` and thrown errors are tool
|
|
266
|
+
errors; the thrown message stays in your server log.
|
|
267
|
+
|
|
268
|
+
- **Check auth inside each action.** A call goes to one page that uses the
|
|
269
|
+
action, and Next's own guidance already says actions must check auth
|
|
270
|
+
themselves rather than rely on `proxy.ts` rules for the page.
|
|
271
|
+
- **Sign-in actions don't work for agents.** A cookie an action sets lands on
|
|
272
|
+
the internal call, not the agent's client. Actions that call
|
|
273
|
+
`cookies().set`/`.delete`, or are named like `login`/`logout`/`register`,
|
|
274
|
+
get a warning if you opt them in.
|
|
275
|
+
- **Next checks each call as it checks a browser's**, Origin against the
|
|
276
|
+
forwarded host. The in-process fallback skips that, so `/mcp/actions/*`
|
|
277
|
+
requires a secret header. In development it's random per process; in
|
|
278
|
+
production, an action no page uses needs `EXPOSIFY_ACTION_SECRET` (or
|
|
279
|
+
`NEXT_SERVER_ACTIONS_ENCRYPTION_KEY`, which it's derived from) set to the
|
|
280
|
+
same value on every instance, since the instance serving a tool call may
|
|
281
|
+
not be the one that sent it. Without either, those actions are off in
|
|
282
|
+
production and the log says so. Actions a page uses need neither.
|
|
283
|
+
- `"use server"` functions written inline in a component close over values
|
|
284
|
+
from the render, so they can't be called on their own; they're never
|
|
285
|
+
exposed.
|
|
286
|
+
|
|
287
|
+
## CLI
|
|
288
|
+
|
|
289
|
+
`npx exposify-next setup | inspect | describe | generate`. The first three
|
|
290
|
+
are the same as `exposify-express`'s; the scan reads your route files
|
|
291
|
+
instead of booting the app.
|
|
292
|
+
|
|
293
|
+
Server Actions get their own rows:
|
|
294
|
+
|
|
295
|
+
- `inspect` lists each action with whether it's a tool and why, its status
|
|
296
|
+
(input known, input partly unknown, or flagged as sign-in/out or taking a
|
|
297
|
+
file), the forms that submit to it, and what the scan couldn't tell. Where
|
|
298
|
+
an input you wrote no longer matches the code (a field the action now
|
|
299
|
+
reads, an option the form dropped), it says so, and `inspect --check` fails
|
|
300
|
+
on it in CI. The `inspect --snapshot` file includes action tools, by their
|
|
301
|
+
`POST /mcp/actions/<tool>` URL.
|
|
302
|
+
- `setup`'s checklist lists actions after the routes: space picks each one,
|
|
303
|
+
and `a`/`!` (all routes, `expose: "*"`) leave them alone. Without a
|
|
304
|
+
terminal, use `--action placeOrder`. It prints their `actions` entries: `true`
|
|
305
|
+
when the scan knows the input, otherwise a zod stub with a
|
|
306
|
+
`// TODO(exposify):` line for each gap. Entries already in your config are
|
|
307
|
+
kept as you wrote them.
|
|
308
|
+
- After a `next build`, actions that Next knows but the scan didn't find
|
|
309
|
+
(re-exported from elsewhere, say) are listed.
|
|
310
|
+
- `describe` writes descriptions for exposed actions too, into
|
|
311
|
+
`exposify.descriptions.json` under each action's tool URL (`POST
|
|
312
|
+
/mcp/actions/place_order`). A `description` in config or `describe()` wins.
|
|
313
|
+
- For an action you've opted in whose input the scan can't complete,
|
|
314
|
+
`describe` can ask the description service to propose one from the action's
|
|
315
|
+
source and its forms' source. It first lists every file whose code would be
|
|
316
|
+
sent (form files go whole) and sends only if you answer `y`; with
|
|
317
|
+
`--no-source` it sends names and fields only. You see each proposed field, and anything it
|
|
318
|
+
still couldn't tell, then accept or skip it. An accepted input goes in
|
|
319
|
+
`exposify.descriptions.json`, and the MCP route uses it once
|
|
320
|
+
`exposify.generated.ts` is regenerated. If the action changes, the input is
|
|
321
|
+
stale and unused until reviewed again: the startup log and `describe
|
|
322
|
+
--check` say so. A written `input` always wins, and actions that take a
|
|
323
|
+
file stay hidden either way.
|
|
324
|
+
- Inline `"use server"` functions get a suggested change from `setup`: a new
|
|
325
|
+
`actions.ts` next to the page with the function exported. The values it
|
|
326
|
+
closed over become its first parameters, and the page passes them with
|
|
327
|
+
`.bind(null, …)`. A `TODO(exposify)` reminds you to check the caller may act
|
|
328
|
+
on them, since the closure was often the authorisation. It's printed,
|
|
329
|
+
never applied.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/routes.ts
|
|
2
|
+
var HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
3
|
+
var DEFAULT_PAGE_EXTENSIONS = ["tsx", "ts", "jsx", "js"];
|
|
4
|
+
function appRoute(dirSegments) {
|
|
5
|
+
const parts = [];
|
|
6
|
+
const params = [];
|
|
7
|
+
for (const seg of dirSegments) {
|
|
8
|
+
if (seg.startsWith("_") || seg.startsWith("@") || seg.startsWith("(.")) return null;
|
|
9
|
+
if (seg.startsWith("(") && seg.endsWith(")")) continue;
|
|
10
|
+
const part = dynamicSegment(seg, params);
|
|
11
|
+
if (part === null) return null;
|
|
12
|
+
parts.push(part);
|
|
13
|
+
}
|
|
14
|
+
return { path: `/${parts.join("/")}`, params };
|
|
15
|
+
}
|
|
16
|
+
function pagesApiRoute(segments) {
|
|
17
|
+
if (segments[0] !== "api") return null;
|
|
18
|
+
const segs = segments.at(-1) === "index" ? segments.slice(0, -1) : segments;
|
|
19
|
+
const parts = [];
|
|
20
|
+
const params = [];
|
|
21
|
+
for (const seg of segs) {
|
|
22
|
+
if (seg.startsWith("_")) return null;
|
|
23
|
+
const part = dynamicSegment(seg, params);
|
|
24
|
+
if (part === null) return null;
|
|
25
|
+
parts.push(part);
|
|
26
|
+
}
|
|
27
|
+
return { path: `/${parts.join("/")}`, params };
|
|
28
|
+
}
|
|
29
|
+
function dynamicSegment(seg, params) {
|
|
30
|
+
const optionalCatchAll = /^\[\[\.\.\.([A-Za-z0-9_]+)\]\]$/.exec(seg);
|
|
31
|
+
if (optionalCatchAll) {
|
|
32
|
+
params.push(optionalCatchAll[1]);
|
|
33
|
+
return `:${optionalCatchAll[1]}*?`;
|
|
34
|
+
}
|
|
35
|
+
const catchAll = /^\[\.\.\.([A-Za-z0-9_]+)\]$/.exec(seg);
|
|
36
|
+
if (catchAll) {
|
|
37
|
+
params.push(catchAll[1]);
|
|
38
|
+
return `:${catchAll[1]}*`;
|
|
39
|
+
}
|
|
40
|
+
const dynamic = /^\[([A-Za-z0-9_]+)\]$/.exec(seg);
|
|
41
|
+
if (dynamic) {
|
|
42
|
+
params.push(dynamic[1]);
|
|
43
|
+
return `:${dynamic[1]}`;
|
|
44
|
+
}
|
|
45
|
+
return seg.includes("[") ? null : seg;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
HTTP_METHODS,
|
|
50
|
+
DEFAULT_PAGE_EXTENSIONS,
|
|
51
|
+
appRoute,
|
|
52
|
+
pagesApiRoute
|
|
53
|
+
};
|