@blinkk/root 1.0.0-beta.9 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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();
@@ -548,7 +676,8 @@ var Renderer = class {
548
676
  if (!scriptDep.src) {
549
677
  return;
550
678
  }
551
- const scriptAsset = await this.assetMap.get(scriptDep.src.slice(1));
679
+ const assetId = String(scriptDep.src).slice(1);
680
+ const scriptAsset = await this.assetMap.get(assetId);
552
681
  if (scriptAsset) {
553
682
  jsDeps.add(scriptAsset.assetUrl);
554
683
  const scriptJsDeps = await scriptAsset.getJsDeps();
@@ -557,16 +686,10 @@ var Renderer = class {
557
686
  })
558
687
  );
559
688
  const styleTags = Array.from(cssDeps).map((cssUrl) => {
560
- return /* @__PURE__ */ jsx4("link", {
561
- rel: "stylesheet",
562
- href: cssUrl
563
- });
689
+ return /* @__PURE__ */ jsx4("link", { rel: "stylesheet", href: cssUrl });
564
690
  });
565
691
  const scriptTags = Array.from(jsDeps).map((jsUrls) => {
566
- return /* @__PURE__ */ jsx4("script", {
567
- type: "module",
568
- src: jsUrls
569
- });
692
+ return /* @__PURE__ */ jsx4("script", { type: "module", src: jsUrls });
570
693
  });
571
694
  const html = await this.renderHtml(mainHtml, {
572
695
  htmlAttrs: htmlContext.htmlAttrs,
@@ -580,6 +703,7 @@ var Renderer = class {
580
703
  });
581
704
  return { html };
582
705
  }
706
+ /** SSG renders a route. */
583
707
  async renderRoute(route, options) {
584
708
  const routeParams = options.routeParams;
585
709
  if (route.locale) {
@@ -592,6 +716,8 @@ var Renderer = class {
592
716
  );
593
717
  }
594
718
  let props = {};
719
+ let locale = route.locale;
720
+ let translations = void 0;
595
721
  if (route.module.getStaticProps) {
596
722
  const propsData = await route.module.getStaticProps({
597
723
  rootConfig: this.rootConfig,
@@ -603,8 +729,25 @@ var Renderer = class {
603
729
  if (propsData.props) {
604
730
  props = propsData.props;
605
731
  }
732
+ if (propsData.locale) {
733
+ locale = propsData.locale;
734
+ }
735
+ if (propsData.translations) {
736
+ translations = propsData.translations;
737
+ }
606
738
  }
607
- return this.renderComponent(Component, props, { route, routeParams });
739
+ const routePath = route.isDefaultLocale ? route.routePath : route.localeRoutePath;
740
+ const currentPath = replaceParams(routePath, {
741
+ ...routeParams,
742
+ locale
743
+ });
744
+ return this.renderComponent(Component, props, {
745
+ currentPath,
746
+ route,
747
+ routeParams,
748
+ locale,
749
+ translations
750
+ });
608
751
  }
609
752
  async getSitemap() {
610
753
  const sitemap = {};
@@ -623,80 +766,85 @@ var Renderer = class {
623
766
  const htmlAttrs = (options == null ? void 0 : options.htmlAttrs) || {};
624
767
  const headAttrs = (options == null ? void 0 : options.headAttrs) || {};
625
768
  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
- });
769
+ const page = /* @__PURE__ */ jsxs4("html", { ...htmlAttrs, children: [
770
+ /* @__PURE__ */ jsxs4("head", { ...headAttrs, children: [
771
+ /* @__PURE__ */ jsx4("meta", { charSet: "utf-8" }),
772
+ options == null ? void 0 : options.headComponents
773
+ ] }),
774
+ /* @__PURE__ */ jsx4("body", { ...bodyAttrs, dangerouslySetInnerHTML: { __html: html } })
775
+ ] });
644
776
  return `<!doctype html>
645
777
  ${renderToString(page)}
646
778
  `;
647
779
  }
648
- async render404() {
780
+ async render404(options) {
781
+ const currentPath = (options == null ? void 0 : options.currentPath) || "/404";
649
782
  const [route, routeParams] = this.routes.get("/404");
650
783
  if (route && route.src === "routes/404.tsx" && route.module.default) {
651
784
  const Component = route.module.default;
652
- return this.renderComponent(Component, {}, { route, routeParams });
785
+ return this.renderComponent(
786
+ Component,
787
+ {},
788
+ { currentPath, route, routeParams, locale: "en" }
789
+ );
653
790
  }
654
791
  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
- })
792
+ /* @__PURE__ */ jsx4(
793
+ ErrorPage,
794
+ {
795
+ code: 404,
796
+ title: "Not found",
797
+ message: "Double-check the URL entered and try again.",
798
+ align: "center"
799
+ }
800
+ )
660
801
  );
661
802
  const html = await this.renderHtml(mainHtml, {
662
803
  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
- })
804
+ /* @__PURE__ */ jsx4("title", { children: "404 Not Found" }),
805
+ /* @__PURE__ */ jsx4(
806
+ "meta",
807
+ {
808
+ name: "viewport",
809
+ content: "width=device-width, initial-scale=1.0"
810
+ }
811
+ )
670
812
  ]
