@jay-framework/production-server 0.25.1 → 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 +160 -8
- package/dist/{init-services-XQt5vxN_.js → init-services-CG_wDJmY.js} +133 -12
- package/dist/{serve-index-CUXzYZBC.d.ts → serve-index-C28bP6tW.d.ts} +54 -1
- 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
|
});
|
|
@@ -465,6 +606,12 @@ async function rebuild(options) {
|
|
|
465
606
|
const urlCount = await generateSitemap(manifest, options.siteBaseUrl, sitemapPath);
|
|
466
607
|
logger.info(`[Rebuild] Sitemap regenerated: ${urlCount} URLs`);
|
|
467
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
|
+
}
|
|
468
615
|
if (orphanedFiles.length > 0) {
|
|
469
616
|
await appendCleanupManifest(buildDir, orphanedFiles);
|
|
470
617
|
logger.info(`[Rebuild] ${orphanedFiles.length} orphaned file(s) queued for cleanup`);
|
|
@@ -690,10 +837,14 @@ function readBody(req) {
|
|
|
690
837
|
}
|
|
691
838
|
export {
|
|
692
839
|
FilesystemArtifactStore,
|
|
840
|
+
buildUrlFromManifest,
|
|
693
841
|
cleanupOrphanedFiles,
|
|
694
842
|
fetchActionRequest,
|
|
695
843
|
fetchPageRequest,
|
|
696
844
|
fetchStaticFile,
|
|
845
|
+
generateLlmsTxt,
|
|
846
|
+
generateRobotsTxtIfAbsent,
|
|
847
|
+
generateSiteJsonLd,
|
|
697
848
|
generateSitemap,
|
|
698
849
|
initializeServices,
|
|
699
850
|
c as initializeServicesFromModules,
|
|
@@ -704,6 +855,7 @@ export {
|
|
|
704
855
|
rebuildContract,
|
|
705
856
|
registerActionsFromManifest,
|
|
706
857
|
e as registerActionsFromModules,
|
|
858
|
+
renderErrorPage,
|
|
707
859
|
resolveContractToRoutes,
|
|
708
860
|
startMainServer,
|
|
709
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,6 +366,8 @@ 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);
|
|
@@ -538,7 +651,12 @@ const MIME_TYPES = {
|
|
|
538
651
|
".ttf": "font/ttf",
|
|
539
652
|
".webp": "image/webp",
|
|
540
653
|
".xml": "application/xml",
|
|
541
|
-
".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"
|
|
542
660
|
};
|
|
543
661
|
async function fetchStaticFile(pathname, frontendDir) {
|
|
544
662
|
const normalizedBase = path.resolve(frontendDir);
|
|
@@ -551,13 +669,15 @@ async function fetchStaticFile(pathname, frontendDir) {
|
|
|
551
669
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
552
670
|
const isHashed = /[-][a-zA-Z0-9_-]{6,}\./.test(path.basename(candidate));
|
|
553
671
|
const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "public, max-age=3600";
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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 });
|
|
561
681
|
} catch {
|
|
562
682
|
}
|
|
563
683
|
}
|
|
@@ -633,6 +753,7 @@ export {
|
|
|
633
753
|
isActionRequest as d,
|
|
634
754
|
registerActionsFromModules as e,
|
|
635
755
|
fetchActionRequest as f,
|
|
756
|
+
renderErrorPage as g,
|
|
636
757
|
initializeServices as i,
|
|
637
758
|
loadPagePartsFromConfig as l,
|
|
638
759
|
matchRequest as m,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as _jay_framework_ssr_runtime from '@jay-framework/ssr-runtime';
|
|
2
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';
|
|
@@ -82,6 +127,14 @@ interface BuildOptions {
|
|
|
82
127
|
minify?: boolean;
|
|
83
128
|
/** Site base URL for sitemap generation (e.g. "https://example.com"). */
|
|
84
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
|
+
};
|
|
85
138
|
}
|
|
86
139
|
interface ServerElementModule {
|
|
87
140
|
renderToStream: (vs: object, ctx: _jay_framework_ssr_runtime.ServerRenderContext) => void;
|
|
@@ -204,4 +257,4 @@ interface PreImportedPlugin {
|
|
|
204
257
|
declare function initializeServicesFromModules(plugins: PreImportedPlugin[], label: string): Promise<void>;
|
|
205
258
|
declare function initializeServices(buildDir: string, projectRoot: string, label: string): Promise<void>;
|
|
206
259
|
|
|
207
|
-
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",
|