@junejs/server 1.0.0-dev.29 → 1.0.0-dev.42

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 (77) hide show
  1. package/dist/agent-compile.d.ts +8 -9
  2. package/dist/agent-discover.d.ts +3 -4
  3. package/dist/agent-durable.d.ts +38 -20
  4. package/dist/agent-durable.js +159 -13
  5. package/dist/agent-native.d.ts +15 -12
  6. package/dist/agent-native.js +72 -8
  7. package/dist/app.d.ts +4 -5
  8. package/dist/app.js +2 -1
  9. package/dist/blob.d.ts +4 -5
  10. package/dist/build.d.ts +6 -6
  11. package/dist/build.js +38 -7
  12. package/dist/client-bundle.d.ts +8 -9
  13. package/dist/{config-freeze-DwI-Rmvq.d.ts → config-freeze-ZWC_Qd4h.d.ts} +1 -1
  14. package/dist/config-freeze.d.ts +1 -1
  15. package/dist/config-loader.d.ts +3 -4
  16. package/dist/connection-auth.d.ts +8 -9
  17. package/dist/content.d.ts +9 -10
  18. package/dist/core-version.d.ts +3 -4
  19. package/dist/core-version.js +1 -1
  20. package/dist/css-modules-ambient.d.ts +2 -1
  21. package/dist/css-modules.d.ts +10 -11
  22. package/dist/css.d.ts +9 -10
  23. package/dist/d1-migrate.d.ts +6 -7
  24. package/dist/d1-migrate.js +3 -2
  25. package/dist/db.d.ts +2 -2
  26. package/dist/deploy.d.ts +3 -4
  27. package/dist/deploy.js +3 -2
  28. package/dist/dev-reload.d.ts +3 -4
  29. package/dist/dev.d.ts +4 -5
  30. package/dist/favicon.d.ts +16 -17
  31. package/dist/host.d.ts +5 -5
  32. package/dist/icon-letter.d.ts +2 -3
  33. package/dist/index.d.ts +4 -4
  34. package/dist/instrumentation.d.ts +2 -3
  35. package/dist/instrumentation.js +2 -1
  36. package/dist/island-registry.d.ts +6 -7
  37. package/dist/isolate-local.d.ts +4 -5
  38. package/dist/kv.d.ts +3 -4
  39. package/dist/llms-links.d.ts +8 -5
  40. package/dist/llms-links.js +31 -1
  41. package/dist/{manifest-gcNWqWoI.d.ts → manifest-DSNX4I-G.d.ts} +2 -2
  42. package/dist/manifest.d.ts +1 -1
  43. package/dist/negotiate.d.ts +4 -3
  44. package/dist/negotiate.js +24 -14
  45. package/dist/{pipeline-cCR6MGNK.d.ts → pipeline-BHUjBO0G.d.ts} +5 -1
  46. package/dist/pipeline.d.ts +2 -2
  47. package/dist/pipeline.js +260 -41
  48. package/dist/resources.d.ts +5 -6
  49. package/dist/router.d.ts +12 -13
  50. package/dist/rsc-build.d.ts +6 -7
  51. package/dist/rsc-bundle.d.ts +6 -7
  52. package/dist/rsc-manifest.d.ts +5 -6
  53. package/dist/rsc-runtime/flight-render-routes.d.ts +3 -4
  54. package/dist/rsc-runtime/flight-render.d.ts +2 -3
  55. package/dist/rsc-runtime/flight-to-html.d.ts +2 -3
  56. package/dist/rsc-runtime/flight-to-html.js +3 -2
  57. package/dist/rsc-runtime/rsdw-shims.d.ts +2 -1
  58. package/dist/segment.d.ts +4 -5
  59. package/dist/sqlite-driver.d.ts +10 -11
  60. package/dist/static-files.d.ts +4 -5
  61. package/dist/tsconfig-jsx.d.ts +4 -5
  62. package/dist/turn-events.d.ts +2 -3
  63. package/dist/web-manifest.d.ts +3 -4
  64. package/dist/web-manifest.js +2 -6
  65. package/dist/{worker-BkFHGtxp.d.ts → worker-Dv4l6Ybs.d.ts} +4 -2
  66. package/dist/worker.d.ts +1 -1
  67. package/dist/worker.js +35 -18
  68. package/package.json +5 -5
  69. package/src/agent-durable.ts +203 -21
  70. package/src/agent-native.ts +94 -7
  71. package/src/build.ts +67 -5
  72. package/src/core-version.ts +1 -1
  73. package/src/llms-links.ts +46 -1
  74. package/src/negotiate.ts +39 -15
  75. package/src/pipeline.ts +433 -45
  76. package/src/web-manifest.ts +2 -6
  77. package/src/worker.ts +55 -17
