@junejs/server 0.1.2 → 0.2.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/assets/icon-latin.LICENSE.txt +92 -0
- package/assets/icon-latin.ttf +0 -0
- package/dist/adapter-DcZ-W1-7.d.ts +62 -0
- package/dist/adapter.d.ts +2 -56
- package/dist/adapter.js +62 -5
- package/dist/agent-compile.d.ts +38 -0
- package/dist/agent-compile.js +145 -0
- package/dist/agent-discover.d.ts +4 -4
- package/dist/agent-discover.js +30 -44
- package/dist/agent-durable.d.ts +94 -19
- package/dist/agent-durable.js +647 -32
- package/dist/agent-native.d.ts +44 -13
- package/dist/agent-native.js +264 -27
- package/dist/app.d.ts +5 -5
- package/dist/app.js +42 -10
- package/dist/blob.d.ts +4 -5
- package/dist/build.d.ts +8 -29
- package/dist/build.js +138 -289
- package/dist/client-bundle.d.ts +8 -9
- package/dist/client-bundle.js +1 -1
- package/dist/config-freeze-CyXvNqi2.d.ts +17 -0
- package/dist/config-freeze.d.ts +2 -0
- package/dist/config-freeze.js +54 -0
- package/dist/config-loader.d.ts +3 -4
- package/dist/connection-auth.d.ts +38 -0
- package/dist/connection-auth.js +31 -0
- package/dist/content-freeze-CyiW2fRK.d.ts +4 -0
- package/dist/content-freeze.d.ts +2 -0
- package/dist/content-freeze.js +83 -0
- package/dist/content.d.ts +22 -9
- package/dist/content.js +43 -3
- package/dist/core-version.d.ts +4 -0
- package/dist/core-version.js +12 -0
- package/dist/css-modules-ambient.d.ts +2 -1
- package/dist/css-modules.d.ts +10 -11
- package/dist/css.d.ts +9 -10
- package/dist/d1-migrate.d.ts +6 -7
- package/dist/d1-migrate.js +3 -2
- package/dist/db.d.ts +2 -2
- package/dist/deploy.d.ts +3 -4
- package/dist/deploy.js +3 -2
- package/dist/dev-reload.d.ts +3 -4
- package/dist/dev.d.ts +8 -5
- package/dist/dev.js +40 -9
- package/dist/favicon.d.ts +37 -0
- package/dist/favicon.js +248 -0
- package/dist/host.d.ts +6 -5
- package/dist/host.js +2 -1
- package/dist/icon-letter.d.ts +3 -0
- package/dist/icon-letter.js +8 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +9 -3
- package/dist/instrumentation.d.ts +2 -3
- package/dist/instrumentation.js +2 -1
- package/dist/island-registry.d.ts +6 -7
- package/dist/isolate-local.d.ts +5 -0
- package/dist/isolate-local.js +12 -0
- package/dist/kv.d.ts +3 -4
- package/dist/llms-links.d.ts +9 -0
- package/dist/llms-links.js +65 -0
- package/dist/manifest-CwTT1zVs.d.ts +11 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +113 -0
- package/dist/negotiate.d.ts +4 -3
- package/dist/negotiate.js +24 -14
- package/dist/{pipeline-CCp9idQ4.d.ts → pipeline-BHUjBO0G.d.ts} +13 -1
- package/dist/pipeline.d.ts +2 -2
- package/dist/pipeline.js +286 -35
- package/dist/resources.d.ts +5 -6
- package/dist/route-rank.d.ts +10 -0
- package/dist/route-rank.js +38 -0
- package/dist/route-scan-BMbBzPIB.d.ts +20 -0
- package/dist/route-scan.d.ts +2 -0
- package/dist/route-scan.js +100 -0
- package/dist/router.d.ts +12 -13
- package/dist/router.js +58 -44
- package/dist/rsc-build.d.ts +6 -7
- package/dist/rsc-bundle.d.ts +6 -7
- package/dist/rsc-manifest.d.ts +5 -6
- package/dist/rsc-runtime/flight-render-routes.d.ts +3 -4
- package/dist/rsc-runtime/flight-render.d.ts +2 -3
- package/dist/rsc-runtime/flight-to-html.d.ts +2 -3
- package/dist/rsc-runtime/flight-to-html.js +3 -2
- package/dist/rsc-runtime/rsc-worker.js +11 -8
- package/dist/rsc-runtime/rsdw-shims.d.ts +2 -1
- package/dist/segment.d.ts +4 -5
- package/dist/sqlite-driver.d.ts +11 -11
- package/dist/sqlite-driver.js +17 -4
- package/dist/static-files.d.ts +4 -5
- package/dist/tsconfig-jsx.d.ts +4 -4
- package/dist/tsconfig-jsx.js +1 -1
- package/dist/turn-events.d.ts +4 -0
- package/dist/turn-events.js +46 -0
- package/dist/web-manifest.d.ts +5 -0
- package/dist/web-manifest.js +42 -0
- package/dist/worker-2qbQjj2c.d.ts +64 -0
- package/dist/worker.d.ts +2 -56
- package/dist/worker.js +121 -103
- package/package.json +22 -9
- package/src/adapter.ts +84 -3
- package/src/agent-compile.ts +271 -0
- package/src/agent-discover.ts +77 -63
- package/src/agent-durable.ts +998 -30
- package/src/agent-native.ts +370 -35
- package/src/app.ts +75 -6
- package/src/build.ts +237 -444
- package/src/config-freeze.ts +109 -0
- package/src/connection-auth.ts +101 -0
- package/src/content-freeze.ts +151 -0
- package/src/content.ts +55 -4
- package/src/core-version.ts +34 -0
- package/src/dev.ts +41 -8
- package/src/favicon.ts +325 -0
- package/src/host.ts +4 -3
- package/src/icon-letter.ts +14 -0
- package/src/index.ts +22 -0
- package/src/isolate-local.ts +33 -0
- package/src/llms-links.ts +96 -0
- package/src/manifest.ts +130 -0
- package/src/negotiate.ts +39 -15
- package/src/pipeline.ts +494 -44
- package/src/route-rank.ts +60 -0
- package/src/route-scan.ts +161 -0
- package/src/router.ts +75 -58
- package/src/rsc-runtime/rsc-worker.tsx +5 -4
- package/src/sqlite-driver.ts +36 -6
- package/src/tsconfig-jsx.ts +2 -2
- package/src/turn-events.ts +43 -0
- package/src/web-manifest.ts +38 -0
- package/src/worker.ts +197 -117
package/src/worker.ts
CHANGED
|
@@ -17,18 +17,35 @@ 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";
|
|
26
|
+
import { compareRoutePatterns, parseSegment } from "./route-rank";
|
|
22
27
|
import { contentTypeFor, RESERVED_PREFIX, safeRelativePath } from "./static-files";
|
|
23
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
|
+
|
|
24
36
|
export type WorkerManifest = {
|
|
25
37
|
// Static paths → route definitions ("/", "/users", ...).
|
|
26
38
|
routes: Record<string, BrandedRoute>;
|
|
27
39
|
// Dynamic patterns in file-route syntax ("/posts/[slug]", "/docs/[...path]").
|
|
28
40
|
dynamicRoutes?: Array<{ pattern: string; def: BrandedRoute }>;
|
|
29
41
|
// Resource routes (app/**/route.*): a raw-Response handler keyed by its pattern
|
|
30
|
-
// (static or dynamic).
|
|
42
|
+
// (static or dynamic). Ranked with the pages by segment shape (route-rank.ts),
|
|
43
|
+
// exactly as `june dev` resolves; a page wins only at the SAME pattern.
|
|
31
44
|
resourceRoutes?: Array<{ pattern: string; handler: ResourceHandler }>;
|
|
45
|
+
// Patterns scanned from .june/routes/ (framework-generated, e.g. Kura). Dev
|
|
46
|
+
// consults app/ first and falls back to .june/routes/, so every one of these
|
|
47
|
+
// ranks after every app/ route. Absent → all routes are app/ routes.
|
|
48
|
+
generatedRoutes?: string[];
|
|
32
49
|
// Layout chains (root→leaf) keyed by route path / dynamic pattern. The build
|
|
33
50
|
// freezes the same chain the dev server loads from app/layout.* files.
|
|
34
51
|
layoutChains?: Record<string, LayoutComponent[]>;
|
|
@@ -45,6 +62,13 @@ export type WorkerManifest = {
|
|
|
45
62
|
// by the build when an agent/ directory exists; absent → no durable agent on
|
|
46
63
|
// the edge. The DO namespace binding is `env.AGENT`.
|
|
47
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)[];
|
|
48
72
|
// Locale routing config, frozen from june.config.ts `i18n`. The locales table
|
|
49
73
|
// is plain data; a `resolveLocale` hook survives the in-process freeze (parity
|
|
50
74
|
// test) but not JSON codegen — worker hook support lands with the codegen pass.
|
|
@@ -66,7 +90,13 @@ export type WorkerManifest = {
|
|
|
66
90
|
services?: (env?: unknown) => unknown;
|
|
67
91
|
};
|
|
68
92
|
|
|
69
|
-
|
|
93
|
+
// One row of the worker's route table: a page (def) or a resource route (handler).
|
|
94
|
+
type Compiled = {
|
|
95
|
+
regex: RegExp;
|
|
96
|
+
names: string[];
|
|
97
|
+
pattern: string;
|
|
98
|
+
generated: boolean;
|
|
99
|
+
} & ({ def: BrandedRoute; handler?: undefined } | { def?: undefined; handler: ResourceHandler });
|
|
70
100
|
|
|
71
101
|
// "/posts/[slug]" | "/docs/[...path]" | "/notes/[[tag]]" → matcher. Optional
|
|
72
102
|
// segments ([[x]], [[...x]]) wrap slash + capture together so absence matches;
|
|
@@ -76,52 +106,47 @@ function compilePattern(pattern: string): { regex: RegExp; names: string[] } {
|
|
|
76
106
|
let source = "";
|
|
77
107
|
let allOptional = true;
|
|
78
108
|
for (const seg of pattern.split("/").filter(Boolean)) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
// Same grammar as the dev matcher (route-rank.ts parseSegment).
|
|
110
|
+
const { kind, name } = parseSegment(seg);
|
|
111
|
+
if (name) names.push(name);
|
|
112
|
+
if (kind === "optionalCatchAll") source += "(?:/(.+))?";
|
|
113
|
+
else if (kind === "optional") source += "(?:/([^/]+))?";
|
|
114
|
+
else {
|
|
115
|
+
if (kind === "catchAll") source += "/(.+)";
|
|
116
|
+
else if (kind === "param") source += "/([^/]+)";
|
|
117
|
+
// A static segment matches literally: escape every RegExp metacharacter,
|
|
118
|
+
// brackets included (`docs[v2`, `[slug].png` are static names).
|
|
119
|
+
else source += "/" + seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
89
120
|
allOptional = false;
|
|
90
|
-
continue;
|
|
91
121
|
}
|
|
92
|
-
const op = seg.match(/^\[\[(\w+)\]\]$/);
|
|
93
|
-
if (op) {
|
|
94
|
-
names.push(op[1]!);
|
|
95
|
-
source += "(?:/([^/]+))?";
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
const p = seg.match(/^\[(\w+)\]$/);
|
|
99
|
-
if (p) {
|
|
100
|
-
names.push(p[1]!);
|
|
101
|
-
source += "/([^/]+)";
|
|
102
|
-
allOptional = false;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
source += "/" + seg.replace(/[.*+?^${}()|\\]/g, "\\$&");
|
|
106
|
-
allOptional = false;
|
|
107
122
|
}
|
|
108
123
|
if (source === "") return { regex: /^\/$/, names };
|
|
109
124
|
return { regex: new RegExp(allOptional ? `^(?:${source}|/)$` : `^${source}$`), names };
|
|
110
125
|
}
|
|
111
126
|
|
|
127
|
+
// opts.staticBuild: set only by the static() build's in-process prerender worker
|
|
128
|
+
// (see PipelineConfig.staticBuild) — never by a deployed entry.
|
|
112
129
|
export function createWorker(
|
|
113
130
|
manifest: WorkerManifest,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
pattern
|
|
124
|
-
|
|
131
|
+
opts: { staticBuild?: boolean } = {},
|
|
132
|
+
): { fetch(request: Request, env?: unknown, ctx?: WorkerExecutionContext): Promise<Response> } {
|
|
133
|
+
// Pages and resource routes in ONE table, ordered the way `june dev` resolves
|
|
134
|
+
// (#312): app/ before .june/routes/, then segment shape (static > [param] >
|
|
135
|
+
// [...catchAll], required before optional), then a page before a resource
|
|
136
|
+
// route at the same pattern. The kind of route never outranks the shape.
|
|
137
|
+
const generated = new Set(manifest.generatedRoutes ?? []);
|
|
138
|
+
const row = (pattern: string) => ({ ...compilePattern(pattern), pattern, generated: generated.has(pattern) });
|
|
139
|
+
const table: Compiled[] = [
|
|
140
|
+
...Object.entries(manifest.routes).map(([pattern, def]) => ({ ...row(pattern), def })),
|
|
141
|
+
...(manifest.dynamicRoutes ?? []).map((d) => ({ ...row(d.pattern), def: d.def })),
|
|
142
|
+
...(manifest.resourceRoutes ?? []).map((r) => ({ ...row(r.pattern), handler: r.handler })),
|
|
143
|
+
];
|
|
144
|
+
table.sort(
|
|
145
|
+
(a, b) =>
|
|
146
|
+
Number(a.generated) - Number(b.generated) ||
|
|
147
|
+
compareRoutePatterns(a.pattern, b.pattern) ||
|
|
148
|
+
Number(!!a.handler) - Number(!!b.handler),
|
|
149
|
+
);
|
|
125
150
|
const routeList = [
|
|
126
151
|
...Object.keys(manifest.routes),
|
|
127
152
|
...(manifest.dynamicRoutes ?? []).map((d) => d.pattern),
|
|
@@ -131,11 +156,28 @@ export function createWorker(
|
|
|
131
156
|
const loadingFor = (key: string): LoadingComponent | undefined => manifest.loadings?.[key];
|
|
132
157
|
const boundaryFor = (key: string): { index: number; key: string } | null =>
|
|
133
158
|
manifest.layoutBoundaries?.[key] ?? null;
|
|
159
|
+
const resolvedPage = (def: BrandedRoute, key: string, params: Record<string, string>): Resolved => {
|
|
160
|
+
const b = boundaryFor(key);
|
|
161
|
+
return {
|
|
162
|
+
def,
|
|
163
|
+
params,
|
|
164
|
+
chain: chainFor(key),
|
|
165
|
+
boundaryIndex: b?.index ?? null,
|
|
166
|
+
boundaryKey: b?.key ?? null,
|
|
167
|
+
loading: loadingFor(key),
|
|
168
|
+
};
|
|
169
|
+
};
|
|
134
170
|
|
|
135
171
|
// The worker's env (D1/KV/R2 bindings) arrives per fetch and is stable across
|
|
136
172
|
// requests in an isolate; we capture the latest and hand it to the env-aware
|
|
137
|
-
// 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.
|
|
138
179
|
let currentEnv: unknown;
|
|
180
|
+
const requestLocals = new WeakMap<Request, { env: unknown; ctx?: WorkerExecutionContext }>();
|
|
139
181
|
const provider = manifest.resources;
|
|
140
182
|
|
|
141
183
|
// App services: the build sets a raw `(env) => services` factory here; memoize it per
|
|
@@ -144,18 +186,52 @@ export function createWorker(
|
|
|
144
186
|
const servicesProvider = manifest.services;
|
|
145
187
|
let servicesCache: { v: unknown } | undefined;
|
|
146
188
|
|
|
147
|
-
// Durable agent (edge): route the chat endpoint
|
|
148
|
-
// env.AGENT. Built only when the app
|
|
149
|
-
// runtime is enabled; the surface is
|
|
150
|
-
// is present, so a worker without
|
|
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.
|
|
151
194
|
const agentSurface =
|
|
152
195
|
manifest.agent.runtime.enabled && manifest.agentName
|
|
153
196
|
? (() => {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
};
|
|
159
235
|
})()
|
|
160
236
|
: undefined;
|
|
161
237
|
|
|
@@ -165,6 +241,7 @@ export function createWorker(
|
|
|
165
241
|
agentSurface,
|
|
166
242
|
i18n: manifest.i18n,
|
|
167
243
|
routeList: () => routeList,
|
|
244
|
+
staticBuild: opts.staticBuild,
|
|
168
245
|
earlyHints: manifest.earlyHints,
|
|
169
246
|
htmlCacheControl: manifest.htmlCacheControl,
|
|
170
247
|
notFoundComponent: manifest.notFound,
|
|
@@ -172,60 +249,30 @@ export function createWorker(
|
|
|
172
249
|
resources: provider ? () => provider(currentEnv) : undefined,
|
|
173
250
|
services: servicesProvider ? () => (servicesCache ??= { v: servicesProvider(currentEnv) }).v : undefined,
|
|
174
251
|
resolve: async (pathname): Promise<Resolved | ResolvedResource | null> => {
|
|
252
|
+
// Fast path: an app/ static page outranks every other route for its path.
|
|
175
253
|
const staticDef = manifest.routes[pathname];
|
|
176
|
-
if (staticDef) {
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
def: staticDef,
|
|
180
|
-
params: {},
|
|
181
|
-
chain: chainFor(pathname),
|
|
182
|
-
boundaryIndex: b?.index ?? null,
|
|
183
|
-
boundaryKey: b?.key ?? null,
|
|
184
|
-
loading: loadingFor(pathname),
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
for (const d of dynamic) {
|
|
188
|
-
const m = pathname.match(d.regex);
|
|
189
|
-
if (m) {
|
|
190
|
-
// Optional segments leave their capture undefined — the param is
|
|
191
|
-
// then absent, matching the dev matcher's semantics.
|
|
192
|
-
const params = Object.fromEntries(
|
|
193
|
-
d.names.flatMap((n, i) => {
|
|
194
|
-
const v = m[i + 1];
|
|
195
|
-
return v === undefined ? [] : [[n, decodeURIComponent(v)]];
|
|
196
|
-
}),
|
|
197
|
-
);
|
|
198
|
-
const b = boundaryFor(d.pattern);
|
|
199
|
-
return {
|
|
200
|
-
def: d.def,
|
|
201
|
-
params,
|
|
202
|
-
chain: chainFor(d.pattern),
|
|
203
|
-
boundaryIndex: b?.index ?? null,
|
|
204
|
-
boundaryKey: b?.key ?? null,
|
|
205
|
-
loading: loadingFor(d.pattern),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
// Resource routes after pages (so a page is never shadowed by one).
|
|
210
|
-
for (const r of resources) {
|
|
254
|
+
if (staticDef && !generated.has(pathname)) return resolvedPage(staticDef, pathname, {});
|
|
255
|
+
for (const r of table) {
|
|
211
256
|
const m = pathname.match(r.regex);
|
|
212
|
-
if (m)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
257
|
+
if (!m) continue;
|
|
258
|
+
// Optional segments leave their capture undefined — the param is
|
|
259
|
+
// then absent, matching the dev matcher's semantics.
|
|
260
|
+
const params = Object.fromEntries(
|
|
261
|
+
r.names.flatMap((n, i) => {
|
|
262
|
+
const v = m[i + 1];
|
|
263
|
+
return v === undefined ? [] : [[n, decodeURIComponent(v)]];
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
return r.handler ? { handler: r.handler, params } : resolvedPage(r.def, r.pattern, params);
|
|
221
267
|
}
|
|
222
268
|
return null;
|
|
223
269
|
},
|
|
224
270
|
});
|
|
225
271
|
|
|
226
272
|
return {
|
|
227
|
-
fetch(request, env) {
|
|
228
|
-
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)
|
|
229
276
|
return pipeline.fetch(request);
|
|
230
277
|
},
|
|
231
278
|
};
|
|
@@ -234,43 +281,71 @@ export function createWorker(
|
|
|
234
281
|
// --- the deployed worker's outermost layer (run_worker_first) ---------------
|
|
235
282
|
// Prerendered pages are served as static ASSETS, which bypass the pipeline —
|
|
236
283
|
// so the agent-ready signals the asset layer can't produce get added HERE:
|
|
237
|
-
// 1.
|
|
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).
|
|
238
287
|
// 2. a `Link` header on every HTML response (RFC 8288/9727 discovery).
|
|
239
288
|
// 3. `x-markdown-tokens` on markdown responses.
|
|
240
289
|
// Static assets are served by the ASSETS binding (no re-render); only genuinely
|
|
241
290
|
// dynamic routes reach the pipeline. With no ASSETS binding (no prerender), this
|
|
242
291
|
// is a transparent pass-through to the pipeline.
|
|
243
292
|
type AssetEnv = { ASSETS?: { fetch(request: Request): Promise<Response> } };
|
|
244
|
-
type FetchPipeline = { fetch(request: Request, env?: unknown): Promise<Response> };
|
|
293
|
+
type FetchPipeline = { fetch(request: Request, env?: unknown, ctx?: WorkerExecutionContext): Promise<Response> };
|
|
245
294
|
|
|
246
295
|
const estimateTokens = (s: string) => String(Math.ceil(s.length / 4));
|
|
247
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
|
+
|
|
248
306
|
export function withAssets(
|
|
249
307
|
pipeline: FetchPipeline,
|
|
250
308
|
opts: { link?: string | null } = {},
|
|
251
|
-
): { fetch(request: Request, env?: AssetEnv): Promise<Response> } {
|
|
309
|
+
): { fetch(request: Request, env?: AssetEnv, ctx?: WorkerExecutionContext): Promise<Response> } {
|
|
252
310
|
return {
|
|
253
|
-
async fetch(request, env) {
|
|
311
|
+
async fetch(request, env, ctx) {
|
|
254
312
|
const assets = env?.ASSETS;
|
|
255
313
|
const url = new URL(request.url);
|
|
256
314
|
const isPagePath = !/\.[a-z0-9]+$/i.test(url.pathname); // no file extension
|
|
257
|
-
|
|
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;
|
|
258
319
|
|
|
259
|
-
// 1.
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
}
|
|
272
342
|
}
|
|
273
|
-
//
|
|
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);
|
|
274
349
|
}
|
|
275
350
|
|
|
276
351
|
// 2. Static assets (prerendered HTML/.md/.json, /client.js, hashed CSS)
|
|
@@ -287,10 +362,15 @@ export function withAssets(
|
|
|
287
362
|
// URL changes when the bytes do, so the browser may cache forever and
|
|
288
363
|
// never revalidate (no 304 round-trip, no stale window).
|
|
289
364
|
const immutable = /\.[a-f0-9]{8,}\.(css|js)$/.test(url.pathname);
|
|
290
|
-
|
|
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) {
|
|
291
370
|
const headers = new Headers(a.headers);
|
|
292
371
|
if (addLink) headers.set("link", opts.link!);
|
|
293
372
|
if (immutable) headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
373
|
+
if (negotiated) varyAccept(headers);
|
|
294
374
|
return new Response(a.body, { status: a.status, headers });
|
|
295
375
|
}
|
|
296
376
|
return a;
|
|
@@ -299,7 +379,7 @@ export function withAssets(
|
|
|
299
379
|
|
|
300
380
|
// 3. Dynamic routes → the render pipeline (already sets Link + negotiates).
|
|
301
381
|
// Env flows through so the pipeline's resources get the D1/KV/R2 bindings.
|
|
302
|
-
return pipeline.fetch(request, env);
|
|
382
|
+
return pipeline.fetch(request, env, ctx);
|
|
303
383
|
},
|
|
304
384
|
};
|
|
305
385
|
}
|