671
813
  });
672
814
  return { html };
673
815
  }
674
- async renderError(err) {
816
+ async renderError(err, options) {
817
+ const currentPath = (options == null ? void 0 : options.currentPath) || "/500";
675
818
  const [route, routeParams] = this.routes.get("/500");
676
819
  if (route && route.src === "routes/500.tsx" && route.module.default) {
677
820
  const Component = route.module.default;
678
821
  return this.renderComponent(
679
822
  Component,
680
823
  { error: err },
681
- { route, routeParams }
824
+ { currentPath, route, routeParams, locale: "en" }
682
825
  );
683
826
  }
684
827
  const mainHtml = renderToString(
685
- /* @__PURE__ */ jsx4(ErrorPage, {
686
- code: 500,
687
- title: "Something went wrong",
688
- message: "An unknown error occurred."
689
- })
828
+ /* @__PURE__ */ jsx4(
829
+ ErrorPage,
830
+ {
831
+ code: 500,
832
+ title: "Something went wrong",
833
+ message: "An unknown error occurred.",
834
+ align: "center"
835
+ }
836
+ )
690
837
  );
691
838
  const html = await this.renderHtml(mainHtml, {
692
839
  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
- })
840
+ /* @__PURE__ */ jsx4("title", { children: "500 Error" }),
841
+ /* @__PURE__ */ jsx4(
842
+ "meta",
843
+ {
844
+ name: "viewport",
845
+ content: "width=device-width, initial-scale=1.0"
846
+ }
847
+ )
700
848
  ]
701
849
  });
702
850
  return { html };
@@ -704,35 +852,35 @@ ${renderToString(page)}
704
852
  async renderDevServer404(req) {
705
853
  const sitemap = await this.getSitemap();
706
854
  const mainHtml = renderToString(
707
- /* @__PURE__ */ jsx4(DevNotFoundPage, {
708
- req,
709
- sitemap
710
- })
855
+ /* @__PURE__ */ jsx4(DevNotFoundPage, { req, sitemap })
711
856
  );
712
857
  const html = await this.renderHtml(mainHtml, {
713
- headComponents: [/* @__PURE__ */ jsx4("title", {
714
- children: "404 Not found | Root.js"
715
- })]
858
+ headComponents: [/* @__PURE__ */ jsx4("title", { children: "404 Not found | Root.js" })]
716
859
  });
717
860
  return { html };
718
861
  }
719
862
  async renderDevServer500(req, error) {
720
863
  const [route, routeParams] = this.routes.get(req.path);
721
864
  const mainHtml = renderToString(
722
- /* @__PURE__ */ jsx4(DevErrorPage, {
723
- req,
724
- route,
725
- routeParams,
726
- error
727
- })
865
+ /* @__PURE__ */ jsx4(
866
+ DevErrorPage,
867
+ {
868
+ req,
869
+ route,
870
+ routeParams,
871
+ error
872
+ }
873
+ )
728
874
  );
729
875
  const html = await this.renderHtml(mainHtml, {
730
- headComponents: [/* @__PURE__ */ jsx4("title", {
731
- children: "500 Error | Root.js"
732
- })]
876
+ headComponents: [/* @__PURE__ */ jsx4("title", { children: "500 Error | Root.js" })]
733
877
  });
734
878
  return { html };
735
879
  }
880
+ /**
881
+ * Parses rendered HTML for custom element tags used on the page and
882
+ * automatically adds the JS/CSS deps to the page.
883
+ */
736
884
  async collectElementDeps(html, jsDeps, cssDeps) {
737
885
  const elementsMap = this.elementGraph.sourceFiles;
738
886
  const assetMap = this.assetMap;