@jk2908/solas 0.14.0 → 0.15.1
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/CHANGELOG.md +16 -0
- package/README.md +74 -29
- package/dist/internal/{runtimes/assets.js → assets.js} +1 -1
- package/dist/internal/env/rsc.d.ts +2 -2
- package/dist/internal/env/rsc.js +1 -1
- package/dist/internal/http-router/router.d.ts +1 -1
- package/dist/internal/prerender.d.ts +1 -1
- package/dist/internal/prerender.js +1 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +1 -1
- package/dist/runtime/cloudflare/assets.d.ts +1 -1
- package/dist/runtime/cloudflare/assets.js +1 -1
- package/dist/runtime/cloudflare/worker.js +1 -1
- package/dist/runtime/node.d.ts +47 -2
- package/dist/runtime/node.js +87 -31
- package/package.json +1 -1
- /package/dist/internal/{runtimes/assets.d.ts → assets.d.ts} +0 -0
- /package/dist/internal/{runtimes/mime.d.ts → mime.d.ts} +0 -0
- /package/dist/internal/{runtimes/mime.js → mime.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.1
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- The README separates build adapters from runtime helpers, so the Node runtime is no longer presented as an adapter.
|
|
8
|
+
|
|
9
|
+
## 0.15.0
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- `@jk2908/solas/runtime/node` exports `createHttpRequestListener` for caller-owned `http` and `https` servers and `createHttp2RequestListener` for `http2` compatibility servers, with an exported `Http2RequestListener` type. HTTP/2 pseudo-headers are filtered out of the forwarded fetch `Request`, and `:authority` is used when there is no `host` header.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Internal asset-store and MIME helpers moved from `internal/runtimes` to `internal/assets.ts` and `internal/mime.ts`. The public `runtime/*` entries are unchanged.
|
|
18
|
+
|
|
3
19
|
## 0.14.0
|
|
4
20
|
|
|
5
21
|
### Breaking
|
package/README.md
CHANGED
|
@@ -566,7 +566,11 @@ export default defineConfig({
|
|
|
566
566
|
})
|
|
567
567
|
```
|
|
568
568
|
|
|
569
|
-
##
|
|
569
|
+
## Adapters
|
|
570
|
+
|
|
571
|
+
Adapters run at build time. Pass one to `solas({ adapter })`; it configures the build output for a platform.
|
|
572
|
+
|
|
573
|
+
### Cloudflare
|
|
570
574
|
|
|
571
575
|
Solas ships a Cloudflare adapter that runs the RSC server as a Worker and serves prerender artifacts and client assets through the Cloudflare assets binding.
|
|
572
576
|
|
|
@@ -608,7 +612,7 @@ export default defineConfig({
|
|
|
608
612
|
|
|
609
613
|
> `cloudflare.config.ts` is an open beta and must be loaded with Node 22.18+ (Bun is not supported), so the Cloudflare build and deploy commands run under Node. See the [`cf` documentation](https://developers.cloudflare.com/cf/) for the format and the `cf dev` / `cf build` / `cf deploy` commands.
|
|
610
614
|
|
|
611
|
-
|
|
615
|
+
#### Bindings
|
|
612
616
|
|
|
613
617
|
Bindings are declared in `cloudflare.config.ts` and read with the standard Cloudflare global. The adapter does not manage them:
|
|
614
618
|
|
|
@@ -654,17 +658,38 @@ The adapter:
|
|
|
654
658
|
|
|
655
659
|
`htmlHandling: none` stops the asset layer serving a prerendered document at its route path. That matters because the asset layer ignores `Accept`, so an RSC navigation (`Accept: text/x-component`) to a fully-prerendered route would otherwise receive HTML and fail to parse. Routing documents through the Worker lets Solas serve the prerender for `text/html` and render RSC otherwise, while non-HTML assets (JS, CSS, fonts, images) are still served straight from the asset layer.
|
|
656
660
|
|
|
657
|
-
|
|
661
|
+
`cloudflare()` forwards its `cloudflare` option to `@cloudflare/vite-plugin`.
|
|
658
662
|
|
|
659
|
-
|
|
663
|
+
### Static
|
|
664
|
+
|
|
665
|
+
The static adapter sets `prerender: 'full'` so every route is rendered to HTML at build time, then assembles a directory any static host can serve. There is no server.
|
|
660
666
|
|
|
661
667
|
```ts
|
|
662
|
-
|
|
668
|
+
// vite.config.ts
|
|
669
|
+
import { defineConfig } from 'vite'
|
|
663
670
|
|
|
664
|
-
|
|
671
|
+
import solas from '@jk2908/solas'
|
|
672
|
+
import staticAdapter from '@jk2908/solas/adapters/static'
|
|
673
|
+
import react from '@vitejs/plugin-react'
|
|
674
|
+
|
|
675
|
+
export default defineConfig({
|
|
676
|
+
plugins: [solas({ adapter: staticAdapter() }), react()],
|
|
677
|
+
})
|
|
665
678
|
```
|
|
666
679
|
|
|
667
|
-
|
|
680
|
+
`vite build` writes the site to `build/` (override with `outDir`): the client assets, each route as `<route>.html` (or `<route>/index.html` under `trailingSlash: 'always'`), its RSC payload as `<route>.rsc.txt`, plus `sitemap.xml` and `robots.txt`. Serve that directory with anything — `npx serve build`, GitHub Pages, Netlify, an S3 bucket, nginx.
|
|
681
|
+
|
|
682
|
+
Client-side navigation works with no server: the browser router fetches each route's `<route>.rsc.txt` payload instead of asking a server for `text/x-component`.
|
|
683
|
+
|
|
684
|
+
Because it forces full prerendering, routes that need a server are not a fit: per-route PPR or SSR opt-ins and dynamic routes without `params()`.
|
|
685
|
+
|
|
686
|
+
Server actions and runtime endpoints also need a server, so the build warns when the graph contains any (a `'use server'` module or a `+endpoint` file).
|
|
687
|
+
|
|
688
|
+
## Runtime helpers
|
|
689
|
+
|
|
690
|
+
Runtime helpers run at request time. They live under `@jk2908/solas/runtime/*` and are never passed to `solas({ adapter })`.
|
|
691
|
+
|
|
692
|
+
### Node
|
|
668
693
|
|
|
669
694
|
`vite dev` and `vite preview` already run the built handler in Node (via `@vitejs/plugin-rsc`'s default server handler), so nothing extra is needed locally. For a production Node server, use `@jk2908/solas/runtime/node`:
|
|
670
695
|
|
|
@@ -679,14 +704,47 @@ await serve()
|
|
|
679
704
|
{
|
|
680
705
|
"scripts": {
|
|
681
706
|
"build": "vite build",
|
|
682
|
-
"start": "node server.ts"
|
|
707
|
+
"start": "NODE_ENV=production node server.ts"
|
|
683
708
|
}
|
|
684
709
|
}
|
|
685
710
|
```
|
|
686
711
|
|
|
712
|
+
Set `NODE_ENV=production` before starting. React reads it when the built handler loads; without it the server renders development payloads, which include debug data and are noticeably larger.
|
|
713
|
+
|
|
687
714
|
`serve()` loads the built RSC entry (`dist/rsc/index.js` by default) and forwards requests to its fetch handler, which already serves prerendered routes, client assets, and RSC/SSR responses. Options: `port` (defaults to `PORT` or `3000`), `host`, `outDir`, and `entry`. Run from the project root so the handler can resolve `dist`.
|
|
688
715
|
|
|
689
|
-
`
|
|
716
|
+
Set `precompress: true` in the Solas config to emit brotli variants of client and prerendered assets at build time. `serve()` negotiates them when the client sends `accept-encoding: br` and falls back to the original file otherwise. Dynamic HTML and RSC responses are streamed per request and are not precompressed, so compress those at a proxy or CDN if you need it.
|
|
717
|
+
|
|
718
|
+
For TLS, mount the handler on your own `https` server:
|
|
719
|
+
|
|
720
|
+
```ts
|
|
721
|
+
import { createServer } from 'node:https'
|
|
722
|
+
|
|
723
|
+
import { createHttpRequestListener } from '@jk2908/solas/runtime/node'
|
|
724
|
+
|
|
725
|
+
import handler from './dist/rsc/index.js'
|
|
726
|
+
|
|
727
|
+
createServer({ key, cert }, createHttpRequestListener(handler)).listen(443)
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
For HTTP/2, use `createHttp2RequestListener` with the compatibility API. Browsers only negotiate HTTP/2 over TLS, and `allowHTTP1` keeps clients that do not speak it working:
|
|
731
|
+
|
|
732
|
+
```ts
|
|
733
|
+
import http2 from 'node:http2'
|
|
734
|
+
|
|
735
|
+
import { createHttp2RequestListener } from '@jk2908/solas/runtime/node'
|
|
736
|
+
|
|
737
|
+
import handler from './dist/rsc/index.js'
|
|
738
|
+
|
|
739
|
+
http2
|
|
740
|
+
.createSecureServer(
|
|
741
|
+
{ key, cert, allowHTTP1: true },
|
|
742
|
+
createHttp2RequestListener(handler),
|
|
743
|
+
)
|
|
744
|
+
.listen(443)
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
`serve()` itself is HTTP/1.1. TLS and HTTP/2 termination still belong at your proxy or CDN when you have one; these listeners are for deployments that terminate at Node.
|
|
690
748
|
|
|
691
749
|
Bun and Deno do not need the helper. Their native servers pass the handler a `Request` directly and expect a `Response`, so the Node-specific HTTP-to-Fetch bridging (streams, `duplex`, backpressure) is not needed:
|
|
692
750
|
|
|
@@ -706,32 +764,19 @@ Deno.serve(request => handler.fetch(request))
|
|
|
706
764
|
|
|
707
765
|
The handler reads `dist` through `node:fs`, which Bun and Deno (2.x, via Node compat) provide. Run from the project root. Bun can also run `@jk2908/solas/runtime/node` as-is, since it implements `node:http`.
|
|
708
766
|
|
|
709
|
-
|
|
767
|
+
### Cloudflare runtime
|
|
710
768
|
|
|
711
|
-
|
|
769
|
+
Because workers have no filesystem, the Worker reads its own output through the `ASSETS` binding. Reads use logical namespaces rather than filesystem paths: `client` maps onto asset-root URLs, and `static` (prerenders) and `artifact` (manifest, ppr) map onto the artifact prefix. During the build-time prerender pass the Worker is invoked without bindings, so the Node asset store is used and reads/writes artifacts on disk.
|
|
712
770
|
|
|
713
|
-
|
|
714
|
-
// vite.config.ts
|
|
715
|
-
import { defineConfig } from 'vite'
|
|
771
|
+
To build a Worker manually, `createWorker` composes the entry behaviour, and `createCloudflareAssets` builds the store directly:
|
|
716
772
|
|
|
717
|
-
|
|
718
|
-
import
|
|
719
|
-
import react from '@vitejs/plugin-react'
|
|
773
|
+
```ts
|
|
774
|
+
import { createCloudflareAssets } from '@jk2908/solas/runtime/cloudflare'
|
|
720
775
|
|
|
721
|
-
|
|
722
|
-
plugins: [solas({ adapter: staticAdapter() }), react()],
|
|
723
|
-
})
|
|
776
|
+
const assets = createCloudflareAssets(env)
|
|
724
777
|
```
|
|
725
778
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
Client-side navigation works with no server: the browser router fetches each route's `<route>.rsc.txt` payload instead of asking a server for `text/x-component`.
|
|
729
|
-
|
|
730
|
-
Because it forces full prerendering, routes that need a server are not a fit: per-route PPR or SSR opt-ins and dynamic routes without `params()`.
|
|
731
|
-
|
|
732
|
-
Server actions and runtime endpoints also need a server, so the build warns when the graph contains any (a `'use server'` module or a `+endpoint` file).
|
|
733
|
-
|
|
734
|
-
## Runtime
|
|
779
|
+
### Asset store
|
|
735
780
|
|
|
736
781
|
At request time Solas reads its output through an `Assets` interface (`exists`/`readText`/`readBuffer`/`mimeType`) injected into `createHandler`, defaulting to the Node standard library (which Bun runs natively). References are logical (`{ namespace, path }`), where the namespaces are `client` (Vite client output), `static` (full prerenders), and `artifact` (runtime manifest and ppr files). Platforms without a filesystem provide their own store; `@jk2908/solas/runtime/cloudflare` builds one around the `ASSETS` binding via `createWorker`. Build-time reads and writes use `node:fs` directly and are not abstracted.
|
|
737
782
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ReactFormState } from 'react-dom/client';
|
|
2
2
|
import type { ImportMap, Manifest, RuntimeConfig } from '../../types.js';
|
|
3
|
-
import type { Assets } from '../
|
|
3
|
+
import type { Assets } from '../assets.js';
|
|
4
4
|
import * as RuntimeManifest from '../../manifest.js';
|
|
5
5
|
import { Metadata } from '../metadata.js';
|
|
6
6
|
export { loadManifest } from '../../manifest.js';
|
|
7
|
-
export type { Assets } from '../
|
|
7
|
+
export type { Assets } from '../assets.js';
|
|
8
8
|
export type HandlerOptions = {
|
|
9
9
|
/**
|
|
10
10
|
* Static asset store used to serve prerendered HTML, PPR artifacts, and
|
package/dist/internal/env/rsc.js
CHANGED
|
@@ -4,6 +4,7 @@ import { applyBasePath, normaliseBasePath, stripBasePath } from '../../utils/bas
|
|
|
4
4
|
import { Logger } from '../../utils/logger.js';
|
|
5
5
|
import * as Config from '../../config.js';
|
|
6
6
|
import * as RuntimeManifest from '../../manifest.js';
|
|
7
|
+
import { nodeAssets } from '../assets.js';
|
|
7
8
|
import { createHttpRouter } from '../http-router/create-http-router.js';
|
|
8
9
|
import { HttpRouter } from '../http-router/router.js';
|
|
9
10
|
import { isRscRequest, normaliseTrailingSlash, resolveRequestPath, } from '../http-router/utils.js';
|
|
@@ -13,7 +14,6 @@ import { isRedirect, toRedirect } from '../navigation/redirect.js';
|
|
|
13
14
|
import * as Prerender from '../prerender.js';
|
|
14
15
|
import { Tree } from '../render/tree.js';
|
|
15
16
|
import { Resolver } from '../resolver.js';
|
|
16
|
-
import { nodeAssets } from '../runtimes/assets.js';
|
|
17
17
|
import { processActionRequest } from '../server/actions.js';
|
|
18
18
|
import DefaultErr from '../ui/defaults/error.js';
|
|
19
19
|
import { RequestContext } from './request-context.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginConfig, SolasRequest } from '../../types.js';
|
|
2
|
-
import type { AssetRef, Assets } from '../
|
|
2
|
+
import type { AssetRef, Assets } from '../assets.js';
|
|
3
3
|
import { CsrfConfig } from '../server/csrf.js';
|
|
4
4
|
export declare namespace HttpRouter {
|
|
5
5
|
type Params = Record<string, string | string[]>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BuildContext } from '../types.js';
|
|
2
|
-
import type { Assets } from './
|
|
2
|
+
import type { Assets } from './assets.js';
|
|
3
3
|
export type Mode = 'full' | 'ppr';
|
|
4
4
|
export type File = 'html' | 'prelude' | 'postponed' | 'metadata';
|
|
5
5
|
export type Value = {
|
|
@@ -4,8 +4,8 @@ import { applyBasePath } from '../utils/base-path.js';
|
|
|
4
4
|
import { Logger } from '../utils/logger.js';
|
|
5
5
|
import * as Config from '../config.js';
|
|
6
6
|
import * as Solas from '../solas.js';
|
|
7
|
+
import { nodeAssets } from './assets.js';
|
|
7
8
|
import { toPathPattern } from './http-router/utils.js';
|
|
8
|
-
import { nodeAssets } from './runtimes/assets.js';
|
|
9
9
|
const logger = new Logger();
|
|
10
10
|
const DEFAULT_CONCURRENCY = 4;
|
|
11
11
|
/**
|
package/dist/manifest.d.ts
CHANGED
package/dist/manifest.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as Config from './config.js';
|
|
2
|
-
import { nodeAssets } from './internal/
|
|
2
|
+
import { nodeAssets } from './internal/assets.js';
|
|
3
3
|
export function getManifestPath(outDir) {
|
|
4
4
|
return [outDir, Config.GENERATED_DIR, Config.RUNTIME_MANIFEST]
|
|
5
5
|
.map((part, index) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { nodeAssets } from '../../internal/assets.js';
|
|
1
2
|
import { createRuntimeHandler } from '../../internal/env/rsc.js';
|
|
2
|
-
import { nodeAssets } from '../../internal/runtimes/assets.js';
|
|
3
3
|
import { createCloudflareAssets } from './assets.js';
|
|
4
4
|
/**
|
|
5
5
|
* Build a Cloudflare Worker around Solas's RSC handler.
|
package/dist/runtime/node.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Http2ServerRequest, Http2ServerResponse } from 'node:http2';
|
|
1
2
|
import http from 'node:http';
|
|
2
3
|
export type ServeOptions = {
|
|
3
4
|
/**
|
|
@@ -26,6 +27,48 @@ export type ServeOptions = {
|
|
|
26
27
|
}) => void;
|
|
27
28
|
};
|
|
28
29
|
export type Server = http.Server & AsyncDisposable;
|
|
30
|
+
export type FetchHandler = {
|
|
31
|
+
fetch: (request: Request) => Response | Promise<Response>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A request listener for an `http2` compatibility server.
|
|
35
|
+
*/
|
|
36
|
+
export type Http2RequestListener = (req: Http2ServerRequest, res: Http2ServerResponse) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Create a request listener for a built fetch handler on an `http` or `https`
|
|
39
|
+
* server.
|
|
40
|
+
*
|
|
41
|
+
* `serve` uses this internally. Exporting it lets you terminate TLS yourself:
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { createServer } from 'node:https'
|
|
45
|
+
* import { createHttpRequestListener } from '@jk2908/solas/runtime/node'
|
|
46
|
+
* import handler from './dist/rsc/index.js'
|
|
47
|
+
*
|
|
48
|
+
* createServer({ key, cert }, createHttpRequestListener(handler)).listen(443)
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare function createHttpRequestListener(handler: FetchHandler): http.RequestListener;
|
|
52
|
+
/**
|
|
53
|
+
* Create a request listener for a built fetch handler on an `http2`
|
|
54
|
+
* compatibility server.
|
|
55
|
+
*
|
|
56
|
+
* Browsers only negotiate HTTP/2 over TLS, so use
|
|
57
|
+
* `http2.createSecureServer({ allowHTTP1: true }, listener)`. `allowHTTP1`
|
|
58
|
+
* keeps clients that do not speak HTTP/2 working:
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* import http2 from 'node:http2'
|
|
62
|
+
* import { createHttp2RequestListener } from '@jk2908/solas/runtime/node'
|
|
63
|
+
* import handler from './dist/rsc/index.js'
|
|
64
|
+
*
|
|
65
|
+
* http2.createSecureServer(
|
|
66
|
+
* { key, cert, allowHTTP1: true },
|
|
67
|
+
* createHttp2RequestListener(handler),
|
|
68
|
+
* ).listen(443)
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare function createHttp2RequestListener(handler: FetchHandler): Http2RequestListener;
|
|
29
72
|
/**
|
|
30
73
|
* Start a production HTTP server for a Node (or Bun) deployment.
|
|
31
74
|
*
|
|
@@ -35,10 +78,12 @@ export type Server = http.Server & AsyncDisposable;
|
|
|
35
78
|
* handler can resolve `dist`. The returned server is `AsyncDisposable`, so it
|
|
36
79
|
* can be released with `await using`.
|
|
37
80
|
*
|
|
38
|
-
*
|
|
81
|
+
* For TLS or HTTP/2, mount `createHttpRequestListener(handler)` or
|
|
82
|
+
* `createHttp2RequestListener(handler)` on your own server instead. Deno and Bun
|
|
83
|
+
* do not need this helper: use `Deno.serve(handler.fetch)` or
|
|
39
84
|
* `Bun.serve({ fetch: handler.fetch })` directly.
|
|
40
85
|
*/
|
|
41
86
|
declare function serve(options?: ServeOptions): Promise<http.Server<typeof http.IncomingMessage, typeof http.ServerResponse> & {
|
|
42
87
|
[Symbol.asyncDispose](): Promise<void>;
|
|
43
88
|
}>;
|
|
44
|
-
export { serve };
|
|
89
|
+
export { createHttp2RequestListener, createHttpRequestListener, serve };
|
package/dist/runtime/node.js
CHANGED
|
@@ -90,9 +90,16 @@ async function webToNode(stream, res) {
|
|
|
90
90
|
* Convert a Node request into a fetch `Request`, streaming the body when present.
|
|
91
91
|
*/
|
|
92
92
|
function toRequest(req, signal) {
|
|
93
|
-
|
|
93
|
+
// HTTP/2's compatibility API also exposes pseudo-headers such as `:path` and
|
|
94
|
+
// `:authority`. Fetch `Headers` rejects names starting with `:`, and the
|
|
95
|
+
// authority replaces the HTTP/1 `host` header, so normalise both here
|
|
96
|
+
const authority = req.headers[':authority'];
|
|
97
|
+
const host = req.headers.host ?? (Array.isArray(authority) ? authority[0] : authority);
|
|
98
|
+
const url = new URL(req.url ?? '/', `http://${host ?? 'localhost'}`);
|
|
94
99
|
const headers = new Headers();
|
|
95
100
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
101
|
+
if (key.startsWith(':'))
|
|
102
|
+
continue;
|
|
96
103
|
if (Array.isArray(value)) {
|
|
97
104
|
for (const item of value)
|
|
98
105
|
headers.append(key, item);
|
|
@@ -134,11 +141,84 @@ async function sendResponse(res, response) {
|
|
|
134
141
|
}
|
|
135
142
|
// send headers now so a delayed first chunk (SSE, slow streams) does not
|
|
136
143
|
// leave the client waiting with no response
|
|
137
|
-
res.flushHeaders();
|
|
144
|
+
res.flushHeaders?.();
|
|
138
145
|
await webToNode(response.body, res);
|
|
139
146
|
if (!res.destroyed)
|
|
140
147
|
res.end();
|
|
141
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Handle one Node request by forwarding it to the fetch handler.
|
|
151
|
+
*/
|
|
152
|
+
async function handleRequest(req, res, handler) {
|
|
153
|
+
const abort = new AbortController();
|
|
154
|
+
// `close` before the response has finished means the client went away;
|
|
155
|
+
// surface that through `req.signal` so long-lived work can stop
|
|
156
|
+
function onClose() {
|
|
157
|
+
if (!res.writableEnded)
|
|
158
|
+
abort.abort(new Error('client disconnected'));
|
|
159
|
+
}
|
|
160
|
+
res.once('close', onClose);
|
|
161
|
+
try {
|
|
162
|
+
const response = await handler.fetch(toRequest(req, abort.signal));
|
|
163
|
+
await sendResponse(res, response);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
console.error('[solas] request failed', err);
|
|
167
|
+
if (res.headersSent) {
|
|
168
|
+
res.destroy();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
res.statusCode = 500;
|
|
172
|
+
res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
173
|
+
res.end('Internal Server Error');
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
res.off('close', onClose);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Create a request listener for a built fetch handler on an `http` or `https`
|
|
181
|
+
* server.
|
|
182
|
+
*
|
|
183
|
+
* `serve` uses this internally. Exporting it lets you terminate TLS yourself:
|
|
184
|
+
*
|
|
185
|
+
* ```ts
|
|
186
|
+
* import { createServer } from 'node:https'
|
|
187
|
+
* import { createHttpRequestListener } from '@jk2908/solas/runtime/node'
|
|
188
|
+
* import handler from './dist/rsc/index.js'
|
|
189
|
+
*
|
|
190
|
+
* createServer({ key, cert }, createHttpRequestListener(handler)).listen(443)
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
function createHttpRequestListener(handler) {
|
|
194
|
+
return (req, res) => {
|
|
195
|
+
void handleRequest(req, res, handler);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Create a request listener for a built fetch handler on an `http2`
|
|
200
|
+
* compatibility server.
|
|
201
|
+
*
|
|
202
|
+
* Browsers only negotiate HTTP/2 over TLS, so use
|
|
203
|
+
* `http2.createSecureServer({ allowHTTP1: true }, listener)`. `allowHTTP1`
|
|
204
|
+
* keeps clients that do not speak HTTP/2 working:
|
|
205
|
+
*
|
|
206
|
+
* ```ts
|
|
207
|
+
* import http2 from 'node:http2'
|
|
208
|
+
* import { createHttp2RequestListener } from '@jk2908/solas/runtime/node'
|
|
209
|
+
* import handler from './dist/rsc/index.js'
|
|
210
|
+
*
|
|
211
|
+
* http2.createSecureServer(
|
|
212
|
+
* { key, cert, allowHTTP1: true },
|
|
213
|
+
* createHttp2RequestListener(handler),
|
|
214
|
+
* ).listen(443)
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
function createHttp2RequestListener(handler) {
|
|
218
|
+
return (req, res) => {
|
|
219
|
+
void handleRequest(req, res, handler);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
142
222
|
/**
|
|
143
223
|
* Start a production HTTP server for a Node (or Bun) deployment.
|
|
144
224
|
*
|
|
@@ -148,7 +228,9 @@ async function sendResponse(res, response) {
|
|
|
148
228
|
* handler can resolve `dist`. The returned server is `AsyncDisposable`, so it
|
|
149
229
|
* can be released with `await using`.
|
|
150
230
|
*
|
|
151
|
-
*
|
|
231
|
+
* For TLS or HTTP/2, mount `createHttpRequestListener(handler)` or
|
|
232
|
+
* `createHttp2RequestListener(handler)` on your own server instead. Deno and Bun
|
|
233
|
+
* do not need this helper: use `Deno.serve(handler.fetch)` or
|
|
152
234
|
* `Bun.serve({ fetch: handler.fetch })` directly.
|
|
153
235
|
*/
|
|
154
236
|
async function serve(options = {}) {
|
|
@@ -157,33 +239,7 @@ async function serve(options = {}) {
|
|
|
157
239
|
const port = options.port ?? (Number(process.env.PORT) || 3000);
|
|
158
240
|
const host = options.host ?? '0.0.0.0';
|
|
159
241
|
const handler = await loadHandler(entry);
|
|
160
|
-
const server = http.createServer(
|
|
161
|
-
const abort = new AbortController();
|
|
162
|
-
// `close` before the response has finished means the client went away;
|
|
163
|
-
// surface that through `req.signal` so long-lived work can stop
|
|
164
|
-
function onClose() {
|
|
165
|
-
if (!res.writableEnded)
|
|
166
|
-
abort.abort(new Error('client disconnected'));
|
|
167
|
-
}
|
|
168
|
-
res.once('close', onClose);
|
|
169
|
-
try {
|
|
170
|
-
const response = await handler.fetch(toRequest(req, abort.signal));
|
|
171
|
-
await sendResponse(res, response);
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
console.error('[solas] request failed', err);
|
|
175
|
-
if (res.headersSent) {
|
|
176
|
-
res.destroy();
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
res.statusCode = 500;
|
|
180
|
-
res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
181
|
-
res.end('Internal Server Error');
|
|
182
|
-
}
|
|
183
|
-
finally {
|
|
184
|
-
res.off('close', onClose);
|
|
185
|
-
}
|
|
186
|
-
});
|
|
242
|
+
const server = http.createServer(createHttpRequestListener(handler));
|
|
187
243
|
await new Promise(resolve => server.listen(port, host, resolve));
|
|
188
244
|
options.onListen?.({ port, host });
|
|
189
245
|
return Object.assign(server, {
|
|
@@ -194,4 +250,4 @@ async function serve(options = {}) {
|
|
|
194
250
|
},
|
|
195
251
|
});
|
|
196
252
|
}
|
|
197
|
-
export { serve };
|
|
253
|
+
export { createHttp2RequestListener, createHttpRequestListener, serve };
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|