@junejs/server 0.0.14 → 0.0.15

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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/pipeline.ts +23 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/server",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,8 +30,8 @@
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "@junejs/core": "0.0.14",
34
- "@junejs/db": "0.0.14",
33
+ "@junejs/core": "0.0.15",
34
+ "@junejs/db": "0.0.15",
35
35
  "marked": "^18.0.5",
36
36
  "rolldown": "^1.1.0",
37
37
  "postcss": "^8.4.0",
package/src/pipeline.ts CHANGED
@@ -124,25 +124,6 @@ function StreamedView({
124
124
  return provideLoaderData(data, def.view ? def.view(data, ctx) : null);
125
125
  }
126
126
 
127
- // renderToReadableStream does not emit the doctype; prepend it without buffering
128
- // the React stream (streaming stays incremental).
129
- function withDoctype(source: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
130
- const reader = source.getReader();
131
- return new ReadableStream<Uint8Array>({
132
- start(controller) {
133
- controller.enqueue(new TextEncoder().encode("<!doctype html>\n"));
134
- },
135
- async pull(controller) {
136
- const { done, value } = await reader.read();
137
- if (done) controller.close();
138
- else controller.enqueue(value);
139
- },
140
- cancel(reason) {
141
- void reader.cancel(reason);
142
- },
143
- });
144
- }
145
-
146
127
  // The default favicon: the site name's first character in a rounded square —
147
128
  // a plain SVG string, so it needs no fonts, no rasterizer, and works for CJK
148
129
  // names as readily as latin ones. Served at /favicon.svg AND /favicon.ico
@@ -187,8 +168,9 @@ export function createPipeline(cfg: PipelineConfig): Pipeline {
187
168
  React.createElement(Document, { config: docConfigForRender(), metadata, children: wrapped }),
188
169
  );
189
170
  await stream.allReady; // fully resolved markup (no streamed Suspense fallbacks)
190
- const html = "<!doctype html>\n" + (await new Response(stream).text());
191
- return new Response(html, { status, headers: htmlHeaders() });
171
+ // React 19 emits <!DOCTYPE html> itself for an <html> root — don't prepend a
172
+ // second one.
173
+ return new Response(stream, { status, headers: htmlHeaders() });
192
174
  }
193
175
 
194
176
  // Route A: the [data-june-root] inner HTML for a soft-nav / live-apply request
@@ -249,8 +231,9 @@ export function createPipeline(cfg: PipelineConfig): Pipeline {
249
231
  React.createElement(Document, { config: docConfigForRender(), metadata, children: wrapped }),
250
232
  { onError: (e: unknown) => console.error("[june] streaming render error:", e) },
251
233
  );
252
- // NO allReady — return the live stream (shell first), doctype prepended.
253
- return new Response(withDoctype(stream), { status: 200, headers: htmlHeaders() });
234
+ // NO allReady — return the live stream (shell first). React 19 streams the
235
+ // <!DOCTYPE html> as the first bytes, so nothing to prepend.
236
+ return new Response(stream, { status: 200, headers: htmlHeaders() });
254
237
  }
255
238
 
256
239
  function notFoundResponse(target: RenderTarget, pathname: string): Promise<Response> | Response {
@@ -300,6 +283,23 @@ export function createPipeline(cfg: PipelineConfig): Pipeline {
300
283
  return notFoundResponse(target, ctx.url.pathname);
301
284
  }
302
285
 
286
+ const res = await renderTarget(target, def, data, ctx, chain);
287
+ // Every projection here is chosen by the Accept header at the SAME url (a full
288
+ // page, its fragment, its .md and .json all live at the clean path). Without
289
+ // Vary, a shared/browser cache can hand a soft-nav fragment to a real page
290
+ // load — which surfaces as a document that starts mid-body. Make Accept part
291
+ // of the cache key.
292
+ res.headers.set("vary", "accept");
293
+ return res;
294
+ }
295
+
296
+ async function renderTarget(
297
+ target: RenderTarget,
298
+ def: Resolved["def"],
299
+ data: unknown,
300
+ ctx: RouteContext,
301
+ chain: LayoutComponent[],
302
+ ): Promise<Response> {
303
303
  if (target === "md") return renderMarkdown(def, data, ctx);
304
304
  if (target === "json") {
305
305
  // Convention: a json() fn customizes; absent → serialize the loader data.