package/src/build.ts CHANGED
@@ -314,7 +314,9 @@ export async function juneBuild(
314
314
  // The Link header is frozen here from the same builder the pipeline uses, so
315
315
  // the static and dynamic surfaces advertise identically. The adapter wraps the
316
316
  // portable pipeline for its target (workers() → withAssets).
317
- const linkHeader = buildLinkHeader(frozen.agent);
317
+ // (catalogs: the same publication rule as the pipeline's catalogOrigin — a
318
+ // basePath site publishes no /.well-known catalog, so it links none.)
319
+ const linkHeader = buildLinkHeader(frozen.agent, { catalogs: !frozen.document.basePath });
318
320
  const adapterEntry = adapter.entry({ linkHeader });
319
321
 
320
322
  // Resources (when declared) are bound from the worker's env (env.DB → D1) by
@@ -415,6 +417,10 @@ export class JuneAgentDO extends DurableObject {
415
417
  fetch(req: Request): Promise<Response> {
416
418
  return this.#agent.fetch(req);
417
419
  }
420
+ // Delivers input announcements left undelivered — a failed hook's retry, or the watchdog (#260).
421
+ alarm(): Promise<void> {
422
+ return this.#agent.alarm();
423
+ }
418
424
  }
419
425
  `
420
426
  : "";
@@ -506,7 +512,16 @@ ${doClass}`;
506
512
  if (cssAsset) manifest.document.styles = `/${cssAsset}`; // prerendered HTML links the hashed sheet
507
513
  if (clientAsset) manifest.document.clientScript = `/${clientAsset}`;
508
514
  if (moduleCssAsset) manifest.document.moduleStyles = `/${moduleCssAsset}`;
509
- const worker = createWorker(manifest);
515
+ // A static site has no server: /mcp (and the WebMCP tools that proxy to it)
516
+ // and the HTTP API (POST /api/<id>, /openapi.json) do not exist there. Render
517
+ // with both projected out, so the published files advertise only what is
518
+ // deployed: llms.txt drops its MCP and HTTP API sections, the catalogs and the
519
+ // generated skill list neither, pages register no WebMCP tools.
520
+ const isStatic = adapter.capabilities.runtime === "static";
521
+ if (isStatic) manifest.agent = { ...manifest.agent, mcp: false, api: false, webmcp: false };
522
+ // The static() target prerenders its sitemap too; that build is where the
523
+ // route contract lets `staticPaths` run, so its sitemap lists those pages.
524
+ const worker = createWorker(manifest, { staticBuild: isStatic });
510
525
  let hasAssets = false;
511
526
 
512
527
  // ---- public/ → assets/ : verbatim static files (favicon, images, fonts) ----
@@ -567,7 +582,6 @@ ${doClass}`;
567
582
  // dynamic routes via their staticPaths, and write <stem>/index.html so clean URLs
568
583
  // resolve on a dumb file host with no rewrite server. Other targets keep the
569
584
  // opt-in `prerender: true` behavior and the flat <stem>.html naming (byte-identical).
570
- const isStatic = adapter.capabilities.runtime === "static";
571
585
  const i18n = frozen.i18n;
572
586
 
573
587
  // Render ONE route pathname through the worker → HTML (+ .md/.json projections).
@@ -604,6 +618,17 @@ ${doClass}`;
604
618
 
605
619
  // Locale variants a static route is emitted at: no i18n → [path]; with i18n → one
606
620
  // per locale (defaultLocale keeps the bare path, others get their localeHref prefix).
621
+ // A static build is ONE file tree on ONE host: a locale on its own domain has no
622
+ // path in it (and a basePath can't apply there), so say so instead of failing
623
+ // later with a prerender 404.
624
+ if (isStatic && i18n) {
625
+ const onDomain = Object.keys(i18n.locales).filter((l) => i18n.locales[l]!.domain);
626
+ if (onDomain.length) {
627
+ throw new Error(
628
+ `[june build] the static() target serves one host, so i18n locales with their own domain are not supported (${onDomain.join(", ")}). Give them a path prefix instead, e.g. { path: "/${onDomain[0]}" }.`,
629
+ );
630
+ }
631
+ }
607
632
  const localeVariants = (path: string): string[] =>
608
633
  isStatic && i18n
609
634
  ? [...new Set(Object.keys(i18n.locales).map((l) => localeHref(i18n, path, l)))]
@@ -624,6 +649,9 @@ ${doClass}`;
624
649
  const sp = dyn.def.staticPaths;
625
650
  if (!sp) continue;
626
651
  const paths = typeof sp === "function" ? await sp() : sp;
652
+ // Resolve a producer ONCE: the prerendered sitemap below reads this same
653
+ // def, so it lists exactly the set prerendered here without a second call.
654
+ dyn.def.staticPaths = paths;
627
655
  for (const p of paths) await prerenderOne(p, dyn.def);
628
656
  }
629
657
  // Framework surfaces the worker would otherwise generate on the fly. Guarded on
@@ -635,11 +663,45 @@ ${doClass}`;
635
663
  ["/llms.txt", "llms.txt"],
636
664
  ["/sitemap.xml", "sitemap.xml"],
637
665
  ];
