@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.
- package/assets/icon-latin.LICENSE.txt +92 -0
- package/assets/icon-latin.ttf +0 -0
- package/dist/adapter-D0UnYBuN.d.ts +64 -0
- package/dist/adapter.d.ts +2 -56
- package/dist/adapter.js +107 -6
- 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/assets.d.ts +23 -0
- package/dist/assets.js +123 -0
- package/dist/blob.d.ts +4 -5
- package/dist/build.d.ts +8 -30
- package/dist/build.js +143 -298
- package/dist/client-bundle.d.ts +8 -9
- package/dist/client-bundle.js +1 -1
- package/dist/config-freeze-r5p-vTWv.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 +6 -7
- 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 +51 -38
- 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 -57
- package/dist/worker.js +69 -27
- package/package.json +28 -9
- package/src/adapter.ts +149 -4
- 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/assets.ts +200 -0
- package/src/build.ts +241 -449
- 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-scan.ts +161 -0
- package/src/router.ts +66 -54
- 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 +131 -35
package/src/favicon.ts
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// The default PNG icons: apple-touch-icon.png (180), icon.png (32), and a real
|
|
2
|
+
// favicon.ico — the site name's first character on the same dark square as the
|
|
3
|
+
// SVG favicon the pipeline serves. Build + dev host only (resvg-wasm, node:fs);
|
|
4
|
+
// the worker never imports this, it serves the files `june build` wrote.
|
|
5
|
+
//
|
|
6
|
+
// A rasterizer has no browser fonts, so each icon picks a font for the script
|
|
7
|
+
// of its character:
|
|
8
|
+
// Latin / Greek / Cyrillic caps, digits → Inter caps, bundled (offline, deterministic)
|
|
9
|
+
// Han → the Noto Sans CJK face for site.lang, via CLDR likely subtags:
|
|
10
|
+
// zh → SC, zh-TW / zh-Hant → TC, zh-HK / zh-MO / yue → HK, ja → JP, ko → KR;
|
|
11
|
+
// a lang with no Han signal (en) → SC, CLDR's default for Han
|
|
12
|
+
// kana → Noto Sans JP · Hangul → Noto Sans KR · emoji → Noto Emoji
|
|
13
|
+
// Arabic, Devanagari, Thai, … → that script's Noto Sans face
|
|
14
|
+
// Non-bundled faces come from Google Fonts subset to that ONE character (a few
|
|
15
|
+
// KB), cached on disk, so only the first build touches the network. Any miss
|
|
16
|
+
// (offline, unknown script, a glyph the font lacks) draws the square without a
|
|
17
|
+
// character, and never fails the build.
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
20
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
21
|
+
import { createRequire } from "node:module";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
|
|
24
|
+
import type { DocumentConfig } from "@junejs/core/document";
|
|
25
|
+
|
|
26
|
+
import { iconLetter } from "./icon-letter";
|
|
27
|
+
|
|
28
|
+
type Site = DocumentConfig["site"];
|
|
29
|
+
|
|
30
|
+
export const ICON_BG = "#1d1d1f";
|
|
31
|
+
export const ICON_FG = "#fbfbf8";
|
|
32
|
+
|
|
33
|
+
// Filenames the icons are served at (from the app root), and the public/ files
|
|
34
|
+
// that mean the app brings its own icon — then June generates none.
|
|
35
|
+
export const GENERATED_ICONS = ["apple-touch-icon.png", "icon.png", "favicon.ico", "icon-192.png", "icon-512.png"] as const;
|
|
36
|
+
export type GeneratedIconFile = (typeof GENERATED_ICONS)[number];
|
|
37
|
+
// The app's own main icon, in preference order: the first one present becomes
|
|
38
|
+
// the page's favicon link (so none of them is ever silently unreferenced).
|
|
39
|
+
const PRIMARY_ICON_FILES = ["favicon.svg", "icon.svg", "favicon.png", "icon.png", "favicon.ico"];
|
|
40
|
+
const CUSTOM_ICON_FILES = [...PRIMARY_ICON_FILES, "apple-touch-icon.png"];
|
|
41
|
+
const MANIFEST_FILES = ["manifest.webmanifest", "manifest.json", "site.webmanifest"];
|
|
42
|
+
|
|
43
|
+
// `inPublic` for resolveIcons over an app's public/ dir. A symlinked public/ is
|
|
44
|
+
// ignored, as the build and dev server ignore it.
|
|
45
|
+
export function publicFileCheck(publicDir: string): (file: string) => boolean {
|
|
46
|
+
let real = false;
|
|
47
|
+
try {
|
|
48
|
+
real = lstatSync(publicDir).isDirectory();
|
|
49
|
+
} catch {
|
|
50
|
+
/* no public/ */
|
|
51
|
+
}
|
|
52
|
+
return (file) => real && existsSync(join(publicDir, file));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Which icons the document links, and whether June generates them. `inPublic`
|
|
56
|
+
// answers whether public/<file> exists. Generated when the app has no icon of
|
|
57
|
+
// its own. Otherwise every icon file it has is linked: the first of
|
|
58
|
+
// PRIMARY_ICON_FILES as the favicon (site.icon still wins in the document),
|
|
59
|
+
// plus icon.png and apple-touch-icon.png when present.
|
|
60
|
+
//
|
|
61
|
+
// The web manifest: the app's own public/manifest.webmanifest (or
|
|
62
|
+
// manifest.json) is linked whenever present. Otherwise June generates one only
|
|
63
|
+
// alongside its own icons — an app with custom icons may have no 192/512 PNGs
|
|
64
|
+
// for a manifest to point at.
|
|
65
|
+
export function resolveIcons(
|
|
66
|
+
site: Site,
|
|
67
|
+
inPublic: (file: string) => boolean,
|
|
68
|
+
): { generate: boolean; icons: NonNullable<DocumentConfig["icons"]> } {
|
|
69
|
+
const ownManifest = MANIFEST_FILES.find(inPublic);
|
|
70
|
+
const custom = Boolean(site.icon) || CUSTOM_ICON_FILES.some(inPublic);
|
|
71
|
+
if (!custom) {
|
|
72
|
+
return {
|
|
73
|
+
generate: true,
|
|
74
|
+
icons: {
|
|
75
|
+
png: "/icon.png",
|
|
76
|
+
appleTouch: "/apple-touch-icon.png",
|
|
77
|
+
manifest: ownManifest ? `/${ownManifest}` : "/manifest.webmanifest",
|
|
78
|
+
generated: !ownManifest,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const primary = PRIMARY_ICON_FILES.find(inPublic);
|
|
83
|
+
return {
|
|
84
|
+
generate: false,
|
|
85
|
+
icons: {
|
|
86
|
+
primary: primary ? `/${primary}` : undefined,
|
|
87
|
+
png: inPublic("icon.png") ? "/icon.png" : undefined,
|
|
88
|
+
appleTouch: inPublic("apple-touch-icon.png") ? "/apple-touch-icon.png" : undefined,
|
|
89
|
+
manifest: ownManifest ? `/${ownManifest}` : undefined,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const is = (re: RegExp) => (ch: string) => re.test(ch);
|
|
95
|
+
const isBundled = is(/^[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}0-9•]/u);
|
|
96
|
+
const isEmoji = is(/^\p{Extended_Pictographic}/u);
|
|
97
|
+
const isKana = is(/^[\p{Script=Hiragana}\p{Script=Katakana}]/u);
|
|
98
|
+
const isHangul = is(/^\p{Script=Hangul}/u);
|
|
99
|
+
const isHan = is(/^\p{Script=Han}/u);
|
|
100
|
+
|
|
101
|
+
// Scripts with a Noto Sans face on Google Fonts, by Unicode script name.
|
|
102
|
+
const NOTO_SCRIPTS = [
|
|
103
|
+
"Arabic", "Armenian", "Bengali", "Devanagari", "Ethiopic", "Georgian", "Gujarati", "Gurmukhi",
|
|
104
|
+
"Hebrew", "Kannada", "Khmer", "Lao", "Malayalam", "Myanmar", "Sinhala", "Tamil", "Telugu", "Thai",
|
|
105
|
+
] as const;
|
|
106
|
+
const NOTO_SCRIPT_RES = NOTO_SCRIPTS.map((s) => [s, new RegExp(`^\\p{Script=${s}}`, "u")] as const);
|
|
107
|
+
|
|
108
|
+
// The Noto Sans CJK face whose Han glyph forms match `lang`. Resolved through
|
|
109
|
+
// CLDR likely subtags (Intl.Locale#maximize) instead of a hand-picked region.
|
|
110
|
+
export function hanFamily(lang: string | undefined): string {
|
|
111
|
+
let loc: Intl.Locale;
|
|
112
|
+
try {
|
|
113
|
+
loc = new Intl.Locale(lang || "und").maximize();
|
|
114
|
+
} catch {
|
|
115
|
+
return "Noto Sans SC";
|
|
116
|
+
}
|
|
117
|
+
if (loc.script === "Jpan") return "Noto Sans JP";
|
|
118
|
+
if (loc.script === "Kore") return "Noto Sans KR";
|
|
119
|
+
if (loc.script === "Hant") return loc.region === "HK" || loc.region === "MO" ? "Noto Sans HK" : "Noto Sans TC";
|
|
120
|
+
return "Noto Sans SC"; // Hans, or a language with no Han signal — CLDR's und-Hani is zh-Hans
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The Google Fonts family for a character; null = the bundled Inter caps;
|
|
124
|
+
// undefined = no known face (draw the square alone).
|
|
125
|
+
export function iconFamily(ch: string, lang: string | undefined): string | null | undefined {
|
|
126
|
+
if (!ch) return undefined;
|
|
127
|
+
if (isBundled(ch)) return null;
|
|
128
|
+
if (isEmoji(ch)) return "Noto Emoji";
|
|
129
|
+
if (isKana(ch)) return "Noto Sans JP";
|
|
130
|
+
if (isHangul(ch)) return "Noto Sans KR";
|
|
131
|
+
if (isHan(ch)) return hanFamily(lang);
|
|
132
|
+
const script = NOTO_SCRIPT_RES.find(([, re]) => re.test(ch))?.[0];
|
|
133
|
+
return script ? `Noto Sans ${script}` : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Fetches a font subset: resolves to the TTF bytes, or null on any failure.
|
|
137
|
+
export type FontFetcher = (family: string, text: string) => Promise<Uint8Array | null>;
|
|
138
|
+
|
|
139
|
+
// A legacy Safari UA makes Google Fonts answer with TTF (resvg reads TTF/OTF,
|
|
140
|
+
// not woff2) — the same trick @junejs/og uses.
|
|
141
|
+
const LEGACY_UA =
|
|
142
|
+
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1";
|
|
143
|
+
|
|
144
|
+
// Google Fonts, subset to `text`, cached under cacheDir by request URL so a
|
|
145
|
+
// rebuild never refetches. timeoutMs bounds each request (offline builds fall
|
|
146
|
+
// back quickly instead of hanging).
|
|
147
|
+
export function googleFontFetcher(
|
|
148
|
+
cacheDir: string,
|
|
149
|
+
opts: { fetch?: typeof fetch; timeoutMs?: number } = {},
|
|
150
|
+
): FontFetcher {
|
|
151
|
+
const doFetch = opts.fetch ?? fetch;
|
|
152
|
+
const timeoutMs = opts.timeoutMs ?? 3000;
|
|
153
|
+
return async (family, text) => {
|
|
154
|
+
const cssUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@600&text=${encodeURIComponent(text)}`;
|
|
155
|
+
const file = join(cacheDir, createHash("sha256").update(cssUrl).digest("hex").slice(0, 32) + ".ttf");
|
|
156
|
+
try {
|
|
157
|
+
return new Uint8Array(await readFile(file));
|
|
158
|
+
} catch {
|
|
159
|
+
/* not cached */
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
163
|
+
const cssRes = await doFetch(cssUrl, { headers: { "User-Agent": LEGACY_UA }, signal });
|
|
164
|
+
if (!cssRes.ok) return null;
|
|
165
|
+
const url = (await cssRes.text()).match(/src: url\((.+?)\)/)?.[1];
|
|
166
|
+
if (!url) return null;
|
|
167
|
+
const fontRes = await doFetch(url, { signal });
|
|
168
|
+
if (!fontRes.ok) return null;
|
|
169
|
+
const bytes = new Uint8Array(await fontRes.arrayBuffer());
|
|
170
|
+
await mkdir(cacheDir, { recursive: true });
|
|
171
|
+
await writeFile(file, bytes);
|
|
172
|
+
return bytes;
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type ResvgModule = typeof import("@resvg/resvg-wasm");
|
|
180
|
+
let resvgReady: Promise<ResvgModule> | undefined;
|
|
181
|
+
function loadResvg(): Promise<ResvgModule> {
|
|
182
|
+
return (resvgReady ??= (async () => {
|
|
183
|
+
const mod = await import("@resvg/resvg-wasm");
|
|
184
|
+
const wasmPath = createRequire(import.meta.url).resolve("@resvg/resvg-wasm/index_bg.wasm");
|
|
185
|
+
await mod.initWasm(await readFile(wasmPath));
|
|
186
|
+
return mod;
|
|
187
|
+
})());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let bundledFont: Promise<Uint8Array> | undefined;
|
|
191
|
+
const loadBundledFont = () =>
|
|
192
|
+
(bundledFont ??= readFile(new URL("../assets/icon-latin.ttf", import.meta.url)).then((b) => new Uint8Array(b)));
|
|
193
|
+
const BUNDLED_FAMILY = "Inter";
|
|
194
|
+
|
|
195
|
+
const escapeXml = (s: string) => s.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
196
|
+
|
|
197
|
+
// Where the glyph goes on the 64×64 square: font size + origin, from its INK
|
|
198
|
+
// box — not its em box — so every script sits optically centred at one size.
|
|
199
|
+
// A probe render measures the ink; cap height and a CJK ideograph both come out
|
|
200
|
+
// INK_H tall, and a wide glyph is held to INK_W. The ink box's corners stay
|
|
201
|
+
// within 0.4 of the size from the centre, which is the maskable-icon safe zone,
|
|
202
|
+
// so the manifest can mark the full-bleed art "maskable" (pinned in a test).
|
|
203
|
+
const PROBE = 1000;
|
|
204
|
+
export const INK_H = 28;
|
|
205
|
+
export const INK_W = 40;
|
|
206
|
+
type Placement = { family: string; font: Uint8Array; size: number; x: number; y: number };
|
|
207
|
+
|
|
208
|
+
function inkBox(resvg: ResvgModule, ch: string, family: string, font: Uint8Array) {
|
|
209
|
+
const probe =
|
|
210
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${PROBE}" height="${PROBE}">` +
|
|
211
|
+
`<text x="${PROBE / 2}" y="${PROBE * 0.7}" font-family="${family}" font-size="${PROBE * 0.6}">${escapeXml(ch)}</text></svg>`;
|
|
212
|
+
const r = new resvg.Resvg(probe, { font: { fontBuffers: [font], loadSystemFonts: false, defaultFontFamily: family } });
|
|
213
|
+
const bbox = r.getBBox();
|
|
214
|
+
const box = bbox && bbox.width > 0 && bbox.height > 0 ? { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height } : null;
|
|
215
|
+
bbox?.free();
|
|
216
|
+
r.free();
|
|
217
|
+
return box;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// A private-use code point no subset font maps: whatever it draws is .notdef.
|
|
221
|
+
const NOTDEF_PROBE = "";
|
|
222
|
+
|
|
223
|
+
function place(resvg: ResvgModule, ch: string, family: string, font: Uint8Array): Placement | null {
|
|
224
|
+
const bbox = inkBox(resvg, ch, family, font);
|
|
225
|
+
if (!bbox) return null;
|
|
226
|
+
// A font without the glyph draws .notdef (tofu), which HAS ink — so compare
|
|
227
|
+
// with the font's own .notdef box. The same box means a missing glyph.
|
|
228
|
+
const notdef = inkBox(resvg, NOTDEF_PROBE, family, font);
|
|
229
|
+
if (notdef && notdef.x === bbox.x && notdef.y === bbox.y && notdef.width === bbox.width && notdef.height === bbox.height) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const scale = Math.min(INK_H / bbox.height, INK_W / bbox.width);
|
|
233
|
+
const cx = bbox.x + bbox.width / 2 - PROBE / 2;
|
|
234
|
+
const cy = bbox.y + bbox.height / 2 - PROBE * 0.7;
|
|
235
|
+
return { family, font, size: PROBE * 0.6 * scale, x: 32 - cx * scale, y: 32 - cy * scale };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// The icon as SVG on a 64 grid. rounded=false for apple-touch-icon: iOS masks
|
|
239
|
+
// the corners itself and wants a full-bleed square.
|
|
240
|
+
function iconSvg(ch: string, p: Placement | null, rounded: boolean): string {
|
|
241
|
+
const glyph = p
|
|
242
|
+
? `<text x="${p.x.toFixed(2)}" y="${p.y.toFixed(2)}" font-family="${p.family}" font-size="${p.size.toFixed(2)}" fill="${ICON_FG}">${escapeXml(ch)}</text>`
|
|
243
|
+
: "";
|
|
244
|
+
return (
|
|
245
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">` +
|
|
246
|
+
`<rect width="64" height="64"${rounded ? ` rx="12"` : ""} fill="${ICON_BG}"/>${glyph}</svg>`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// A single-image .ico whose entry is a PNG (valid since Windows Vista; every
|
|
251
|
+
// current browser reads it).
|
|
252
|
+
export function pngToIco(png: Uint8Array, size: number): Uint8Array<ArrayBuffer> {
|
|
253
|
+
const out = new Uint8Array(22 + png.length);
|
|
254
|
+
const v = new DataView(out.buffer);
|
|
255
|
+
v.setUint16(2, 1, true); // type: icon
|
|
256
|
+
v.setUint16(4, 1, true); // one image
|
|
257
|
+
out[6] = size >= 256 ? 0 : size; // width
|
|
258
|
+
out[7] = size >= 256 ? 0 : size; // height
|
|
259
|
+
v.setUint16(10, 1, true); // colour planes
|
|
260
|
+
v.setUint16(12, 32, true); // bits per pixel
|
|
261
|
+
v.setUint32(14, png.length, true);
|
|
262
|
+
v.setUint32(18, 22, true); // image data offset
|
|
263
|
+
out.set(png, 22);
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export type GeneratedIcons = {
|
|
268
|
+
"apple-touch-icon.png": Uint8Array<ArrayBuffer>;
|
|
269
|
+
"icon.png": Uint8Array<ArrayBuffer>;
|
|
270
|
+
"favicon.ico": Uint8Array<ArrayBuffer>;
|
|
271
|
+
// The web manifest's icons (Android home screen). Full-bleed like the apple
|
|
272
|
+
// icon: the glyph's ink stays inside the maskable safe zone (a centred circle
|
|
273
|
+
// of radius 0.4), so one image serves "any" and "maskable".
|
|
274
|
+
"icon-192.png": Uint8Array<ArrayBuffer>;
|
|
275
|
+
"icon-512.png": Uint8Array<ArrayBuffer>;
|
|
276
|
+
// What was drawn, for the build log and tests: the character and its font
|
|
277
|
+
// family, or family null when the square went out without one.
|
|
278
|
+
letter: string;
|
|
279
|
+
family: string | null;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export async function renderIcons(opts: {
|
|
283
|
+
site: Site;
|
|
284
|
+
fetchFont: FontFetcher;
|
|
285
|
+
warn?: (msg: string) => void;
|
|
286
|
+
}): Promise<GeneratedIcons> {
|
|
287
|
+
const resvg = await loadResvg();
|
|
288
|
+
const ch = iconLetter(opts.site.name);
|
|
289
|
+
const family = iconFamily(ch, opts.site.lang);
|
|
290
|
+
let placement: Placement | null = null;
|
|
291
|
+
if (family === null) {
|
|
292
|
+
placement = place(resvg, ch, BUNDLED_FAMILY, await loadBundledFont());
|
|
293
|
+
} else if (family !== undefined) {
|
|
294
|
+
const font = await opts.fetchFont(family, ch);
|
|
295
|
+
if (font) placement = place(resvg, ch, family, font);
|
|
296
|
+
}
|
|
297
|
+
if (ch && !placement) {
|
|
298
|
+
opts.warn?.(
|
|
299
|
+
`[june] default icon: no font for "${ch}"${family ? ` (${family} unavailable — offline?)` : ""}; ` +
|
|
300
|
+
`drew the square without it. Set site.icon or add public/icon.png to use your own.`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const fonts = placement ? [placement.font] : [];
|
|
304
|
+
const render = (size: number, rounded: boolean) => {
|
|
305
|
+
const r = new resvg.Resvg(iconSvg(ch, placement, rounded), {
|
|
306
|
+
fitTo: { mode: "width", value: size },
|
|
307
|
+
font: { fontBuffers: fonts, loadSystemFonts: false, defaultFontFamily: placement?.family ?? BUNDLED_FAMILY },
|
|
308
|
+
});
|
|
309
|
+
const image = r.render();
|
|
310
|
+
const png = image.asPng() as Uint8Array<ArrayBuffer>; // a JS-owned copy, not a view into wasm memory
|
|
311
|
+
image.free();
|
|
312
|
+
r.free();
|
|
313
|
+
return png;
|
|
314
|
+
};
|
|
315
|
+
const icon32 = render(32, true);
|
|
316
|
+
return {
|
|
317
|
+
"apple-touch-icon.png": render(180, false),
|
|
318
|
+
"icon.png": icon32,
|
|
319
|
+
"favicon.ico": pngToIco(icon32, 32),
|
|
320
|
+
"icon-192.png": render(192, false),
|
|
321
|
+
"icon-512.png": render(512, false),
|
|
322
|
+
letter: ch,
|
|
323
|
+
family: placement?.family ?? null,
|
|
324
|
+
};
|
|
325
|
+
}
|
package/src/host.ts
CHANGED
|
@@ -42,7 +42,8 @@ export interface JuneHost {
|
|
|
42
42
|
readonly name: "bun" | "node";
|
|
43
43
|
serve(
|
|
44
44
|
handler: (req: Request) => Promise<Response>,
|
|
45
|
-
|
|
45
|
+
// hostname: the address to bind. Absent = all interfaces (production, containers).
|
|
46
|
+
opts: { port: number; hostname?: string; earlyHints?: () => string[] },
|
|
46
47
|
): ServeHandle;
|
|
47
48
|
// Spawn a module in a child runtime (the react-server Flight renderer, which
|
|
48
49
|
// must run under a different module-resolution condition). Phase 4 supersedes
|
|
@@ -63,7 +64,7 @@ function bunHost(): JuneHost {
|
|
|
63
64
|
// idleTimeout 0: Bun's 10s default kills quiet long-lived responses —
|
|
64
65
|
// the dev live-reload SSE most visibly (the browser then reconnects,
|
|
65
66
|
// reads it as a server restart, and reloads the page every 10s).
|
|
66
|
-
const server = Bun.serve({ port: opts.port, fetch: handler, idleTimeout: 0 });
|
|
67
|
+
const server = Bun.serve({ port: opts.port, hostname: opts.hostname, fetch: handler, idleTimeout: 0 });
|
|
67
68
|
return { port: server.port ?? opts.port, stop: (force) => void server.stop(force) };
|
|
68
69
|
},
|
|
69
70
|
spawnModule(entry, args, opts) {
|
|
@@ -125,7 +126,7 @@ function nodeHost(): JuneHost {
|
|
|
125
126
|
res.end("Internal Server Error");
|
|
126
127
|
}
|
|
127
128
|
});
|
|
128
|
-
server.listen(opts.port);
|
|
129
|
+
server.listen(opts.port, opts.hostname);
|
|
129
130
|
return {
|
|
130
131
|
get port() {
|
|
131
132
|
const addr = server.address();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The default icon's character — shared by the worker's SVG favicon
|
|
2
|
+
// (pipeline.ts) and the build's PNG icons (favicon.ts), so both draw the same
|
|
3
|
+
// one. Worker-safe: no node:* imports.
|
|
4
|
+
|
|
5
|
+
// The name's first grapheme, uppercased. A grapheme — not charAt(0) — so an
|
|
6
|
+
// emoji, a flag, or a character outside the BMP stays whole instead of
|
|
7
|
+
// becoming half a surrogate pair. "" when the name has none.
|
|
8
|
+
export function iconLetter(name: string | undefined): string {
|
|
9
|
+
const trimmed = (name ?? "").trim();
|
|
10
|
+
if (!trimmed) return "";
|
|
11
|
+
const first = new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(trimmed)[Symbol.iterator]().next()
|
|
12
|
+
.value?.segment;
|
|
13
|
+
return (first ?? "").toUpperCase();
|
|
14
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -81,6 +81,20 @@ export {
|
|
|
81
81
|
type Resolved,
|
|
82
82
|
type LayoutComponent,
|
|
83
83
|
} from "./pipeline";
|
|
84
|
+
// Connection auth helpers — turn an authenticated principal into a per-call
|
|
85
|
+
// bearer token for an outbound connection (provider/mcp/openapi). The auth
|
|
86
|
+
// INTEGRATION lives in the host layer by design, so these sit here (not in pure
|
|
87
|
+
// @junejs/core). Generic + overridable; the Better Auth variant is structural
|
|
88
|
+
// (adds no dependency).
|
|
89
|
+
export {
|
|
90
|
+
linkedAccountAuth,
|
|
91
|
+
betterAuthAccessToken,
|
|
92
|
+
betterAuthAccountTokenStore,
|
|
93
|
+
type AccountTokenStore,
|
|
94
|
+
type LinkedAccountAuthOptions,
|
|
95
|
+
type ConnectionAuth,
|
|
96
|
+
type BetterAuthLike,
|
|
97
|
+
} from "./connection-auth";
|
|
84
98
|
export { createApp, type JuneApp, type CreateAppOptions } from "./app";
|
|
85
99
|
export { createWorker, type WorkerManifest } from "./worker";
|
|
86
100
|
export {
|
|
@@ -91,6 +105,14 @@ export {
|
|
|
91
105
|
freezeConfig,
|
|
92
106
|
type BuildResult,
|
|
93
107
|
} from "./build";
|
|
108
|
+
export {
|
|
109
|
+
generateAgentModule,
|
|
110
|
+
findAgentDir,
|
|
111
|
+
scanAgentDir,
|
|
112
|
+
emitAgentModule,
|
|
113
|
+
AGENT_MODULE_FILE,
|
|
114
|
+
type AgentDirScan,
|
|
115
|
+
} from "./agent-compile";
|
|
94
116
|
export { juneDeploy, type DeployResult } from "./deploy";
|
|
95
117
|
export { migrateD1, wranglerD1, resolveD1Database, type D1Exec } from "./d1-migrate";
|
|
96
118
|
export { startDevServer, type DevServer, type DevServerOptions } from "./dev";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// isolate-local.ts — read @junejs/db's `isolateLocal` WITHOUT a named import.
|
|
2
|
+
//
|
|
3
|
+
// Same hazard core-version.ts exists for: a NAMED import of an export the
|
|
4
|
+
// resolved copy doesn't have dies at module LINK time with an unhelpful
|
|
5
|
+
// SyntaxError, before any of our own diagnostics can run. `isolateLocal` is new
|
|
6
|
+
// in @junejs/db 0.1.0-dev.2, and a version range cannot save us here — the
|
|
7
|
+
// floor has to stay satisfiable by the workspace's own db (which is the very
|
|
8
|
+
// version that lacks it), and `bun pm pack` rewrites a `workspace:*` dep to
|
|
9
|
+
// whatever version it resolves, which can be OLDER still.
|
|
10
|
+
//
|
|
11
|
+
// So: namespace import + optional read, with a local fallback. Unlike the core
|
|
12
|
+
// runtime contract, this one does NOT warrant throwing — isolate-scoped
|
|
13
|
+
// memoization is a lifetime optimization, not a correctness contract. The
|
|
14
|
+
// fallback memoizes in this module instance, so callers keep their cache; the
|
|
15
|
+
// only thing lost against an older db is sharing one value across DUPLICATE
|
|
16
|
+
// copies of the module (workspace symlinks), which costs a missed cache hit,
|
|
17
|
+
// never a wrong answer.
|
|
18
|
+
import * as juneDb from "@junejs/db";
|
|
19
|
+
|
|
20
|
+
export type IsolateLocal = <T>(key: string | symbol, make: () => T) => T;
|
|
21
|
+
|
|
22
|
+
// Exported for tests: the ambient read below can only ever see the workspace's
|
|
23
|
+
// own (matching) db, so the degraded path needs a direct handle to be provable.
|
|
24
|
+
export function makeIsolateLocalFallback(): IsolateLocal {
|
|
25
|
+
const values = new Map<string | symbol, unknown>();
|
|
26
|
+
return <T>(key: string | symbol, make: () => T): T => {
|
|
27
|
+
if (!values.has(key)) values.set(key, make());
|
|
28
|
+
return values.get(key) as T;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const isolateLocal: IsolateLocal =
|
|
33
|
+
(juneDb as { isolateLocal?: IsolateLocal }).isolateLocal ?? makeIsolateLocalFallback();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// /llms.txt links, collected from the routes themselves. Each route may export
|
|
2
|
+
// `llms` (see LlmsEntry in @junejs/core/route): a static route defaults to one link
|
|
3
|
+
// under "Pages" built from its static metadata; a dynamic route — a [param]
|
|
4
|
+
// template an agent can't fetch — lists each real page it serves, or is skipped.
|
|
5
|
+
// Links point at the markdown projection (<path>.md) unless the route turned md off,
|
|
6
|
+
// so an agent reads the page rather than its HTML (llmstxt.org's recommendation).
|
|
7
|
+
import type { LlmsLink, SitemapPage } from "@junejs/core/discovery";
|
|
8
|
+
import type { LlmsEntry } from "@junejs/core/route";
|
|
9
|
+
|
|
10
|
+
import type { RouteResolver } from "./pipeline";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_SECTION = "Pages";
|
|
13
|
+
|
|
14
|
+
const mdPath = (path: string) => (path === "/" ? "/index.md" : `${path.replace(/\/+$/, "")}.md`);
|
|
15
|
+
|
|
16
|
+
export async function collectLlmsLinks(origin: string, routes: string[], resolve: RouteResolver): Promise<LlmsLink[]> {
|
|
17
|
+
const links: LlmsLink[] = [];
|
|
18
|
+
const seen = new Set<string>();
|
|
19
|
+
for (const route of routes) {
|
|
20
|
+
// A dynamic template resolves against its own pattern ("/docs/[slug]" matches
|
|
21
|
+
// /docs/:slug), which is all we need: the route definition, not its data.
|
|
22
|
+
const resolved = await resolve(route);
|
|
23
|
+
if (!resolved || !("def" in resolved)) continue; // resource routes have no page to list
|
|
24
|
+
const { def } = resolved;
|
|
25
|
+
if (def.llms === false) continue;
|
|
26
|
+
|
|
27
|
+
const dynamic = route.includes("[");
|
|
28
|
+
const declared = typeof def.llms === "function" ? await def.llms() : def.llms;
|
|
29
|
+
const entries: LlmsEntry[] = declared ? (Array.isArray(declared) ? declared : [declared]) : dynamic ? [] : [{}];
|
|
30
|
+
// Static metadata only: a metadata FUNCTION needs loader data we don't run here.
|
|
31
|
+
const meta = typeof def.metadata === "object" ? def.metadata : undefined;
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const path = entry.path ?? (dynamic ? undefined : route);
|
|
35
|
+
if (!path) continue; // a dynamic entry must name its page
|
|
36
|
+
const url = origin + (def.md === false ? path : mdPath(path));
|
|
37
|
+
// dedupe on the FINAL url: "/a" and "/a/" both render /a.md, so they're one page
|
|
38
|
+
if (seen.has(url)) continue;
|
|
39
|
+
seen.add(url);
|
|
40
|
+
const own = path === route; // the route's own page → its metadata describes it
|
|
41
|
+
links.push({
|
|
42
|
+
title: entry.title ?? (own ? meta?.title : undefined) ?? path,
|
|
43
|
+
url,
|
|
44
|
+
description: entry.description ?? (own ? meta?.description : undefined),
|
|
45
|
+
section: entry.section ?? DEFAULT_SECTION,
|
|
46
|
+
optional: entry.optional === true,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return links;
|
|
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/manifest.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// The FREEZE, in-process: import route modules + layouts, build the manifest a
|
|
2
|
+
// createWorker() can run immediately. Extracted from build.ts; used by
|
|
3
|
+
// prerender and by the parity test (its render path is identical to the
|
|
4
|
+
// Rolldown-bundled worker).
|
|
5
|
+
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { basename, join } from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
|
|
10
|
+
import { routeFromModule, type BrandedRoute } from "@junejs/core/route";
|
|
11
|
+
import { freezeConfig } from "./config-freeze";
|
|
12
|
+
import type { ExtraHandler, LayoutComponent, LoadingComponent, ResourceHandler } from "./pipeline";
|
|
13
|
+
import { scanAppRoutes } from "./route-scan";
|
|
14
|
+
import { findMiddlewareFile } from "./router";
|
|
15
|
+
import { resolveBoundary } from "./segment";
|
|
16
|
+
import type { WorkerManifest } from "./worker";
|
|
17
|
+
|
|
18
|
+
export type ImportedLayout = { component: LayoutComponent; boundary: boolean };
|
|
19
|
+
export async function importLayout(file: string): Promise<ImportedLayout | null> {
|
|
20
|
+
const mod = (await import(pathToFileURL(file).href)) as {
|
|
21
|
+
default?: LayoutComponent;
|
|
22
|
+
segmentBoundary?: unknown;
|
|
23
|
+
};
|
|
24
|
+
return typeof mod.default === "function"
|
|
25
|
+
? { component: mod.default, boundary: mod.segmentBoundary === true }
|
|
26
|
+
: null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function buildManifest(appRoot: string): Promise<WorkerManifest> {
|
|
30
|
+
const appDir = join(appRoot, "app");
|
|
31
|
+
const frozen = await freezeConfig(appRoot);
|
|
32
|
+
const scanned = await scanAppRoutes(appRoot);
|
|
33
|
+
|
|
34
|
+
const layoutCache = new Map<string, ImportedLayout | null>();
|
|
35
|
+
const loadCached = async (f: string): Promise<ImportedLayout | null> => {
|
|
36
|
+
if (!layoutCache.has(f)) layoutCache.set(f, await importLayout(f));
|
|
37
|
+
return layoutCache.get(f) ?? null;
|
|
38
|
+
};
|
|
39
|
+
// The chain (root→leaf) + boundary index + shell key, via the SHARED resolver
|
|
40
|
+
// (the one place the deepest-wins rule lives), so the frozen manifest and the
|
|
41
|
+
// dev resolver can't drift — the parity contract.
|
|
42
|
+
const componentsFor = async (
|
|
43
|
+
files: string[],
|
|
44
|
+
): Promise<{ chain: LayoutComponent[]; boundaryIndex: number | null; key: string | null }> => {
|
|
45
|
+
const items = [];
|
|
46
|
+
for (const f of files) {
|
|
47
|
+
const c = await loadCached(f);
|
|
48
|
+
items.push({ file: f, entry: c?.component ?? null, boundary: !!c?.boundary });
|
|
49
|
+
}
|
|
50
|
+
return resolveBoundary(items);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const loadingCache = new Map<string, LoadingComponent | null>();
|
|
54
|
+
const loadingFor = async (file?: string): Promise<LoadingComponent | undefined> => {
|
|
55
|
+
if (!file) return undefined;
|
|
56
|
+
if (!loadingCache.has(file)) {
|
|
57
|
+
const loaded = await importLayout(file);
|
|
58
|
+
loadingCache.set(file, loaded ? (loaded.component as LoadingComponent) : null);
|
|
59
|
+
}
|
|
60
|
+
return loadingCache.get(file) ?? undefined;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const routes: Record<string, BrandedRoute> = {};
|
|
64
|
+
const dynamicRoutes: Array<{ pattern: string; def: BrandedRoute }> = [];
|
|
65
|
+
const resourceRoutes: Array<{ pattern: string; handler: ResourceHandler }> = [];
|
|
66
|
+
const generatedRoutes = scanned.filter((r) => r.generated).map((r) => r.path);
|
|
67
|
+
const layoutChains: Record<string, LayoutComponent[]> = {};
|
|
68
|
+
const layoutBoundaries: Record<string, { index: number; key: string }> = {};
|
|
69
|
+
const loadings: Record<string, LoadingComponent> = {};
|
|
70
|
+
|
|
71
|
+
for (const r of scanned) {
|
|
72
|
+
const mod = await import(pathToFileURL(r.file).href);
|
|
73
|
+
// Resource route (route.*): the default export is the Response handler.
|
|
74
|
+
if (r.resource) {
|
|
75
|
+
const handler = (mod as { default?: unknown }).default;
|
|
76
|
+
if (typeof handler === "function") resourceRoutes.push({ pattern: r.path, handler: handler as ResourceHandler });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const def = routeFromModule(mod);
|
|
80
|
+
if (!def) continue;
|
|
81
|
+
const { chain, boundaryIndex, key } = await componentsFor(r.layouts);
|
|
82
|
+
const loading = await loadingFor(r.loading);
|
|
83
|
+
if (r.dynamic) {
|
|
84
|
+
dynamicRoutes.push({ pattern: r.path, def });
|
|
85
|
+
layoutChains[r.path] = chain;
|
|
86
|
+
} else {
|
|
87
|
+
routes[r.path] = def;
|
|
88
|
+
layoutChains[r.path] = chain;
|
|
89
|
+
}
|
|
90
|
+
if (boundaryIndex !== null && key !== null) layoutBoundaries[r.path] = { index: boundaryIndex, key };
|
|
91
|
+
if (loading) loadings[r.path] = loading;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let extra: ExtraHandler | undefined;
|
|
95
|
+
const extraFile = findMiddlewareFile(appDir);
|
|
96
|
+
if (extraFile) {
|
|
97
|
+
const mod = (await import(pathToFileURL(extraFile).href)) as { default?: unknown };
|
|
98
|
+
if (typeof mod.default === "function") extra = mod.default as ExtraHandler;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The durable agent's DO address (createWorker routes the chat endpoint by
|
|
102
|
+
// it). Set whenever an agent/ directory exists — the surface is inert without
|
|
103
|
+
// an env.AGENT binding, so in-process consumers (prerender, the parity test)
|
|
104
|
+
// are unaffected.
|
|
105
|
+
let agentName: string | undefined;
|
|
106
|
+
const agentDir = join(appDir, frozen.agent.runtime.dir);
|
|
107
|
+
if (frozen.agent.runtime.enabled && existsSync(agentDir)) {
|
|
108
|
+
const cfgFile = join(agentDir, "agent.ts");
|
|
109
|
+
const cfg = existsSync(cfgFile)
|
|
110
|
+
? ((await import(pathToFileURL(cfgFile).href)).default as { name?: string } | undefined)
|
|
111
|
+
: undefined;
|
|
112
|
+
agentName = cfg?.name ?? basename(agentDir);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
routes,
|
|
117
|
+
dynamicRoutes,
|
|
118
|
+
resourceRoutes,
|
|
119
|
+
...(generatedRoutes.length ? { generatedRoutes } : {}),
|
|
120
|
+
layoutChains,
|
|
121
|
+
layoutBoundaries,
|
|
122
|
+
loadings,
|
|
123
|
+
document: frozen.document,
|
|
124
|
+
agent: frozen.agent,
|
|
125
|
+
agentName,
|
|
126
|
+
i18n: frozen.i18n,
|
|
127
|
+
earlyHints: frozen.earlyHints,
|
|
128
|
+
extra,
|
|
129
|
+
};
|
|
130
|
+
}
|