@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.
Files changed (130) hide show
  1. package/assets/icon-latin.LICENSE.txt +92 -0
  2. package/assets/icon-latin.ttf +0 -0
  3. package/dist/adapter-DcZ-W1-7.d.ts +62 -0
  4. package/dist/adapter.d.ts +2 -56
  5. package/dist/adapter.js +62 -5
  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/blob.d.ts +4 -5
  17. package/dist/build.d.ts +8 -29
  18. package/dist/build.js +138 -289
  19. package/dist/client-bundle.d.ts +8 -9
  20. package/dist/client-bundle.js +1 -1
  21. package/dist/config-freeze-CyXvNqi2.d.ts +17 -0
  22. package/dist/config-freeze.d.ts +2 -0
  23. package/dist/config-freeze.js +54 -0
  24. package/dist/config-loader.d.ts +3 -4
  25. package/dist/connection-auth.d.ts +38 -0
  26. package/dist/connection-auth.js +31 -0
  27. package/dist/content-freeze-CyiW2fRK.d.ts +4 -0
  28. package/dist/content-freeze.d.ts +2 -0
  29. package/dist/content-freeze.js +83 -0
  30. package/dist/content.d.ts +22 -9
  31. package/dist/content.js +43 -3
  32. package/dist/core-version.d.ts +4 -0
  33. package/dist/core-version.js +12 -0
  34. package/dist/css-modules-ambient.d.ts +2 -1
  35. package/dist/css-modules.d.ts +10 -11
  36. package/dist/css.d.ts +9 -10
  37. package/dist/d1-migrate.d.ts +6 -7
  38. package/dist/d1-migrate.js +3 -2
  39. package/dist/db.d.ts +2 -2
  40. package/dist/deploy.d.ts +3 -4
  41. package/dist/deploy.js +3 -2
  42. package/dist/dev-reload.d.ts +3 -4
  43. package/dist/dev.d.ts +8 -5
  44. package/dist/dev.js +40 -9
  45. package/dist/favicon.d.ts +37 -0
  46. package/dist/favicon.js +248 -0
  47. package/dist/host.d.ts +6 -5
  48. package/dist/host.js +2 -1
  49. package/dist/icon-letter.d.ts +3 -0
  50. package/dist/icon-letter.js +8 -0
  51. package/dist/index.d.ts +11 -5
  52. package/dist/index.js +9 -3
  53. package/dist/instrumentation.d.ts +2 -3
  54. package/dist/instrumentation.js +2 -1
  55. package/dist/island-registry.d.ts +6 -7
  56. package/dist/isolate-local.d.ts +5 -0
  57. package/dist/isolate-local.js +12 -0
  58. package/dist/kv.d.ts +3 -4
  59. package/dist/llms-links.d.ts +9 -0
  60. package/dist/llms-links.js +65 -0
  61. package/dist/manifest-CwTT1zVs.d.ts +11 -0
  62. package/dist/manifest.d.ts +2 -0
  63. package/dist/manifest.js +113 -0
  64. package/dist/negotiate.d.ts +4 -3
  65. package/dist/negotiate.js +24 -14
  66. package/dist/{pipeline-CCp9idQ4.d.ts → pipeline-BHUjBO0G.d.ts} +13 -1
  67. package/dist/pipeline.d.ts +2 -2
  68. package/dist/pipeline.js +286 -35
  69. package/dist/resources.d.ts +5 -6
  70. package/dist/route-rank.d.ts +10 -0
  71. package/dist/route-rank.js +38 -0
  72. package/dist/route-scan-BMbBzPIB.d.ts +20 -0
  73. package/dist/route-scan.d.ts +2 -0
  74. package/dist/route-scan.js +100 -0
  75. package/dist/router.d.ts +12 -13
  76. package/dist/router.js +58 -44
  77. package/dist/rsc-build.d.ts +6 -7
  78. package/dist/rsc-bundle.d.ts +6 -7
  79. package/dist/rsc-manifest.d.ts +5 -6
  80. package/dist/rsc-runtime/flight-render-routes.d.ts +3 -4
  81. package/dist/rsc-runtime/flight-render.d.ts +2 -3
  82. package/dist/rsc-runtime/flight-to-html.d.ts +2 -3
  83. package/dist/rsc-runtime/flight-to-html.js +3 -2
  84. package/dist/rsc-runtime/rsc-worker.js +11 -8
  85. package/dist/rsc-runtime/rsdw-shims.d.ts +2 -1
  86. package/dist/segment.d.ts +4 -5
  87. package/dist/sqlite-driver.d.ts +11 -11
  88. package/dist/sqlite-driver.js +17 -4
  89. package/dist/static-files.d.ts +4 -5
  90. package/dist/tsconfig-jsx.d.ts +4 -4
  91. package/dist/tsconfig-jsx.js +1 -1
  92. package/dist/turn-events.d.ts +4 -0
  93. package/dist/turn-events.js +46 -0
  94. package/dist/web-manifest.d.ts +5 -0
  95. package/dist/web-manifest.js +42 -0
  96. package/dist/worker-2qbQjj2c.d.ts +64 -0
  97. package/dist/worker.d.ts +2 -56
  98. package/dist/worker.js +121 -103
  99. package/package.json +22 -9
  100. package/src/adapter.ts +84 -3
  101. package/src/agent-compile.ts +271 -0
  102. package/src/agent-discover.ts +77 -63
  103. package/src/agent-durable.ts +998 -30
  104. package/src/agent-native.ts +370 -35
  105. package/src/app.ts +75 -6
  106. package/src/build.ts +237 -444
  107. package/src/config-freeze.ts +109 -0
  108. package/src/connection-auth.ts +101 -0
  109. package/src/content-freeze.ts +151 -0
  110. package/src/content.ts +55 -4
  111. package/src/core-version.ts +34 -0
  112. package/src/dev.ts +41 -8
  113. package/src/favicon.ts +325 -0
  114. package/src/host.ts +4 -3
  115. package/src/icon-letter.ts +14 -0
  116. package/src/index.ts +22 -0
  117. package/src/isolate-local.ts +33 -0
  118. package/src/llms-links.ts +96 -0
  119. package/src/manifest.ts +130 -0
  120. package/src/negotiate.ts +39 -15
  121. package/src/pipeline.ts +494 -44
  122. package/src/route-rank.ts +60 -0
  123. package/src/route-scan.ts +161 -0
  124. package/src/router.ts +75 -58
  125. package/src/rsc-runtime/rsc-worker.tsx +5 -4
  126. package/src/sqlite-driver.ts +36 -6
  127. package/src/tsconfig-jsx.ts +2 -2
  128. package/src/turn-events.ts +43 -0
  129. package/src/web-manifest.ts +38 -0
  130. package/src/worker.ts +197 -117