638
- for (const [reqPath, file] of extra) {
666
+ // A sitemap needs absolute URLs, and the prerender host is a placeholder: with
667
+ // no public origin configured the pipeline answers 404 for it (and the loop
668
+ // below skips it), so say why rather than silently ship no sitemap.
669
+ if (manifest.agent.discovery && !manifest.document.site.url && !manifest.document.deployOrigin) {
670
+ console.warn(
671
+ "[june build] no sitemap.xml: set site.url (or deploy.domain) in june.config.ts so its URLs are absolute.",
672
+ );
673
+ }
674
+ // The agent catalogs (ARD, Agent Skills) — JSON/Markdown files a static host
675
+ // can serve as-is — only when this site publishes them: agent discovery on, a
676
+ // root deploy (a basePath site doesn't own /.well-known), and a known public
677
+ // origin (the files name absolute URLs). Same rule as the pipeline's
678
+ // catalogOrigin, stated here too so an app catch-all route can never answer
679
+ // these paths into the build. The generated skill's path carries its name, so
680
+ // it is read back from the index.
681
+ const doc = manifest.document;
682
+ if (manifest.agent.discovery && !doc.basePath && (doc.site.url || doc.deployOrigin)) {
683
+ extra.push(
684
+ ["/.well-known/ai-catalog.json", ".well-known/ai-catalog.json"],
685
+ ["/.well-known/ard.json", ".well-known/ard.json"],
686
+ ["/.well-known/agent-skills/index.json", ".well-known/agent-skills/index.json"],
687
+ );
688
+ }
689
+ for (let i = 0; i < extra.length; i++) {
690
+ const [reqPath, file] = extra[i]!;
639
691
  const res = await worker.fetch(new Request(`${PRERENDER_ORIGIN}${reqPath}`));
640
692
  if (!res.ok) continue;
641
- await writeFile(join(assetsDir, file), Buffer.from(await res.arrayBuffer()));
693
+ const bytes = Buffer.from(await res.arrayBuffer());
694
+ await mkdir(dirname(join(assetsDir, file)), { recursive: true });
695
+ await writeFile(join(assetsDir, file), bytes);
642
696
  hasAssets = true;
697
+ if (file.endsWith("agent-skills/index.json")) {
698
+ const index = JSON.parse(bytes.toString("utf8")) as { skills?: Array<{ url?: string }> };
699
+ for (const s of index.skills ?? []) {
700
+ if (!s.url) continue;
701
+ const p = new URL(s.url, PRERENDER_ORIGIN).pathname;
702
+ extra.push([p, p.slice(1)]);
703
+ }
704
+ }
643
705
  }
644
706
  // 404.html — GitHub Pages serves it for any unmatched URL. A deliberately-missing
645
707
  // path renders June's not-found HTML (body is written regardless of the 404 status).
@@ -15,7 +15,7 @@ import * as coreRuntime from "@junejs/core/agent-runtime";
15
15
 
16
16
  // The contract number this server tree is built against. Bump in lockstep with
17
17
  // core's RUNTIME_API_VERSION whenever the server↔core runtime contract changes.
18
- const EXPECTED_CORE_RUNTIME_API = 2;
18
+ const EXPECTED_CORE_RUNTIME_API = 6;
19
19
 
20
20
  export function assertCoreRuntimeVersion(surface: string): void {
21
21
  checkCoreRuntimeVersion(surface, (coreRuntime as { RUNTIME_API_VERSION?: number }).RUNTIME_API_VERSION);
package/src/llms-links.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // template an agent can't fetch — lists each real page it serves, or is skipped.
5
5
  // Links point at the markdown projection (<path>.md) unless the route turned md off,
6
6
  // so an agent reads the page rather than its HTML (llmstxt.org's recommendation).
7
- import type { LlmsLink } from "@junejs/core/discovery";
7
+ import type { LlmsLink, SitemapPage } from "@junejs/core/discovery";
8
8
  import type { LlmsEntry } from "@junejs/core/route";
9
9
 
10
10
  import type { RouteResolver } from "./pipeline";
@@ -49,3 +49,48 @@ export async function collectLlmsLinks(origin: string, routes: string[], resolve
49
49
  }
50
50
  return links;
51
51
  }
52
+
53
+ // /sitemap.xml pages, enumerated like /llms.txt. A static route is one page; a
54
+ // dynamic route lists its real pages via its `llms` entries (their `lastModified`
55
+ // becomes <lastmod>) — the runtime-safe hook /llms.txt already runs. A route
56
+ // that names none stays out: a [param] template isn't a URL. Resource routes
57
+ // (route.*) are machine endpoints, not pages. `llms = false` keeps a static
58
+ // route listed (its URL is known without entries); on a dynamic route it leaves
59
+ // no entries, so its pages drop out of the runtime sitemap too.
60
+ // `staticPaths` join in only when opts.staticPaths is set — the static() build's
61
+ // prerender, the one place the route contract runs them. Even then they're
62
+ // skipped under i18n: they arrive locale-prefixed, while the sitemap derives
63
+ // each page's locale variants from its canonical path.
64
+ export async function collectSitemapPages(
65
+ routes: string[],
66
+ resolve: RouteResolver,
67
+ opts: { i18n?: boolean; staticPaths?: boolean } = {},
68
+ ): Promise<SitemapPage[]> {
69
+ const pages = new Map<string, SitemapPage>();
70
+ const add = (path: string, lastModified?: string | Date) => {
71
+ const key = path === "/" ? "/" : path.replace(/\/+$/, "");
72
+ const prev = pages.get(key);
73
+ if (!prev) pages.set(key, lastModified ? { path: key, lastModified } : { path: key });
74
+ else if (lastModified && !prev.lastModified) prev.lastModified = lastModified;
75
+ };
76
+ for (const route of routes) {
77
+ const resolved = await resolve(route);
78
+ if (!resolved || !("def" in resolved)) continue;
79
+ const { def } = resolved;
80
+ const dynamic = route.includes("[");
81
+ const declared = def.llms ? (typeof def.llms === "function" ? await def.llms() : def.llms) : undefined;
82
+ const entries: LlmsEntry[] = declared ? (Array.isArray(declared) ? declared : [declared]) : [];
83
+ // A static route is exactly one URL — its own. An entry naming another path
84
+ // (an llms.txt pointer elsewhere) is not a page this route serves.
85
+ if (!dynamic) {
86
+ add(route, entries.find((e) => (e.path ?? route) === route)?.lastModified);
87
+ continue;
88
+ }
89
+ for (const e of entries) if (e.path) add(e.path, e.lastModified);
90
+ if (opts.staticPaths && !opts.i18n && def.staticPaths) {
91
+ const sp = typeof def.staticPaths === "function" ? await def.staticPaths() : def.staticPaths;
92
+ for (const p of sp) add(p);
93
+ }
94
+ }
95
+ return [...pages.values()];
96
+ }
package/src/negotiate.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  // header — a link is unambiguous; Accept is a hint. The clean pathname (with
7
7
  // the projection extension stripped) is what the router matches.
8
8
 
9
+ import { FRAGMENT_ACCEPT } from "@junejs/core/nav-protocol";
9
10
  import type { RenderTarget } from "@junejs/core/route";
10
11
 
11
12
  // The fragment media type + title header are the client-router wire protocol —
@@ -18,12 +19,43 @@ const EXT_TARGET: Record<string, RenderTarget> = {
18
19
  ".md": "md",
19
20
  };
20
21
 
21
- const ACCEPT_TARGET: Array<[test: RegExp, target: RenderTarget]> = [
22
- // fragment first: its media type is exact + can't collide with browser Accepts.
23
- [/text\/vnd\.june\.fragment\+html/, "fragment"],
24
- [/text\/markdown/, "md"],
25
- [/application\/json/, "json"],
26
- ];
22
+ // RFC 9110 §12.4.2 qvalue: 0–1 with at most three decimals ("0", "0.5", "1.000").
23
+ const QVALUE = /^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/;
24
+
25
+ // Parse an Accept header into media-range → q (RFC 9110 §12.5.1). A q outside the
26
+ // qvalue grammar ("2", "-0.5", "0.1234") is malformed and reads as the default 1:
27
+ // the client still listed the type, so it stays acceptable at the default weight
28
+ // rather than being dropped or excluded. Parameters other than q are ignored.
29
+ function acceptRanges(accept: string): Map<string, number> {
30
+ const ranges = new Map<string, number>();
31
+ for (const part of accept.split(",")) {
32
+ const [type, ...params] = part.split(";").map((s) => s.trim().toLowerCase());
33
+ if (!type) continue;
34
+ const raw = params.find((p) => p.startsWith("q="))?.slice(2).trim();
35
+ ranges.set(type, raw !== undefined && QVALUE.test(raw) ? Number(raw) : 1);
36
+ }
37
+ return ranges;
38
+ }
39
+
40
+ // The projection an Accept header asks for — null when it asks for none of the
41
+ // agent projections (→ the HTML view). ONE decision shared by the pipeline and
42
+ // the deployed worker's asset layer (withAssets), so the two can't disagree.
43
+ // - The client router's fragment media type (exact range, q > 0) wins outright.
44
+ // - Markdown / JSON are chosen when listed with q > 0 and at least as preferred
45
+ // as HTML; HTML's quality is text/html's, else the text/* or */* wildcard's.
46
+ // So `text/markdown, text/html;q=0.9` → md, `text/html, text/markdown;q=0.5`
47
+ // → HTML, and a plain browser Accept (no markdown/json) → HTML.
48
+ // - Ties go to the agent projection (md before json), as they always have.
49
+ export function acceptTarget(accept: string): RenderTarget | null {
50
+ const ranges = acceptRanges(accept);
51
+ if ((ranges.get(FRAGMENT_ACCEPT) ?? 0) > 0) return "fragment";
52
+ const html = ranges.get("text/html") ?? ranges.get("text/*") ?? ranges.get("*/*") ?? 0;
53
+ const md = ranges.get("text/markdown") ?? 0;
54
+ const json = ranges.get("application/json") ?? 0;
55
+ const best = Math.max(md, json);
56
+ if (best <= 0 || best < html) return null;
57
+ return md >= json ? "md" : "json";
58
+ }
27
59
 
28
60
  export type Negotiated = {
29
61
  target: RenderTarget;
@@ -61,15 +93,7 @@ export function negotiate(url: URL, request: Request, basePath?: string): Negoti
61
93
  pathname = original;
62
94
  }
63
95
 
64
- if (!target) {
65
- const accept = request.headers.get("accept") ?? "";
66
- for (const [re, t] of ACCEPT_TARGET) {
67
- if (re.test(accept)) {
68
- target = t;
69
- break;
70
- }
71
- }
72
- }
96
+ if (!target) target = acceptTarget(request.headers.get("accept") ?? "");
73
97
 
74
98
  // A speculative request (Sec-Purpose: prefetch / prerender) may never be seen
75
99
  // — load()s read it to skip side effects (analytics, rate limits, counters).