@blinkk/root 1.0.0-beta.8 → 1.0.0-rc.0

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/render.js CHANGED
@@ -1,26 +1,296 @@
1
- import {
2
- htmlMinify,
3
- htmlPretty,
4
- parseTagNames
5
- } from "./chunk-WVRC46JG.js";
6
1
  import {
7
2
  HTML_CONTEXT,
8
3
  I18N_CONTEXT,
9
4
  REQUEST_CONTEXT,
10
5
  getTranslations
11
- } from "./chunk-WTSNW7BB.js";
6
+ } from "./chunk-MJCIAH6K.js";
7
+ import {
8
+ htmlMinify,
9
+ htmlPretty,
10
+ parseTagNames
11
+ } from "./chunk-DFBTOMQF.js";
12
12
 
13
13
  // src/render/render.tsx
14
14
  import renderToString from "preact-render-to-string";
15
15
 
16
+ // src/core/pages/ErrorPage.tsx
17
+ import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
18
+ var STYLES = `
19
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
20
+
21
+ :root {
22
+ --font-family-text: "Inter", sans-serif;
23
+ }
24
+
25
+ body {
26
+ font-family: var(--font-family-text);
27
+ background: #F5F5F5;
28
+ padding: 40px 16px;
29
+ }
30
+
31
+ .root {
32
+ max-width: 1200px;
33
+ margin: 0 auto;
34
+ }
35
+
36
+ .root.align-center {
37
+ text-align: center;
38
+ }
39
+
40
+ h1.title {
41
+ margin-top: 0;
42
+ margin-bottom: 24px;
43
+ }
44
+
45
+ p.message {
46
+ margin-top: 0;
47
+ margin-bottom: 0;
48
+ }
49
+
50
+ .box {
51
+ font-size: 16px;
52
+ line-height: 1.5;
53
+ padding: 16px;
54
+ border-radius: 12px;
55
+ background: #ffffff;
56
+ }
57
+
58
+ pre.box {
59
+ white-space: pre-wrap;
60
+ }
61
+
62
+ @media (min-width: 500px) {
63
+ body {
64
+ padding: 40px;
65
+ }
66
+
67
+ .box {
68
+ padding: 24px;
69
+ }
70
+ }
71
+
72
+ @media (min-width: 1024px) {
73
+ body {
74
+ padding: 100px;
75
+ }
76
+ }
77
+ `;
78
+ function ErrorPage(props) {
79
+ const { code, message } = props;
80
+ const title = props.title || code;
81
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
82
+ /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: STYLES } }),
83
+ /* @__PURE__ */ jsxs("div", { className: `root align-${props.align || "left"}`, children: [
84
+ /* @__PURE__ */ jsx("h1", { className: "title", children: title }),
85
+ message && /* @__PURE__ */ jsx("p", { className: "message", children: message }),
86
+ props.children
87
+ ] })
88
+ ] });
89
+ }
90
+
91
+ // src/core/pages/DevErrorPage.tsx
92
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
93
+ function DevErrorPage(props) {
94
+ var _a;
95
+ const req = props.req;
96
+ const err = props.error;
97
+ const route = props.route;
98
+ const routeParams = props.routeParams;
99
+ let errMsg = String(err);
100
+ if (err && err.stack) {
101
+ errMsg = err.stack.replace(/\(.*node_modules/g, "(node_modules").replace(/at \/.*node_modules/g, "at node_modules");
102
+ if ((_a = req.rootConfig) == null ? void 0 : _a.rootDir) {
103
+ errMsg = errMsg.replaceAll(req.rootConfig.rootDir, "<root>");
104
+ }
105
+ if (process.env.HOME) {
106
+ errMsg = errMsg.replaceAll(process.env.HOME, "$HOME");
107
+ }
108
+ }
109
+ return /* @__PURE__ */ jsxs2(ErrorPage, { code: 500, title: "Something went wrong", children: [
110
+ errMsg && /* @__PURE__ */ jsxs2(Fragment2, { children: [
111
+ /* @__PURE__ */ jsx2("h2", { children: "Error" }),
112
+ /* @__PURE__ */ jsx2("pre", { className: "box", children: /* @__PURE__ */ jsx2("code", { children: errMsg }) })
113
+ ] }),
114
+ /* @__PURE__ */ jsx2("h2", { children: "Debug Info" }),
115
+ /* @__PURE__ */ jsx2("pre", { className: "box", children: /* @__PURE__ */ jsx2("code", { children: `url: ${req.originalUrl}
116
+ route: ${(route == null ? void 0 : route.src) || "null"}
117
+ routeParams: ${routeParams && JSON.stringify(routeParams) || "null"}` }) })
118
+ ] });
119
+ }
120
+
121
+ // src/core/pages/DevNotFoundPage.tsx
122
+ import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
123
+ function DevNotFoundPage(props) {
124
+ const req = props.req;
125
+ const routesListMap = {};
126
+ let srcMaxLength = 0;
127
+ Object.keys(props.sitemap).forEach((urlPath) => {
128
+ const route = props.sitemap[urlPath].route;
129
+ routesListMap[route.src] ??= [];
130
+ routesListMap[route.src].push({ route, urlPath });
131
+ if (route.src.length > srcMaxLength) {
132
+ srcMaxLength = route.src.length;
133
+ }
134
+ });
135
+ const routeSrcs = Object.keys(routesListMap).sort(sortRouteFiles);
136
+ const lines = [];
137
+ routeSrcs.forEach((routeSrc) => {
138
+ const routeUrls = routesListMap[routeSrc].sort(sortRouteURLs);
139
+ routeUrls.forEach((routeUrl, i) => {
140
+ const urlPath = routeUrl.urlPath;
141
+ if (i === 0) {
142
+ lines.push(`${routeSrc.padEnd(srcMaxLength, " ")} => ${urlPath}`);
143
+ } else {
144
+ lines.push(`${"".padEnd(srcMaxLength, " ")} => ${urlPath}`);
145
+ }
146
+ });
147
+ });
148
+ const routesListString = lines.join("\n");
149
+ return /* @__PURE__ */ jsxs3(ErrorPage, { code: 404, title: "Not found", children: [
150
+ /* @__PURE__ */ jsx3("h2", { children: "Routes" }),
151
+ Object.keys(routesListMap).length > 0 ? /* @__PURE__ */ jsx3("pre", { className: "box", children: /* @__PURE__ */ jsx3("code", { children: routesListString }) }) : /* @__PURE__ */ jsxs3("div", { className: "box", children: [
152
+ "Add your first route at ",
153
+ /* @__PURE__ */ jsx3("code", { children: "/routes/index.tsx" })
154
+ ] }),
155
+ /* @__PURE__ */ jsx3("h2", { children: "Debug Info" }),
156
+ /* @__PURE__ */ jsx3("pre", { className: "box", children: /* @__PURE__ */ jsx3("code", { children: `url: ${req.originalUrl}` }) })
157
+ ] });
158
+ }
159
+ function sortRouteFiles(a, b) {
160
+ if (a === "routes/index.tsx") {
161
+ return -1;
162
+ }
163
+ if (b === "routes/index.tsx") {
164
+ return 1;
165
+ }
166
+ return a.localeCompare(b);
167
+ }
168
+ function sortRouteURLs(a, b) {
169
+ if (a.route.isDefaultLocale && !b.route.isDefaultLocale) {
170
+ return -1;
171
+ }
172
+ if (!a.route.isDefaultLocale && b.route.isDefaultLocale) {
173
+ return 1;
174
+ }
175
+ return a.urlPath.localeCompare(b.urlPath);
176
+ }
177
+
178
+ // src/render/accept-language.ts
179
+ var ACCEPT_LANG_RE = /((([a-zA-Z]+(-[a-zA-Z0-9]+){0,2})|\*)(;q=[0-1](\.[0-9]+)?)?)*/g;
180
+ function parseAcceptLanguage(value) {
181
+ const matches = String(value).match(ACCEPT_LANG_RE);
182
+ if (!matches) {
183
+ return [];
184
+ }
185
+ const results = [];
186
+ matches.forEach((m) => {
187
+ if (!m) {
188
+ return;
189
+ }
190
+ const parts = m.split(";");
191
+ const ietf = parts[0].split("-");
192
+ const hasScript = ietf.length === 3;
193
+ results.push({
194
+ code: ietf[0],
195
+ script: hasScript ? ietf[1] : void 0,
196
+ region: hasScript ? ietf[2] : ietf[1],
197
+ quality: parts[1] ? parseFloat(parts[1].split("=")[1]) : 1
198
+ });
199
+ });
200
+ results.sort((a, b) => b.quality - a.quality);
201
+ return results;
202
+ }
203
+
204
+ // src/render/i18n-fallbacks.ts
205
+ var UNKNOWN_COUNTRY = "zz";
206
+ function getFallbackLocales(req) {
207
+ var _a, _b;
208
+ const hl = getFirstQueryParam(req, "hl");
209
+ const countryCode = getCountry(req);
210
+ if (isWebCrawler(req)) {
211
+ const defaultLocale = ((_b = (_a = req.rootConfig) == null ? void 0 : _a.i18n) == null ? void 0 : _b.defaultLocale) || "en";
212
+ if (hl && hl !== defaultLocale) {
213
+ return [hl, defaultLocale];
214
+ }
215
+ return [defaultLocale];
216
+ }
217
+ const locales = /* @__PURE__ */ new Set();
218
+ if (hl) {
219
+ const langCode = hl;
220
+ locales.add(`${langCode}_${countryCode}`);
221
+ locales.add(`${langCode}_ALL`);
222
+ locales.add(langCode);
223
+ }
224
+ const langs = getFallbackLanguages(req);
225
+ langs.forEach((langCode) => {
226
+ locales.add(`${langCode}_${countryCode}`);
227
+ });
228
+ locales.add(`ALL_${countryCode}`);
229
+ langs.forEach((langCode) => {
230
+ locales.add(`${langCode}_ALL`);
231
+ locales.add(langCode);
232
+ });
233
+ return Array.from(locales);
234
+ }
235
+ function getCountry(req) {
236
+ const normalize = (countryCode) => String(countryCode).toLowerCase();
237
+ const gl = getFirstQueryParam(req, "gl");
238
+ if (gl) {
239
+ return normalize(gl);
240
+ }
241
+ const gaeCountry = req.get("x-country-code") || req.get("x-appengine-country");
242
+ if (gaeCountry) {
243
+ return normalize(gaeCountry);
244
+ }
245
+ return UNKNOWN_COUNTRY;
246
+ }
247
+ function getFallbackLanguages(req) {
248
+ const langs = /* @__PURE__ */ new Set();
249
+ const acceptLangHeader = req.get("accept-language") || "";
250
+ if (acceptLangHeader) {
251
+ parseAcceptLanguage(acceptLangHeader).forEach((lang) => {
252
+ if (lang.region) {
253
+ langs.add(`${lang.code}-${lang.region}`);
254
+ }
255
+ langs.add(lang.code);
256
+ });
257
+ }
258
+ langs.add("en");
259
+ return Array.from(langs);
260
+ }
261
+ function getFirstQueryParam(req, key) {
262
+ const val = req.query[key];
263
+ if (val === null || val === void 0) {
264
+ return null;
265
+ }
266
+ if (Array.isArray(val)) {
267
+ if (val.length === 0) {
268
+ return null;
269
+ }
270
+ return String(val[0]);
271
+ }
272
+ return String(val);
273
+ }
274
+ function isWebCrawler(req) {
275
+ const userAgentHeader = req.get("User-Agent");
276
+ if (!userAgentHeader) {
277
+ return false;
278
+ }
279
+ const userAgent = userAgentHeader.toLowerCase();
280
+ return userAgent.includes("googlebot") || userAgent.includes("bingbot") || userAgent.includes("twitterbot");
281
+ }
282
+
16
283
  // src/render/router.ts
17
284
  import path from "node:path";
18
285
 
19
286
  // src/render/route-trie.ts
20
- var RouteTrie = class {
287
+ var RouteTrie = class _RouteTrie {
21
288
  constructor() {
22
289
  this.children = {};
23
290
  }
291
+ /**
292
+ * Adds a route to the trie.
293
+ */
24
294
  add(path2, route) {
25
295
  path2 = this.normalizePath(path2);
26
296
  if (path2 === "") {
@@ -35,25 +305,35 @@ var RouteTrie = class {
35
305
  }
36
306
  let nextNode;
37
307
  if (head.startsWith("[") && head.endsWith("]")) {
38
- if (!this.paramChild) {
39
- const paramName = head.slice(1, -1);
40
- this.paramChild = new ParamChild(paramName);
308
+ if (!this.paramChildren) {
309
+ this.paramChildren = {};
41
310
  }
42
- nextNode = this.paramChild.trie;
311
+ const paramName = head.slice(1, -1);
312
+ if (!this.paramChildren[paramName]) {
313
+ this.paramChildren[paramName] = new ParamChild(paramName);
314
+ }
315
+ nextNode = this.paramChildren[paramName].trie;
43
316
  } else {
44
317
  nextNode = this.children[head];
45
318
  if (!nextNode) {
46
- nextNode = new RouteTrie();
319
+ nextNode = new _RouteTrie();
47
320
  this.children[head] = nextNode;
48
321
  }
49
322
  }
50
323
  nextNode.add(tail, route);
51
324
  }
325
+ /**
326
+ * Returns a route mapped to the given path and any parameter values from the
327
+ * URL.
328
+ */
52
329
  get(path2) {
53
330
  const params = {};
54
331
  const route = this.getRoute(path2, params);
55
332
  return [route, params];
56
333
  }
334
+ /**
335
+ * Walks the route trie and calls a callback function for each route.
336
+ */
57
337
  walk(cb) {
58
338
  const promises = [];
59
339
  const addPromise = (promise) => {
@@ -64,11 +344,13 @@ var RouteTrie = class {
64
344
  if (this.route) {
65
345
  addPromise(cb("/", this.route));
66
346
  }
67
- if (this.paramChild) {
68
- const param = `[${this.paramChild.name}]`;
69
- this.paramChild.trie.walk((childPath, route) => {
70
- const paramUrlPath = `/${param}${childPath}`;
71
- addPromise(cb(paramUrlPath, route));
347
+ if (this.paramChildren) {
348
+ Object.values(this.paramChildren).forEach((paramChild) => {
349
+ const param = `[${paramChild.name}]`;
350
+ paramChild.trie.walk((childPath, route) => {
351
+ const paramUrlPath = `/${param}${childPath}`;
352
+ addPromise(cb(paramUrlPath, route));
353
+ });
72
354
  });
73
355
  }
74
356
  if (this.wildcardChild) {
@@ -84,9 +366,12 @@ var RouteTrie = class {
84
366
  return Promise.all(promises).then(() => {
85
367
  });
86
368
  }
369
+ /**
370
+ * Removes all routes from the trie.
371
+ */
87
372
  clear() {
88
373
  this.children = {};
89
- this.paramChild = void 0;
374
+ this.paramChildren = void 0;
90
375
  this.wildcardChild = void 0;
91
376
  this.route = void 0;
92
377
  }
@@ -103,11 +388,13 @@ var RouteTrie = class {
103
388
  return route;
104
389
  }
105
390
  }
106
- if (this.paramChild) {
107
- const route = this.paramChild.trie.getRoute(tail, params);
108
- if (route) {
109
- params[this.paramChild.name] = head;
110
- return route;
391
+ if (this.paramChildren) {
392
+ for (const paramChild of Object.values(this.paramChildren)) {
393
+ const route = paramChild.trie.getRoute(tail, params);
394
+ if (route) {
395
+ params[paramChild.name] = head;
396
+ return route;
397
+ }
111
398
  }
112
399
  }
113
400
  if (this.wildcardChild) {
@@ -116,9 +403,17 @@ var RouteTrie = class {
116
403
  }
117
404
  return void 0;
118
405
  }
406
+ /**
407
+ * Normalizes a path for inclusion into the route trie.
408
+ */
119
409
  normalizePath(path2) {
120
410
  return path2.replace(/^\/+/g, "");
121
411
  }
412
+ /**
413
+ * Splits the parent directory from its children, e.g.:
414
+ *
415
+ * splitPath("foo/bar/baz") -> ["foo", "bar/baz"]
416
+ */
122
417
  splitPath(path2) {
123
418
  const i = path2.indexOf("/");
124
419
  if (i === -1) {
@@ -170,8 +465,9 @@ function getRoutes(config) {
170
465
  src,
171
466
  module: routes[modulePath],
172
467
  locale: defaultLocale,
173
- routePath,
174
- localeRoutePath
468
+ isDefaultLocale: true,
469
+ routePath: normalizeUrlPath(routePath),
470
+ localeRoutePath: normalizeUrlPath(localeRoutePath)
175
471
  });
176
472
  locales.forEach((locale) => {
177
473
  const localePath = localeRoutePath.replace("[locale]", locale);
@@ -180,6 +476,7 @@ function getRoutes(config) {
180
476
  src,
181
477
  module: routes[modulePath],
182
478
  locale,
479
+ isDefaultLocale: false,
183
480
  routePath,
184
481
  localeRoutePath
185
482
  });
@@ -206,19 +503,19 @@ async function getAllPathsForRoute(urlPathFormat, route) {
206
503
  );
207
504
  } else {
208
505
  urlPaths.push({
209
- urlPath: replaceParams(urlPathFormat, pathParams.params),
506
+ urlPath: normalizeUrlPath(urlPath),
210
507
  params: pathParams.params || {}
211
508
  });
212
509
  }
213
510
  }
214
511
  );
215
512
  }
216
- } else if (pathContainsPlaceholders(urlPathFormat)) {
513
+ } else if (pathContainsPlaceholders(urlPathFormat) && !routeModule.handle) {
217
514
  console.warn(
218
515
  `path contains placeholders: ${urlPathFormat}, did you forget to define getStaticPaths()? more info: https://rootjs.dev/guide/routes#getStaticPaths`
219
516
  );
220
517
  } else {
221
- urlPaths.push({ urlPath: urlPathFormat, params: {} });
518
+ urlPaths.push({ urlPath: normalizeUrlPath(urlPathFormat), params: {} });
222
519
  }
223
520
  return urlPaths;
224
521
  }
@@ -235,6 +532,12 @@ function replaceParams(urlPathFormat, params) {
235
532
  );
236
533
  return urlPath;
237
534
  }
535
+ function normalizeUrlPath(urlPath) {
536
+ if (urlPath !== "/" && urlPath.endsWith("/")) {
537
+ urlPath = urlPath.replace(/\/*$/g, "");
538
+ }
539
+ return urlPath;
540
+ }
238
541
  function pathContainsPlaceholders(urlPath) {
239
542
  const segments = urlPath.split("/");
240
543
  return segments.some((segment) => {
@@ -242,194 +545,6 @@ function pathContainsPlaceholders(urlPath) {
242
545
  });
243
546
  }
244
547
 
245
- // src/core/pages/ErrorPage.tsx
246
- import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
247
- var STYLES = `
248
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
249
-
250
- :root {
251
- --font-family-text: "Inter", sans-serif;
252
- }
253
-
254
- body {
255
- font-family: var(--font-family-text);
256
- background: #F5F5F5;
257
- padding: 40px 16px;
258
- }
259
-
260
- .root {
261
- max-width: 1200px;
262
- margin: 0 auto;
263
- }
264
-
265
- .root.align-center {
266
- text-align: center;
267
- }
268
-
269
- h1.title {
270
- margin-top: 0;
271
- margin-bottom: 24px;
272
- }
273
-
274
- p.message {
275
- margin-top: 0;
276
- margin-bottom: 0;
277
- }
278
-
279
- .box {
280
- font-size: 16px;
281
- line-height: 1.5;
282
- padding: 16px;
283
- border-radius: 12px;
284
- background: #ffffff;
285
- }
286
-
287
- pre.box {
288
- white-space: pre-wrap;
289
- }
290
-
291
- @media (min-width: 500px) {
292
- body {
293
- padding: 40px;
294
- }
295
-
296
- .box {
297
- padding: 24px;
298
- }
299
- }
300
-
301
- @media (min-width: 1024px) {
302
- body {
303
- padding: 100px;
304
- }
305
- }
306
- `;
307
- function ErrorPage(props) {
308
- const { code, message } = props;
309
- const title = props.title || code;
310
- return /* @__PURE__ */ jsxs(Fragment, {
311
- children: [
312
- /* @__PURE__ */ jsx("style", {
313
- dangerouslySetInnerHTML: { __html: STYLES }
314
- }),
315
- /* @__PURE__ */ jsxs("div", {
316
- className: `root align-${props.align || "left"}`,
317
- children: [
318
- /* @__PURE__ */ jsx("h1", {
319
- className: "title",
320
- children: title
321
- }),
322
- message && /* @__PURE__ */ jsx("p", {
323
- className: "message",
324
- children: message
325
- }),
326
- props.children
327
- ]
328
- })
329
- ]
330
- });
331
- }
332
-
333
- // src/core/pages/DevNotFoundPage.tsx
334
- import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
335
- function DevNotFoundPage(props) {
336
- const req = props.req;
337
- const routesList = [];
338
- let urlMaxLength = 0;
339
- Object.keys(props.sitemap).forEach((urlPath) => {
340
- const route = props.sitemap[urlPath].route;
341
- routesList.push(Object.assign({}, route, { urlPath }));
342
- if (urlPath.length > urlMaxLength) {
343
- urlMaxLength = urlPath.length;
344
- }
345
- });
346
- const routesListString = routesList.map((route) => {
347
- return `${route.urlPath.padEnd(urlMaxLength, " ")} => ${route.src}`;
348
- }).join("\n");
349
- return /* @__PURE__ */ jsxs2(ErrorPage, {
350
- code: 404,
351
- title: "Not found",
352
- children: [
353
- /* @__PURE__ */ jsx2("h2", {
354
- children: "Routes"
355
- }),
356
- routesList.length > 0 ? /* @__PURE__ */ jsx2("pre", {
357
- className: "box",
358
- children: /* @__PURE__ */ jsx2("code", {
359
- children: routesListString
360
- })
361
- }) : /* @__PURE__ */ jsxs2("div", {
362
- className: "box",
363
- children: [
364
- "Add your first route at ",
365
- /* @__PURE__ */ jsx2("code", {
366
- children: "/routes/index.tsx"
367
- })
368
- ]
369
- }),
370
- /* @__PURE__ */ jsx2("h2", {
371
- children: "Debug Info"
372
- }),
373
- /* @__PURE__ */ jsx2("pre", {
374
- className: "box",
375
- children: /* @__PURE__ */ jsx2("code", {
376
- children: `url: ${req.originalUrl}`
377
- })
378
- })
379
- ]
380
- });
381
- }
382
-
383
- // src/core/pages/DevErrorPage.tsx
384
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
385
- function DevErrorPage(props) {
386
- var _a;
387
- const req = props.req;
388
- const err = props.error;
389
- const route = props.route;
390
- const routeParams = props.routeParams;
391
- let errMsg = String(err);
392
- if (err && err.stack) {
393
- errMsg = err.stack.replace(/\(.*node_modules/g, "(node_modules").replace(/at \/.*node_modules/g, "at node_modules");
394
- if ((_a = req.rootConfig) == null ? void 0 : _a.rootDir) {
395
- errMsg = errMsg.replaceAll(req.rootConfig.rootDir, "<root>");
396
- }
397
- if (process.env.HOME) {
398
- errMsg = errMsg.replaceAll(process.env.HOME, "$HOME");
399
- }
400
- }
401
- return /* @__PURE__ */ jsxs3(ErrorPage, {
402
- code: 500,
403
- title: "Something went wrong",
404
- children: [
405
- errMsg && /* @__PURE__ */ jsxs3(Fragment2, {
406
- children: [
407
- /* @__PURE__ */ jsx3("h2", {
408
- children: "Error"
409
- }),
410
- /* @__PURE__ */ jsx3("pre", {
411
- className: "box",
412
- children: /* @__PURE__ */ jsx3("code", {
413
- children: errMsg
414
- })
415
- })
416
- ]
417
- }),
418
- /* @__PURE__ */ jsx3("h2", {
419
- children: "Debug Info"
420
- }),
421
- /* @__PURE__ */ jsx3("pre", {
422
- className: "box",
423
- children: /* @__PURE__ */ jsx3("code", {
424
- children: `url: ${req.originalUrl}
425
- route: ${(route == null ? void 0 : route.src) || "null"}
426
- routeParams: ${routeParams && JSON.stringify(routeParams) || "null"}`
427
- })
428
- })
429
- ]
430
- });
431
- }
432
-
433
548
  // src/render/render.tsx
434
549
  import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
435
550
  var Renderer = class {
@@ -440,7 +555,7 @@ var Renderer = class {
440
555
  this.elementGraph = options.elementGraph;
441
556
  }
442
557
  async handle(req, res, next) {
443
- const url = req.path;
558
+ const url = req.path.toLowerCase();
444
559
  const [route, routeParams] = this.routes.get(url);
445
560
  if (!route) {
446
561
  next();
@@ -449,18 +564,35 @@ var Renderer = class {
449
564
  if (route.locale) {
450
565
  routeParams.$locale = route.locale;
451
566
  }
567
+ const fallbackLocales = route.isDefaultLocale ? getFallbackLocales(req) : [route.locale];
568
+ const getPreferredLocale = (availableLocales) => {
569
+ var _a, _b;
570
+ const lowerLocales = availableLocales.map((l) => l.toLowerCase());
571
+ for (const fallbackLocale of fallbackLocales) {
572
+ if (lowerLocales.includes(fallbackLocale.toLowerCase())) {
573
+ return fallbackLocale;
574
+ }
575
+ }
576
+ return ((_b = (_a = req.rootConfig) == null ? void 0 : _a.i18n) == null ? void 0 : _b.defaultLocale) || "en";
577
+ };
452
578
  const render404 = async () => {
453
579
  next();
454
580
  };
455
- const render = async (props2) => {
581
+ const render = async (props2, options) => {
456
582
  if (!route.module.default) {
457
583
  console.error(`no default component exported in route: ${route.src}`);
458
584
  render404();
459
585
  return;
460
586
  }
587
+ const currentPath = req.path;
588
+ const locale = (options == null ? void 0 : options.locale) || route.locale;
589
+ const translations = options == null ? void 0 : options.translations;
461
590
  const output = await this.renderComponent(route.module.default, props2, {
591
+ currentPath,
462
592
  route,
463
- routeParams
593
+ routeParams,
594
+ locale,
595
+ translations
464
596
  });
465
597
  let html = output.html;
466
598
  if (this.rootConfig.prettyHtml) {
@@ -469,7 +601,7 @@ var Renderer = class {
469
601
  html = await htmlMinify(html, this.rootConfig.minifyHtmlOptions);
470
602
  }
471
603
  if (req.viteServer) {
472
- html = await req.viteServer.transformIndexHtml(req.originalUrl, html);
604
+ html = await req.viteServer.transformIndexHtml(currentPath, html);
473
605
  }
474
606
  let statusCode = 200;
475
607
  if (route.src === "routes/404.tsx") {
@@ -477,12 +609,15 @@ var Renderer = class {
477
609
  } else if (route.src === "routes/500.tsx") {
478
610
  statusCode = 500;
479
611
  }
612
+ req.hooks.trigger("preRender");
480
613
  res.status(statusCode).set({ "Content-Type": "text/html" }).end(html);
481
614
  };
482
615
  if (route.module.handle) {
483
616
  const handlerContext = {
484
617
  route,
485
618
  params: routeParams,
619
+ i18nFallbackLocales: fallbackLocales,
620
+ getPreferredLocale,
486
621
  render,
487
622
  render404
488
623
  };
@@ -505,10 +640,14 @@ var Renderer = class {
505
640
  await render(props);
506
641
  }
507
642
  async renderComponent(Component, props, options) {
508
- const { route, routeParams } = options;
509
- const locale = route.locale;
510
- const translations = getTranslations(locale);
643
+ const { currentPath, route, routeParams } = options;
644
+ const locale = options.locale;
645
+ const translations = {
646
+ ...getTranslations(locale),
647
+ ...options.translations || {}
648
+ };
511
649
  const ctx = {
650
+ currentPath,
512
651
  route,
513
652
  props,
514
653
  routeParams,
@@ -522,18 +661,7 @@ var Renderer = class {
522
661
  bodyAttrs: {},
523
662
  scriptDeps: []
524
663
  };
525
- const vdom = /* @__PURE__ */ jsx4(REQUEST_CONTEXT.Provider, {
526
- value: ctx,
527
- children: /* @__PURE__ */ jsx4(I18N_CONTEXT.Provider, {
528
- value: { locale, translations },
529
- children: /* @__PURE__ */ jsx4(HTML_CONTEXT.Provider, {
530
- value: htmlContext,
531
- children: /* @__PURE__ */ jsx4(Component, {
532
- ...props
533
- })
534
- })
535
- })
536
- });
664
+ const vdom = /* @__PURE__ */ jsx4(REQUEST_CONTEXT.Provider, { value: ctx, children: /* @__PURE__ */ jsx4(I18N_CONTEXT.Provider, { value: { locale, translations }, children: /* @__PURE__ */ jsx4(HTML_CONTEXT.Provider, { value: htmlContext, children: /* @__PURE__ */ jsx4(Component, { ...props }) }) }) });
537
665
  const mainHtml = renderToString(vdom);
538
666
  const jsDeps = /* @__PURE__ */ new Set();
539
667
  const cssDeps = /* @__PURE__ */ new Set();
@@ -557,16 +685,10 @@ var Renderer = class {
557
685
  })
558
686
  );
559
687
  const styleTags = Array.from(cssDeps).map((cssUrl) => {
560
- return /* @__PURE__ */ jsx4("link", {
561
- rel: "stylesheet",
562
- href: cssUrl
563
- });
688
+ return /* @__PURE__ */ jsx4("link", { rel: "stylesheet", href: cssUrl });
564
689
  });
565
690
  const scriptTags = Array.from(jsDeps).map((jsUrls) => {
566
- return /* @__PURE__ */ jsx4("script", {
567
- type: "module",
568
- src: jsUrls
569
- });
691
+ return /* @__PURE__ */ jsx4("script", { type: "module", src: jsUrls });
570
692
  });
571
693
  const html = await this.renderHtml(mainHtml, {
572
694
  htmlAttrs: htmlContext.htmlAttrs,
@@ -580,6 +702,7 @@ var Renderer = class {
580
702
  });
581
703
  return { html };
582
704
  }
705
+ /** SSG renders a route. */
583
706
  async renderRoute(route, options) {
584
707
  const routeParams = options.routeParams;
585
708
  if (route.locale) {
@@ -592,6 +715,8 @@ var Renderer = class {
592
715
  );
593
716
  }
594
717
  let props = {};
718
+ let locale = route.locale;
719
+ let translations = void 0;
595
720
  if (route.module.getStaticProps) {
596
721
  const propsData = await route.module.getStaticProps({
597
722
  rootConfig: this.rootConfig,
@@ -603,8 +728,25 @@ var Renderer = class {
603
728
  if (propsData.props) {
604
729
  props = propsData.props;
605
730
  }
731
+ if (propsData.locale) {
732
+ locale = propsData.locale;
733
+ }
734
+ if (propsData.translations) {
735
+ translations = propsData.translations;
736
+ }
606
737
  }
607
- return this.renderComponent(Component, props, { route, routeParams });
738
+ const routePath = route.isDefaultLocale ? route.routePath : route.localeRoutePath;
739
+ const currentPath = replaceParams(routePath, {
740
+ ...routeParams,
741
+ locale
742
+ });
743
+ return this.renderComponent(Component, props, {
744
+ currentPath,
745
+ route,
746
+ routeParams,
747
+ locale,
748
+ translations
749
+ });
608
750
  }
609
751
  async getSitemap() {
610
752
  const sitemap = {};
@@ -623,80 +765,85 @@ var Renderer = class {
623
765
  const htmlAttrs = (options == null ? void 0 : options.htmlAttrs) || {};
624
766
  const headAttrs = (options == null ? void 0 : options.headAttrs) || {};
625
767
  const bodyAttrs = (options == null ? void 0 : options.bodyAttrs) || {};
626
- const page = /* @__PURE__ */ jsxs4("html", {
627
- ...htmlAttrs,
628
- children: [
629
- /* @__PURE__ */ jsxs4("head", {
630
- ...headAttrs,
631
- children: [
632
- /* @__PURE__ */ jsx4("meta", {
633
- charSet: "utf-8"
634
- }),
635
- options == null ? void 0 : options.headComponents
636
- ]
637
- }),
638
- /* @__PURE__ */ jsx4("body", {
639
- ...bodyAttrs,
640
- dangerouslySetInnerHTML: { __html: html }
641
- })
642
- ]
643
- });
768
+ const page = /* @__PURE__ */ jsxs4("html", { ...htmlAttrs, children: [
769
+ /* @__PURE__ */ jsxs4("head", { ...headAttrs, children: [
770
+ /* @__PURE__ */ jsx4("meta", { charSet: "utf-8" }),
771
+ options == null ? void 0 : options.headComponents
772
+ ] }),
773
+ /* @__PURE__ */ jsx4("body", { ...bodyAttrs, dangerouslySetInnerHTML: { __html: html } })
774
+ ] });
644
775
  return `<!doctype html>
645
776
  ${renderToString(page)}
646
777
  `;
647
778
  }
648
- async render404() {
779
+ async render404(options) {
780
+ const currentPath = (options == null ? void 0 : options.currentPath) || "/404";
649
781
  const [route, routeParams] = this.routes.get("/404");
650
782
  if (route && route.src === "routes/404.tsx" && route.module.default) {
651
783
  const Component = route.module.default;
652
- return this.renderComponent(Component, {}, { route, routeParams });
784
+ return this.renderComponent(
785
+ Component,
786
+ {},
787
+ { currentPath, route, routeParams, locale: "en" }
788
+ );
653
789
  }
654
790
  const mainHtml = renderToString(
655
- /* @__PURE__ */ jsx4(ErrorPage, {
656
- code: 404,
657
- title: "Not found",
658
- message: "Double-check the URL entered and try again."
659
- })
791
+ /* @__PURE__ */ jsx4(
792
+ ErrorPage,
793
+ {
794
+ code: 404,
795
+ title: "Not found",
796
+ message: "Double-check the URL entered and try again.",
797
+ align: "center"
798
+ }
799
+ )
660
800
  );
661
801
  const html = await this.renderHtml(mainHtml, {
662
802
  headComponents: [
663
- /* @__PURE__ */ jsx4("title", {
664
- children: "404 Not Found"
665
- }),
666
- /* @__PURE__ */ jsx4("meta", {
667
- name: "viewport",
668
- content: "width=device-width, initial-scale=1.0"
669
- })
803
+ /* @__PURE__ */ jsx4("title", { children: "404 Not Found" }),
804
+ /* @__PURE__ */ jsx4(
805
+ "meta",
806
+ {
807
+ name: "viewport",
808
+ content: "width=device-width, initial-scale=1.0"
809
+ }
810
+ )
670
811
  ]
671
812
  });
672
813
  return { html };
673
814
  }
674
- async renderError(err) {
815
+ async renderError(err, options) {
816
+ const currentPath = (options == null ? void 0 : options.currentPath) || "/500";
675
817
  const [route, routeParams] = this.routes.get("/500");
676
818
  if (route && route.src === "routes/500.tsx" && route.module.default) {
677
819
  const Component = route.module.default;
678
820
  return this.renderComponent(
679
821
  Component,
680
822
  { error: err },
681
- { route, routeParams }
823
+ { currentPath, route, routeParams, locale: "en" }
682
824
  );
683
825
  }
684
826
  const mainHtml = renderToString(
685
- /* @__PURE__ */ jsx4(ErrorPage, {
686
- code: 500,
687
- title: "Something went wrong",
688
- message: "An unknown error occurred."
689
- })
827
+ /* @__PURE__ */ jsx4(
828
+ ErrorPage,
829
+ {
830
+ code: 500,
831
+ title: "Something went wrong",
832
+ message: "An unknown error occurred.",
833
+ align: "center"
834
+ }
835
+ )
690
836
  );
691
837
  const html = await this.renderHtml(mainHtml, {
692
838
  headComponents: [
693
- /* @__PURE__ */ jsx4("title", {
694
- children: "500 Error"
695
- }),
696
- /* @__PURE__ */ jsx4("meta", {
697
- name: "viewport",
698
- content: "width=device-width, initial-scale=1.0"
699
- })
839
+ /* @__PURE__ */ jsx4("title", { children: "500 Error" }),
840
+ /* @__PURE__ */ jsx4(
841
+ "meta",
842
+ {
843
+ name: "viewport",
844
+ content: "width=device-width, initial-scale=1.0"
845
+ }
846
+ )
700
847
  ]
701
848
  });
702
849
  return { html };
@@ -704,35 +851,35 @@ ${renderToString(page)}
704
851
  async renderDevServer404(req) {
705
852
  const sitemap = await this.getSitemap();
706
853
  const mainHtml = renderToString(
707
- /* @__PURE__ */ jsx4(DevNotFoundPage, {
708
- req,
709
- sitemap
710
- })
854
+ /* @__PURE__ */ jsx4(DevNotFoundPage, { req, sitemap })
711
855
  );
712
856
  const html = await this.renderHtml(mainHtml, {
713
- headComponents: [/* @__PURE__ */ jsx4("title", {
714
- children: "404 Not found | Root.js"
715
- })]
857
+ headComponents: [/* @__PURE__ */ jsx4("title", { children: "404 Not found | Root.js" })]
716
858
  });
717
859
  return { html };
718
860
  }
719
861
  async renderDevServer500(req, error) {
720
862
  const [route, routeParams] = this.routes.get(req.path);
721
863
  const mainHtml = renderToString(
722
- /* @__PURE__ */ jsx4(DevErrorPage, {
723
- req,
724
- route,
725
- routeParams,
726
- error
727
- })
864
+ /* @__PURE__ */ jsx4(
865
+ DevErrorPage,
866
+ {
867
+ req,
868
+ route,
869
+ routeParams,
870
+ error
871
+ }
872
+ )
728
873
  );
729
874
  const html = await this.renderHtml(mainHtml, {
730
- headComponents: [/* @__PURE__ */ jsx4("title", {
731
- children: "500 Error | Root.js"
732
- })]
875
+ headComponents: [/* @__PURE__ */ jsx4("title", { children: "500 Error | Root.js" })]
733
876
  });
734
877
  return { html };
735
878
  }
879
+ /**
880
+ * Parses rendered HTML for custom element tags used on the page and
881
+ * automatically adds the JS/CSS deps to the page.
882
+ */
736
883
  async collectElementDeps(html, jsDeps, cssDeps) {
737
884
  const elementsMap = this.elementGraph.sourceFiles;
738
885
  const assetMap = this.assetMap;