vike-lite 1.15.8 → 1.15.10

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.
@@ -1,10 +1,4 @@
1
- //#region src/__internal/shared/matchRoute.d.ts
2
- declare function matchRoute(urlPathname: string, routes: typeof import('virtual:vike-lite/routes').routes): {
3
- route: any;
4
- routeParams: Record<string, string>;
5
- } | null;
6
- //#endregion
7
- //#region src/__internal/shared/index.d.ts
1
+ //#region src/__internal/shared.d.ts
8
2
  interface RenderContext {
9
3
  pageContext: any;
10
4
  Page: unknown;
@@ -19,5 +13,11 @@ interface RenderContext {
19
13
  };
20
14
  nonce?: string;
21
15
  }
16
+ declare function matchRoute(urlPathname: string, routes: typeof import('virtual:vike-lite/routes').routes): {
17
+ route: any;
18
+ routeParams: Record<string, string>;
19
+ } | null;
20
+ declare const BASE_URL: string;
21
+ declare function stripBase(pathname: string): string;
22
22
  //#endregion
23
- export { RenderContext, matchRoute };
23
+ export { BASE_URL, RenderContext, matchRoute, stripBase };
@@ -1,2 +1,54 @@
1
- import { t as matchRoute } from "../matchRoute-nTNPxoMq.mjs";
2
- export { matchRoute };
1
+ //#region src/__internal/shared.ts
2
+ const regexCache = /* @__PURE__ */ new Map();
3
+ function escapeRegex(str) {
4
+ return str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
5
+ }
6
+ function matchRoute(urlPathname, routes) {
7
+ for (const route of routes) {
8
+ if (!route.path.includes(":")) {
9
+ if (route.path === urlPathname || route.path + "/" === urlPathname) return {
10
+ route,
11
+ routeParams: {}
12
+ };
13
+ continue;
14
+ }
15
+ let compiled = regexCache.get(route.path);
16
+ if (!compiled) {
17
+ const paramNames = [];
18
+ const regexPath = route.path.split("/").map((segment) => {
19
+ if (segment.startsWith(":")) {
20
+ paramNames.push(segment.slice(1));
21
+ return "([^/]+)";
22
+ }
23
+ return escapeRegex(segment);
24
+ }).join("/");
25
+ compiled = {
26
+ regex: new RegExp(`^${regexPath}/?$`),
27
+ paramNames
28
+ };
29
+ regexCache.set(route.path, compiled);
30
+ }
31
+ const match = urlPathname.match(compiled.regex);
32
+ if (match) {
33
+ const routeParams = {};
34
+ for (let i = 0; i < compiled.paramNames.length; i++) routeParams[compiled.paramNames[i]] = decodeURIComponent(match[i + 1]);
35
+ return {
36
+ route,
37
+ routeParams
38
+ };
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ const BASE_URL = (() => {
44
+ const { BASE_URL } = import.meta.env;
45
+ return BASE_URL.endsWith("/") ? BASE_URL.slice(0, -1) : BASE_URL;
46
+ })();
47
+ function stripBase(pathname) {
48
+ if (BASE_URL === "") return pathname;
49
+ if (pathname === BASE_URL) return "/";
50
+ if (pathname.startsWith(BASE_URL + "/")) return pathname.slice(BASE_URL.length);
51
+ return pathname;
52
+ }
53
+ //#endregion
54
+ export { BASE_URL, matchRoute, stripBase };
@@ -1,3 +1,4 @@
1
+ import { BASE_URL } from "../__internal/shared.mjs";
1
2
  //#region src/client/router.ts
2
3
  /**
3
4
  * Change page programmatically on the client without reloading the browser.
@@ -7,10 +8,7 @@
7
8
  function navigate(url, options) {
8
9
  if (typeof globalThis === "undefined") throw new Error("navigate() can only be called on the client side.");
9
10
  let finalUrl = url;
10
- if (finalUrl.startsWith("/")) {
11
- const { BASE_URL } = import.meta.env;
12
- finalUrl = (BASE_URL.endsWith("/") ? BASE_URL.slice(0, -1) : BASE_URL) + (finalUrl === "/" ? "" : finalUrl);
13
- }
11
+ if (finalUrl.startsWith("/")) finalUrl = BASE_URL + (finalUrl === "/" ? "" : finalUrl);
14
12
  globalThis.history.pushState({ triggeredBy: "vike-lite" }, "", finalUrl);
15
13
  globalThis.dispatchEvent(new CustomEvent("vike-navigate", { detail: {
16
14
  keepScrollPosition: options?.keepScrollPosition,
package/dist/server.mjs CHANGED
@@ -1,2 +1,216 @@
1
- import { t as renderPage } from "./server-DSrh6B3f.mjs";
1
+ import { BASE_URL, matchRoute, stripBase } from "./__internal/shared.mjs";
2
+ import { AbortRedirect, AbortRender } from "./server/abort.mjs";
3
+ import { n as store } from "./store-BCs8gL_o.mjs";
4
+ //#region src/utils/serializeContext.ts
5
+ const ESCAPE_LOOKUP = {
6
+ "&": String.raw`\u0026`,
7
+ ">": String.raw`\u003e`,
8
+ "<": String.raw`\u003c`,
9
+ "\u2028": String.raw`\u2028`,
10
+ "\u2029": String.raw`\u2029`
11
+ };
12
+ const ESCAPE_REGEX = /[&><\u{2028}\u{2029}]/gu;
13
+ function serializeContext(data) {
14
+ return JSON.stringify(data).replaceAll(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
15
+ }
16
+ //#endregion
17
+ //#region src/server/renderPage.ts
18
+ const isProd = process.env.NODE_ENV === "production";
19
+ function withBase(file) {
20
+ return `${BASE_URL === "" ? "" : BASE_URL}${file.startsWith("/") ? file : `/${file}`}`;
21
+ }
22
+ const assetsCache = /* @__PURE__ */ new WeakMap();
23
+ function computeAssetFiles(route) {
24
+ const cssFiles = /* @__PURE__ */ new Set();
25
+ const jsFiles = /* @__PURE__ */ new Map();
26
+ const { manifest } = store;
27
+ function getVirtualEntryClientKey() {
28
+ for (const key in manifest) if (manifest[key].isEntry) return key;
29
+ throw new Error("entry-client not found in manifest");
30
+ }
31
+ function collectAssets(key, isCritical) {
32
+ const chunk = manifest[key];
33
+ if (!chunk) return;
34
+ const current = jsFiles.get(chunk.file);
35
+ if (current === true) return;
36
+ if (current === false && !isCritical) return;
37
+ jsFiles.set(chunk.file, isCritical);
38
+ if (chunk.css) for (const css of chunk.css) cssFiles.add(css);
39
+ if (chunk.imports) for (const imp of chunk.imports) collectAssets(imp, false);
40
+ }
41
+ const virtualEntryClientKey = getVirtualEntryClientKey();
42
+ collectAssets(virtualEntryClientKey, true);
43
+ const { page, layout, head } = route;
44
+ collectAssets(page, true);
45
+ if (head) collectAssets(head, true);
46
+ if (layout) collectAssets(layout, true);
47
+ const criticalJs = [];
48
+ const sharedJs = [];
49
+ for (const [file, isCritical] of jsFiles) (isCritical ? criticalJs : sharedJs).push(file);
50
+ return {
51
+ cssFiles: [...cssFiles],
52
+ criticalJs,
53
+ sharedJs,
54
+ entryClientFile: manifest[virtualEntryClientKey].file
55
+ };
56
+ }
57
+ function getAssets(route, nonce) {
58
+ if (!isProd) return {
59
+ cssLinks: "",
60
+ jsPreloads: "",
61
+ entryClient: withBase("@id/virtual:vike-lite/entry-client")
62
+ };
63
+ let files = assetsCache.get(route);
64
+ if (!files) {
65
+ files = computeAssetFiles(route);
66
+ assetsCache.set(route, files);
67
+ }
68
+ const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
69
+ return {
70
+ cssLinks: files.cssFiles.map((href) => `<link rel="stylesheet" href="${withBase(href)}"${nonceAttr}>`).join(""),
71
+ jsPreloads: [...files.criticalJs.map((href) => `<link rel="modulepreload" href="${withBase(href)}" crossorigin fetchpriority="high"${nonceAttr}>`), ...files.sharedJs.map((href) => `<link rel="modulepreload" href="${withBase(href)}" crossorigin${nonceAttr}>`)].join(""),
72
+ entryClient: withBase(files.entryClientFile)
73
+ };
74
+ }
75
+ async function buildPageContext(urlPathname, urlOriginal, isJsonRequest) {
76
+ const matched = matchRoute(urlPathname, store.routes);
77
+ if (!matched) return null;
78
+ const { route, routeParams } = matched;
79
+ const pageContext = {
80
+ routeParams,
81
+ urlOriginal,
82
+ urlPathname
83
+ };
84
+ const [dataMod, titleMod, PageModule, HeadModule, LayoutModule] = await Promise.all([
85
+ route.Data?.() ?? null,
86
+ route.Title?.() ?? null,
87
+ isJsonRequest ? null : route.Page(),
88
+ isJsonRequest ? null : route.Head?.() ?? null,
89
+ isJsonRequest ? null : route.Layout?.() ?? null
90
+ ]);
91
+ if (dataMod) try {
92
+ pageContext.data = await (dataMod.data ?? dataMod.default)(pageContext);
93
+ } catch (error) {
94
+ if (!(error instanceof AbortRedirect || error instanceof AbortRender)) console.error("+data hook failed at:", urlPathname);
95
+ throw error;
96
+ }
97
+ if (titleMod) try {
98
+ const titleFn = titleMod.title ?? titleMod.default;
99
+ pageContext.title = typeof titleFn === "function" ? titleFn(pageContext) : titleFn;
100
+ } catch (error) {
101
+ if (!(error instanceof AbortRedirect || error instanceof AbortRender)) console.error("+title hook failed at:", urlPathname);
102
+ throw error;
103
+ }
104
+ return {
105
+ pageContext,
106
+ route,
107
+ PageModule,
108
+ HeadModule,
109
+ LayoutModule
110
+ };
111
+ }
112
+ async function renderErrorPage(req, status, urlPathname, error, nonce) {
113
+ let errorMessage;
114
+ let is500;
115
+ if (status === 500) {
116
+ errorMessage = isProd ? "Internal Server Error" : error instanceof Error ? error.message : "Unknown error";
117
+ is500 = true;
118
+ } else is500 = false;
119
+ const fallbackText = status === 404 ? "Not Found" : "Internal Server Error";
120
+ if (!store.errorRoute) return new Response(fallbackText, {
121
+ status,
122
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
123
+ });
124
+ try {
125
+ const [PageModule, HeadModule, LayoutModule] = await Promise.all([
126
+ store.errorRoute.Page(),
127
+ store.errorRoute.Head?.() ?? null,
128
+ store.errorRoute.Layout?.() ?? null
129
+ ]);
130
+ const pageContext = {
131
+ urlOriginal: req.url,
132
+ urlPathname,
133
+ routeParams: {},
134
+ is404: status === 404,
135
+ is500,
136
+ errorMessage
137
+ };
138
+ const html = await store.config.onRenderHtml({
139
+ pageContext,
140
+ Page: PageModule.Page ?? PageModule.default,
141
+ Layout: LayoutModule ? LayoutModule.Layout ?? LayoutModule.default : void 0,
142
+ Head: HeadModule ? HeadModule.Head ?? HeadModule.default : void 0,
143
+ pageTitleTag: `<title>${status === 404 ? "Page Not Found" : "Server Error"}</title>`,
144
+ serializedContext: serializeContext(pageContext),
145
+ assets: getAssets(store.errorRoute, nonce),
146
+ nonce
147
+ });
148
+ return new Response(html, {
149
+ status,
150
+ headers: { "Content-Type": "text/html; charset=utf-8" }
151
+ });
152
+ } catch (renderError) {
153
+ console.error("[vike-lite] Error page render failed:", renderError);
154
+ return new Response(fallbackText, {
155
+ status,
156
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
157
+ });
158
+ }
159
+ }
160
+ async function renderPage(req, { nonce } = {}) {
161
+ let { pathname } = new URL(req.url);
162
+ pathname = stripBase(pathname);
163
+ const isJsonRequest = pathname.endsWith(".pageContext.json");
164
+ let targetPathname = pathname;
165
+ if (isJsonRequest) {
166
+ targetPathname = targetPathname.replace(/\.pageContext\.json$/, "");
167
+ if (targetPathname === "/index") targetPathname = "/";
168
+ }
169
+ try {
170
+ const resolved = await buildPageContext(targetPathname, req.url, isJsonRequest);
171
+ if (!resolved) {
172
+ if (isJsonRequest) return Response.json({ is404: true }, { status: 404 });
173
+ return renderErrorPage(req, 404, targetPathname, nonce);
174
+ }
175
+ const { pageContext, route, PageModule, HeadModule, LayoutModule } = resolved;
176
+ if (isJsonRequest) return Response.json(pageContext);
177
+ const html = await store.config.onRenderHtml({
178
+ pageContext,
179
+ Page: PageModule.Page ?? PageModule.default,
180
+ Head: HeadModule ? HeadModule.Head ?? HeadModule.default : void 0,
181
+ Layout: LayoutModule ? LayoutModule.Layout ?? LayoutModule.default : void 0,
182
+ pageTitleTag: pageContext.title ? `<title>${pageContext.title}</title>` : "",
183
+ serializedContext: serializeContext(pageContext),
184
+ assets: getAssets(route, nonce),
185
+ nonce
186
+ });
187
+ return new Response(html, {
188
+ status: 200,
189
+ headers: { "Content-Type": "text/html; charset=utf-8" }
190
+ });
191
+ } catch (error) {
192
+ if (error instanceof AbortRedirect) {
193
+ let redirectUrl = error.url;
194
+ if (redirectUrl.startsWith("/")) redirectUrl = BASE_URL + (redirectUrl === "/" ? "" : redirectUrl);
195
+ if (isJsonRequest) return Response.json({ _redirect: redirectUrl }, { status: 200 });
196
+ return new Response(null, {
197
+ status: error.statusCode,
198
+ headers: { Location: redirectUrl }
199
+ });
200
+ }
201
+ if (error instanceof AbortRender) {
202
+ if (isJsonRequest) return Response.json({
203
+ is404: error.statusCode === 404,
204
+ is500: error.statusCode >= 500,
205
+ isError: true,
206
+ reason: error.reason
207
+ }, { status: error.statusCode });
208
+ return renderErrorPage(req, error.statusCode, targetPathname, error.reason, nonce);
209
+ }
210
+ console.error("Render Error:", error);
211
+ if (isJsonRequest) return Response.json({ is500: true }, { status: 500 });
212
+ return renderErrorPage(req, 500, targetPathname, error, nonce);
213
+ }
214
+ }
215
+ //#endregion
2
216
  export { renderPage };
package/dist/vite.mjs CHANGED
@@ -1,4 +1,3 @@
1
- import { t as renderPage } from "./server-DSrh6B3f.mjs";
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { Readable } from "node:stream";
@@ -112,6 +111,7 @@ function vikeLite({ pagesDir = "pages", apiPrefix = "/api", prerender = false, s
112
111
  let viteConfigRoot;
113
112
  let outDir;
114
113
  let hasAnyPrerender;
114
+ let baseUrl;
115
115
  const VIRTUAL = {
116
116
  routes: "virtual:vike-lite/routes",
117
117
  manifest: "virtual:vike-lite/client-manifest",
@@ -128,6 +128,8 @@ function vikeLite({ pagesDir = "pages", apiPrefix = "/api", prerender = false, s
128
128
  return {
129
129
  name: "vike-lite",
130
130
  config(config, { mode }) {
131
+ const rawBase = config.base || "/";
132
+ baseUrl = rawBase === "/" ? "" : rawBase.endsWith("/") ? rawBase.slice(0, -1) : rawBase;
131
133
  const envVariables = loadEnv(mode, config.envDir || process.cwd(), "");
132
134
  for (const key in envVariables) if (process.env[key] === void 0) process.env[key] = envVariables[key];
133
135
  outDir = config.build?.outDir ?? "dist";
@@ -287,15 +289,12 @@ function vikeLite({ pagesDir = "pages", apiPrefix = "/api", prerender = false, s
287
289
  const defaultServerEntryContent = isProd ? fs.readFileSync(path.resolve("vike-lite/__internal/vite/defaultServerEntry.mjs"), "utf8") : `import{renderPage}from'vike-lite/server';`;
288
290
  return importSetup + defaultServerEntryContent + "export default{fetch:renderPage};";
289
291
  }
290
- if (id === RESOLVED.entryPrerender) return importSetup + `export{routes}from'${VIRTUAL.routes}';`;
292
+ if (id === RESOLVED.entryPrerender) return importSetup + `export{routes}from'${VIRTUAL.routes}';export{renderPage}from'vike-lite/server';`;
291
293
  },
292
294
  async closeBundle() {
293
- if (!isProd || this.environment.name !== "ssr") return;
295
+ if (!isProd || !hasAnyPrerender || this.environment.name !== "ssr") return;
294
296
  const { pathToFileURL } = await import("node:url");
295
- const prerenderPath = path.join(viteConfigRoot, outDir, "server/prerender.mjs");
296
- if (!fs.existsSync(prerenderPath)) return;
297
- const { routes } = await import(pathToFileURL(prerenderPath).href);
298
- if (!routes) return;
297
+ const { routes, renderPage } = await import(pathToFileURL(path.join(viteConfigRoot, outDir, "server/prerender.mjs")).href);
299
298
  const urlsToPrerender = /* @__PURE__ */ new Set();
300
299
  for (const route of routes) {
301
300
  let shouldPrerender = prerender;
@@ -322,19 +321,17 @@ function vikeLite({ pagesDir = "pages", apiPrefix = "/api", prerender = false, s
322
321
  }
323
322
  if (urlsToPrerender.size === 0) return;
324
323
  console.log("[vike-lite] 📦 Starting Static Site Generation (SSG)…");
325
- const { BASE_URL } = import.meta.env;
326
- const baseNoSlash = BASE_URL.endsWith("/") ? BASE_URL.slice(0, -1) : BASE_URL;
327
324
  let generatedCount = 0;
328
325
  const clientDir = path.join(viteConfigRoot, outDir, "client");
329
326
  for (const urlPath of urlsToPrerender) {
330
- const htmlRes = await renderPage(new Request(`http://localhost${baseNoSlash}${urlPath}`));
327
+ const htmlRes = await renderPage(new Request(`http://localhost${baseUrl}${urlPath}`));
331
328
  if (htmlRes?.ok && htmlRes.headers.get("content-type")?.includes("text/html")) {
332
329
  const outDirRoute = path.join(clientDir, urlPath === "/" ? "" : urlPath);
333
330
  fs.mkdirSync(outDirRoute, { recursive: true });
334
331
  fs.writeFileSync(path.join(outDirRoute, "index.html"), await htmlRes.text());
335
332
  } else throw new Error(`[vike-lite] ❌ SSG HTML Error for "${urlPath}"`);
336
333
  const jsonTarget = urlPath === "/" ? "/index" : urlPath;
337
- const jsonRes = await renderPage(new Request(`http://localhost${baseNoSlash}${jsonTarget}.pageContext.json`));
334
+ const jsonRes = await renderPage(new Request(`http://localhost${baseUrl}${jsonTarget}.pageContext.json`));
338
335
  if (jsonRes?.ok) {
339
336
  const jsonOutPath = path.join(clientDir, `${jsonTarget}.pageContext.json`);
340
337
  fs.mkdirSync(path.dirname(jsonOutPath), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vike-lite",
3
- "version": "1.15.8",
3
+ "version": "1.15.10",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,44 +0,0 @@
1
- //#region src/__internal/shared/matchRoute.ts
2
- const regexCache = /* @__PURE__ */ new Map();
3
- function escapeRegex(str) {
4
- return str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
5
- }
6
- function matchRoute(urlPathname, routes) {
7
- for (const route of routes) {
8
- if (!route.path.includes(":")) {
9
- if (route.path === urlPathname || route.path + "/" === urlPathname) return {
10
- route,
11
- routeParams: {}
12
- };
13
- continue;
14
- }
15
- let compiled = regexCache.get(route.path);
16
- if (!compiled) {
17
- const paramNames = [];
18
- const regexPath = route.path.split("/").map((segment) => {
19
- if (segment.startsWith(":")) {
20
- paramNames.push(segment.slice(1));
21
- return "([^/]+)";
22
- }
23
- return escapeRegex(segment);
24
- }).join("/");
25
- compiled = {
26
- regex: new RegExp(`^${regexPath}/?$`),
27
- paramNames
28
- };
29
- regexCache.set(route.path, compiled);
30
- }
31
- const match = urlPathname.match(compiled.regex);
32
- if (match) {
33
- const routeParams = {};
34
- for (let i = 0; i < compiled.paramNames.length; i++) routeParams[compiled.paramNames[i]] = decodeURIComponent(match[i + 1]);
35
- return {
36
- route,
37
- routeParams
38
- };
39
- }
40
- }
41
- return null;
42
- }
43
- //#endregion
44
- export { matchRoute as t };
@@ -1,222 +0,0 @@
1
- import { t as matchRoute } from "./matchRoute-nTNPxoMq.mjs";
2
- import { AbortRedirect, AbortRender } from "./server/abort.mjs";
3
- import { n as store } from "./store-BCs8gL_o.mjs";
4
- //#region src/utils/serializeContext.ts
5
- const ESCAPE_LOOKUP = {
6
- "&": String.raw`\u0026`,
7
- ">": String.raw`\u003e`,
8
- "<": String.raw`\u003c`,
9
- "\u2028": String.raw`\u2028`,
10
- "\u2029": String.raw`\u2029`
11
- };
12
- const ESCAPE_REGEX = /[&><\u{2028}\u{2029}]/gu;
13
- function serializeContext(data) {
14
- return JSON.stringify(data).replaceAll(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
15
- }
16
- //#endregion
17
- //#region src/server/renderPage.ts
18
- const isProd = process.env.NODE_ENV === "production";
19
- function withBase(file) {
20
- const { BASE_URL } = import.meta.env;
21
- return `${BASE_URL === "/" ? "" : BASE_URL.endsWith("/") ? BASE_URL.slice(0, -1) : BASE_URL}${file.startsWith("/") ? file : `/${file}`}`;
22
- }
23
- const assetsCache = /* @__PURE__ */ new WeakMap();
24
- function computeAssetFiles(route) {
25
- const cssFiles = /* @__PURE__ */ new Set();
26
- const jsFiles = /* @__PURE__ */ new Map();
27
- const { manifest } = store;
28
- function getVirtualEntryClientKey() {
29
- for (const key in manifest) if (manifest[key].isEntry) return key;
30
- throw new Error("entry-client not found in manifest");
31
- }
32
- function collectAssets(key, isCritical) {
33
- const chunk = manifest[key];
34
- if (!chunk) return;
35
- const current = jsFiles.get(chunk.file);
36
- if (current === true) return;
37
- if (current === false && !isCritical) return;
38
- jsFiles.set(chunk.file, isCritical);
39
- if (chunk.css) for (const css of chunk.css) cssFiles.add(css);
40
- if (chunk.imports) for (const imp of chunk.imports) collectAssets(imp, false);
41
- }
42
- const virtualEntryClientKey = getVirtualEntryClientKey();
43
- collectAssets(virtualEntryClientKey, true);
44
- const { page, layout, head } = route;
45
- collectAssets(page, true);
46
- if (head) collectAssets(head, true);
47
- if (layout) collectAssets(layout, true);
48
- const criticalJs = [];
49
- const sharedJs = [];
50
- for (const [file, isCritical] of jsFiles) (isCritical ? criticalJs : sharedJs).push(file);
51
- return {
52
- cssFiles: [...cssFiles],
53
- criticalJs,
54
- sharedJs,
55
- entryClientFile: manifest[virtualEntryClientKey].file
56
- };
57
- }
58
- function getAssets(route, nonce) {
59
- if (!isProd) return {
60
- cssLinks: "",
61
- jsPreloads: "",
62
- entryClient: withBase("@id/virtual:vike-lite/entry-client")
63
- };
64
- let files = assetsCache.get(route);
65
- if (!files) {
66
- files = computeAssetFiles(route);
67
- assetsCache.set(route, files);
68
- }
69
- const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
70
- return {
71
- cssLinks: files.cssFiles.map((href) => `<link rel="stylesheet" href="${withBase(href)}"${nonceAttr}>`).join(""),
72
- jsPreloads: [...files.criticalJs.map((href) => `<link rel="modulepreload" href="${withBase(href)}" crossorigin fetchpriority="high"${nonceAttr}>`), ...files.sharedJs.map((href) => `<link rel="modulepreload" href="${withBase(href)}" crossorigin${nonceAttr}>`)].join(""),
73
- entryClient: withBase(files.entryClientFile)
74
- };
75
- }
76
- async function buildPageContext(urlPathname, urlOriginal, isJsonRequest) {
77
- const matched = matchRoute(urlPathname, store.routes);
78
- if (!matched) return null;
79
- const { route, routeParams } = matched;
80
- const pageContext = {
81
- routeParams,
82
- urlOriginal,
83
- urlPathname
84
- };
85
- const [dataMod, titleMod, PageModule, HeadModule, LayoutModule] = await Promise.all([
86
- route.Data?.() ?? null,
87
- route.Title?.() ?? null,
88
- isJsonRequest ? null : route.Page(),
89
- isJsonRequest ? null : route.Head?.() ?? null,
90
- isJsonRequest ? null : route.Layout?.() ?? null
91
- ]);
92
- if (dataMod) try {
93
- pageContext.data = await (dataMod.data ?? dataMod.default)(pageContext);
94
- } catch (error) {
95
- if (!(error instanceof AbortRedirect || error instanceof AbortRender)) console.error("+data hook failed at:", urlPathname);
96
- throw error;
97
- }
98
- if (titleMod) try {
99
- const titleFn = titleMod.title ?? titleMod.default;
100
- pageContext.title = typeof titleFn === "function" ? titleFn(pageContext) : titleFn;
101
- } catch (error) {
102
- if (!(error instanceof AbortRedirect || error instanceof AbortRender)) console.error("+title hook failed at:", urlPathname);
103
- throw error;
104
- }
105
- return {
106
- pageContext,
107
- route,
108
- PageModule,
109
- HeadModule,
110
- LayoutModule
111
- };
112
- }
113
- async function renderErrorPage(req, status, urlPathname, error, nonce) {
114
- let errorMessage;
115
- let is500;
116
- if (status === 500) {
117
- errorMessage = isProd ? "Internal Server Error" : error instanceof Error ? error.message : "Unknown error";
118
- is500 = true;
119
- } else is500 = false;
120
- const fallbackText = status === 404 ? "Not Found" : "Internal Server Error";
121
- if (!store.errorRoute) return new Response(fallbackText, {
122
- status,
123
- headers: { "Content-Type": "text/plain; charset=utf-8" }
124
- });
125
- try {
126
- const [PageModule, HeadModule, LayoutModule] = await Promise.all([
127
- store.errorRoute.Page(),
128
- store.errorRoute.Head?.() ?? null,
129
- store.errorRoute.Layout?.() ?? null
130
- ]);
131
- const pageContext = {
132
- urlOriginal: req.url,
133
- urlPathname,
134
- routeParams: {},
135
- is404: status === 404,
136
- is500,
137
- errorMessage
138
- };
139
- const html = await store.config.onRenderHtml({
140
- pageContext,
141
- Page: PageModule.Page ?? PageModule.default,
142
- Layout: LayoutModule ? LayoutModule.Layout ?? LayoutModule.default : void 0,
143
- Head: HeadModule ? HeadModule.Head ?? HeadModule.default : void 0,
144
- pageTitleTag: `<title>${status === 404 ? "Page Not Found" : "Server Error"}</title>`,
145
- serializedContext: serializeContext(pageContext),
146
- assets: getAssets(store.errorRoute, nonce),
147
- nonce
148
- });
149
- return new Response(html, {
150
- status,
151
- headers: { "Content-Type": "text/html; charset=utf-8" }
152
- });
153
- } catch (renderError) {
154
- console.error("[vike-lite] Error page render failed:", renderError);
155
- return new Response(fallbackText, {
156
- status,
157
- headers: { "Content-Type": "text/plain; charset=utf-8" }
158
- });
159
- }
160
- }
161
- async function renderPage(req, { nonce } = {}) {
162
- let { pathname } = new URL(req.url);
163
- const { BASE_URL } = import.meta.env;
164
- if (BASE_URL !== "/") {
165
- const baseSlashed = BASE_URL.endsWith("/") ? BASE_URL : BASE_URL + "/";
166
- const baseNoSlash = baseSlashed.slice(0, -1);
167
- pathname = pathname === baseNoSlash ? "/" : pathname.slice(baseSlashed.length - 1);
168
- }
169
- const isJsonRequest = pathname.endsWith(".pageContext.json");
170
- let targetPathname = pathname;
171
- if (isJsonRequest) {
172
- targetPathname = targetPathname.replace(/\.pageContext\.json$/, "");
173
- if (targetPathname === "/index") targetPathname = "/";
174
- }
175
- try {
176
- const resolved = await buildPageContext(targetPathname, req.url, isJsonRequest);
177
- if (!resolved) {
178
- if (isJsonRequest) return Response.json({ is404: true }, { status: 404 });
179
- return renderErrorPage(req, 404, targetPathname, nonce);
180
- }
181
- const { pageContext, route, PageModule, HeadModule, LayoutModule } = resolved;
182
- if (isJsonRequest) return Response.json(pageContext);
183
- const html = await store.config.onRenderHtml({
184
- pageContext,
185
- Page: PageModule.Page ?? PageModule.default,
186
- Head: HeadModule ? HeadModule.Head ?? HeadModule.default : void 0,
187
- Layout: LayoutModule ? LayoutModule.Layout ?? LayoutModule.default : void 0,
188
- pageTitleTag: pageContext.title ? `<title>${pageContext.title}</title>` : "",
189
- serializedContext: serializeContext(pageContext),
190
- assets: getAssets(route, nonce),
191
- nonce
192
- });
193
- return new Response(html, {
194
- status: 200,
195
- headers: { "Content-Type": "text/html; charset=utf-8" }
196
- });
197
- } catch (error) {
198
- if (error instanceof AbortRedirect) {
199
- let redirectUrl = error.url;
200
- if (redirectUrl.startsWith("/")) redirectUrl = (BASE_URL.endsWith("/") ? BASE_URL.slice(0, -1) : BASE_URL) + (redirectUrl === "/" ? "" : redirectUrl);
201
- if (isJsonRequest) return Response.json({ _redirect: redirectUrl }, { status: 200 });
202
- return new Response(null, {
203
- status: error.statusCode,
204
- headers: { Location: redirectUrl }
205
- });
206
- }
207
- if (error instanceof AbortRender) {
208
- if (isJsonRequest) return Response.json({
209
- is404: error.statusCode === 404,
210
- is500: error.statusCode >= 500,
211
- isError: true,
212
- reason: error.reason
213
- }, { status: error.statusCode });
214
- return renderErrorPage(req, error.statusCode, targetPathname, error.reason, nonce);
215
- }
216
- console.error("Render Error:", error);
217
- if (isJsonRequest) return Response.json({ is500: true }, { status: 500 });
218
- return renderErrorPage(req, 500, targetPathname, error, nonce);
219
- }
220
- }
221
- //#endregion
222
- export { renderPage as t };