@junejs/server 0.1.3 → 0.2.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.
Files changed (131) hide show
  1. package/assets/icon-latin.LICENSE.txt +92 -0
  2. package/assets/icon-latin.ttf +0 -0
  3. package/dist/adapter-D0UnYBuN.d.ts +64 -0
  4. package/dist/adapter.d.ts +2 -56
  5. package/dist/adapter.js +107 -6
  6. package/dist/agent-compile.d.ts +38 -0
  7. package/dist/agent-compile.js +145 -0
  8. package/dist/agent-discover.d.ts +4 -4
  9. package/dist/agent-discover.js +30 -44
  10. package/dist/agent-durable.d.ts +94 -19
  11. package/dist/agent-durable.js +647 -32
  12. package/dist/agent-native.d.ts +44 -13
  13. package/dist/agent-native.js +264 -27
  14. package/dist/app.d.ts +5 -5
  15. package/dist/app.js +42 -10
  16. package/dist/assets.d.ts +23 -0
  17. package/dist/assets.js +123 -0
  18. package/dist/blob.d.ts +4 -5
  19. package/dist/build.d.ts +8 -30
  20. package/dist/build.js +143 -298
  21. package/dist/client-bundle.d.ts +8 -9
  22. package/dist/client-bundle.js +1 -1
  23. package/dist/config-freeze-r5p-vTWv.d.ts +17 -0
  24. package/dist/config-freeze.d.ts +2 -0
  25. package/dist/config-freeze.js +54 -0
  26. package/dist/config-loader.d.ts +3 -4
  27. package/dist/connection-auth.d.ts +38 -0
  28. package/dist/connection-auth.js +31 -0
  29. package/dist/content-freeze-CyiW2fRK.d.ts +4 -0
  30. package/dist/content-freeze.d.ts +2 -0
  31. package/dist/content-freeze.js +83 -0
  32. package/dist/content.d.ts +22 -9
  33. package/dist/content.js +43 -3
  34. package/dist/core-version.d.ts +4 -0
  35. package/dist/core-version.js +12 -0
  36. package/dist/css-modules-ambient.d.ts +2 -1
  37. package/dist/css-modules.d.ts +10 -11
  38. package/dist/css.d.ts +9 -10
  39. package/dist/d1-migrate.d.ts +6 -7
  40. package/dist/d1-migrate.js +3 -2
  41. package/dist/db.d.ts +2 -2
  42. package/dist/deploy.d.ts +3 -4
  43. package/dist/deploy.js +3 -2
  44. package/dist/dev-reload.d.ts +3 -4
  45. package/dist/dev.d.ts +8 -5
  46. package/dist/dev.js +40 -9
  47. package/dist/favicon.d.ts +37 -0
  48. package/dist/favicon.js +248 -0
  49. package/dist/host.d.ts +6 -5
  50. package/dist/host.js +2 -1
  51. package/dist/icon-letter.d.ts +3 -0
  52. package/dist/icon-letter.js +8 -0
  53. package/dist/index.d.ts +11 -5
  54. package/dist/index.js +9 -3
  55. package/dist/instrumentation.d.ts +2 -3
  56. package/dist/instrumentation.js +2 -1
  57. package/dist/island-registry.d.ts +6 -7
  58. package/dist/isolate-local.d.ts +5 -0
  59. package/dist/isolate-local.js +12 -0
  60. package/dist/kv.d.ts +3 -4
  61. package/dist/llms-links.d.ts +9 -0
  62. package/dist/llms-links.js +65 -0
  63. package/dist/manifest-CwTT1zVs.d.ts +11 -0
  64. package/dist/manifest.d.ts +2 -0
  65. package/dist/manifest.js +113 -0
  66. package/dist/negotiate.d.ts +4 -3
  67. package/dist/negotiate.js +24 -14
  68. package/dist/{pipeline-CCp9idQ4.d.ts → pipeline-BHUjBO0G.d.ts} +13 -1
  69. package/dist/pipeline.d.ts +2 -2
  70. package/dist/pipeline.js +286 -35
  71. package/dist/resources.d.ts +5 -6
  72. package/dist/route-rank.d.ts +6 -7
  73. package/dist/route-scan-BMbBzPIB.d.ts +20 -0
  74. package/dist/route-scan.d.ts +2 -0
  75. package/dist/route-scan.js +100 -0
  76. package/dist/router.d.ts +12 -13
  77. package/dist/router.js +51 -38
  78. package/dist/rsc-build.d.ts +6 -7
  79. package/dist/rsc-bundle.d.ts +6 -7
  80. package/dist/rsc-manifest.d.ts +5 -6
  81. package/dist/rsc-runtime/flight-render-routes.d.ts +3 -4
  82. package/dist/rsc-runtime/flight-render.d.ts +2 -3
  83. package/dist/rsc-runtime/flight-to-html.d.ts +2 -3
  84. package/dist/rsc-runtime/flight-to-html.js +3 -2
  85. package/dist/rsc-runtime/rsc-worker.js +11 -8
  86. package/dist/rsc-runtime/rsdw-shims.d.ts +2 -1
  87. package/dist/segment.d.ts +4 -5
  88. package/dist/sqlite-driver.d.ts +11 -11
  89. package/dist/sqlite-driver.js +17 -4
  90. package/dist/static-files.d.ts +4 -5
  91. package/dist/tsconfig-jsx.d.ts +4 -4
  92. package/dist/tsconfig-jsx.js +1 -1
  93. package/dist/turn-events.d.ts +4 -0
  94. package/dist/turn-events.js +46 -0
  95. package/dist/web-manifest.d.ts +5 -0
  96. package/dist/web-manifest.js +42 -0
  97. package/dist/worker-2qbQjj2c.d.ts +64 -0
  98. package/dist/worker.d.ts +2 -57
  99. package/dist/worker.js +69 -27
  100. package/package.json +28 -9
  101. package/src/adapter.ts +149 -4
  102. package/src/agent-compile.ts +271 -0
  103. package/src/agent-discover.ts +77 -63
  104. package/src/agent-durable.ts +998 -30
  105. package/src/agent-native.ts +370 -35
  106. package/src/app.ts +75 -6
  107. package/src/assets.ts +200 -0
  108. package/src/build.ts +241 -449
  109. package/src/config-freeze.ts +109 -0
  110. package/src/connection-auth.ts +101 -0
  111. package/src/content-freeze.ts +151 -0
  112. package/src/content.ts +55 -4
  113. package/src/core-version.ts +34 -0
  114. package/src/dev.ts +41 -8
  115. package/src/favicon.ts +325 -0
  116. package/src/host.ts +4 -3
  117. package/src/icon-letter.ts +14 -0
  118. package/src/index.ts +22 -0
  119. package/src/isolate-local.ts +33 -0
  120. package/src/llms-links.ts +96 -0
  121. package/src/manifest.ts +130 -0
  122. package/src/negotiate.ts +39 -15
  123. package/src/pipeline.ts +494 -44
  124. package/src/route-scan.ts +161 -0
  125. package/src/router.ts +66 -54
  126. package/src/rsc-runtime/rsc-worker.tsx +5 -4
  127. package/src/sqlite-driver.ts +36 -6
  128. package/src/tsconfig-jsx.ts +2 -2
  129. package/src/turn-events.ts +43 -0
  130. package/src/web-manifest.ts +38 -0
  131. package/src/worker.ts +131 -35
