@jay-framework/production-server 0.25.0 → 0.25.2
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/dist/index.d.ts +71 -4
- package/dist/index.js +163 -10
- package/dist/{init-services-CVQ-U9iX.js → init-services-CG_wDJmY.js} +138 -16
- package/dist/{serve-index-C2Tfaz1E.d.ts → serve-index-C28bP6tW.d.ts} +57 -2
- package/dist/serve-index.d.ts +2 -1
- package/dist/serve-index.js +1 -1
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,60 @@
|
|
|
1
|
-
import { R as RouteManifest, a as RouteEntry } from './serve-index-
|
|
2
|
-
export {
|
|
1
|
+
import { R as RouteManifest, a as RouteEntry, A as ArtifactStore } from './serve-index-C28bP6tW.js';
|
|
2
|
+
export { b as ActionEntry, B as BuildMetadata, c as BuildOptions, C as CacheEntry, E as ErrorPageEntry, F as FilesystemArtifactStore, I as InstanceEntry, M as MatchResult, P as PageModule, d as PluginEntry, e as PreImportedPlugin, f as RouteSegment, S as ServerElementModule, g as fetchActionRequest, h as fetchPageRequest, i as fetchStaticFile, j as initializeServices, k as initializeServicesFromModules, l as isActionRequest, m as loadPagePartsFromConfig, n as matchRequest, r as registerActionsFromManifest, o as registerActionsFromModules } from './serve-index-C28bP6tW.js';
|
|
3
|
+
import { HeadTag } from '@jay-framework/fullstack-component';
|
|
4
|
+
import { ErrorInfo } from '@jay-framework/stack-server-runtime';
|
|
3
5
|
import '@jay-framework/ssr-runtime';
|
|
4
|
-
import '@jay-framework/stack-server-runtime';
|
|
5
6
|
|
|
6
7
|
declare function generateSitemap(manifest: RouteManifest, baseUrl: string, outputPath: string): Promise<number>;
|
|
8
|
+
declare function buildUrlFromManifest(pattern: string, params: Record<string, string>): string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Site-level identity config that drives Organization + WebSite JSON-LD (DL#213).
|
|
12
|
+
* Mirrors the `site` block of `.jay` config.
|
|
13
|
+
*/
|
|
14
|
+
interface SiteJsonLdConfig {
|
|
15
|
+
name?: string;
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
logo?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
sameAs?: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build the site-level `Organization` + `WebSite` JSON-LD head tags from `.jay` `site` config.
|
|
23
|
+
*
|
|
24
|
+
* Returns `[]` when `site.name` is absent — there is no identity to describe, and a validation
|
|
25
|
+
* warning (DL#213 Part 5) nudges the author. Emitted once at build (next to the sitemap) and
|
|
26
|
+
* carried on the RouteManifest so every served page gets identical site identity.
|
|
27
|
+
*/
|
|
28
|
+
declare function generateSiteJsonLd(site: SiteJsonLdConfig | undefined): HeadTag[];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Site identity used for the llms.txt header (DL#214). Mirrors the `.jay` `site` config subset
|
|
32
|
+
* that `generateSiteJsonLd` consumes.
|
|
33
|
+
*/
|
|
34
|
+
interface LlmsTxtSiteConfig {
|
|
35
|
+
name?: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Generate an `llms.txt` (llmstxt.org) index from the build manifest + site config (DL#214).
|
|
41
|
+
* A curated, markdown map of the site for LLMs: site H1 + summary, then per-page links with
|
|
42
|
+
* descriptions, grouped into `##` sections by top-level path segment.
|
|
43
|
+
*
|
|
44
|
+
* Returns the number of page links written. Requires `site.name` (the H1); callers skip the
|
|
45
|
+
* emission when it is absent (parallel to the sitemap requiring `baseUrl`).
|
|
46
|
+
*/
|
|
47
|
+
declare function generateLlmsTxt(manifest: RouteManifest, site: LlmsTxtSiteConfig, backendDir: string, outputPath: string): Promise<number>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate a default `robots.txt` (DL#214) — **only when the author has not provided one**
|
|
51
|
+
* (mirrors the sitemap "don't clobber a static file" convention). Returns true if written,
|
|
52
|
+
* false if an author file already exists at `outputPath`.
|
|
53
|
+
*
|
|
54
|
+
* The `Sitemap:` line is emitted only when `baseUrl` is set; without it, the sitemap URL is
|
|
55
|
+
* unknown (and the existing `site.baseUrl` validation warning already fires).
|
|
56
|
+
*/
|
|
57
|
+
declare function generateRobotsTxtIfAbsent(outputPath: string, baseUrl: string | undefined): Promise<boolean>;
|
|
7
58
|
|
|
8
59
|
interface MainServerOptions {
|
|
9
60
|
buildRoot: string;
|
|
@@ -50,6 +101,12 @@ interface RebuildOptions {
|
|
|
50
101
|
minify?: boolean;
|
|
51
102
|
/** Site base URL for sitemap regeneration (e.g. "https://example.com"). */
|
|
52
103
|
siteBaseUrl?: string;
|
|
104
|
+
/** Full site identity for llms.txt regeneration (DL#214). `name` drives the llms.txt H1. */
|
|
105
|
+
site?: {
|
|
106
|
+
name?: string;
|
|
107
|
+
description?: string;
|
|
108
|
+
baseUrl?: string;
|
|
109
|
+
};
|
|
53
110
|
}
|
|
54
111
|
interface RebuildResult {
|
|
55
112
|
affected: number;
|
|
@@ -76,4 +133,14 @@ declare function rebuildContract(options: {
|
|
|
76
133
|
}): Promise<RebuildResult>;
|
|
77
134
|
declare function cleanupOrphanedFiles(buildRoot: string, version: string): Promise<number>;
|
|
78
135
|
|
|
79
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Render a designed error page for a 4xx/5xx outcome (DL#218). Resolves the page by status
|
|
138
|
+
* (exact → family) from `manifest.errorPages`; when none matches — or when rendering throws —
|
|
139
|
+
* falls back to the built-in plain-text response. The response always carries `info.status`.
|
|
140
|
+
*
|
|
141
|
+
* Error pages are a pass-through: no fast render runs; the framework injects the `error` ViewState
|
|
142
|
+
* (`buildErrorViewState`). Interactivity (hydration) still works via the compiled client bundle.
|
|
143
|
+
*/
|
|
144
|
+
declare function renderErrorPage(manifest: RouteManifest, artifacts: ArtifactStore, staticBaseUrl: string, info: ErrorInfo): Promise<Response>;
|
|
145
|
+
|
|
146
|
+
export { ArtifactStore, type LlmsTxtSiteConfig, type MainServerOptions, type RebuildOptions, type RebuildResult, type RebuildTarget, type RendererServerOptions, RouteEntry, RouteManifest, type SiteJsonLdConfig, buildUrlFromManifest, cleanupOrphanedFiles, generateLlmsTxt, generateRobotsTxtIfAbsent, generateSiteJsonLd, generateSitemap, rebuild, rebuildContract, renderErrorPage, resolveContractToRoutes, startMainServer, startRendererServer };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { headMetaToHeadTags, parseCookies, scanPlugins, DevSlowlyChangingPhase, slowRenderInstances } from "@jay-framework/stack-server-runtime";
|
|
2
4
|
import http from "node:http";
|
|
3
5
|
import { Readable } from "node:stream";
|
|
4
|
-
import path from "node:path";
|
|
5
6
|
import { getLogger } from "@jay-framework/logger";
|
|
6
|
-
import { F as FilesystemArtifactStore, i as initializeServices, r as registerActionsFromManifest, d as isActionRequest, f as fetchActionRequest, b as fetchStaticFile, m as matchRequest, a as fetchPageRequest, l as loadPagePartsFromConfig } from "./init-services-
|
|
7
|
-
import { c, e } from "./init-services-
|
|
8
|
-
import { parseCookies, scanPlugins, DevSlowlyChangingPhase, slowRenderInstances } from "@jay-framework/stack-server-runtime";
|
|
7
|
+
import { F as FilesystemArtifactStore, i as initializeServices, r as registerActionsFromManifest, d as isActionRequest, f as fetchActionRequest, b as fetchStaticFile, m as matchRequest, g as renderErrorPage, a as fetchPageRequest, l as loadPagePartsFromConfig } from "./init-services-CG_wDJmY.js";
|
|
8
|
+
import { c, e } from "./init-services-CG_wDJmY.js";
|
|
9
9
|
import { isJayWebhook } from "@jay-framework/fullstack-component";
|
|
10
10
|
import crypto from "node:crypto";
|
|
11
11
|
async function generateSitemap(manifest, baseUrl, outputPath) {
|
|
@@ -49,6 +49,133 @@ function buildUrlFromManifest(pattern, params) {
|
|
|
49
49
|
function escapeXml(str) {
|
|
50
50
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
51
51
|
}
|
|
52
|
+
function resolveUrl(baseUrl, value) {
|
|
53
|
+
if (/^https?:\/\//i.test(value)) return value;
|
|
54
|
+
if (!baseUrl) return value;
|
|
55
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
56
|
+
const rel = value.startsWith("/") ? value : `/${value}`;
|
|
57
|
+
return `${base}${rel}`;
|
|
58
|
+
}
|
|
59
|
+
function generateSiteJsonLd(site) {
|
|
60
|
+
if (!site?.name) return [];
|
|
61
|
+
const base = site.baseUrl?.replace(/\/$/, "");
|
|
62
|
+
const organization = {
|
|
63
|
+
"@context": "https://schema.org",
|
|
64
|
+
"@type": "Organization",
|
|
65
|
+
name: site.name
|
|
66
|
+
};
|
|
67
|
+
if (base) organization.url = base;
|
|
68
|
+
if (site.logo) organization.logo = resolveUrl(site.baseUrl, site.logo);
|
|
69
|
+
if (site.description) organization.description = site.description;
|
|
70
|
+
if (site.sameAs && site.sameAs.length > 0) organization.sameAs = site.sameAs;
|
|
71
|
+
const website = {
|
|
72
|
+
"@context": "https://schema.org",
|
|
73
|
+
"@type": "WebSite",
|
|
74
|
+
name: site.name
|
|
75
|
+
};
|
|
76
|
+
if (base) website.url = base;
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
tag: "script",
|
|
80
|
+
attrs: { type: "application/ld+json" },
|
|
81
|
+
children: JSON.stringify(organization)
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
tag: "script",
|
|
85
|
+
attrs: { type: "application/ld+json" },
|
|
86
|
+
children: JSON.stringify(website)
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
const UNRESOLVED_BINDING = /\{[A-Za-z_$][\w$]*(?:\.[\w$]+)*\}/;
|
|
91
|
+
function hasUnresolved(value) {
|
|
92
|
+
return !!value && UNRESOLVED_BINDING.test(value);
|
|
93
|
+
}
|
|
94
|
+
async function loadSlowViewState(backendDir, cachePath) {
|
|
95
|
+
try {
|
|
96
|
+
const raw = await fs.readFile(path.join(backendDir, cachePath), "utf8");
|
|
97
|
+
return JSON.parse(raw).slowViewState ?? {};
|
|
98
|
+
} catch {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function resolveTitleAndDescription(route, viewState) {
|
|
103
|
+
const tags = headMetaToHeadTags(route.headMeta, viewState);
|
|
104
|
+
const titleTag = tags.find((t) => t.tag.toLowerCase() === "title");
|
|
105
|
+
const descTag = tags.find(
|
|
106
|
+
(t) => t.tag.toLowerCase() === "meta" && t.attrs?.name === "description"
|
|
107
|
+
);
|
|
108
|
+
const title = hasUnresolved(titleTag?.children) ? void 0 : titleTag?.children;
|
|
109
|
+
const description = hasUnresolved(descTag?.attrs?.content) ? void 0 : descTag?.attrs?.content;
|
|
110
|
+
return { title, description };
|
|
111
|
+
}
|
|
112
|
+
async function collectPageLinks(manifest, base, backendDir) {
|
|
113
|
+
const links = [];
|
|
114
|
+
for (const route of manifest.routes) {
|
|
115
|
+
if (route.devOnly || route.noIndex) continue;
|
|
116
|
+
if (route.instances.length === 0) {
|
|
117
|
+
const hasDynamic = route.segments.some((s) => s.type !== "static");
|
|
118
|
+
if (hasDynamic) continue;
|
|
119
|
+
const urlPath = route.pattern === "/" ? "/" : route.pattern;
|
|
120
|
+
const { title, description } = resolveTitleAndDescription(route, {});
|
|
121
|
+
links.push({ title: title ?? urlPath, url: `${base}${urlPath}`, description });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const instance of route.instances) {
|
|
125
|
+
const urlPath = buildUrlFromManifest(route.pattern, instance.params);
|
|
126
|
+
const viewState = await loadSlowViewState(backendDir, instance.cachePath);
|
|
127
|
+
const { title, description } = resolveTitleAndDescription(route, viewState);
|
|
128
|
+
links.push({ title: title ?? urlPath, url: `${base}${urlPath}`, description });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return links;
|
|
132
|
+
}
|
|
133
|
+
function sectionOf(url, base) {
|
|
134
|
+
const pathOnly = url.startsWith(base) ? url.slice(base.length) : url;
|
|
135
|
+
const seg = pathOnly.replace(/^\//, "").split("/")[0];
|
|
136
|
+
return seg || "Home";
|
|
137
|
+
}
|
|
138
|
+
async function generateLlmsTxt(manifest, site, backendDir, outputPath) {
|
|
139
|
+
const base = (site.baseUrl ?? "").replace(/\/$/, "");
|
|
140
|
+
const links = await collectPageLinks(manifest, base, backendDir);
|
|
141
|
+
const lines = [`# ${site.name}`, ""];
|
|
142
|
+
if (site.description) {
|
|
143
|
+
lines.push(`> ${site.description}`, "");
|
|
144
|
+
}
|
|
145
|
+
const bySection = /* @__PURE__ */ new Map();
|
|
146
|
+
for (const link of links) {
|
|
147
|
+
const section = sectionOf(link.url, base);
|
|
148
|
+
const bucket = bySection.get(section) ?? [];
|
|
149
|
+
bucket.push(link);
|
|
150
|
+
bySection.set(section, bucket);
|
|
151
|
+
}
|
|
152
|
+
for (const [section, bucket] of bySection) {
|
|
153
|
+
lines.push(`## ${section}`, "");
|
|
154
|
+
for (const link of bucket) {
|
|
155
|
+
const suffix = link.description ? `: ${link.description}` : "";
|
|
156
|
+
lines.push(`- [${link.title}](${link.url})${suffix}`);
|
|
157
|
+
}
|
|
158
|
+
lines.push("");
|
|
159
|
+
}
|
|
160
|
+
const tmpPath = outputPath + ".tmp";
|
|
161
|
+
await fs.writeFile(tmpPath, lines.join("\n").replace(/\n+$/, "\n"));
|
|
162
|
+
await fs.rename(tmpPath, outputPath);
|
|
163
|
+
return links.length;
|
|
164
|
+
}
|
|
165
|
+
async function generateRobotsTxtIfAbsent(outputPath, baseUrl) {
|
|
166
|
+
try {
|
|
167
|
+
await fs.access(outputPath);
|
|
168
|
+
return false;
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
const base = baseUrl?.replace(/\/$/, "");
|
|
172
|
+
const lines = ["User-agent: *", "Allow: /"];
|
|
173
|
+
if (base) lines.push(`Sitemap: ${base}/sitemap.xml`);
|
|
174
|
+
const tmpPath = outputPath + ".tmp";
|
|
175
|
+
await fs.writeFile(tmpPath, lines.join("\n") + "\n");
|
|
176
|
+
await fs.rename(tmpPath, outputPath);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
52
179
|
function toFetchRequest(req) {
|
|
53
180
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
54
181
|
const headers = new Headers();
|
|
@@ -137,8 +264,11 @@ async function startMainServer(options) {
|
|
|
137
264
|
const currentManifest = await artifacts.readManifest();
|
|
138
265
|
const match = matchRequest(currentManifest, url.pathname);
|
|
139
266
|
if (!match) {
|
|
140
|
-
|
|
141
|
-
|
|
267
|
+
const response2 = await renderErrorPage(currentManifest, artifacts, staticBaseUrl, {
|
|
268
|
+
status: 404,
|
|
269
|
+
url: url.pathname
|
|
270
|
+
});
|
|
271
|
+
await pipeFetchResponse(response2, res);
|
|
142
272
|
return;
|
|
143
273
|
}
|
|
144
274
|
const cookies = parseCookies(req.headers.cookie);
|
|
@@ -155,8 +285,19 @@ async function startMainServer(options) {
|
|
|
155
285
|
logger.error(`[Server] Error handling ${url.pathname}: ${err.message}`);
|
|
156
286
|
if (err.stack) logger.error(err.stack);
|
|
157
287
|
if (!res.headersSent) {
|
|
158
|
-
|
|
159
|
-
|
|
288
|
+
try {
|
|
289
|
+
const currentManifest = await artifacts.readManifest();
|
|
290
|
+
const response = await renderErrorPage(
|
|
291
|
+
currentManifest,
|
|
292
|
+
artifacts,
|
|
293
|
+
staticBaseUrl,
|
|
294
|
+
{ status: 500, url: url.pathname, message: err?.message }
|
|
295
|
+
);
|
|
296
|
+
await pipeFetchResponse(response, res);
|
|
297
|
+
} catch {
|
|
298
|
+
res.writeHead(500);
|
|
299
|
+
res.end("Internal Server Error");
|
|
300
|
+
}
|
|
160
301
|
}
|
|
161
302
|
}
|
|
162
303
|
});
|
|
@@ -298,10 +439,11 @@ async function rebuildInstance(route, params, backendDir, rebuildSuffix) {
|
|
|
298
439
|
} catch (err) {
|
|
299
440
|
return { status: "skipped", reason: `Failed to load page-parts.json: ${err.message}` };
|
|
300
441
|
}
|
|
442
|
+
const pageUrl = buildUrlFromManifest(route.pattern, params);
|
|
301
443
|
const slowPhase = new DevSlowlyChangingPhase();
|
|
302
444
|
const slowResult = await slowPhase.runSlowlyForPage(
|
|
303
445
|
params,
|
|
304
|
-
{
|
|
446
|
+
{ language: "en", url: pageUrl },
|
|
305
447
|
pageParts.parts,
|
|
306
448
|
pageParts.discoveredInstances,
|
|
307
449
|
pageParts.headlessInstanceComponents
|
|
@@ -324,7 +466,7 @@ async function rebuildInstance(route, params, backendDir, rebuildSuffix) {
|
|
|
324
466
|
{
|
|
325
467
|
pageViewState: slowViewState,
|
|
326
468
|
pageParams: params,
|
|
327
|
-
pageProps: { language: "en", url:
|
|
469
|
+
pageProps: { language: "en", url: pageUrl }
|
|
328
470
|
}
|
|
329
471
|
);
|
|
330
472
|
if (instanceSlowResult) {
|
|
@@ -464,6 +606,12 @@ async function rebuild(options) {
|
|
|
464
606
|
const urlCount = await generateSitemap(manifest, options.siteBaseUrl, sitemapPath);
|
|
465
607
|
logger.info(`[Rebuild] Sitemap regenerated: ${urlCount} URLs`);
|
|
466
608
|
}
|
|
609
|
+
const site = options.site ?? (options.siteBaseUrl ? { baseUrl: options.siteBaseUrl } : {});
|
|
610
|
+
if (site.name) {
|
|
611
|
+
const llmsPath = path.join(frontendDir, "llms.txt");
|
|
612
|
+
const count = await generateLlmsTxt(manifest, site, backendDir, llmsPath);
|
|
613
|
+
logger.info(`[Rebuild] llms.txt regenerated: ${count} pages`);
|
|
614
|
+
}
|
|
467
615
|
if (orphanedFiles.length > 0) {
|
|
468
616
|
await appendCleanupManifest(buildDir, orphanedFiles);
|
|
469
617
|
logger.info(`[Rebuild] ${orphanedFiles.length} orphaned file(s) queued for cleanup`);
|
|
@@ -689,10 +837,14 @@ function readBody(req) {
|
|
|
689
837
|
}
|
|
690
838
|
export {
|
|
691
839
|
FilesystemArtifactStore,
|
|
840
|
+
buildUrlFromManifest,
|
|
692
841
|
cleanupOrphanedFiles,
|
|
693
842
|
fetchActionRequest,
|
|
694
843
|
fetchPageRequest,
|
|
695
844
|
fetchStaticFile,
|
|
845
|
+
generateLlmsTxt,
|
|
846
|
+
generateRobotsTxtIfAbsent,
|
|
847
|
+
generateSiteJsonLd,
|
|
696
848
|
generateSitemap,
|
|
697
849
|
initializeServices,
|
|
698
850
|
c as initializeServicesFromModules,
|
|
@@ -703,6 +855,7 @@ export {
|
|
|
703
855
|
rebuildContract,
|
|
704
856
|
registerActionsFromManifest,
|
|
705
857
|
e as registerActionsFromModules,
|
|
858
|
+
renderErrorPage,
|
|
706
859
|
resolveContractToRoutes,
|
|
707
860
|
startMainServer,
|
|
708
861
|
startRendererServer
|
|
@@ -3,11 +3,11 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
|
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveErrorPageToken, buildErrorViewState, headMetaToHeadTags, mergeHeadTags, serializeHeadTags, serializeScripts, getClientInitData, makePassthroughInstanceComponent, renderFastChangingData, actionRegistry, setClientInitData } from "@jay-framework/stack-server-runtime";
|
|
7
7
|
import { deepMergeViewStates } from "@jay-framework/view-state-merge";
|
|
8
8
|
import { asyncSwapScript } from "@jay-framework/ssr-runtime";
|
|
9
|
-
import { isJayAction, isJayStreamAction } from "@jay-framework/fullstack-component";
|
|
10
9
|
import { getLogger } from "@jay-framework/logger";
|
|
10
|
+
import { isJayAction, isJayStreamAction } from "@jay-framework/fullstack-component";
|
|
11
11
|
class FilesystemArtifactStore {
|
|
12
12
|
constructor(basePath) {
|
|
13
13
|
__publicField(this, "manifestCache");
|
|
@@ -137,6 +137,112 @@ function buildImportMap(sharedManifest, publicBasePath, sharedDir = "shared") {
|
|
|
137
137
|
}
|
|
138
138
|
return imports;
|
|
139
139
|
}
|
|
140
|
+
async function renderErrorPage(manifest, artifacts, staticBaseUrl, info) {
|
|
141
|
+
const plainText = () => new Response(info.message || info.title || "Error", {
|
|
142
|
+
status: info.status,
|
|
143
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
144
|
+
});
|
|
145
|
+
const errorPages = manifest.errorPages;
|
|
146
|
+
if (!errorPages) return plainText();
|
|
147
|
+
const token = resolveErrorPageToken(Object.keys(errorPages), info.status);
|
|
148
|
+
if (!token) return plainText();
|
|
149
|
+
const entry = errorPages[token];
|
|
150
|
+
try {
|
|
151
|
+
return await streamErrorPage(entry, manifest, artifacts, staticBaseUrl, info);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
getLogger().error(
|
|
154
|
+
`[Server] Error page "${token}" render failed (${info.status} ${info.url}): ${err?.message}`
|
|
155
|
+
);
|
|
156
|
+
return plainText();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function streamErrorPage(entry, manifest, artifacts, staticBaseUrl, info) {
|
|
160
|
+
const slowViewState = entry.slowCache?.slowViewState ?? {};
|
|
161
|
+
const fullViewState = buildErrorViewState(info, slowViewState);
|
|
162
|
+
const headTagSources = [];
|
|
163
|
+
if (manifest.siteHeadTags && manifest.siteHeadTags.length > 0)
|
|
164
|
+
headTagSources.push(manifest.siteHeadTags);
|
|
165
|
+
const templateHeadTags = headMetaToHeadTags(entry.headMeta, fullViewState);
|
|
166
|
+
const headTags = headTagSources.length > 0 || templateHeadTags.length > 0 ? mergeHeadTags(headTagSources, templateHeadTags) : [];
|
|
167
|
+
const hasCustomTitle = headTags.some((t) => t.tag?.toLowerCase() === "title");
|
|
168
|
+
const titleTag = hasCustomTitle ? "" : ` <title>${info.status} Error</title>
|
|
169
|
+
`;
|
|
170
|
+
const preconnectTags = headTags.filter(
|
|
171
|
+
(t) => t.tag === "link" && t.attrs?.rel === "preconnect"
|
|
172
|
+
);
|
|
173
|
+
const otherHeadTags = headTags.filter(
|
|
174
|
+
(t) => !(t.tag === "link" && t.attrs?.rel === "preconnect")
|
|
175
|
+
);
|
|
176
|
+
const preconnectHtml = preconnectTags.length > 0 ? serializeHeadTags(preconnectTags) + "\n" : "";
|
|
177
|
+
const headTagsHtml = otherHeadTags.length > 0 ? serializeHeadTags(otherHeadTags) + "\n" : "";
|
|
178
|
+
const headScriptsHtml = serializeScripts(entry.scripts, "head");
|
|
179
|
+
const bodyScriptsHtml = serializeScripts(entry.scripts, "body");
|
|
180
|
+
const serverElement = await artifacts.loadServerElement(entry.serverElementPath);
|
|
181
|
+
const asyncPromises = [];
|
|
182
|
+
const importMap = buildImportMap(manifest.sharedManifest, staticBaseUrl);
|
|
183
|
+
if (entry.routeHydratePath) {
|
|
184
|
+
importMap["jay-route-hydrate"] = `${staticBaseUrl}${entry.routeHydratePath}`;
|
|
185
|
+
}
|
|
186
|
+
const clientBundleUrl = entry.routeClientBundlePath ? `${staticBaseUrl}${entry.routeClientBundlePath}` : "";
|
|
187
|
+
const sharedPreloads = entry.sharedDeps ? entry.sharedDeps.map((dep) => importMap[dep]).filter(Boolean) : Object.values(importMap);
|
|
188
|
+
const preloadUrls = clientBundleUrl ? [...sharedPreloads, clientBundleUrl] : [];
|
|
189
|
+
const modulePreloads = preloadUrls.map((url) => ` <link rel="modulepreload" href="${url}" />`).join("\n");
|
|
190
|
+
const cssUrl = entry.routeCssPath ? `${staticBaseUrl}${entry.routeCssPath}` : "";
|
|
191
|
+
const cssPreload = cssUrl ? ` <link rel="preload" href="${cssUrl}" as="style" />` : "";
|
|
192
|
+
const cssLink = cssUrl ? ` <link rel="stylesheet" href="${cssUrl}" />` : "";
|
|
193
|
+
const cssImportPreloads = (entry.cssImports ?? []).map((url) => ` <link rel="preload" href="${url}" as="style" />`).join("\n");
|
|
194
|
+
const encoder = new TextEncoder();
|
|
195
|
+
const stream = new ReadableStream({
|
|
196
|
+
async start(controller) {
|
|
197
|
+
const write = (s) => controller.enqueue(encoder.encode(s));
|
|
198
|
+
const headParts = [headTagsHtml, modulePreloads, cssLink, headScriptsHtml].filter(Boolean).join("\n");
|
|
199
|
+
write(`<!doctype html>
|
|
200
|
+
<html lang="en">
|
|
201
|
+
<head>
|
|
202
|
+
<meta charset="UTF-8" />
|
|
203
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
204
|
+
${titleTag}${[preconnectHtml, cssPreload, cssImportPreloads].filter(Boolean).map((l) => l + "\n").join(
|
|
205
|
+
""
|
|
206
|
+
)} <script type="importmap">${JSON.stringify({ imports: importMap })}<\/script>
|
|
207
|
+
${headParts}
|
|
208
|
+
</head>
|
|
209
|
+
<body>
|
|
210
|
+
<div id="target">`);
|
|
211
|
+
serverElement.renderToStream(fullViewState, {
|
|
212
|
+
write: (chunk) => write(chunk),
|
|
213
|
+
onAsync: (promise, id, templates) => {
|
|
214
|
+
asyncPromises.push(
|
|
215
|
+
promise.then(
|
|
216
|
+
(val) => asyncSwapScript(id, templates.resolved(val)),
|
|
217
|
+
(err) => asyncSwapScript(id, templates.rejected(err))
|
|
218
|
+
)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
write("</div>");
|
|
223
|
+
const asyncScripts = (await Promise.all(asyncPromises)).filter((s) => s).join("");
|
|
224
|
+
if (asyncScripts) write(asyncScripts);
|
|
225
|
+
if (clientBundleUrl) {
|
|
226
|
+
const clientInitData = getClientInitData();
|
|
227
|
+
const initArgs = `${JSON.stringify({})}, ${JSON.stringify(fullViewState)}, ${JSON.stringify({})}, ${JSON.stringify(clientInitData)}`;
|
|
228
|
+
write(`
|
|
229
|
+
<script type="module">
|
|
230
|
+
import { init } from '${clientBundleUrl}';
|
|
231
|
+
await init(${initArgs});
|
|
232
|
+
<\/script>`);
|
|
233
|
+
}
|
|
234
|
+
if (bodyScriptsHtml) write("\n " + bodyScriptsHtml);
|
|
235
|
+
write(`
|
|
236
|
+
</body>
|
|
237
|
+
</html>`);
|
|
238
|
+
controller.close();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
return new Response(stream, {
|
|
242
|
+
status: info.status,
|
|
243
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
244
|
+
});
|
|
245
|
+
}
|
|
140
246
|
async function loadPagePartsFromConfig(configPath, artifacts) {
|
|
141
247
|
const config = await artifacts.readPagePartsConfig(configPath);
|
|
142
248
|
async function importModule(entry) {
|
|
@@ -237,8 +343,13 @@ async function fetchPageRequest(match, manifest, requestUrl, artifacts, staticBa
|
|
|
237
343
|
});
|
|
238
344
|
}
|
|
239
345
|
if (fastResult.kind === "ServerError" || fastResult.kind === "ClientError") {
|
|
240
|
-
return
|
|
241
|
-
status: fastResult.status
|
|
346
|
+
return renderErrorPage(manifest, artifacts, staticBaseUrl, {
|
|
347
|
+
status: fastResult.status,
|
|
348
|
+
url: requestUrl.pathname,
|
|
349
|
+
title: fastResult.title,
|
|
350
|
+
message: fastResult.message,
|
|
351
|
+
code: fastResult.code,
|
|
352
|
+
details: fastResult.details
|
|
242
353
|
});
|
|
243
354
|
}
|
|
244
355
|
const fastViewState = fastResult.rendered || {};
|
|
@@ -255,12 +366,13 @@ async function fetchPageRequest(match, manifest, requestUrl, artifacts, staticBa
|
|
|
255
366
|
url: { path: requestUrl.pathname }
|
|
256
367
|
};
|
|
257
368
|
const headTagSources = [];
|
|
369
|
+
if (manifest.siteHeadTags && manifest.siteHeadTags.length > 0)
|
|
370
|
+
headTagSources.push(manifest.siteHeadTags);
|
|
258
371
|
const slowHeadTags = cf.__slowHeadTags;
|
|
259
372
|
if (slowHeadTags) headTagSources.push(...slowHeadTags);
|
|
260
373
|
if (fastResult.headTags) headTagSources.push(fastResult.headTags);
|
|
261
374
|
const templateHeadTags = headMetaToHeadTags(route.headMeta, fullViewState);
|
|
262
|
-
|
|
263
|
-
const headTags = headTagSources.length > 0 ? mergeHeadTags(headTagSources) : [];
|
|
375
|
+
const headTags = headTagSources.length > 0 || templateHeadTags.length > 0 ? mergeHeadTags(headTagSources, templateHeadTags) : [];
|
|
264
376
|
const hasCustomTitle = headTags.some((t) => t.tag?.toLowerCase() === "title");
|
|
265
377
|
const titleTag = hasCustomTitle ? "" : " <title>Vite + TS</title>\n";
|
|
266
378
|
const preconnectTags = headTags.filter(
|
|
@@ -271,6 +383,8 @@ async function fetchPageRequest(match, manifest, requestUrl, artifacts, staticBa
|
|
|
271
383
|
);
|
|
272
384
|
const preconnectHtml = preconnectTags.length > 0 ? serializeHeadTags(preconnectTags) + "\n" : "";
|
|
273
385
|
const headTagsHtml = otherHeadTags.length > 0 ? serializeHeadTags(otherHeadTags) + "\n" : "";
|
|
386
|
+
const headScriptsHtml = serializeScripts(route.scripts, "head");
|
|
387
|
+
const bodyScriptsHtml = serializeScripts(route.scripts, "body");
|
|
274
388
|
const serverElementPath = route.serverElementPath || instance.serverElementPath;
|
|
275
389
|
const tLoadStart = Date.now();
|
|
276
390
|
const serverElement = await artifacts.loadServerElement(serverElementPath);
|
|
@@ -292,7 +406,7 @@ async function fetchPageRequest(match, manifest, requestUrl, artifacts, staticBa
|
|
|
292
406
|
const stream = new ReadableStream({
|
|
293
407
|
async start(controller) {
|
|
294
408
|
const write = (s) => controller.enqueue(encoder.encode(s));
|
|
295
|
-
const headParts = [headTagsHtml, modulePreloads, cssLink].filter(Boolean).join("\n");
|
|
409
|
+
const headParts = [headTagsHtml, modulePreloads, cssLink, headScriptsHtml].filter(Boolean).join("\n");
|
|
296
410
|
write(`<!doctype html>
|
|
297
411
|
<html lang="en">
|
|
298
412
|
<head>
|
|
@@ -340,7 +454,7 @@ ${headParts}
|
|
|
340
454
|
const _t=performance.now();
|
|
341
455
|
await init(${initArgs});
|
|
342
456
|
console.log('[jay] hydrate: '+(performance.now()-_t).toFixed(1)+'ms');
|
|
343
|
-
<\/script
|
|
457
|
+
<\/script>${bodyScriptsHtml ? "\n " + bodyScriptsHtml : ""}
|
|
344
458
|
</body>
|
|
345
459
|
</html>`);
|
|
346
460
|
controller.close();
|
|
@@ -537,7 +651,12 @@ const MIME_TYPES = {
|
|
|
537
651
|
".ttf": "font/ttf",
|
|
538
652
|
".webp": "image/webp",
|
|
539
653
|
".xml": "application/xml",
|
|
540
|
-
".txt": "text/plain"
|
|
654
|
+
".txt": "text/plain",
|
|
655
|
+
// Agent-readiness resources (DL#217): SKILL.md + packaged skill archives.
|
|
656
|
+
".md": "text/markdown",
|
|
657
|
+
".gz": "application/gzip",
|
|
658
|
+
// `.tar.gz` resolves via its last extension `.gz`
|
|
659
|
+
".tar": "application/x-tar"
|
|
541
660
|
};
|
|
542
661
|
async function fetchStaticFile(pathname, frontendDir) {
|
|
543
662
|
const normalizedBase = path.resolve(frontendDir);
|
|
@@ -550,13 +669,15 @@ async function fetchStaticFile(pathname, frontendDir) {
|
|
|
550
669
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
551
670
|
const isHashed = /[-][a-zA-Z0-9_-]{6,}\./.test(path.basename(candidate));
|
|
552
671
|
const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "public, max-age=3600";
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
672
|
+
const headers = {
|
|
673
|
+
"Content-Type": contentType,
|
|
674
|
+
"Content-Length": String(content.length),
|
|
675
|
+
"Cache-Control": cacheControl
|
|
676
|
+
};
|
|
677
|
+
if (pathname.startsWith("/.well-known/")) {
|
|
678
|
+
headers["Access-Control-Allow-Origin"] = "*";
|
|
679
|
+
}
|
|
680
|
+
return new Response(new Uint8Array(content), { headers });
|
|
560
681
|
} catch {
|
|
561
682
|
}
|
|
562
683
|
}
|
|
@@ -632,6 +753,7 @@ export {
|
|
|
632
753
|
isActionRequest as d,
|
|
633
754
|
registerActionsFromModules as e,
|
|
634
755
|
fetchActionRequest as f,
|
|
756
|
+
renderErrorPage as g,
|
|
635
757
|
initializeServices as i,
|
|
636
758
|
loadPagePartsFromConfig as l,
|
|
637
759
|
matchRequest as m,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as _jay_framework_ssr_runtime from '@jay-framework/ssr-runtime';
|
|
2
|
-
import { JayHtmlHeadMeta, DevServerPagePart, HeadlessInstanceComponent, ForEachHeadlessInstance, ActionRegistry } from '@jay-framework/stack-server-runtime';
|
|
2
|
+
import { JayHtmlHeadMeta, JayHtmlScript, DevServerPagePart, HeadlessInstanceComponent, ForEachHeadlessInstance, ActionRegistry } from '@jay-framework/stack-server-runtime';
|
|
3
|
+
import { HeadTag } from '@jay-framework/fullstack-component';
|
|
3
4
|
|
|
4
5
|
interface RouteManifest {
|
|
5
6
|
version: string;
|
|
@@ -8,6 +9,50 @@ interface RouteManifest {
|
|
|
8
9
|
routes: RouteEntry[];
|
|
9
10
|
actions: ActionEntry[];
|
|
10
11
|
plugins: PluginEntry[];
|
|
12
|
+
/**
|
|
13
|
+
* Site-level JSON-LD (Organization + WebSite) head tags, generated once at build from
|
|
14
|
+
* `.jay` `site` config and injected into every served page's `<head>` (DL#213). Empty/absent
|
|
15
|
+
* when `site.name` is not configured.
|
|
16
|
+
*/
|
|
17
|
+
siteHeadTags?: HeadTag[];
|
|
18
|
+
/**
|
|
19
|
+
* Compiled error pages keyed by status token (`"404"`, `"502"`, `"5xx"`, …) (DL#218).
|
|
20
|
+
* Resolved by status (exact → family), never by URL — kept out of `routes[]` so they are
|
|
21
|
+
* non-navigable and excluded from sitemap/llms.txt/robots/internal-link validation.
|
|
22
|
+
*/
|
|
23
|
+
errorPages?: Record<string, ErrorPageEntry>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A compiled error page (DL#218). Mirrors the per-route compiled refs of a single-instance
|
|
27
|
+
* route, minus URL/param machinery: an error page is resolved by status, not by matching a URL.
|
|
28
|
+
*/
|
|
29
|
+
interface ErrorPageEntry {
|
|
30
|
+
/** Status token this page serves — exact (`"404"`, `"502"`) or family (`"4xx"`, `"5xx"`). */
|
|
31
|
+
token: string;
|
|
32
|
+
/** Per-page server element path (relative to backend dir). */
|
|
33
|
+
serverElementPath: string;
|
|
34
|
+
/** Per-page CSS file path (relative to frontend dir). */
|
|
35
|
+
routeCssPath?: string;
|
|
36
|
+
/** Per-page hydrate script path (relative to frontend dir). */
|
|
37
|
+
routeHydratePath?: string;
|
|
38
|
+
/** Per-page client bundle path (relative to frontend dir). */
|
|
39
|
+
routeClientBundlePath?: string;
|
|
40
|
+
/** External @import URLs extracted from CSS at build time. */
|
|
41
|
+
cssImports?: string[];
|
|
42
|
+
/** Head metadata from the error page's jay-html <head>. */
|
|
43
|
+
headMeta?: JayHtmlHeadMeta;
|
|
44
|
+
/** Passthrough `<script jay-script="allow">` tags from the template. */
|
|
45
|
+
scripts?: JayHtmlScript[];
|
|
46
|
+
/** Shared chunk package names this page's hydrate/client bundle imports (DL#177). */
|
|
47
|
+
sharedDeps?: string[];
|
|
48
|
+
/** trackBy map threaded to the client hydration entry. */
|
|
49
|
+
trackByMap?: Record<string, string>;
|
|
50
|
+
/**
|
|
51
|
+
* Optional pre-rendered slow (static chrome) ViewState. Unused in v1 — an error page is a
|
|
52
|
+
* pass-through of the injected `error` ViewState (DL#218 Q6); its static chrome renders
|
|
53
|
+
* from the template without slow data. The renderer merges `slowCache?.slowViewState ?? {}`.
|
|
54
|
+
*/
|
|
55
|
+
slowCache?: CacheEntry;
|
|
11
56
|
}
|
|
12
57
|
interface RouteSegment {
|
|
13
58
|
type: 'static' | 'param' | 'optional' | 'catchAll' | 'optionalCatchAll';
|
|
@@ -32,6 +77,8 @@ interface RouteEntry {
|
|
|
32
77
|
cssImports?: string[];
|
|
33
78
|
/** Head metadata from jay-html <head> (title, meta tags). */
|
|
34
79
|
headMeta?: JayHtmlHeadMeta;
|
|
80
|
+
/** Passthrough `<script jay-script="allow">` tags from the jay-html template (DL#149 Phase 2). */
|
|
81
|
+
scripts?: JayHtmlScript[];
|
|
35
82
|
/** True when page has static <meta name="robots" content="noindex"> */
|
|
36
83
|
noIndex?: boolean;
|
|
37
84
|
instances: InstanceEntry[];
|
|
@@ -80,6 +127,14 @@ interface BuildOptions {
|
|
|
80
127
|
minify?: boolean;
|
|
81
128
|
/** Site base URL for sitemap generation (e.g. "https://example.com"). */
|
|
82
129
|
siteBaseUrl?: string;
|
|
130
|
+
/** Site identity for Organization + WebSite JSON-LD generation (DL#213). */
|
|
131
|
+
site?: {
|
|
132
|
+
name?: string;
|
|
133
|
+
baseUrl?: string;
|
|
134
|
+
logo?: string;
|
|
135
|
+
description?: string;
|
|
136
|
+
sameAs?: string[];
|
|
137
|
+
};
|
|
83
138
|
}
|
|
84
139
|
interface ServerElementModule {
|
|
85
140
|
renderToStream: (vs: object, ctx: _jay_framework_ssr_runtime.ServerRenderContext) => void;
|
|
@@ -202,4 +257,4 @@ interface PreImportedPlugin {
|
|
|
202
257
|
declare function initializeServicesFromModules(plugins: PreImportedPlugin[], label: string): Promise<void>;
|
|
203
258
|
declare function initializeServices(buildDir: string, projectRoot: string, label: string): Promise<void>;
|
|
204
259
|
|
|
205
|
-
export { type
|
|
260
|
+
export { type ArtifactStore as A, type BuildMetadata as B, type CacheEntry as C, type ErrorPageEntry as E, FilesystemArtifactStore as F, type InstanceEntry as I, type MatchResult as M, type PageModule as P, type RouteManifest as R, type ServerElementModule as S, type RouteEntry as a, type ActionEntry as b, type BuildOptions as c, type PluginEntry as d, type PreImportedPlugin as e, type RouteSegment as f, fetchActionRequest as g, fetchPageRequest as h, fetchStaticFile as i, initializeServices as j, initializeServicesFromModules as k, isActionRequest as l, loadPagePartsFromConfig as m, matchRequest as n, registerActionsFromModules as o, registerActionsFromManifest as r };
|
package/dist/serve-index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { A as ArtifactStore, C as CacheEntry, F as FilesystemArtifactStore, I as InstanceEntry, M as MatchResult, e as PreImportedPlugin, a as RouteEntry, R as RouteManifest, S as ServerElementModule, g as fetchActionRequest, h as fetchPageRequest, i as fetchStaticFile, j as initializeServices, k as initializeServicesFromModules, l as isActionRequest, n as matchRequest, r as registerActionsFromManifest, o as registerActionsFromModules } from './serve-index-C28bP6tW.js';
|
|
2
2
|
import '@jay-framework/ssr-runtime';
|
|
3
3
|
import '@jay-framework/stack-server-runtime';
|
|
4
|
+
import '@jay-framework/fullstack-component';
|
package/dist/serve-index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/production-server",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"test:watch": "vitest"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@jay-framework/fullstack-component": "^0.25.
|
|
34
|
-
"@jay-framework/logger": "^0.25.
|
|
35
|
-
"@jay-framework/ssr-runtime": "^0.25.
|
|
36
|
-
"@jay-framework/stack-server-runtime": "^0.25.
|
|
37
|
-
"@jay-framework/view-state-merge": "^0.25.
|
|
33
|
+
"@jay-framework/fullstack-component": "^0.25.2",
|
|
34
|
+
"@jay-framework/logger": "^0.25.2",
|
|
35
|
+
"@jay-framework/ssr-runtime": "^0.25.2",
|
|
36
|
+
"@jay-framework/stack-server-runtime": "^0.25.2",
|
|
37
|
+
"@jay-framework/view-state-merge": "^0.25.2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@jay-framework/dev-environment": "^0.25.
|
|
40
|
+
"@jay-framework/dev-environment": "^0.25.2",
|
|
41
41
|
"@types/node": "^22.15.21",
|
|
42
42
|
"rimraf": "^5.0.5",
|
|
43
43
|
"tsup": "^8.0.1",
|