@stacksjs/defaults 0.72.56 → 0.72.57
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.
|
@@ -340,9 +340,96 @@ route.get('/admin', handler).middleware('abilities:admin,write')
|
|
|
340
340
|
// Params accessible in middleware via: (request as any)._middlewareParams.abilities === 'admin,write'
|
|
341
341
|
```
|
|
342
342
|
|
|
343
|
+
## Which client to reach for
|
|
344
|
+
|
|
345
|
+
Stacks has three, and they answer different questions. Pick by who is calling.
|
|
346
|
+
|
|
347
|
+
| Caller | Client | Why |
|
|
348
|
+
|---|---|---|
|
|
349
|
+
| TypeScript in this repo or a workspace package | **Typed client** (below) | Full input/output inference with **no generation step**. Change a route, the call site stops compiling. |
|
|
350
|
+
| Anything that is not TypeScript here - native iOS/Android via the Craft bridge, third-party integrators, Swagger UI | **Generated REST client** (`buddy generate:openapi`) | Needs a real spec. That pipeline is unchanged and permanent. |
|
|
351
|
+
| One-off calls to any URL | **`Fetcher`** (below) | No route awareness, and none intended. |
|
|
352
|
+
|
|
353
|
+
The first two are both permanent. This is not a migration off REST.
|
|
354
|
+
|
|
355
|
+
## Typed client (zero generation)
|
|
356
|
+
|
|
357
|
+
Register routes with `createTypedRouter()` and the compiler can see both ends —
|
|
358
|
+
no `buddy generate:openapi`, no committed file to go stale.
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// routes/api.ts
|
|
362
|
+
import IndexAction from '../app/Actions/Project/IndexAction'
|
|
363
|
+
import StoreAction from '../app/Actions/Project/StoreAction'
|
|
364
|
+
import { createTypedRouter } from '@stacksjs/router'
|
|
365
|
+
|
|
366
|
+
export const api = createTypedRouter()
|
|
367
|
+
.get('/v1/projects', IndexAction)
|
|
368
|
+
.get('/v1/projects/{id}', ShowAction)
|
|
369
|
+
.post('/v1/projects', StoreAction, { middleware: 'auth' })
|
|
370
|
+
|
|
371
|
+
export type AppRoutes = typeof api
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// any TypeScript consumer
|
|
376
|
+
import type { AppRoutes } from '../routes/api'
|
|
377
|
+
import { createTypedClient } from '@stacksjs/router'
|
|
378
|
+
|
|
379
|
+
const client = createTypedClient<AppRoutes>({ baseUrl: 'https://api.example.com' })
|
|
380
|
+
|
|
381
|
+
const projects = await client.get('/v1/projects')
|
|
382
|
+
const one = await client.get('/v1/projects/{id}', { params: { id: '42' } })
|
|
383
|
+
const created = await client.post('/v1/projects', { name: 'apollo', budget: 1200 })
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
An unknown path is a compile error. A body that does not match the action's
|
|
387
|
+
`validations` is a compile error. The result is the action's own return type.
|
|
388
|
+
|
|
389
|
+
**How it infers.** Input comes from the action's `validations` — the same object
|
|
390
|
+
the validator runs, so the two cannot drift. Output comes from `handle`'s return
|
|
391
|
+
type. An action returning a `Response` or a stream is typed `unknown`, honestly:
|
|
392
|
+
it took over the wire format.
|
|
393
|
+
|
|
394
|
+
**Per-route settings are an argument**, not a chained call — chaining
|
|
395
|
+
`.middleware()` would return the route and lose the accumulated type:
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
.post('/v1/projects', StoreAction, {
|
|
399
|
+
middleware: ['auth', 'can:create,project'],
|
|
400
|
+
name: 'projects.store',
|
|
401
|
+
skipCsrf: true,
|
|
402
|
+
rateLimit: { max: 10, window: 'minute' },
|
|
403
|
+
})
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**No `.group()`.** A prefix applied only at runtime makes every accumulated path
|
|
407
|
+
type wrong; one applied only in the type is a second place for the URL to live.
|
|
408
|
+
Write the full path.
|
|
409
|
+
|
|
410
|
+
**The string form is untouched.** `route.get('/x', 'Actions/Foo')` stays exactly
|
|
411
|
+
as it is — lazy import, hot-reload friendly — and simply produces no inferred
|
|
412
|
+
types. A route wanting inference opts in by using the builder instead. Routes
|
|
413
|
+
registered either way still land in the generated OpenAPI document.
|
|
414
|
+
|
|
415
|
+
**Generated CRUD.** A `useApi` model's endpoints are describable too, without
|
|
416
|
+
touching them:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
import type { ApiRoutesFor } from '@stacksjs/orm'
|
|
420
|
+
import type Product from '../app/Models/Product'
|
|
421
|
+
|
|
422
|
+
type AppRoutes = RoutesOf<typeof api> & ApiRoutesFor<typeof Product>
|
|
423
|
+
await client.get('/api/products') // typed listing envelope
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
The builder, the client and the route-map contract all live in
|
|
427
|
+
`@stacksjs/bun-router` and are re-exported by `@stacksjs/router`. Import
|
|
428
|
+
`createTypedClient` from `@stacksjs/bun-router` directly in a browser bundle.
|
|
429
|
+
|
|
343
430
|
## Fetcher (HTTP Client)
|
|
344
431
|
|
|
345
|
-
A fluent HTTP client for
|
|
432
|
+
A fluent HTTP client for ad hoc requests, with no route awareness:
|
|
346
433
|
|
|
347
434
|
```typescript
|
|
348
435
|
import { fetcher } from '@stacksjs/api'
|
|
@@ -313,7 +313,7 @@ interface PageState {
|
|
|
313
313
|
mainStatus: number | null
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
async function gotoAndInstrument(cdp: Cdp, url: string, opts: { viewport?: { w: number, h: number }, scale?: number, timeoutMs?: number, cookies?: string[], settleMs?: number, scheme?: string } = {}): Promise<PageState> {
|
|
316
|
+
async function gotoAndInstrument(cdp: Cdp, url: string, opts: { viewport?: { w: number, h: number }, scale?: number, timeoutMs?: number, cookies?: string[], settleMs?: number, scheme?: string, reducedMotion?: boolean } = {}): Promise<PageState> {
|
|
317
317
|
let unsubscribe = () => {}
|
|
318
318
|
const state: PageState = {
|
|
319
319
|
consoleErrors: [],
|
|
@@ -356,12 +356,29 @@ async function gotoAndInstrument(cdp: Cdp, url: string, opts: { viewport?: { w:
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
// Emulate light/dark so prefers-color-scheme pages can be QA'd in both
|
|
359
|
-
// schemes without flipping the host OS setting.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
359
|
+
// schemes without flipping the host OS setting. `reducedMotion` rides the
|
|
360
|
+
// same call (a second `setEmulatedMedia` would replace this one's features
|
|
361
|
+
// rather than add to them) - it's set for `--full`/`--element` captures,
|
|
362
|
+
// which paint beyond the current viewport via `captureBeyondViewport`.
|
|
363
|
+
// That API paints pixels beyond the viewport WITHOUT moving `scrollY`, so
|
|
364
|
+
// a scroll-driven CSS reveal (`animation-timeline: view()`) never
|
|
365
|
+
// resolves past its initial (typically invisible) keyframe for anything
|
|
366
|
+
// that was never really scrolled past - a `--full` capture would silently
|
|
367
|
+
// screenshot every such section as blank. `prefers-reduced-motion: reduce`
|
|
368
|
+
// sidesteps that instead of fighting it with a scroll trick that would
|
|
369
|
+
// itself misplace `position: sticky`/`fixed` elements: this project's own
|
|
370
|
+
// marketing CSS already turns every scroll/entrance animation off and
|
|
371
|
+
// leaves elements at their plain (opaque) resting state under reduced
|
|
372
|
+
// motion (`@media (prefers-reduced-motion: reduce)`), which is exactly
|
|
373
|
+
// the already-shipped, already-tested fallback a real accessibility user
|
|
374
|
+
// gets - reusing it here means nothing new to prove correct.
|
|
375
|
+
const features: { name: string, value: string }[] = []
|
|
376
|
+
if (opts.scheme === 'light' || opts.scheme === 'dark')
|
|
377
|
+
features.push({ name: 'prefers-color-scheme', value: opts.scheme })
|
|
378
|
+
if (opts.reducedMotion)
|
|
379
|
+
features.push({ name: 'prefers-reduced-motion', value: 'reduce' })
|
|
380
|
+
if (features.length)
|
|
381
|
+
await cdp.send('Emulation.setEmulatedMedia', { features })
|
|
365
382
|
|
|
366
383
|
unsubscribe = cdp.on((e) => {
|
|
367
384
|
if (e.method === 'Runtime.consoleAPICalled') {
|
|
@@ -404,23 +421,11 @@ async function title(cdp: Cdp): Promise<string> {
|
|
|
404
421
|
}
|
|
405
422
|
|
|
406
423
|
/**
|
|
407
|
-
* Scroll the real page to `y` and let
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
* moving `scrollY` - correct for a top-to-bottom review, but useless for
|
|
413
|
-
* checking anything that depends on an actual mid-scroll viewport: a
|
|
414
|
-
* `position: sticky` nav overlapping content beneath it, a CSS
|
|
415
|
-
* `animation-timeline: view()` reveal mid-transition, or simply "what does
|
|
416
|
-
* the fold look like 800px down". `window.scrollTo(..., { behavior:
|
|
417
|
-
* 'instant' })` forces the jump synchronously even when the page sets
|
|
418
|
-
* `scroll-behavior: smooth`, which would otherwise animate the scroll and
|
|
419
|
-
* leave a screenshot taken immediately after mid-flight. The two rAF
|
|
420
|
-
* round-trips that follow give the compositor one full frame to repaint
|
|
421
|
-
* sticky/fixed layers and any scroll-driven animation at the new offset
|
|
422
|
-
* before `Page.captureScreenshot` reads back pixels, which
|
|
423
|
-
* `Runtime.evaluate` returning does not by itself guarantee.
|
|
424
|
+
* Scroll the real page to `y` and let two animation-frame round-trips settle
|
|
425
|
+
* before the caller reads pixels back - `behavior: 'instant'` forces the
|
|
426
|
+
* jump synchronously even when the page sets `scroll-behavior: smooth`,
|
|
427
|
+
* which would otherwise animate the scroll and leave a screenshot taken
|
|
428
|
+
* immediately after mid-flight.
|
|
424
429
|
*/
|
|
425
430
|
async function scrollTo(cdp: Cdp, y: number): Promise<void> {
|
|
426
431
|
const r = await cdp.send('Runtime.evaluate', {
|
|
@@ -753,7 +758,7 @@ async function main() {
|
|
|
753
758
|
console.log('Usage: bun browse.ts <navigate|screenshot|responsive|monitor|snapshot|scenario|crawl> <url> [flags]')
|
|
754
759
|
console.log(' --cookie "name=value" repeatable; pre-seeds cookies (e.g. coming-soon bypass)')
|
|
755
760
|
console.log(' --settle 1500 ms to wait after load before acting (default 700; stretch for entrance animations)')
|
|
756
|
-
console.log(' --scheme dark emulate prefers-color-scheme (light|dark)
|
|
761
|
+
console.log(' --scheme dark emulate prefers-color-scheme (light|dark); defaults to light when omitted')
|
|
757
762
|
console.log(' --scroll-y 800 screenshot: jump to this document Y first, then capture just the viewport')
|
|
758
763
|
console.log(' (--full renders beyond-viewport without ever scrolling, so it cannot show a')
|
|
759
764
|
console.log(' position:sticky/fixed element overlapping content, or a scroll-linked')
|
|
@@ -764,7 +769,17 @@ async function main() {
|
|
|
764
769
|
let session = await launch()
|
|
765
770
|
const cookies = flagList(flags.cookie)
|
|
766
771
|
const settleMs = flags.settle ? Number(flags.settle) : undefined
|
|
767
|
-
|
|
772
|
+
// Defaults to 'light' rather than leaving prefers-color-scheme unset:
|
|
773
|
+
// headless Chromium's own default for that media feature isn't
|
|
774
|
+
// documented or guaranteed, and was observed reporting 'dark' matches
|
|
775
|
+
// with nothing here asking for it - every screenshot taken without
|
|
776
|
+
// --scheme would silently show this site's dark theme instead of its
|
|
777
|
+
// actual 'colored' (light) default. Forcing 'light' makes an unflagged
|
|
778
|
+
// capture deterministic and representative of what a visitor with no
|
|
779
|
+
// saved choice and no OS dark-mode preference actually sees.
|
|
780
|
+
if (flags.scheme !== undefined && flags.scheme !== 'light' && flags.scheme !== 'dark')
|
|
781
|
+
throw new TypeError(`Invalid --scheme "${String(flags.scheme)}". Expected "light" or "dark".`)
|
|
782
|
+
const scheme = typeof flags.scheme === 'string' ? flags.scheme : 'light'
|
|
768
783
|
try {
|
|
769
784
|
if (command === 'navigate' || command === 'go') {
|
|
770
785
|
const cdp = await openPage(session.port)
|
|
@@ -794,7 +809,7 @@ async function main() {
|
|
|
794
809
|
throw new TypeError(`--full, --element, and --scroll-y are mutually exclusive; got ${modes.join(', ')}.`)
|
|
795
810
|
const out = (flags.out as string) || `storage/framework/runtime/shots/${new URL(url).pathname.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '') || 'home'}.png`
|
|
796
811
|
mkdirSync(out.split('/').slice(0, -1).join('/') || '.', { recursive: true })
|
|
797
|
-
const state = await gotoAndInstrument(cdp, url, { viewport, scale, cookies, settleMs, scheme })
|
|
812
|
+
const state = await gotoAndInstrument(cdp, url, { viewport, scale, cookies, settleMs, scheme, reducedMotion: !!flags.full || !!flags.element })
|
|
798
813
|
const png = await captureScreenshot(cdp, { full: !!flags.full, element: flags.element as string | undefined, scrollY: scrollY ?? undefined })
|
|
799
814
|
await Bun.write(out, png)
|
|
800
815
|
console.log(JSON.stringify({ url, out, viewport: `${viewport.w}x${viewport.h}`, scale, full: !!flags.full, element: flags.element ?? null, scrollY: scrollY ?? null, bytes: png.length }, null, 2))
|
|
@@ -808,7 +823,7 @@ async function main() {
|
|
|
808
823
|
const results: any[] = []
|
|
809
824
|
for (const bp of BREAKPOINTS) {
|
|
810
825
|
const cdp = await openPage(session.port)
|
|
811
|
-
const state = await gotoAndInstrument(cdp, url, { viewport: { w: bp.w, h: bp.h }, cookies, settleMs, scheme })
|
|
826
|
+
const state = await gotoAndInstrument(cdp, url, { viewport: { w: bp.w, h: bp.h }, cookies, settleMs, scheme, reducedMotion: true })
|
|
812
827
|
const overflow = await cdp.send('Runtime.evaluate', {
|
|
813
828
|
expression: 'document.body.scrollWidth > window.innerWidth ? document.body.scrollWidth - window.innerWidth : 0',
|
|
814
829
|
returnByValue: true,
|
|
@@ -874,7 +889,7 @@ async function main() {
|
|
|
874
889
|
|
|
875
890
|
const cdp = await openPage(session.port)
|
|
876
891
|
const viewport = parseViewport(typeof flags.viewport === 'string' ? flags.viewport : undefined)
|
|
877
|
-
const state = await gotoAndInstrument(cdp, url, { viewport, cookies, settleMs, scheme })
|
|
892
|
+
const state = await gotoAndInstrument(cdp, url, { viewport, cookies, settleMs, scheme, reducedMotion: !!flags.full })
|
|
878
893
|
const results: Record<string, unknown>[] = []
|
|
879
894
|
await cdp.send('Page.bringToFront')
|
|
880
895
|
await cdp.send('Runtime.evaluate', {
|
|
@@ -121,6 +121,33 @@ route.group({ prefix: '/articles', middleware: ['auth'] }, () => {
|
|
|
121
121
|
|
|
122
122
|
Or rely on auto-generated routes from `useApi` trait — they're created automatically.
|
|
123
123
|
|
|
124
|
+
### When a TypeScript client will call these
|
|
125
|
+
|
|
126
|
+
Register through `createTypedRouter()` instead, and the client gets full
|
|
127
|
+
input/output inference with no `buddy generate:openapi` step in between:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// routes/api.ts
|
|
131
|
+
import CreateArticle from '../app/Actions/CreateArticle'
|
|
132
|
+
import ListArticles from '../app/Actions/ListArticles'
|
|
133
|
+
import { createTypedRouter } from '@stacksjs/router'
|
|
134
|
+
|
|
135
|
+
export const api = createTypedRouter()
|
|
136
|
+
.get('/articles', ListArticles, { middleware: 'auth' })
|
|
137
|
+
.post('/articles', CreateArticle, { middleware: 'auth' })
|
|
138
|
+
|
|
139
|
+
export type AppRoutes = typeof api
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const client = createTypedClient<AppRoutes>({ baseUrl })
|
|
144
|
+
const created = await client.post('/articles', { title: 'x', content: 'y' })
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Same runtime path, same middleware, same OpenAPI document — the difference is
|
|
148
|
+
entirely at compile time. Keep the string form for routes no TypeScript consumer
|
|
149
|
+
calls; it stays lazily imported. See the `stacks-api` and `stacks-router` skills.
|
|
150
|
+
|
|
124
151
|
## Step 5: Add Event Listeners (Optional)
|
|
125
152
|
|
|
126
153
|
```typescript
|
|
@@ -47,10 +47,103 @@ route.group({ prefix: '/api/v1', middleware: ['auth', 'throttle'] }, () => {
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
### Handler Types
|
|
50
|
-
- Function: `(req
|
|
51
|
-
|
|
50
|
+
- Function: `(req) => …` — return a `Response`, or any value `formatResult`
|
|
51
|
+
handles: an object/array becomes JSON, a string becomes text, `null` becomes
|
|
52
|
+
204, a `ReadableStream` streams. `req.params` is narrowed to the path's own
|
|
53
|
+
placeholders, so `req.params.slugTypo` is a compile error rather than
|
|
54
|
+
`undefined` at runtime.
|
|
55
|
+
- Action string: `'Actions/CreateUser'` — auto-loads action, lazily
|
|
56
|
+
- Action object: an imported action, passed directly — see typed routes below
|
|
52
57
|
- Controller: `'Controllers/UserController@index'` — calls controller method
|
|
53
58
|
|
|
59
|
+
## The strings are typed (run `buddy generate:types`)
|
|
60
|
+
|
|
61
|
+
Action paths, middleware aliases and route names are all checked at compile
|
|
62
|
+
time against what this application actually has. `buddy generate:types`
|
|
63
|
+
discovers them and writes them into the router's type registry
|
|
64
|
+
(`storage/framework/types/actions.d.ts`); nothing is maintained by hand.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
route.get('/login', 'Actions/Auth/LogniAction') // ✗ no such action
|
|
68
|
+
route.get('/admin', handler).middleware('atuh') // ✗ no such middleware alias
|
|
69
|
+
url('email.unsubscrbe', { token }) // ✗ no such route name
|
|
70
|
+
url('user.post', { id: 42 }) // params come from the path
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The middleware one is the one that matters most: a typo'd alias used to serve
|
|
74
|
+
the route **without** the protection, silently.
|
|
75
|
+
|
|
76
|
+
Notes:
|
|
77
|
+
- Controllers stay a pattern (`'Controllers/X@method'`) — the method half is a
|
|
78
|
+
member name, not a filename.
|
|
79
|
+
- Negated (`'!auth'`) and parameterised (`'throttle:60,1'`) middleware forms are
|
|
80
|
+
both accepted.
|
|
81
|
+
- Regenerate after adding an action, a middleware alias, or a `.name()`. A stale
|
|
82
|
+
file rejects code that is correct.
|
|
83
|
+
- `resource()` takes a BASE, and composes `Actions/<Base><Kind>Action` from it.
|
|
84
|
+
`route.resource('posts', 'Post')` → `Actions/PostIndexAction`, matching where
|
|
85
|
+
`buddy make:crud` writes. The base is checked against the actions that exist;
|
|
86
|
+
which of the five siblings you need depends on `only`/`except`, so that part
|
|
87
|
+
is settled when the route is hit.
|
|
88
|
+
|
|
89
|
+
### Path params arrive decoded
|
|
90
|
+
|
|
91
|
+
`/users/{name}` given `/users/caf%C3%A9` hands the handler `café`, and `%2F`
|
|
92
|
+
becomes a real `/`. Decoded exactly once, in bun-router — do NOT decode again in
|
|
93
|
+
an action or middleware: two passes turn `%2520` into a space, which is how a
|
|
94
|
+
filter that rejects `../` gets walked past. A malformed escape (`%ZZ`) passes
|
|
95
|
+
through raw rather than failing the request.
|
|
96
|
+
|
|
97
|
+
A decoded param can contain `/`, so anything joining one into a filesystem path
|
|
98
|
+
still has to sanitise. Decoding makes the value correct, not safe.
|
|
99
|
+
|
|
100
|
+
## Typed Routes (zero generation)
|
|
101
|
+
|
|
102
|
+
`route.get('/x', 'Actions/Foo')` resolves its action by a dynamic `import()` of a
|
|
103
|
+
string. Good for the runtime — lazy, hot-reload friendly — and completely opaque
|
|
104
|
+
to the compiler, so no client can be typed from it without a generation step.
|
|
105
|
+
|
|
106
|
+
`createTypedRouter()` registers through the same router while accumulating a
|
|
107
|
+
route map into its own type:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import IndexAction from '../app/Actions/Project/IndexAction'
|
|
111
|
+
import StoreAction from '../app/Actions/Project/StoreAction'
|
|
112
|
+
import { createTypedRouter } from '@stacksjs/router'
|
|
113
|
+
|
|
114
|
+
export const api = createTypedRouter()
|
|
115
|
+
.get('/v1/projects', IndexAction)
|
|
116
|
+
.post('/v1/projects', StoreAction, { middleware: 'auth', rateLimit: { max: 10 } })
|
|
117
|
+
|
|
118
|
+
export type AppRoutes = typeof api
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Any TypeScript consumer then gets full inference with **no CLI step**:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import { createTypedClient } from '@stacksjs/router'
|
|
125
|
+
|
|
126
|
+
const client = createTypedClient<AppRoutes>({ baseUrl })
|
|
127
|
+
const projects = await client.get('/v1/projects') // typed from the action
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Facts worth knowing before using it:
|
|
131
|
+
|
|
132
|
+
- **One runtime path.** A directly-registered action goes through the same
|
|
133
|
+
`wrapAction` as a string-registered one — validation, `authorize`, `before`,
|
|
134
|
+
`formatResult`, error reporting. Only the compile-time story differs.
|
|
135
|
+
- **Input from `validations`, output from `handle`'s return type.** An action
|
|
136
|
+
returning a `Response` is typed `unknown`; it took over the wire format.
|
|
137
|
+
- **Options are an argument**, not chained — chaining would return the route and
|
|
138
|
+
lose the accumulated type.
|
|
139
|
+
- **No `.group()`.** A runtime-only prefix makes every path type wrong; a
|
|
140
|
+
type-only prefix is a second place for the URL to live.
|
|
141
|
+
- **Both forms feed OpenAPI.** Directly-registered actions are reported by
|
|
142
|
+
`listRegisteredRoutes()`, so the generator reads their schema with no file
|
|
143
|
+
path to import.
|
|
144
|
+
- The builder, the client and the contract live in `@stacksjs/bun-router` and
|
|
145
|
+
are re-exported here. See the `stacks-api` skill for the full client story.
|
|
146
|
+
|
|
54
147
|
## Route Registry (app/Routes.ts)
|
|
55
148
|
|
|
56
149
|
```typescript
|
package/ide/vscode/package.json
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.72.
|
|
5
|
+
"version": "0.72.57",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@iconify-json/f7": "^1.2.2",
|
|
53
53
|
"@iconify-json/hugeicons": "^1.2.27",
|
|
54
|
-
"@stacksjs/mobile": "^0.72.
|
|
54
|
+
"@stacksjs/mobile": "^0.72.57",
|
|
55
55
|
"@stacksjs/sanitizer": "^0.2.113"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|