package/dist/pipeline.js CHANGED
@@ -1,11 +1,16 @@
1
+ import { iconLetter } from "./icon-letter.js";
2
+ import { collectLlmsLinks, collectSitemapPages } from "./llms-links.js";
3
+ import { webManifest } from "./web-manifest.js";
1
4
  import { SEGMENT_HEADER, TITLE_HEADER, encodeTitle, negotiate } from "./negotiate.js";
2
5
  import { ensureScope, runInScope, setRequestLocale } from "@junejs/db";
3
6
  import { LoaderDataContext } from "@junejs/core/route";
7
+ import { siteShortName } from "@junejs/core/config";
4
8
  import React, { Suspense, use } from "react";
5
9
  import { renderToReadableStream } from "react-dom/server";
6
- import { Document } from "@junejs/core/document";
7
- import { apiCatalog, buildLinkHeader, llmsTxt, mcpServerCard, robotsTxt, sitemapXml } from "@junejs/core/discovery";
8
- import { mcpHandler, mcpTools } from "@junejs/core/mcp";
10
+ import { Document, PRERENDER_ORIGIN, documentTitle, pageCanonical } from "@junejs/core/document";
11
+ import { AGENT_SKILLS_INDEX_PATH, AI_CATALOG_PATH, API_CATALOG_CONTENT_TYPE, ARD_PATH, MCP_SERVER_CARD_TYPE, agentSkillsIndex, aiCatalog, apiCatalog, buildLinkHeader, llmsTxt, mcpServerCard, robotsTxt, siteSkill, sitemapXml } from "@junejs/core/discovery";
12
+ import { mcpForbidden, mcpHandler, mcpServerIdentity, mcpTools, originRejection } from "@junejs/core/mcp";
13
+ import { OPENAPI_MEDIA_TYPE, apiActionId, apiForbidden, apiHandler, apiNamespaceResponse, isApiNamespace, openApiDocument } from "@junejs/core/api";
9
14
  import { LOCALE_COOKIE, localeAlternates, localeDir, matchPinnedLocale, negotiateLocale } from "@junejs/core/i18n";
10
15
  //#region src/pipeline.ts
11
16
  function readCookie(request, name) {
@@ -18,6 +23,7 @@ function readCookie(request, name) {
18
23
  }
19
24
  }
20
25
  const DefaultNotFound = ({ pathname }) => React.createElement("main", null, React.createElement("h1", null, "404 — Not found"), React.createElement("p", null, pathname));
