@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/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
|
|
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({
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
184
|
+
const servesCatalog = !!pageUrl && catalogOrigin(new URL(pageUrl)) !== null;
|
|
185
|
+
return {
|
|
98
186
|
...docConfig,
|
|
99
|
-
webmcpTools
|
|
100
|
-
|
|
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
|
|
126
|
-
return
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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":
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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")
|
|
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
|
|
233
|
-
services
|
|
483
|
+
resources,
|
|
484
|
+
services
|
|
234
485
|
}, () => handleRequest(request));
|
|
235
486
|
} };
|
|
236
487
|
}
|
|
237
488
|
//#endregion
|
|
238
|
-
export { createPipeline };
|
|
489
|
+
export { createPipeline, htmlPath, isDecodablePath, markdownPath };
|
package/dist/resources.d.ts
CHANGED
|
@@ -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
|
package/dist/route-rank.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
//#region src/route-rank.d.ts
|
|
2
|
-
type SegmentKind = "static" | "param" | "optional" | "catchAll" | "optionalCatchAll";
|
|
3
|
-
declare function parseSegment(segment: string): {
|
|
2
|
+
export type SegmentKind = "static" | "param" | "optional" | "catchAll" | "optionalCatchAll";
|
|
3
|
+
export declare function parseSegment(segment: string): {
|
|
4
4
|
kind: SegmentKind;
|
|
5
5
|
name?: string;
|
|
6
6
|
};
|
|
7
|
-
declare function segmentRank(segment: string): number;
|
|
8
|
-
declare function compareSegments(a: string, b: string): number;
|
|
9
|
-
declare function compareRoutePatterns(a: string, b: string): number;
|
|
10
|
-
//#endregion
|
|
11
|
-
export { SegmentKind, compareRoutePatterns, compareSegments, parseSegment, segmentRank };
|
|
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,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,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 };
|