@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.
@@ -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 static host then serves it with the type its
284
- extension says. The `GET` has to answer `200`; anything else stops the build
285
- naming the pathname. A file answers nothing but `GET`, so a `route.ts`
286
- exporting another method is refused by name, in the build and in `vite dev`:
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 layout from '../src/routes/layout';
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, and under `mode: 'server'` the
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": "0.1.0",
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.1",
65
- "@vitejs/plugin-rsc": "0.5.35",
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.0.0",
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
- "publint": "0.3.24",
83
- "react": "19.3.0",
84
- "react-dom": "19.3.0",
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@0.3.2",
87
- "vite-plus": "0.3.2",
88
- "vitest": "5.0.2"
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.0.0",
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
  },