26
+ const CORS = { "access-control-allow-origin": "*" };
21
27
  function text(body, contentType, init) {
22
28
  const headers = new Headers(init?.headers);
23
29
  headers.set("content-type", contentType);
@@ -34,14 +40,51 @@ function StreamedView({ loadPromise, def, ctx }) {
34
40
  return provideLoaderData(data, def.view ? def.view(data, ctx) : null);
35
41
  }
36
42
  function letterFavicon(siteName) {
37
- return text(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#1d1d1f"/><text x="32" y="32" dy=".36em" text-anchor="middle" font-family="ui-sans-serif, system-ui, sans-serif" font-size="34" font-weight="600" fill="#fbfbf8">${((siteName ?? "").trim().charAt(0) || "•").toUpperCase().replace(/[<>&"']/g, "") || "•"}</text></svg>\n`, "image/svg+xml", { headers: { "cache-control": "public, max-age=86400" } });
43
+ return text(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#1d1d1f"/><text x="32" y="32" dy=".36em" text-anchor="middle" font-family="ui-sans-serif, system-ui, sans-serif" font-size="34" font-weight="600" fill="#fbfbf8">${(iconLetter(siteName) || "•").replace(/[<>&"']/g, "") || "•"}</text></svg>\n`, "image/svg+xml", { headers: { "cache-control": "public, max-age=86400" } });
44
+ }
45
+ function markdownPath(pathname, isHome) {
46
+ const path = pathname.replace(/\/+$/, "");
47
+ return isHome ? `${path.replace(/\/index$/, "")}/index.md` : `${path}.md`;
48
+ }
49
+ function htmlPath(pathname, isHome) {
50
+ const path = pathname.endsWith(".md") ? pathname.slice(0, -3) : pathname;
51
+ if (!isHome) return path;
52
+ return path.replace(/\/+$/, "").replace(/\/index$/, "") || "/";
53
+ }
54
+ const SERVER_CARD_PATH = "/.well-known/mcp/server-card.json";
55
+ const SERVER_CARD_CORS = {
56
+ "access-control-allow-origin": "*",
57
+ "access-control-allow-methods": "GET, HEAD",
58
+ "access-control-allow-headers": "Content-Type, If-None-Match",
59
+ "access-control-expose-headers": "ETag"
60
+ };
61
+ function humanizeSegment(segment) {
62
+ let s = segment;
63
+ try {
64
+ s = decodeURIComponent(segment);
65
+ } catch {}
66
+ s = s.replace(/[-_]+/g, " ").trim();
67
+ return s.charAt(0).toUpperCase() + s.slice(1);
68
+ }
69
+ const routePathOf = /* @__PURE__ */ new WeakMap();
70
+ function isDecodablePath(pathname) {
71
+ try {
72
+ for (const segment of pathname.split("/"));
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
38
77
  }
39
78
  function createPipeline(cfg) {
40
79
  const { docConfig, agent } = cfg;
41
80
  const NotFound = cfg.notFoundComponent ?? DefaultNotFound;
81
+ const publishesCatalogs = agent.discovery && !docConfig.basePath;
42
82
  function htmlHeaders() {
43
- const headers = new Headers({ "content-type": "text/html; charset=utf-8" });
44
- const links = [buildLinkHeader(agent), ...cfg.earlyHints ?? []].filter(Boolean);
83
+ const headers = new Headers({
84
+ "content-type": "text/html; charset=utf-8",
85
+ vary: "accept"
86
+ });
87
+ const links = [buildLinkHeader(agent, { catalogs: publishesCatalogs }), ...cfg.earlyHints ?? []].filter(Boolean);
45
88
  if (links.length) headers.set("link", links.join(", "));
46
89
  if (cfg.htmlCacheControl) headers.set("cache-control", cfg.htmlCacheControl);
47
90
  return headers;
@@ -62,14 +105,57 @@ function createPipeline(cfg) {
62
105
  protocol: ctx.url.protocol.replace(":", "")
63
106
  });
64
107
  }
65
- async function renderDocument(node, metadata, status, chain, locale, boundaryKey, alternates) {
108
+ function pageProps(ctx, def) {
109
+ const isHome = routePathOf.get(ctx) === "/";
110
+ return {
111
+ pageUrl: ctx.url.href,
112
+ isHome,
113
+ onLocaleDomain: onLocaleDomain(ctx),
114
+ markdownHref: agent.discovery && def.md !== false ? markdownPath(ctx.url.pathname, isHome) : void 0
115
+ };
116
+ }
117
+ async function breadcrumbsFor(ctx, meta) {
118
+ const routePath = routePathOf.get(ctx);
119
+ if (!routePath || routePath === "/" || meta?.breadcrumb === false) return void 0;
120
+ const urlPath = ctx.url.pathname.replace(/\/+$/, "") || "/";
121
+ const route = routePath.replace(/\/+$/, "");
122
+ const prefix = urlPath.endsWith(route) ? urlPath.slice(0, urlPath.length - route.length) : "";
123
+ const home = {
124
+ name: siteShortName(docConfig.site) ?? documentTitle(void 0, docConfig.site),
125
+ path: prefix || "/"
126
+ };
127
+ if (Array.isArray(meta?.breadcrumb)) return [home, ...meta.breadcrumb];
128
+ const segments = route.split("/").filter(Boolean);
129
+ const trail = [home];
130
+ for (let i = 1; i < segments.length; i++) {
131
+ const ancestor = "/" + segments.slice(0, i).join("/");
132
+ const r = await resolveRoute(ancestor);
133
+ if (!r || "handler" in r || Object.keys(r.params).length > 0) continue;
134
+ const title = typeof r.def.metadata === "object" ? r.def.metadata?.title : void 0;
135
+ trail.push({
136
+ name: title || humanizeSegment(segments[i - 1]),
137
+ path: prefix + ancestor
138
+ });
139
+ }
140
+ trail.push({
141
+ name: meta?.title || humanizeSegment(segments[segments.length - 1]),
142
+ path: urlPath
143
+ });
144
+ return trail;
145
+ }
146
+ function onLocaleDomain(ctx) {
147
+ const host = ctx.url.hostname.toLowerCase();
148
+ return cfg.i18n ? Object.values(cfg.i18n.locales).some((l) => l.domain?.toLowerCase() === host) : false;
149
+ }
150
+ async function renderDocument(node, metadata, status, chain, locale, boundaryKey, alternates, page) {
66
151
  const wrapped = chain.reduceRight((acc, L) => React.createElement(L, null, acc), node);
67
152
  const stream = await renderToReadableStream(React.createElement(Document, {
68
- config: docConfigForRender(),
153
+ config: docConfigForRender(page?.pageUrl),
69
154
  metadata,
70
155
  children: wrapped,
71
156
  shellKey: boundaryKey,
72
157
  alternates,
158
+ ...page,
73
159
  ...langDir(locale)
74
160
  }));
75
161
  await stream.allReady;
@@ -80,7 +166,8 @@ function createPipeline(cfg) {
80
166
  }
81
167
  async function renderFragment(node, metadata, chain, boundaryIndex, boundaryKey) {
82
168
  const segmented = typeof boundaryIndex === "number" && boundaryKey != null;
83
- const stream = await renderToReadableStream((segmented ? chain.slice(boundaryIndex + 1) : chain).reduceRight((acc, L) => React.createElement(L, null, acc), node));
169
+ const wrapped = (segmented ? chain.slice(boundaryIndex + 1) : chain).reduceRight((acc, L) => React.createElement(L, null, acc), node);
170
+ const stream = await renderToReadableStream(wrapped);
84
171
  await stream.allReady;
85
172
  const html = await new Response(stream).text();
86
173
  const headers = new Headers({ "content-type": "text/html; charset=utf-8" });
@@ -92,14 +179,16 @@ function createPipeline(cfg) {
92
179
  headers
93
180
  });
94
181
  }
95
- function docConfigForRender() {
182
+ function docConfigForRender(pageUrl) {
96
183
  const webmcpTools = agent.webmcp && agent.mcp ? mcpTools() : null;
97
- return webmcpTools?.length ? {
184
+ const servesCatalog = !!pageUrl && catalogOrigin(new URL(pageUrl)) !== null;
185
+ return {
98
186
  ...docConfig,
99
- webmcpTools
100
- } : docConfig;
187
+ ...webmcpTools?.length ? { webmcpTools } : {},
188
+ ...servesCatalog ? { aiCatalog: AI_CATALOG_PATH } : {}
189
+ };
101
190
  }
102
- async function renderStreamingDocument(resolved, loadPromise, ctx) {
191
+ async function renderStreamingDocument(resolved, loadPromise, ctx, breadcrumbs) {
103
192
  const { def, chain, loading: Loading, boundaryKey } = resolved;
104
193
  const leaf = React.createElement(StreamedView, {
105
194
  loadPromise,
@@ -110,11 +199,13 @@ function createPipeline(cfg) {
110
199
  const wrapped = chain.reduceRight((acc, L) => React.createElement(L, null, acc), boundary);
111
200
  const metadata = typeof def.metadata === "object" ? def.metadata : void 0;
112
201
  const stream = await renderToReadableStream(React.createElement(Document, {
113
- config: docConfigForRender(),
202
+ config: docConfigForRender(ctx.url.href),
114
203
  metadata,
115
204
  children: wrapped,
116
205
  shellKey: boundaryKey,
117
206
  alternates: alternatesFor(ctx),
207
+ ...pageProps(ctx, def),
208
+ breadcrumbs,
118
209
  ...langDir(ctx.locale)
119
210
  }), { onError: (e) => console.error("[june] streaming render error:", e) });
120
211
  return new Response(stream, {
@@ -122,23 +213,74 @@ function createPipeline(cfg) {
122
213
  headers: htmlHeaders()
123
214
  });
124
215
  }
125
- function notFoundResponse(target, pathname, locale) {
126
- return target !== "view" ? Response.json({
216
+ async function resolveRoute(pathname) {
217
+ return isDecodablePath(pathname) ? cfg.resolve(pathname) : null;
218
+ }
219
+ function notFoundResponse(target, pathname, locale, unmatched) {
220
+ if (unmatched && agent.api) {
221
+ const url = new URL(unmatched.url);
222
+ if (isApiNamespace(url.pathname)) return apiNamespaceResponse(unmatched, url.origin);
223
+ }
224
+ if (target === "md") return discoveryLinks().then((links) => text(notFoundMarkdown(pathname, links), "text/markdown; charset=utf-8", {
225
+ status: 404,
226
+ headers: { vary: "accept" }
227
+ }));
228
+ if (target !== "view") return discoveryLinks().then((links) => Response.json({
127
229
  error: "Not Found",
128
- path: pathname
129
- }, { status: 404 }) : renderDocument(React.createElement(NotFound, { pathname }), {
230
+ code: "not_found",
231
+ path: pathname,
232
+ hint: notFoundHint(links)
233
+ }, {
234
+ status: 404,
235
+ headers: { vary: "accept" }
236
+ }));
237
+ return renderDocument(React.createElement(NotFound, { pathname }), {
130
238
  title: "Not found",
131
239
  robots: "noindex"
132
240
  }, 404, [], locale);
133
241
  }
242
+ async function discoveryLinks() {
243
+ const links = [];
244
+ if (agent.discovery) links.push(["llms.txt — the site index for agents", "/llms.txt"], ["sitemap.xml — every page", "/sitemap.xml"]);
245
+ if (agent.mcp) links.push(["MCP server — this site's tools", "/mcp"]);
246
+ if (agent.api) links.push(["OpenAPI — the same tools over HTTP", "/openapi.json"]);
247
+ const home = await cfg.resolve("/");
248
+ if (home && "def" in home && home.def.md !== false) links.push(["Home page (Markdown)", "/index.md"]);
249
+ return links;
250
+ }
251
+ function notFoundHint(links) {
252
+ return links.length ? `No page at this path. Start from ${links.map(([, h]) => h).join(", ")}.` : "No page at this path.";
253
+ }
254
+ function notFoundMarkdown(pathname, links) {
255
+ const site = docConfig.site.name ? ` on ${docConfig.site.name}` : "";
256
+ return [
257
+ "# 404 — Not found",
258
+ "",
259
+ links.length ? `There is no page at \`${pathname}\`${site}. Try one of these instead:` : `There is no page at \`${pathname}\`${site}.`,
260
+ "",
261
+ ...links.map(([label, href]) => `- [${label}](${href})`),
262
+ ""
263
+ ].join("\n");
264
+ }
134
265
  function resolveMeta(def, data, ctx) {
135
266
  return typeof def.metadata === "function" ? def.metadata(data, ctx) : def.metadata;
136
267
  }
137
268
  async function renderMarkdown(def, data, ctx) {
138
269
  const jsonData = typeof def.json === "function" ? await def.json(data, ctx) : def.json === false ? null : data;
139
- const body = typeof def.md === "function" ? await def.md(data, ctx) : "```json\n" + JSON.stringify(jsonData, null, 2) + "\n```\n";
270
+ const body = withFrontmatter(typeof def.md === "function" ? await def.md(data, ctx) : "```json\n" + JSON.stringify(jsonData, null, 2) + "\n```\n", resolveMeta(def, data, ctx), ctx);
140
271
  return text(body, "text/markdown; charset=utf-8", { headers: { "x-markdown-tokens": String(Math.ceil(body.length / 4)) } });
141
272
  }
273
+ function withFrontmatter(body, meta, ctx) {
274
+ if (/^---\r?\n/.test(body)) return body;
275
+ const title = meta?.title || documentTitle(meta, docConfig.site);
276
+ const description = meta?.description ?? docConfig.site.description;
277
+ const canonical = pageCanonical(docConfig, meta, ctx.url.href, htmlPath(ctx.url.pathname, routePathOf.get(ctx) === "/"), onLocaleDomain(ctx));
278
+ const lines = ["---", `title: ${JSON.stringify(title)}`];
279
+ if (description) lines.push(`description: ${JSON.stringify(description)}`);
280
+ if (canonical) lines.push(`canonical: ${JSON.stringify(canonical)}`);
281
+ lines.push("---", "");
282
+ return lines.join("\n") + body;
283
+ }
142
284
  async function renderProjection(resolved, target, data, ctx) {
143
285
  const { def, chain, boundaryIndex, boundaryKey } = resolved;
144
286
  if (target !== "fragment" && def[target] === false) return notFoundResponse(target, ctx.url.pathname, ctx.locale);
@@ -155,27 +297,129 @@ function createPipeline(cfg) {
155
297
  const node = provideLoaderData(data, def.view ? def.view(data, ctx) : null);
156
298
  const meta = resolveMeta(def, data, ctx);
157
299
  if (target === "fragment") return renderFragment(node, meta, chain, boundaryIndex, boundaryKey);
158
- return renderDocument(node, meta, 200, chain, ctx.locale, boundaryKey, alternatesFor(ctx));
300
+ const page = {
301
+ ...pageProps(ctx, def),
302
+ breadcrumbs: await breadcrumbsFor(ctx, meta)
303
+ };
304
+ return renderDocument(node, meta, 200, chain, ctx.locale, boundaryKey, alternatesFor(ctx), page);
305
+ }
306
+ function catalogOrigin(url) {
307
+ return publishesCatalogs ? publicOrigin(url) : null;
308
+ }
309
+ function publicOrigin(url) {
310
+ if (url.origin !== PRERENDER_ORIGIN) return url.origin;
311
+ return docConfig.site.url ? new URL(docConfig.site.url).origin : docConfig.deployOrigin ?? null;
312
+ }
313
+ function publicBase(url) {
314
+ const origin = publicOrigin(url);
315
+ return origin === null ? null : origin + (docConfig.basePath ?? "");
316
+ }
317
+ async function agentCatalog(url) {
318
+ const { pathname } = url;
319
+ if (!pathname.startsWith("/.well-known/")) return null;
320
+ const origin = catalogOrigin(url);
321
+ if (!origin) return null;
322
+ if (pathname === AGENT_SKILLS_INDEX_PATH) return Response.json(await agentSkillsIndex(origin, agent, docConfig.site), { headers: CORS });
323
+ if (pathname === AI_CATALOG_PATH || pathname === ARD_PATH) return Response.json(aiCatalog(origin, agent, docConfig.site), { headers: CORS });
324
+ const skill = siteSkill(origin, agent, docConfig.site);
325
+ if (pathname === new URL(skill.url).pathname) return text(skill.markdown, "text/markdown; charset=utf-8", { headers: CORS });
326
+ if (pathname.startsWith("/.well-known/agent-skills/")) return Response.json({
327
+ error: "Not Found",
328
+ path: pathname
329
+ }, {
330
+ status: 404,
331
+ headers: CORS
332
+ });
333
+ return null;
159
334
  }
160
335
  async function discovery(url) {
336
+ const catalog = await agentCatalog(url);
337
+ if (catalog) return catalog;
161
338
  switch (url.pathname) {
162
- case "/llms.txt": return text(llmsTxt(url.origin, await cfg.routeList(), agent, docConfig.site), "text/markdown; charset=utf-8");
163
- case "/robots.txt": return text(robotsTxt(url.origin), "text/plain; charset=utf-8");
164
- case "/sitemap.xml": return text(sitemapXml(url.origin, await cfg.routeList(), cfg.i18n), "application/xml; charset=utf-8");
165
- case "/.well-known/api-catalog": return text(JSON.stringify(apiCatalog(url.origin, agent)), "application/linkset+json");
166
- case "/.well-known/mcp/server-card.json": return agent.mcp ? Response.json(mcpServerCard(url.origin)) : null;
339
+ case "/llms.txt": {
340
+ const routes = await cfg.routeList();
341
+ const base = publicBase(url) ?? docConfig.basePath ?? "";
342
+ const links = await collectLlmsLinks(base, routes, cfg.resolve);
343
+ return text(llmsTxt(base, routes, agent, docConfig.site, links), "text/markdown; charset=utf-8");
344
+ }
345
+ case "/robots.txt": {
346
+ const base = publicBase(url);
347
+ if (base === null) return text("Not Found\n", "text/plain; charset=utf-8", { status: 404 });
348
+ return text(robotsTxt(base, { catalogs: catalogOrigin(url) !== null }), "text/plain; charset=utf-8");
349
+ }
350
+ case "/sitemap.xml": {
351
+ const base = publicBase(url);
352
+ if (base === null) return text("Not Found\n", "text/plain; charset=utf-8", { status: 404 });
353
+ return text(sitemapXml(base, await collectSitemapPages(await cfg.routeList(), cfg.resolve, {
354
+ i18n: !!cfg.i18n,
355
+ staticPaths: cfg.staticBuild === true
356
+ }), cfg.i18n), "application/xml; charset=utf-8");
357
+ }
358
+ case "/.well-known/api-catalog": return text(JSON.stringify(apiCatalog(url.origin, agent)), API_CATALOG_CONTENT_TYPE, { headers: { link: `</.well-known/api-catalog>; rel="api-catalog"` } });
359
+ case SERVER_CARD_PATH: return agent.mcp ? text(JSON.stringify(mcpServerCard(url.origin, {
360
+ site: docConfig.site,
361
+ agent,
362
+ icons: docConfig.icons,
363
+ basePath: docConfig.basePath
364
+ })), MCP_SERVER_CARD_TYPE, { headers: {
365
+ ...SERVER_CARD_CORS,
366
+ "cache-control": "public, max-age=3600"
367
+ } }) : null;
167
368
  default: return null;
168
369
  }
169
370
  }
371
+ const originPolicy = {
372
+ allowedOrigins: agent.allowedOrigins,
373
+ allowedHosts: agent.allowedHosts
374
+ };
375
+ const callerIdentity = async (request) => request.method === "POST" && cfg.identity ? await cfg.identity(request) : void 0;
170
376
  async function handleRequest(request) {
171
377
  const url = new URL(request.url);
172
378
  if (url.pathname === "/mcp") {
173
379
  if (!agent.mcp) return notFoundResponse("view", url.pathname);
174
- return mcpHandler(request, { request });
380
+ const refused = originRejection(request, originPolicy);
381
+ if (refused !== void 0) return mcpForbidden(refused);
382
+ const identity = await callerIdentity(request);
383
+ return mcpHandler(request, {
384
+ request,
385
+ ...identity
386
+ }, mcpServerIdentity(url.origin, {
387
+ site: docConfig.site,
388
+ agent
389
+ }), originPolicy);
175
390
  }
176
- if (request.method === "GET" && agent.discovery) {
391
+ if (agent.api) {
392
+ if (url.pathname === "/openapi.json" && (request.method === "GET" || request.method === "HEAD")) {
393
+ const doc = Response.json(openApiDocument(url.origin, docConfig.site), { headers: {
394
+ "content-type": OPENAPI_MEDIA_TYPE,
395
+ "access-control-allow-origin": "*"
396
+ } });
397
+ return request.method === "HEAD" ? new Response(null, {
398
+ status: doc.status,
399
+ headers: doc.headers
400
+ }) : doc;
401
+ }
402
+ const actionId = apiActionId(url.pathname);
403
+ if (actionId) {
404
+ const refused = originRejection(request, originPolicy);
405
+ if (refused !== void 0) return apiForbidden(request, refused);
406
+ const identity = await callerIdentity(request);
407
+ return apiHandler(request, actionId, {
408
+ request,
409
+ ...identity
410
+ }, originPolicy);
411
+ }
412
+ }
413
+ if (request.method === "OPTIONS" && agent.discovery && agent.mcp && url.pathname === SERVER_CARD_PATH) return new Response(null, {
414
+ status: 204,
415
+ headers: SERVER_CARD_CORS
416
+ });
417
+ if ((request.method === "GET" || request.method === "HEAD") && agent.discovery) {
177
418
  const d = await discovery(url);
178
- if (d) return d;
419
+ if (d) return request.method === "HEAD" ? new Response(null, {
420
+ status: d.status,
421
+ headers: d.headers
422
+ }) : d;
179
423
  }
180
424
  if (agent.runtime.enabled && cfg.agentSurface) {
181
425
  const a = await cfg.agentSurface(request, url);
@@ -186,6 +430,7 @@ function createPipeline(cfg) {
186
430
  if (out) return out;
187
431
  }
188
432
  if (request.method === "GET" && !docConfig.site.icon && (url.pathname === "/favicon.svg" || url.pathname === "/favicon.ico")) return letterFavicon(docConfig.site.name);
433
+ if (request.method === "GET" && docConfig.icons?.generated && url.pathname === "/manifest.webmanifest") return text(webManifest(docConfig), "application/manifest+json", { headers: { "cache-control": "public, max-age=86400" } });
189
434
  let locale;
190
435
  let routeBase = url.pathname;
191
436
  if (cfg.i18n) {
@@ -206,8 +451,8 @@ function createPipeline(cfg) {
206
451
  if (locale) setRequestLocale(locale);
207
452
  }
208
453
  const { target, pathname, speculative } = negotiate(url, request, routeBase);
209
- const resolved = await cfg.resolve(pathname);
210
- if (!resolved) return notFoundResponse(target, pathname, locale);
454
+ const resolved = await resolveRoute(pathname);
455
+ if (!resolved) return notFoundResponse(target, pathname, locale, request);
211
456
  const ctx = {
212
457
  request,
213
458
  url,
@@ -216,8 +461,12 @@ function createPipeline(cfg) {
216
461
  locale,
217
462
  speculative
218
463
  };
464
+ routePathOf.set(ctx, pathname);
219
465
  if ("handler" in resolved) return resolved.handler(request, ctx);
220
- if (target === "view" && resolved.loading && typeof resolved.def.metadata !== "function") return renderStreamingDocument(resolved, Promise.resolve(resolved.def.load ? resolved.def.load(ctx) : void 0), ctx);
466
+ if (target === "view" && resolved.loading && typeof resolved.def.metadata !== "function") {
467
+ const breadcrumbs = await breadcrumbsFor(ctx, resolved.def.metadata);
468
+ return renderStreamingDocument(resolved, Promise.resolve(resolved.def.load ? resolved.def.load(ctx) : void 0), ctx, breadcrumbs);
469
+ }
221
470
  let data;
222
471
  try {
223
472
  data = resolved.def.load ? await resolved.def.load(ctx) : void 0;
@@ -228,11 +477,13 @@ function createPipeline(cfg) {
228
477
  }
229
478
  return { async fetch(request) {
230
479
  await ensureScope();
480
+ const resources = cfg.resources ? await cfg.resources() : {};
481
+ const services = cfg.services ? await cfg.services() : void 0;
231
482
  return runInScope({
232
- resources: cfg.resources ? await cfg.resources() : {},
233
- services: cfg.services ? await cfg.services() : void 0
483
+ resources,
484
+ services
234
485
  }, () => handleRequest(request));
235
486
  } };
236
487
  }
237
488
  //#endregion
238
- export { createPipeline };
489
+ export { createPipeline, htmlPath, isDecodablePath, markdownPath };
@@ -1,15 +1,14 @@
1
1
  import { t as D1Database } from "./d1-BkdUg9kg.js";
2
2
  import { ResourceConfig, Resources } from "@junejs/core/resources";
3
3
  //#region src/resources.d.ts
4
- declare function memoizeResources(config?: ResourceConfig): () => Promise<Resources>;
5
- type WorkerEnv = {
4
+ export declare function memoizeResources(config?: ResourceConfig): () => Promise<Resources>;
5
+ export type WorkerEnv = {
6
6
  DB?: D1Database;
7
7
  } & Record<string, unknown>;
8
- type ResourceFlags = {
8
+ export type ResourceFlags = {
9
9
  db?: boolean;
10
10
  kv?: boolean;
11
11
  blob?: boolean;
12
12
  };
13
- declare function bindWorkerResources(flags: ResourceFlags): (env?: unknown) => Promise<Resources>;
14
- //#endregion
15
- export { ResourceFlags, WorkerEnv, bindWorkerResources, memoizeResources };
13
+ export declare function bindWorkerResources(flags: ResourceFlags): (env?: unknown) => Promise<Resources>;
14
+ //#endregion
@@ -0,0 +1,10 @@
1
+ //#region src/route-rank.d.ts
2
+ export type SegmentKind = "static" | "param" | "optional" | "catchAll" | "optionalCatchAll";
3
+ export declare function parseSegment(segment: string): {
4
+ kind: SegmentKind;
5
+ name?: string;
6
+ };
7
+ export declare function segmentRank(segment: string): number;
8
+ export declare function compareSegments(a: string, b: string): number;
9
+ export declare function compareRoutePatterns(a: string, b: string): number;
10
+ //#endregion
@@ -0,0 +1,38 @@
1
+ //#region src/route-rank.ts
2
+ const DYNAMIC = /^\[(\[)?(\.\.\.)?([A-Za-z_][A-Za-z0-9_]*)\](\])?$/;
3
+ function parseSegment(segment) {
4
+ const m = segment.match(DYNAMIC);
5
+ if (!m || !!m[1] !== !!m[4]) return { kind: "static" };
6
+ const optional = !!m[1];
7
+ return {
8
+ kind: !!m[2] ? optional ? "optionalCatchAll" : "catchAll" : optional ? "optional" : "param",
9
+ name: m[3]
10
+ };
11
+ }
12
+ const RANK = {
13
+ static: 0,
14
+ param: 1,
15
+ optional: 2,
16
+ catchAll: 3,
17
+ optionalCatchAll: 4
18
+ };
19
+ function segmentRank(segment) {
20
+ return RANK[parseSegment(segment).kind];
21
+ }
22
+ function compareSegments(a, b) {
23
+ return segmentRank(a) - segmentRank(b) || (a < b ? -1 : a > b ? 1 : 0);
24
+ }
25
+ function compareRoutePatterns(a, b) {
26
+ const as = a.split("/").filter(Boolean);
27
+ const bs = b.split("/").filter(Boolean);
28
+ for (let i = 0; i < Math.max(as.length, bs.length); i++) {
29
+ const x = as[i];
30
+ const y = bs[i];
31
+ if (x === void 0) return -1;
32
+ if (y === void 0) return 1;
33
+ if (x !== y) return compareSegments(x, y);
34
+ }
35
+ return 0;
36
+ }
37
+ //#endregion
38
+ export { compareRoutePatterns, compareSegments, parseSegment, segmentRank };
@@ -0,0 +1,20 @@
1
+ //#region src/route-scan.d.ts
2
+ type RouteEntry = {
3
+ path: string;
4
+ file: string;
5
+ dynamic: boolean;
6
+ resource?: boolean;
7
+ generated?: boolean;
8
+ layouts: string[];
9
+ loading?: string;
10
+ };
11
+ declare function scanRoutes(appDir: string, dir?: string, layouts?: string[], out?: RouteEntry[], loading?: string): Promise<RouteEntry[]>;
12
+ declare function scanAppRoutes(appRoot: string): Promise<RouteEntry[]>;
13
+ declare function routeConflicts(routes: RouteEntry[]): Array<{
14
+ path: string;
15
+ files: string[];
16
+ }>;
17
+ declare function misplacedSegments(routes: RouteEntry[]): RouteEntry[];
18
+ declare function routeProblems(trees: RouteEntry[][], appRoot: string): string | null;
19
+ //#endregion
20
+ export { scanAppRoutes as a, routeProblems as i, misplacedSegments as n, scanRoutes as o, routeConflicts as r, RouteEntry as t };
@@ -0,0 +1,2 @@
1
+ import { a as scanAppRoutes, i as routeProblems, n as misplacedSegments, o as scanRoutes, r as routeConflicts, t as RouteEntry } from "./route-scan-BMbBzPIB.js";
2
+ export { RouteEntry, misplacedSegments, routeConflicts, routeProblems, scanAppRoutes, scanRoutes };
@@ -0,0 +1,100 @@
1
+ import { parseSegment } from "./route-rank.js";
2
+ import { existsSync } from "node:fs";
3
+ import { readdir } from "node:fs/promises";
4
+ import { basename, join, relative, sep } from "node:path";
5
+ //#region src/route-scan.ts
6
+ const PAGE_BASENAMES = /* @__PURE__ */ new Set(["page", "index"]);
7
+ const ROUTE_EXTS = [
8
+ ".tsx",
9
+ ".jsx",
10
+ ".ts",
11
+ ".js"
12
+ ];
13
+ const isRouteGroup = (name) => /^\(.+\)$/.test(name);
14
+ function segmentFile(dir, base) {
15
+ return ROUTE_EXTS.map((e) => join(dir, `${base}${e}`)).find(existsSync);
16
+ }
17
+ async function scanRoutes(appDir, dir = appDir, layouts = [], out = [], loading) {
18
+ const ownLayout = segmentFile(dir, "layout");
19
+ const chain = ownLayout ? [...layouts, ownLayout] : layouts;
20
+ const nearestLoading = segmentFile(dir, "loading") ?? loading;
21
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
22
+ for (const e of entries) {
23
+ if (e.name.startsWith("_") || e.name.startsWith(".")) continue;
24
+ const full = join(dir, e.name);
25
+ if (e.isDirectory()) {
26
+ await scanRoutes(appDir, full, chain, out, nearestLoading);
27
+ continue;
28
+ }
29
+ const ext = e.name.match(/\.[^.]+$/)?.[0] ?? "";
30
+ if (!ROUTE_EXTS.includes(ext)) continue;
31
+ const base = basename(e.name, ext);
32
+ const resource = base === "route";
33
+ if (!PAGE_BASENAMES.has(base) && !resource) continue;
34
+ const relDir = relative(appDir, dir);
35
+ const path = "/" + (relDir === "" ? [] : relDir.split(sep).filter((s) => !isRouteGroup(s))).join("/");
36
+ out.push({
37
+ path: path === "/" ? "/" : path,
38
+ file: full,
39
+ dynamic: /\[.+\]/.test(path),
40
+ resource,
41
+ layouts: chain,
42
+ loading: nearestLoading
43
+ });
44
+ }
45
+ return out;
46
+ }
47
+ async function scanAppRoutes(appRoot) {
48
+ const appDir = join(appRoot, "app");
49
+ const juneRoutesDir = join(appRoot, ".june", "routes");
50
+ const appRoutes = await scanRoutes(appDir);
51
+ const frameworkRoutes = existsSync(juneRoutesDir) ? await scanRoutes(juneRoutesDir) : [];
52
+ const problems = routeProblems([appRoutes, frameworkRoutes], appRoot);
53
+ if (problems) throw new Error(problems);
54
+ const appPaths = new Set(appRoutes.map((r) => r.path));
55
+ const generated = frameworkRoutes.filter((r) => !appPaths.has(r.path)).map((r) => ({
56
+ ...r,
57
+ generated: true
58
+ }));
59
+ return [...appRoutes, ...generated].sort((a, b) => a.path.localeCompare(b.path));
60
+ }
61
+ function routeConflicts(routes) {
62
+ const byKey = /* @__PURE__ */ new Map();
63
+ for (const r of routes) {
64
+ const key = `${r.resource ? "route" : "page"} ${r.path}`;
65
+ const hit = byKey.get(key);
66
+ if (hit) hit.files.push(r.file);
67
+ else byKey.set(key, {
68
+ path: r.path,
69
+ files: [r.file]
70
+ });
71
+ }
72
+ return [...byKey.values()].filter((c) => c.files.length > 1).map((c) => ({
73
+ path: c.path,
74
+ files: c.files.sort()
75
+ })).sort((a, b) => a.path.localeCompare(b.path));
76
+ }
77
+ const ENDS_PATH = /* @__PURE__ */ new Set([
78
+ "optional",
79
+ "catchAll",
80
+ "optionalCatchAll"
81
+ ]);
82
+ const beforeLast = (path) => path.split("/").filter(Boolean).slice(0, -1).map((s) => parseSegment(s).kind);
83
+ function misplacedSegments(routes) {
84
+ return routes.filter((r) => beforeLast(r.path).some((k) => ENDS_PATH.has(k))).sort((a, b) => a.path.localeCompare(b.path) || a.file.localeCompare(b.file));
85
+ }
86
+ function routeProblems(trees, appRoot) {
87
+ const rel = (f) => relative(appRoot, f).split(sep).join("/");
88
+ const sections = [];
89
+ const conflicts = trees.flatMap(routeConflicts);
90
+ if (conflicts.length) sections.push("[june] more than one route file resolves to the same path — keep one per path:\n" + conflicts.map((c) => ` ${c.path}: ${c.files.map(rel).join(", ")}`).join("\n"));
91
+ const misplaced = trees.flatMap(misplacedSegments);
92
+ if (misplaced.length) {
93
+ const lines = misplaced.map((r) => ` ${r.path}: ${rel(r.file)}`);
94
+ if (misplaced.some((r) => beforeLast(r.path).includes("optional"))) lines.push(" Unlike SvelteKit, June does not skip a [[param]] mid-path. For a locale prefix, set i18n.locales in june.config.ts instead of a [[lang]] directory.");
95
+ sections.push("[june] an optional or catch-all segment must be the last segment of a route path — nothing may follow it:\n" + lines.join("\n"));
96
+ }
97
+ return sections.length ? sections.join("\n") : null;
98
+ }
99
+ //#endregion
100
+ export { misplacedSegments, routeConflicts, routeProblems, scanAppRoutes, scanRoutes };