@jk2908/solas 0.13.0 → 0.14.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/CHANGELOG.md +20 -0
- package/README.md +7 -5
- package/dist/adapters/cloudflare/vite.js +1 -1
- package/dist/internal/codegen/environments.js +1 -1
- package/dist/{adapters → runtime}/node.d.ts +2 -2
- package/dist/{adapters → runtime}/node.js +64 -12
- package/package.json +9 -9
- /package/dist/{adapters → runtime}/cloudflare/assets.d.ts +0 -0
- /package/dist/{adapters → runtime}/cloudflare/assets.js +0 -0
- /package/dist/{adapters/cloudflare/runtime.d.ts → runtime/cloudflare/index.d.ts} +0 -0
- /package/dist/{adapters/cloudflare/runtime.js → runtime/cloudflare/index.js} +0 -0
- /package/dist/{adapters → runtime}/cloudflare/worker.d.ts +0 -0
- /package/dist/{adapters → runtime}/cloudflare/worker.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- Runtime helpers moved out of `@jk2908/solas/adapters/*`: `serve` is now a named export from `@jk2908/solas/runtime/node`, and `createWorker`/`createCloudflareAssets` are now `@jk2908/solas/runtime/cloudflare`. The old `@jk2908/solas/adapters/node` and `@jk2908/solas/adapters/cloudflare/runtime` entries are removed. `@jk2908/solas/adapters/*` now contains build adapters only.
|
|
8
|
+
|
|
9
|
+
## 0.13.2
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The Node adapter's response-stream loop and close handlers use declarations and `while (true)` for readability. No behaviour change.
|
|
14
|
+
|
|
15
|
+
## 0.13.1
|
|
16
|
+
|
|
17
|
+
### Fixes
|
|
18
|
+
|
|
19
|
+
- The Node adapter now aborts `request.signal` when the client disconnects, so streaming endpoints such as SSE can stop their producers.
|
|
20
|
+
- The Node adapter flushes response headers before streaming the body, so a delayed first chunk no longer leaves the client waiting with no response.
|
|
21
|
+
- The Node adapter cancels the response stream when the client disconnects, instead of continuing to write into a dead socket.
|
|
22
|
+
|
|
3
23
|
## 0.13.0
|
|
4
24
|
|
|
5
25
|
### Features
|
package/README.md
CHANGED
|
@@ -659,18 +659,18 @@ Because workers have no filesystem, the Worker reads its own output through the
|
|
|
659
659
|
`cloudflare()` forwards its `cloudflare` option to `@cloudflare/vite-plugin`. To build a Worker manually, `createWorker` composes the entry behaviour, and `createCloudflareAssets` builds the store directly:
|
|
660
660
|
|
|
661
661
|
```ts
|
|
662
|
-
import { createCloudflareAssets } from '@jk2908/solas/
|
|
662
|
+
import { createCloudflareAssets } from '@jk2908/solas/runtime/cloudflare'
|
|
663
663
|
|
|
664
664
|
const assets = createCloudflareAssets(env)
|
|
665
665
|
```
|
|
666
666
|
|
|
667
667
|
## Node
|
|
668
668
|
|
|
669
|
-
`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/
|
|
669
|
+
`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
670
|
|
|
671
671
|
```ts
|
|
672
672
|
// server.ts
|
|
673
|
-
import serve from '@jk2908/solas/
|
|
673
|
+
import { serve } from '@jk2908/solas/runtime/node'
|
|
674
674
|
|
|
675
675
|
await serve()
|
|
676
676
|
```
|
|
@@ -686,6 +686,8 @@ await serve()
|
|
|
686
686
|
|
|
687
687
|
`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
688
|
|
|
689
|
+
`serve` is a runtime helper, not a build adapter: it starts an HTTP server and returns it, so it belongs in `server.ts`. Do not pass it to `solas({ adapter })`; that option expects an `Adapter` with `name` and `plugins`, such as `cloudflare()` or the static adapter.
|
|
690
|
+
|
|
689
691
|
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:
|
|
690
692
|
|
|
691
693
|
```ts
|
|
@@ -702,7 +704,7 @@ import handler from './dist/rsc/index.js'
|
|
|
702
704
|
Deno.serve(request => handler.fetch(request))
|
|
703
705
|
```
|
|
704
706
|
|
|
705
|
-
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/
|
|
707
|
+
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`.
|
|
706
708
|
|
|
707
709
|
## Static
|
|
708
710
|
|
|
@@ -731,7 +733,7 @@ Server actions and runtime endpoints also need a server, so the build warns when
|
|
|
731
733
|
|
|
732
734
|
## Runtime
|
|
733
735
|
|
|
734
|
-
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/
|
|
736
|
+
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.
|
|
735
737
|
|
|
736
738
|
## Scripts
|
|
737
739
|
|
|
@@ -4,7 +4,7 @@ import { cloudflare as cloudflareVitePlugin } from '@cloudflare/vite-plugin';
|
|
|
4
4
|
import * as Config from '../../config.js';
|
|
5
5
|
import { writeCloudflareEntry } from '../../internal/codegen/environments.js';
|
|
6
6
|
import { setPrerenderImport, setRscEntry } from '../../internal/postbuild.js';
|
|
7
|
-
import { NAMESPACE_PREFIX } from '
|
|
7
|
+
import { NAMESPACE_PREFIX } from '../../runtime/cloudflare/assets.js';
|
|
8
8
|
import { withBindingsShim } from './bindings.js';
|
|
9
9
|
const VIRTUAL_ENTRY = 'virtual:solas-cloudflare-entry';
|
|
10
10
|
/**
|
|
@@ -31,7 +31,7 @@ export function writeCloudflareEntry() {
|
|
|
31
31
|
return source `
|
|
32
32
|
${AUTOGEN_MSG}
|
|
33
33
|
|
|
34
|
-
import { createWorker } from '${Config.PKG_NAME}/
|
|
34
|
+
import { createWorker } from '${Config.PKG_NAME}/runtime/cloudflare'
|
|
35
35
|
|
|
36
36
|
import { manifest } from './manifest.js'
|
|
37
37
|
import { importMap } from './maps.js'
|
|
@@ -38,7 +38,7 @@ export type Server = http.Server & AsyncDisposable;
|
|
|
38
38
|
* Deno and Bun do not need this helper: use `Deno.serve(handler.fetch)` or
|
|
39
39
|
* `Bun.serve({ fetch: handler.fetch })` directly.
|
|
40
40
|
*/
|
|
41
|
-
declare function
|
|
41
|
+
declare function serve(options?: ServeOptions): Promise<http.Server<typeof http.IncomingMessage, typeof http.ServerResponse> & {
|
|
42
42
|
[Symbol.asyncDispose](): Promise<void>;
|
|
43
43
|
}>;
|
|
44
|
-
export {
|
|
44
|
+
export { serve };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { once } from 'node:events';
|
|
2
1
|
import http from 'node:http';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { pathToFileURL } from 'node:url';
|
|
@@ -38,26 +37,59 @@ function nodeToWeb(req) {
|
|
|
38
37
|
*/
|
|
39
38
|
async function webToNode(stream, res) {
|
|
40
39
|
const reader = stream.getReader();
|
|
40
|
+
let closed = false;
|
|
41
|
+
// a client that goes away leaves nothing to write to; cancel the source so
|
|
42
|
+
// its producer stops instead of buffering into a dead socket
|
|
43
|
+
function onClose() {
|
|
44
|
+
closed = true;
|
|
45
|
+
void reader.cancel().catch(() => { });
|
|
46
|
+
}
|
|
47
|
+
res.once('close', onClose);
|
|
41
48
|
try {
|
|
42
49
|
while (true) {
|
|
50
|
+
if (closed)
|
|
51
|
+
break;
|
|
43
52
|
const { done, value } = await reader.read();
|
|
44
53
|
if (done)
|
|
45
54
|
break;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
if (closed || res.destroyed || res.writableEnded)
|
|
56
|
+
break;
|
|
57
|
+
try {
|
|
58
|
+
// `write` returns false when the socket buffer is full. That is not an
|
|
59
|
+
// error: wait for the `'drain'` event before writing more, otherwise a
|
|
60
|
+
// fast producer and a slow client would buffer the whole body in memory
|
|
61
|
+
if (!res.write(value)) {
|
|
62
|
+
await new Promise(resolve => {
|
|
63
|
+
function settle() {
|
|
64
|
+
res.off('drain', settle);
|
|
65
|
+
res.off('close', settle);
|
|
66
|
+
resolve();
|
|
67
|
+
}
|
|
68
|
+
res.on('drain', settle);
|
|
69
|
+
res.on('close', settle);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// the socket died between the checks above
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
51
77
|
}
|
|
52
78
|
}
|
|
53
79
|
finally {
|
|
54
|
-
|
|
80
|
+
res.off('close', onClose);
|
|
81
|
+
try {
|
|
82
|
+
reader.releaseLock();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// a pending read can outlive a cancel by a tick
|
|
86
|
+
}
|
|
55
87
|
}
|
|
56
88
|
}
|
|
57
89
|
/**
|
|
58
90
|
* Convert a Node request into a fetch `Request`, streaming the body when present.
|
|
59
91
|
*/
|
|
60
|
-
function toRequest(req) {
|
|
92
|
+
function toRequest(req, signal) {
|
|
61
93
|
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
62
94
|
const headers = new Headers();
|
|
63
95
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
@@ -77,6 +109,7 @@ function toRequest(req) {
|
|
|
77
109
|
headers,
|
|
78
110
|
body: hasBody ? nodeToWeb(req) : undefined,
|
|
79
111
|
duplex: 'half',
|
|
112
|
+
signal,
|
|
80
113
|
};
|
|
81
114
|
return new Request(url, init);
|
|
82
115
|
}
|
|
@@ -99,8 +132,12 @@ async function sendResponse(res, response) {
|
|
|
99
132
|
res.end();
|
|
100
133
|
return;
|
|
101
134
|
}
|
|
135
|
+
// send headers now so a delayed first chunk (SSE, slow streams) does not
|
|
136
|
+
// leave the client waiting with no response
|
|
137
|
+
res.flushHeaders();
|
|
102
138
|
await webToNode(response.body, res);
|
|
103
|
-
res.
|
|
139
|
+
if (!res.destroyed)
|
|
140
|
+
res.end();
|
|
104
141
|
}
|
|
105
142
|
/**
|
|
106
143
|
* Start a production HTTP server for a Node (or Bun) deployment.
|
|
@@ -114,23 +151,38 @@ async function sendResponse(res, response) {
|
|
|
114
151
|
* Deno and Bun do not need this helper: use `Deno.serve(handler.fetch)` or
|
|
115
152
|
* `Bun.serve({ fetch: handler.fetch })` directly.
|
|
116
153
|
*/
|
|
117
|
-
async function
|
|
154
|
+
async function serve(options = {}) {
|
|
118
155
|
const outDir = options.outDir ?? 'dist';
|
|
119
156
|
const entry = path.resolve(options.entry ?? path.join(outDir, 'rsc', 'index.js'));
|
|
120
157
|
const port = options.port ?? (Number(process.env.PORT) || 3000);
|
|
121
158
|
const host = options.host ?? '0.0.0.0';
|
|
122
159
|
const handler = await loadHandler(entry);
|
|
123
160
|
const server = http.createServer(async (req, res) => {
|
|
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);
|
|
124
169
|
try {
|
|
125
|
-
const response = await handler.fetch(toRequest(req));
|
|
170
|
+
const response = await handler.fetch(toRequest(req, abort.signal));
|
|
126
171
|
await sendResponse(res, response);
|
|
127
172
|
}
|
|
128
173
|
catch (err) {
|
|
129
174
|
console.error('[solas] request failed', err);
|
|
175
|
+
if (res.headersSent) {
|
|
176
|
+
res.destroy();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
130
179
|
res.statusCode = 500;
|
|
131
180
|
res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
132
181
|
res.end('Internal Server Error');
|
|
133
182
|
}
|
|
183
|
+
finally {
|
|
184
|
+
res.off('close', onClose);
|
|
185
|
+
}
|
|
134
186
|
});
|
|
135
187
|
await new Promise(resolve => server.listen(port, host, resolve));
|
|
136
188
|
options.onListen?.({ port, host });
|
|
@@ -142,4 +194,4 @@ async function adapter(options = {}) {
|
|
|
142
194
|
},
|
|
143
195
|
});
|
|
144
196
|
}
|
|
145
|
-
export {
|
|
197
|
+
export { serve };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jk2908/solas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "A Vite + React meta-framework exploring streaming, Server Components, and partial prerendering. Designed for simplicity and lightness",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -73,17 +73,17 @@
|
|
|
73
73
|
"types": "./dist/adapters/cloudflare/index.d.ts",
|
|
74
74
|
"import": "./dist/adapters/cloudflare/index.js"
|
|
75
75
|
},
|
|
76
|
-
"./adapters/cloudflare/runtime": {
|
|
77
|
-
"types": "./dist/adapters/cloudflare/runtime.d.ts",
|
|
78
|
-
"import": "./dist/adapters/cloudflare/runtime.js"
|
|
79
|
-
},
|
|
80
|
-
"./adapters/node": {
|
|
81
|
-
"types": "./dist/adapters/node.d.ts",
|
|
82
|
-
"import": "./dist/adapters/node.js"
|
|
83
|
-
},
|
|
84
76
|
"./adapters/static": {
|
|
85
77
|
"types": "./dist/adapters/static.d.ts",
|
|
86
78
|
"import": "./dist/adapters/static.js"
|
|
79
|
+
},
|
|
80
|
+
"./runtime/cloudflare": {
|
|
81
|
+
"types": "./dist/runtime/cloudflare/index.d.ts",
|
|
82
|
+
"import": "./dist/runtime/cloudflare/index.js"
|
|
83
|
+
},
|
|
84
|
+
"./runtime/node": {
|
|
85
|
+
"types": "./dist/runtime/node.d.ts",
|
|
86
|
+
"import": "./dist/runtime/node.js"
|
|
87
87
|
}
|
|
88
88
|
},
|
|
89
89
|
"scripts": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|