package/src/worker.ts CHANGED
@@ -17,11 +17,22 @@ import type { DocumentConfig } from "@junejs/core/document";
17
17
  import type { I18nConfig } from "@junejs/core/i18n";
18
18
  import type { Resources } from "@junejs/core/resources";
19
19
 
20
+ import type { Channel, ChannelFactory } from "@junejs/core/agent-config";
21
+ import { blobFileStore } from "@junejs/core/agent-files";
22
+
20
23
  import { createPipeline, type ExtraHandler, type LayoutComponent, type LoadingComponent, type Resolved, type ResolvedResource, type ResourceHandler } from "./pipeline";
21
- import { durableAgentSurface, type DurableObjectNamespace } from "./agent-durable";
24
+ import { durableAgentSurface, durableChannelSurface, type DurableObjectNamespace } from "./agent-durable";
25
+ import { acceptTarget } from "./negotiate";
22
26
  import { compareRoutePatterns, parseSegment } from "./route-rank";
23
27
  import { contentTypeFor, RESERVED_PREFIX, safeRelativePath } from "./static-files";
24
28
 
29
+ // The slice of workerd's ExecutionContext the worker threads through: webhook
30
+ // channels ACK fast and finish their work (post the reply out-of-band) on
31
+ // waitUntil — without it a floating promise can be killed when the isolate is
32
+ // reclaimed. Optional end to end; a host without it (tests, other targets)
33
+ // simply doesn't extend the invocation.
34
+ export type WorkerExecutionContext = { waitUntil?(p: Promise<unknown>): void };
35
+
25
36
  export type WorkerManifest = {
26
37
  // Static paths → route definitions ("/", "/users", ...).
27
38
  routes: Record<string, BrandedRoute>;
@@ -51,6 +62,13 @@ export type WorkerManifest = {
51
62
  // by the build when an agent/ directory exists; absent → no durable agent on
52
63
  // the edge. The DO namespace binding is `env.AGENT`.
53
64
  agentName?: string;
65
+ // The agent's channels (Channel or `(env) => Channel` factories, from the
66
+ // compiled agent module) — mounted as webhook routes on the worker via
67
+ // durableChannelSurface when agent.runtime.channels is on. The SAME factories
68
+ // are also wired into the DO (DoAgentDef.channels) by the generated entry, so
69
+ // capability tools and host-side delivery work; this field is the WORKER half
70
+ // (verify the webhook, run the turn on the DO, reply on waitUntil).
71
+ agentChannels?: (Channel | ChannelFactory)[];
54
72
  // Locale routing config, frozen from june.config.ts `i18n`. The locales table
55
73
  // is plain data; a `resolveLocale` hook survives the in-process freeze (parity
56
74
  // test) but not JSON codegen — worker hook support lands with the codegen pass.
@@ -106,9 +124,12 @@ function compilePattern(pattern: string): { regex: RegExp; names: string[] } {
106
124
  return { regex: new RegExp(allOptional ? `^(?:${source}|/)$` : `^${source}$`), names };
107
125
  }
108
126
 
127
+ // opts.staticBuild: set only by the static() build's in-process prerender worker
128
+ // (see PipelineConfig.staticBuild) — never by a deployed entry.
109
129
  export function createWorker(
110
130
  manifest: WorkerManifest,
111
- ): { fetch(request: Request, env?: unknown): Promise<Response> } {
131
+ opts: { staticBuild?: boolean } = {},
132
+ ): { fetch(request: Request, env?: unknown, ctx?: WorkerExecutionContext): Promise<Response> } {
112
133
  // Pages and resource routes in ONE table, ordered the way `june dev` resolves
113
134
  // (#312): app/ before .june/routes/, then segment shape (static > [param] >
114
135
  // [...catchAll], required before optional), then a page before a resource
@@ -149,8 +170,14 @@ export function createWorker(
149
170
 
150
171
  // The worker's env (D1/KV/R2 bindings) arrives per fetch and is stable across
151
172
  // requests in an isolate; we capture the latest and hand it to the env-aware
152
- // provider, which memoizes the opened handles on first call.
173
+ // provider, which memoizes the opened handles on first call. That "latest"
174
+ // pattern is safe ONLY for per-isolate-stable values — concurrent fetches
175
+ // interleave at await points, so anything per-INVOCATION (the execution
176
+ // context, and env used at a specific moment) must come from request-scoped
177
+ // storage instead: `requestLocals`, keyed by the Request identity that flows
178
+ // unchanged from fetch() through the pipeline to the agent surface.
153
179
  let currentEnv: unknown;
180
+ const requestLocals = new WeakMap<Request, { env: unknown; ctx?: WorkerExecutionContext }>();
154
181
  const provider = manifest.resources;
155
182
 
156
183
  // App services: the build sets a raw `(env) => services` factory here; memoize it per
@@ -159,18 +186,52 @@ export function createWorker(
159
186
  const servicesProvider = manifest.services;
160
187
  let servicesCache: { v: unknown } | undefined;
161
188
 
162
- // Durable agent (edge): route the chat endpoint to the per-session DO bound at
163
- // env.AGENT. Built only when the app has an agent (manifest.agentName) and the
164
- // runtime is enabled; the surface is inert (null → fall through) until env.AGENT
165
- // is present, so a worker without the DO binding is unaffected.
189
+ // Durable agent (edge): route the chat endpoint — and the agent's channel
190
+ // webhooks — to the per-session DO bound at env.AGENT. Built only when the app
191
+ // has an agent (manifest.agentName) and the runtime is enabled; the surface is
192
+ // inert (null → fall through) until env.AGENT is present, so a worker without
193
+ // the DO binding is unaffected.
166
194
  const agentSurface =
167
195
  manifest.agent.runtime.enabled && manifest.agentName
168
196
  ? (() => {
169
- const route = durableAgentSurface(
170
- () => (currentEnv as { AGENT?: DurableObjectNamespace } | undefined)?.AGENT,
171
- { agentName: manifest.agentName, chatPath: manifest.agent.runtime.chat.path },
172
- );
173
- return (req: Request) => route(req);
197
+ const agentName = manifest.agentName;
198
+ const wantChannels = manifest.agent.runtime.channels !== false && (manifest.agentChannels?.length ?? 0) > 0;
199
+ return async (req: Request): Promise<Response | null> => {
200
+ // REQUEST-scoped, not isolate-scoped: the pipeline awaits between
201
+ // fetch() and this call, so a concurrent fetch may have run since —
202
+ // a module-level "current request" variable here would hand this
203
+ // webhook ANOTHER request's env (wrong AGENT namespace, factories
204
+ // resolved against foreign secrets) and register its background
205
+ // work on another invocation's waitUntil. Everything below reads
206
+ // THIS request's locals.
207
+ const locals = requestLocals.get(req);
208
+ const ns = () => (locals?.env as { AGENT?: DurableObjectNamespace } | undefined)?.AGENT;
209
+ const chat = durableAgentSurface(ns, { agentName, chatPath: manifest.agent.runtime.chat.path });
210
+ const chatRes = await chat(req);
211
+ if (chatRes) return chatRes;
212
+ if (!wantChannels) return null;
213
+ // Constructed per request — cheap by design: resolveChannel is a
214
+ // call, and resolveServices memoizes the app services bag per
215
+ // (env, agentName), exactly for this construction pattern (see
216
+ // durableChannelSurface's services doc).
217
+ const channels = durableChannelSurface(ns, {
218
+ agentName,
219
+ channels: manifest.agentChannels!,
220
+ env: locals?.env,
221
+ services: servicesProvider,
222
+ waitUntil: locals?.ctx?.waitUntil?.bind(locals.ctx),
223
+ // The blob resource the agent's Durable Object stores files in, opened from
224
+ // this isolate's env — the channels read attached files back from it.
225
+ files: provider
226
+ ? blobFileStore(async () => {
227
+ const blob = (await provider(locals?.env)).blob;
228
+ if (!blob) throw new Error(`agent "${agentName}": no blob resource bound in this isolate — attached files can't be read`);
229
+ return blob;
230
+ })
231
+ : undefined,
232
+ });
233
+ return channels(req);
234
+ };
174
235
  })()
175
236
  : undefined;
176
237
 
@@ -180,6 +241,7 @@ export function createWorker(
180
241
  agentSurface,
181
242
  i18n: manifest.i18n,
182
243
  routeList: () => routeList,
244
+ staticBuild: opts.staticBuild,
183
245
  earlyHints: manifest.earlyHints,
184
246
  htmlCacheControl: manifest.htmlCacheControl,
185
247
  notFoundComponent: manifest.notFound,
@@ -208,8 +270,9 @@ export function createWorker(
208
270
  });
209
271
 
210
272
  return {
211
- fetch(request, env) {
212
- currentEnv = env;
273
+ fetch(request, env, ctx) {
274
+ currentEnv = env; // per-isolate-stable consumers only (resources/services)
275
+ requestLocals.set(request, { env, ctx }); // per-invocation consumers (agent surface)
213
276
  return pipeline.fetch(request);
214
277
  },
215
278
  };
@@ -218,43 +281,71 @@ export function createWorker(
218
281
  // --- the deployed worker's outermost layer (run_worker_first) ---------------
219
282
  // Prerendered pages are served as static ASSETS, which bypass the pipeline —
220
283
  // so the agent-ready signals the asset layer can't produce get added HERE:
221
- // 1. `Accept: text/markdown` on a page → the prerendered `.md` asset.
284
+ // 1. a negotiated projection on a page (Accept: text/markdown / application/
285
+ // json → the prerendered .md / .json asset; else, and for the client
286
+ // router's fragment, the pipeline — never the page's HTML asset).
222
287
  // 2. a `Link` header on every HTML response (RFC 8288/9727 discovery).
223
288
  // 3. `x-markdown-tokens` on markdown responses.
224
289
  // Static assets are served by the ASSETS binding (no re-render); only genuinely
225
290
  // dynamic routes reach the pipeline. With no ASSETS binding (no prerender), this
226
291
  // is a transparent pass-through to the pipeline.
227
292
  type AssetEnv = { ASSETS?: { fetch(request: Request): Promise<Response> } };
228
- type FetchPipeline = { fetch(request: Request, env?: unknown): Promise<Response> };
293
+ type FetchPipeline = { fetch(request: Request, env?: unknown, ctx?: WorkerExecutionContext): Promise<Response> };
229
294
 
230
295
  const estimateTokens = (s: string) => String(Math.ceil(s.length / 4));
231
296
 
297
+ // Add Accept to a response's Vary, keeping whatever the asset layer already set.
298
+ function varyAccept(headers: Headers): void {
299
+ const vary = headers.get("vary");
300
+ if (!vary) headers.set("vary", "accept");
301
+ else if (vary.trim() !== "*" && !vary.split(",").some((v) => v.trim().toLowerCase() === "accept")) {
302
+ headers.set("vary", `${vary}, accept`);
303
+ }
304
+ }
305
+
232
306
  export function withAssets(
233
307
  pipeline: FetchPipeline,
234
308
  opts: { link?: string | null } = {},
235
- ): { fetch(request: Request, env?: AssetEnv): Promise<Response> } {
309
+ ): { fetch(request: Request, env?: AssetEnv, ctx?: WorkerExecutionContext): Promise<Response> } {
236
310
  return {
237
- async fetch(request, env) {
311
+ async fetch(request, env, ctx) {
238
312
  const assets = env?.ASSETS;
239
313
  const url = new URL(request.url);
240
314
  const isPagePath = !/\.[a-z0-9]+$/i.test(url.pathname); // no file extension
241
- const accept = request.headers.get("accept") ?? "";
315
+ // The pipeline's own Accept decision (q-values included). A page path that
316
+ // negotiates a projection other than the HTML view must get THAT projection
317
+ // or the pipeline's answer for it — never the prerendered HTML asset.
318
+ const target = isPagePath ? acceptTarget(request.headers.get("accept") ?? "") : null;
242
319
 
243
- // 1. Markdown content negotiation on a page path → prerendered .md asset.
244
- if (assets && request.method === "GET" && isPagePath && /text\/markdown/.test(accept)) {
245
- const base = url.pathname === "/" ? "/index" : url.pathname.replace(/\/+$/, "");
246
- const mdUrl = new URL(url);
247
- mdUrl.pathname = `${base}.md`;
248
- const a = await assets.fetch(new Request(mdUrl.toString(), { headers: request.headers }));
249
- if (a.ok) {
250
- const body = await a.text();
251
- const headers = new Headers(a.headers);
252
- headers.set("content-type", "text/markdown; charset=utf-8");
253
- headers.set("x-markdown-tokens", estimateTokens(body));
254
- if (opts.link) headers.set("link", opts.link);
255
- return new Response(body, { status: 200, headers });
320
+ // 1. Negotiated projections on a page path.
321
+ if (target && (request.method === "GET" || request.method === "HEAD")) {
322
+ // md / json → the prerendered <path>.md / <path>.json asset when the build
323
+ // emitted one (.md unless md = false; .json only for a json() route). Only
324
+ // the flat file — never <path>/index.*, a distinct /path/index route's twin
325
+ // (locale homes' <locale>/index.* are a static()-target artifact; this
326
+ // workers()-only layer never has them).
327
+ if (assets && request.method === "GET" && (target === "md" || target === "json")) {
328
+ const base = url.pathname === "/" ? "/index" : url.pathname.replace(/\/+$/, "");
329
+ const assetUrl = new URL(url);
330
+ assetUrl.pathname = `${base}.${target}`;
331
+ const a = await assets.fetch(new Request(assetUrl.toString(), { headers: request.headers }));
332
+ if (a.ok) {
333
+ const headers = new Headers(a.headers);
334
+ if (opts.link) headers.set("link", opts.link);
335
+ varyAccept(headers);
336
+ if (target === "json") return new Response(a.body, { status: 200, headers });
337
+ const body = await a.text();
338
+ headers.set("content-type", "text/markdown; charset=utf-8");
339
+ headers.set("x-markdown-tokens", estimateTokens(body));
340
+ return new Response(body, { status: 200, headers });
341
+ }
256
342
  }
257
- // no prerendered .md → fall through; a dynamic route renders it below.
343
+ // No such asset (or a fragment, or HEAD) → the pipeline (step 3). It renders
344
+ // a dynamic route's projection, derives .json from the loader data, 404s a
345
+ // disabled projection, and renders the client router's soft-nav fragment —
346
+ // the HTML asset is a full DOCUMENT, which the router would morph into its
347
+ // [data-june-root] as if it were the root's inner HTML.
348
+ return pipeline.fetch(request, env, ctx);
258
349
  }
259
350
 
260
351
  // 2. Static assets (prerendered HTML/.md/.json, /client.js, hashed CSS)
@@ -271,10 +362,15 @@ export function withAssets(
271
362
  // URL changes when the bytes do, so the browser may cache forever and
272
363
  // never revalidate (no 304 round-trip, no stale window).
273
364
  const immutable = /\.[a-f0-9]{8,}\.(css|js)$/.test(url.pathname);
274
- if (addLink || immutable) {
365
+ // A page path answers HTML here but Markdown under Accept: text/markdown
366
+ // (step 1) — the same URL, so caches must key on Accept or they hand an
367
+ // agent the HTML (or a browser the Markdown).
368
+ const negotiated = isPagePath && ct.includes("text/html");
369
+ if (addLink || immutable || negotiated) {
275
370
  const headers = new Headers(a.headers);
276
371
  if (addLink) headers.set("link", opts.link!);
277
372
  if (immutable) headers.set("cache-control", "public, max-age=31536000, immutable");
373
+ if (negotiated) varyAccept(headers);
278
374
  return new Response(a.body, { status: a.status, headers });
279
375
  }
280
376
  return a;
@@ -283,7 +379,7 @@ export function withAssets(
283
379
 
284
380
  // 3. Dynamic routes → the render pipeline (already sets Link + negotiates).
285
381
  // Env flows through so the pipeline's resources get the D1/KV/R2 bindings.
286
- return pipeline.fetch(request, env);
382
+ return pipeline.fetch(request, env, ctx);
287
383
  },
288
384
  };
289
385
  }