@k8ordo/framework 0.1.0 → 1.1.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/README.md +14 -8
- package/dist/generated.mjs +1 -2
- package/dist/index.mjs +1 -2
- package/dist/runtime/app-router.d.mts +4 -0
- package/dist/runtime/app-router.mjs +12 -15
- package/dist/runtime/browser-payloads.d.mts +8 -0
- package/dist/runtime/browser-payloads.mjs +23 -0
- package/dist/runtime/entry.browser.mjs +3 -1
- package/dist/runtime/fallback-boundary.d.mts +2 -1
- package/dist/runtime/guard.d.mts +2 -1
- package/dist/runtime/page-boundary.d.mts +2 -1
- package/dist/runtime/params.d.mts +2 -1
- package/dist/runtime/redirect-file.d.mts +2 -1
- package/dist/runtime/redirect.d.mts +2 -1
- package/dist/runtime/route.d.mts +2 -1
- package/dist/serve.mjs +1 -1
- package/dist/static-BaGdAVF1.d.mts +66 -0
- package/dist/vercel.d.mts +49 -3
- package/dist/vercel.mjs +34 -15
- package/dist/vite.d.mts +2 -59
- package/dist/vite.mjs +106 -39
- package/docs/GUIDE.md +26 -9
- package/docs/llms.txt +19 -6
- package/docs/references/csp.md +12 -3
- package/docs/references/deploy.md +158 -1
- package/docs/references/routing.md +46 -7
- package/package.json +18 -11
|
@@ -465,8 +465,8 @@ on evil.test would pass the check.
|
|
|
465
465
|
|
|
466
466
|
```ts
|
|
467
467
|
// vite.config.ts
|
|
468
|
-
import { framework } from '@k8ordo/framework/vite';
|
|
469
468
|
import { vercel } from '@k8ordo/framework/vercel';
|
|
469
|
+
import { framework } from '@k8ordo/framework/vite';
|
|
470
470
|
import { defineConfig } from 'vite';
|
|
471
471
|
|
|
472
472
|
export default defineConfig({
|
|
@@ -505,6 +505,163 @@ case-sensitively, as the route table matches; then `404.html` under a `404`,
|
|
|
505
505
|
when the build wrote one. `vercel()` writes them once the last page is
|
|
506
506
|
written, whichever side of `framework()` it sits.
|
|
507
507
|
|
|
508
|
+
**A file a `route.ts` wrote is served with the type its `GET` answered**,
|
|
509
|
+
the `Content-Type` `vite dev` sends, written as that file's entry in the
|
|
510
|
+
`overrides` of `config.json`: a feed at `blog/feed` is
|
|
511
|
+
`application/rss+xml`, not the `application/octet-stream` of a file with no
|
|
512
|
+
extension. The type is written even where the extension says the same, so
|
|
513
|
+
no file is left to the type Vercel gives an extension; only a `GET` that
|
|
514
|
+
named none — a body of bytes with no `Content-Type` — leaves its file to it.
|
|
515
|
+
`vite preview` and `serve` still go by the extension
|
|
516
|
+
([A route.ts under static mode](routing.md#under-static-mode)).
|
|
517
|
+
|
|
518
|
+
### Routes of your own
|
|
519
|
+
|
|
520
|
+
Under `mode: 'static'`, `vercel()` also takes routes of the application's
|
|
521
|
+
own, in the language Vercel's Build Output API reads — `src`, a regular
|
|
522
|
+
expression for the pathname, with `dest`, `status`, `headers`, `continue`,
|
|
523
|
+
`check`, `caseSensitive` and `has` — and writes them into
|
|
524
|
+
`.vercel/output/config.json` as given, in two places among its own:
|
|
525
|
+
|
|
526
|
+
```text
|
|
527
|
+
...beforeFiles
|
|
528
|
+
{ handle: 'filesystem' } the files
|
|
529
|
+
...the shells' rewrites
|
|
530
|
+
...beforeNotFound
|
|
531
|
+
404.html under a 404 when the build wrote one
|
|
532
|
+
{ handle: 'hit' }
|
|
533
|
+
assets/ sent immutable
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
`beforeFiles` is tried before any file, so it is where headers for every
|
|
537
|
+
answer go, and a redirect or a rewrite that has to win over a file that is
|
|
538
|
+
there. `beforeNotFound` is tried once neither a file nor a shell answered,
|
|
539
|
+
so a catch-all of yours there never hides a shell: it is where a URL the
|
|
540
|
+
build wrote nothing for is proxied to another origin, or answered with a 404
|
|
541
|
+
of another body.
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
// vite.config.ts
|
|
545
|
+
import { vercel } from '@k8ordo/framework/vercel';
|
|
546
|
+
import { framework } from '@k8ordo/framework/vite';
|
|
547
|
+
import { defineConfig } from 'vite';
|
|
548
|
+
|
|
549
|
+
const acceptsMarkdown = [
|
|
550
|
+
{ type: 'header', key: 'accept', value: '.*text/markdown.*' },
|
|
551
|
+
] as const;
|
|
552
|
+
|
|
553
|
+
export default defineConfig({
|
|
554
|
+
plugins: [
|
|
555
|
+
framework({ mode: 'static' }),
|
|
556
|
+
vercel({
|
|
557
|
+
beforeFiles: [
|
|
558
|
+
{
|
|
559
|
+
src: '^/.*$',
|
|
560
|
+
headers: { 'X-Content-Type-Options': 'nosniff' },
|
|
561
|
+
continue: true,
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
src: '^/((?:[^/]+/)*[^/]+)/$',
|
|
565
|
+
status: 308,
|
|
566
|
+
headers: { Location: '/$1' },
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
src: '^/posts/([a-z0-9-]+)$',
|
|
570
|
+
has: acceptsMarkdown,
|
|
571
|
+
dest: '/posts/$1/article.md',
|
|
572
|
+
check: true,
|
|
573
|
+
caseSensitive: true,
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
src: '^/feed$',
|
|
577
|
+
headers: { 'Vercel-CDN-Cache-Control': 'max-age=3600' },
|
|
578
|
+
continue: true,
|
|
579
|
+
caseSensitive: true,
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
beforeNotFound: [
|
|
583
|
+
{
|
|
584
|
+
src: '^/feed$',
|
|
585
|
+
dest: 'https://api.example.com/feed',
|
|
586
|
+
caseSensitive: true,
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
src: '^/.*$',
|
|
590
|
+
has: acceptsMarkdown,
|
|
591
|
+
dest: '/not-found.md',
|
|
592
|
+
status: 404,
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
}),
|
|
596
|
+
],
|
|
597
|
+
});
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
- A route that only adds headers says `continue: true`, and comes before
|
|
601
|
+
the route that answers: an answering route ends the matching. What
|
|
602
|
+
`beforeFiles` adds this way is on every answer the routes lead to — a
|
|
603
|
+
file, a redirect, `404.html`, a `dest` with a `status`, an answer proxied
|
|
604
|
+
from another origin.
|
|
605
|
+
- The redirect takes the trailing slash off and keeps the query:
|
|
606
|
+
`/about/?x=1` goes to `/about?x=1`, which Vercel answers from
|
|
607
|
+
`about/index.html` as it is. Its pattern takes no empty segment, so it
|
|
608
|
+
never answers with `Location: //evil.test`, which a browser reads as
|
|
609
|
+
another origin. Vercel itself answers `//evil.test/` before any route, with
|
|
610
|
+
a 308 to `/evil.test/` that carries none of your headers.
|
|
611
|
+
- `check: true` looks for `dest` among the files. When none is there, the
|
|
612
|
+
matching goes on with `dest` as the pathname: `beforeNotFound` sees
|
|
613
|
+
`/posts/nope/article.md`, not `/posts/nope`. Here a `route.ts` at
|
|
614
|
+
`routes/posts/[slug]/article.md/` wrote each post's markdown beside its
|
|
615
|
+
page, and a client asking for markdown is answered with it. Have `paths`
|
|
616
|
+
give the pages and their markdown from one list of posts, so no post is
|
|
617
|
+
written without the other.
|
|
618
|
+
- `has` matches the header's text against `value`, a regular expression,
|
|
619
|
+
and negotiates nothing: `.*text/markdown.*` takes
|
|
620
|
+
`Accept: text/markdown, text/html;q=0.9`, and not a browser's
|
|
621
|
+
`text/html,application/xhtml+xml,*/*;q=0.8`, though `*/*` would take
|
|
622
|
+
markdown.
|
|
623
|
+
- A proxy is two routes: its headers ahead of the files, and the `dest` that
|
|
624
|
+
answers after them. The `dest` is asked with the request's query:
|
|
625
|
+
`/feed?page=2` fetches `https://api.example.com/feed?page=2`.
|
|
626
|
+
- `dest` with `status` answers with that file under that status, as the
|
|
627
|
+
build's own `404.html` route does.
|
|
628
|
+
|
|
629
|
+
What to keep in mind:
|
|
630
|
+
|
|
631
|
+
- **`src` matches the whole pathname**, never just its start, and never the
|
|
632
|
+
query: `^/posts/` does not match `/posts/first`, and `^/posts/.+$` does.
|
|
633
|
+
- **Only Vercel applies them.** `vite dev`, `vite preview` and `serve`
|
|
634
|
+
answer as if they were not there — no header, no redirect, nothing decided
|
|
635
|
+
by `Accept` — since an imitation of Vercel's routing that differed from it
|
|
636
|
+
would pass locally and fail once deployed. A URL only a route of yours
|
|
637
|
+
answers is a 404 there.
|
|
638
|
+
- **No `Cache-Control` in a route every path takes.** It reaches every file
|
|
639
|
+
too, those under `assets/` among them, which `vercel()` marks `immutable`
|
|
640
|
+
in the `hit` phase, after yours. Give a `Cache-Control` a `src` that names
|
|
641
|
+
the paths it is for.
|
|
642
|
+
- **Vercel matches `src` regardless of case** unless the route says
|
|
643
|
+
`caseSensitive: true`, while the routes `vercel()` writes itself are
|
|
644
|
+
case-sensitive, as the route table is. A route without it answers
|
|
645
|
+
`/POSTS/first` as well.
|
|
646
|
+
- **`has` names a header in lowercase**, as Vercel compares it. `vercel()`
|
|
647
|
+
refuses another spelling when it is called:
|
|
648
|
+
|
|
649
|
+
```text
|
|
650
|
+
vercel() route ^/posts/([a-z0-9-]+)$ has the header key "Accept" — write it lowercase ("accept"), as Vercel compares it
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
- **Nothing else is checked.** The routes are written as given — under a
|
|
654
|
+
`base` their `src` and `dest` carry it — and their patterns are Vercel's
|
|
655
|
+
(PCRE), which JavaScript's `RegExp` would misjudge, so a mistake shows once
|
|
656
|
+
deployed. Try a change on a preview deployment first.
|
|
657
|
+
- **Static mode only.** Under `mode: 'server'` the request handler answers
|
|
658
|
+
every URL no file names, and a `guard.ts` or a `route.ts` decides what it
|
|
659
|
+
sends. The build refuses them there:
|
|
660
|
+
|
|
661
|
+
```text
|
|
662
|
+
vercel() takes beforeFiles and beforeNotFound under mode: 'static' only — under mode: 'server' the handler answers every URL no file names
|
|
663
|
+
```
|
|
664
|
+
|
|
508
665
|
## A tab opened before a deploy
|
|
509
666
|
|
|
510
667
|
A tab keeps running the script it loaded, while every payload it fetches
|
|
@@ -280,10 +280,14 @@ with params needs `paths`, as a page's does
|
|
|
280
280
|
([Parameters](params.md#routes-with-parameters)) — with a request whose
|
|
281
281
|
origin is `site` when that option is given (an RSS feed's links are
|
|
282
282
|
absolute), and writes what it answered as the file at that pathname:
|
|
283
|
-
`dist/client/feed.xml`. A
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
283
|
+
`dist/client/feed.xml`. A file keeps no type of its own, so a static host
|
|
284
|
+
serves it with the type its extension says — `vite preview` and `serve` too,
|
|
285
|
+
which send a file with no extension as `application/octet-stream`. On
|
|
286
|
+
Vercel, `vercel()` serves it with the `Content-Type` its `GET` answered, the
|
|
287
|
+
type `vite dev` sends ([Deploying to Vercel](deploy.md#deploying-to-vercel)).
|
|
288
|
+
The `GET` has to answer `200`; anything else stops the build naming the
|
|
289
|
+
pathname. A file answers nothing but `GET`, so a `route.ts` exporting
|
|
290
|
+
another method is refused by name, in the build and in `vite dev`:
|
|
287
291
|
|
|
288
292
|
```
|
|
289
293
|
static build writes a route.ts as the file its GET answers, and a file cannot answer another method — these export one:
|
|
@@ -299,6 +303,11 @@ A `route.ts` at `/` cannot be a file (the host serves `/` from `index.html`),
|
|
|
299
303
|
nor one with pages written below it, which would need a directory of the same
|
|
300
304
|
name. What it writes is not a page, so `sitemap.xml` leaves it out.
|
|
301
305
|
|
|
306
|
+
Name the type in the `Response`. A string body with none is sent as
|
|
307
|
+
`text/plain;charset=UTF-8` — the `Response` sets it — so a `sitemap.xml`
|
|
308
|
+
returned that way is plain text on Vercel and in `vite dev`, whatever its
|
|
309
|
+
extension says. `vite dev` shows it before a deploy does.
|
|
310
|
+
|
|
302
311
|
## The generated files
|
|
303
312
|
|
|
304
313
|
The framework writes `.k8ordo/` and keeps it in step with the directories.
|
|
@@ -311,10 +320,10 @@ import { defineRoutes } from '@k8ordo/framework/generated';
|
|
|
311
320
|
import type { ParamsOf, ParsedParams } from '@k8ordo/framework/generated';
|
|
312
321
|
import type { ComponentType, ReactNode } from 'react';
|
|
313
322
|
|
|
323
|
+
import layout from '../src/routes/layout';
|
|
314
324
|
import page from '../src/routes/page';
|
|
315
|
-
import products_page from '../src/routes/products/page';
|
|
316
325
|
import products_id_page from '../src/routes/products/[id]/page';
|
|
317
|
-
import
|
|
326
|
+
import products_page from '../src/routes/products/page';
|
|
318
327
|
|
|
319
328
|
type Page<P extends string, S extends readonly unknown[] = []> = ComponentType<{
|
|
320
329
|
params: ParsedParams<P, S>;
|
|
@@ -384,6 +393,35 @@ import { href } from '@k8ordo/framework';
|
|
|
384
393
|
<a href={href('/products/:id', { id })}>…</a>; // checked against routes/
|
|
385
394
|
```
|
|
386
395
|
|
|
396
|
+
It registers the locale set with `@k8ordo/i18n` too, when the application
|
|
397
|
+
has one: its `package.json` lists `@k8ordo/i18n` (in `dependencies` or
|
|
398
|
+
`devDependencies`), and `src/i18n.ts` exports the set as `locales`. That is
|
|
399
|
+
the `Register` an application outside the framework writes by hand, so here
|
|
400
|
+
it writes none, and a message missing a locale does not compile:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
// .k8ordo/register.gen.ts (the part for @k8ordo/i18n)
|
|
404
|
+
import type { LocaleOf } from '@k8ordo/i18n';
|
|
405
|
+
|
|
406
|
+
import type { locales } from '../src/i18n';
|
|
407
|
+
|
|
408
|
+
declare module '@k8ordo/i18n' {
|
|
409
|
+
interface Register {
|
|
410
|
+
locale: LocaleOf<typeof locales>;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The export is read from the file's syntax, as a route file's are, and never
|
|
416
|
+
by running it: `export const locales = defineLocales(…)` and
|
|
417
|
+
`export { locales } from './locales'` count, a type-only export and
|
|
418
|
+
`export *` do not. A `src/i18n.ts` without the export gets nothing
|
|
419
|
+
generated and a warning naming it, since every message would compile
|
|
420
|
+
unchecked; an application that keeps its set elsewhere writes the
|
|
421
|
+
`Register` by hand. `vite dev` writes the file again as `src/i18n.ts` comes,
|
|
422
|
+
goes, or gains or loses the export. A hand-written augmentation left in the
|
|
423
|
+
application still compiles beside it, since both declare the same type.
|
|
424
|
+
|
|
387
425
|
Neither file names `@k8ordo/router`: they import from
|
|
388
426
|
`@k8ordo/framework/generated`, so the application's code, generated or not,
|
|
389
427
|
reads only the framework.
|
|
@@ -397,7 +435,8 @@ vite build && tsc --noEmit
|
|
|
397
435
|
```
|
|
398
436
|
|
|
399
437
|
Without the files, `tsc` checks against no table: `href` takes any string,
|
|
400
|
-
params typed by a `paramsSchema` are strings,
|
|
438
|
+
params typed by a `paramsSchema` are strings, a message missing a locale
|
|
439
|
+
compiles, and under `mode: 'server'` the
|
|
401
440
|
`request` and `search` props are not in `PageProps` or `LayoutProps`, so a
|
|
402
441
|
page that reads them fails with `Property 'request' does not exist`. That is
|
|
403
442
|
the error static mode gives on purpose, but here the mode is not the cause.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k8ordo/framework",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "The k8ordo framework: routes/ compiled into React Server Components, run with or without a server.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"k8ordo",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"registry": "https://registry.npmjs.org"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@vitejs/plugin-react": "6.1.
|
|
65
|
-
"@vitejs/plugin-rsc": "0.5.
|
|
64
|
+
"@vitejs/plugin-react": "6.1.2",
|
|
65
|
+
"@vitejs/plugin-rsc": "0.5.38",
|
|
66
66
|
"compressible": "2.0.18",
|
|
67
67
|
"fresh": "2.0.0",
|
|
68
68
|
"mime-types": "3.0.2",
|
|
@@ -73,26 +73,33 @@
|
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@arethetypeswrong/cli": "0.18.5",
|
|
75
75
|
"@k8ordo/framework-engine": "^0.1.1",
|
|
76
|
-
"@k8ordo/router": "^1.
|
|
76
|
+
"@k8ordo/router": "^1.1.0",
|
|
77
77
|
"@types/compressible": "2.0.3",
|
|
78
78
|
"@types/fresh": "0.5.3",
|
|
79
79
|
"@types/mime-types": "3.0.1",
|
|
80
80
|
"@types/range-parser": "1.2.7",
|
|
81
81
|
"@types/react": "19.3.0",
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"react
|
|
82
|
+
"oxc-transform-react": "0.154.0",
|
|
83
|
+
"publint": "0.3.25",
|
|
84
|
+
"react": "19.3.1",
|
|
85
|
+
"react-dom": "19.3.1",
|
|
85
86
|
"typescript": "7.0.2",
|
|
86
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@
|
|
87
|
-
"vite-plus": "
|
|
88
|
-
"vitest": "5.0.
|
|
87
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
|
|
88
|
+
"vite-plus": "1.3.0",
|
|
89
|
+
"vitest": "5.0.5"
|
|
89
90
|
},
|
|
90
91
|
"peerDependencies": {
|
|
91
|
-
"@k8ordo/router": "^1.
|
|
92
|
+
"@k8ordo/router": "^1.1.0",
|
|
93
|
+
"oxc-transform-react": "^0.152.0",
|
|
92
94
|
"react": ">=19.3.0",
|
|
93
95
|
"react-dom": ">=19.3.0",
|
|
94
96
|
"vite": ">=8.0.0"
|
|
95
97
|
},
|
|
98
|
+
"peerDependenciesMeta": {
|
|
99
|
+
"oxc-transform-react": {
|
|
100
|
+
"optional": true
|
|
101
|
+
}
|
|
102
|
+
},
|
|
96
103
|
"engines": {
|
|
97
104
|
"node": ">=24.0.0"
|
|
98
105
|
},
|