@blinkk/root 1.0.0-rc.4 → 1.0.0-rc.41
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/LICENSE +22 -0
- package/bin/root.js +6 -32
- package/dist/{chunk-MJCIAH6K.js → chunk-7IDJ3PBT.js} +1 -1
- package/dist/{chunk-MJCIAH6K.js.map → chunk-7IDJ3PBT.js.map} +1 -1
- package/dist/chunk-IUQLRDFW.js +237 -0
- package/dist/chunk-IUQLRDFW.js.map +1 -0
- package/dist/{chunk-WNXIRMFF.js → chunk-TZAHHHA4.js} +27 -7
- package/dist/chunk-TZAHHHA4.js.map +1 -0
- package/dist/{chunk-J2ANSYAE.js → chunk-UYHD775U.js} +82 -9
- package/dist/chunk-UYHD775U.js.map +1 -0
- package/dist/{chunk-CY3DVKXO.js → chunk-XOOP364R.js} +445 -117
- package/dist/chunk-XOOP364R.js.map +1 -0
- package/dist/cli.d.ts +18 -3
- package/dist/cli.js +8 -4
- package/dist/core.d.ts +5 -3
- package/dist/core.js +8 -3
- package/dist/core.js.map +1 -1
- package/dist/functions.d.ts +1 -1
- package/dist/functions.js +7 -7
- package/dist/functions.js.map +1 -1
- package/dist/middleware.d.ts +12 -5
- package/dist/middleware.js +3 -1
- package/dist/node.d.ts +10 -2
- package/dist/node.js +5 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.js +351 -289
- package/dist/render.js.map +1 -1
- package/dist/{types-9209ea89.d.ts → types-403nR8i5.d.ts} +96 -8
- package/package.json +38 -35
- package/dist/chunk-CY3DVKXO.js.map +0 -1
- package/dist/chunk-DFBTOMQF.js +0 -61
- package/dist/chunk-DFBTOMQF.js.map +0 -1
- package/dist/chunk-J2ANSYAE.js.map +0 -1
- package/dist/chunk-WNXIRMFF.js.map +0 -1
package/dist/render.js
CHANGED
|
@@ -3,14 +3,19 @@ import {
|
|
|
3
3
|
I18N_CONTEXT,
|
|
4
4
|
REQUEST_CONTEXT,
|
|
5
5
|
getTranslations
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-7IDJ3PBT.js";
|
|
7
7
|
import {
|
|
8
|
+
RouteTrie,
|
|
8
9
|
htmlMinify,
|
|
9
10
|
htmlPretty,
|
|
10
11
|
parseTagNames
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-IUQLRDFW.js";
|
|
12
13
|
|
|
13
14
|
// src/render/render.tsx
|
|
15
|
+
import crypto from "node:crypto";
|
|
16
|
+
import {
|
|
17
|
+
options as preactOptions
|
|
18
|
+
} from "preact";
|
|
14
19
|
import renderToString from "preact-render-to-string";
|
|
15
20
|
|
|
16
21
|
// src/core/pages/ErrorPage.tsx
|
|
@@ -91,7 +96,6 @@ function ErrorPage(props) {
|
|
|
91
96
|
// src/core/pages/DevErrorPage.tsx
|
|
92
97
|
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
|
|
93
98
|
function DevErrorPage(props) {
|
|
94
|
-
var _a;
|
|
95
99
|
const req = props.req;
|
|
96
100
|
const err = props.error;
|
|
97
101
|
const route = props.route;
|
|
@@ -99,7 +103,7 @@ function DevErrorPage(props) {
|
|
|
99
103
|
let errMsg = String(err);
|
|
100
104
|
if (err && err.stack) {
|
|
101
105
|
errMsg = err.stack.replace(/\(.*node_modules/g, "(node_modules").replace(/at \/.*node_modules/g, "at node_modules");
|
|
102
|
-
if (
|
|
106
|
+
if (req.rootConfig?.rootDir) {
|
|
103
107
|
errMsg = errMsg.replaceAll(req.rootConfig.rootDir, "<root>");
|
|
104
108
|
}
|
|
105
109
|
if (process.env.HOME) {
|
|
@@ -113,7 +117,7 @@ function DevErrorPage(props) {
|
|
|
113
117
|
] }),
|
|
114
118
|
/* @__PURE__ */ jsx2("h2", { children: "Debug Info" }),
|
|
115
119
|
/* @__PURE__ */ jsx2("pre", { className: "box", children: /* @__PURE__ */ jsx2("code", { children: `url: ${req.originalUrl}
|
|
116
|
-
route: ${
|
|
120
|
+
route: ${route?.src || "null"}
|
|
117
121
|
routeParams: ${routeParams && JSON.stringify(routeParams) || "null"}` }) })
|
|
118
122
|
] });
|
|
119
123
|
}
|
|
@@ -203,12 +207,51 @@ function parseAcceptLanguage(value) {
|
|
|
203
207
|
|
|
204
208
|
// src/render/i18n-fallbacks.ts
|
|
205
209
|
var UNKNOWN_COUNTRY = "zz";
|
|
210
|
+
var ES_419_COUNTRIES = [
|
|
211
|
+
"ar",
|
|
212
|
+
// Argentina
|
|
213
|
+
"bo",
|
|
214
|
+
// Bolivia
|
|
215
|
+
"cl",
|
|
216
|
+
// Chile
|
|
217
|
+
"co",
|
|
218
|
+
// Colombia
|
|
219
|
+
"cr",
|
|
220
|
+
// Costa Rica
|
|
221
|
+
"cu",
|
|
222
|
+
// Cuba
|
|
223
|
+
"do",
|
|
224
|
+
// Dominican Republic
|
|
225
|
+
"ec",
|
|
226
|
+
// Ecuador
|
|
227
|
+
"sv",
|
|
228
|
+
// El Salvador
|
|
229
|
+
"gt",
|
|
230
|
+
// Guatemala
|
|
231
|
+
"hn",
|
|
232
|
+
// Honduras
|
|
233
|
+
"mx",
|
|
234
|
+
// Mexico
|
|
235
|
+
"ni",
|
|
236
|
+
// Nicaragua
|
|
237
|
+
"pa",
|
|
238
|
+
// Panama
|
|
239
|
+
"py",
|
|
240
|
+
// Paraguay
|
|
241
|
+
"pe",
|
|
242
|
+
// Peru
|
|
243
|
+
"pr",
|
|
244
|
+
// Puerto Rico
|
|
245
|
+
"uy",
|
|
246
|
+
// Uruguay
|
|
247
|
+
"ve"
|
|
248
|
+
// Venezuela
|
|
249
|
+
];
|
|
206
250
|
function getFallbackLocales(req) {
|
|
207
|
-
var _a, _b;
|
|
208
251
|
const hl = getFirstQueryParam(req, "hl");
|
|
209
252
|
const countryCode = getCountry(req);
|
|
210
253
|
if (isWebCrawler(req)) {
|
|
211
|
-
const defaultLocale =
|
|
254
|
+
const defaultLocale = req.rootConfig?.i18n?.defaultLocale || "en";
|
|
212
255
|
if (hl && hl !== defaultLocale) {
|
|
213
256
|
return [hl, defaultLocale];
|
|
214
257
|
}
|
|
@@ -226,7 +269,12 @@ function getFallbackLocales(req) {
|
|
|
226
269
|
locales.add(`${langCode}_${countryCode}`);
|
|
227
270
|
});
|
|
228
271
|
locales.add(`ALL_${countryCode}`);
|
|
272
|
+
const isEs419Country = test419Country(countryCode);
|
|
229
273
|
langs.forEach((langCode) => {
|
|
274
|
+
if (langCode === "es" && isEs419Country) {
|
|
275
|
+
locales.add("es-419_ALL");
|
|
276
|
+
locales.add("es-419");
|
|
277
|
+
}
|
|
230
278
|
locales.add(`${langCode}_ALL`);
|
|
231
279
|
locales.add(langCode);
|
|
232
280
|
});
|
|
@@ -251,6 +299,9 @@ function getFallbackLanguages(req) {
|
|
|
251
299
|
parseAcceptLanguage(acceptLangHeader).forEach((lang) => {
|
|
252
300
|
if (lang.region) {
|
|
253
301
|
langs.add(`${lang.code}-${lang.region}`);
|
|
302
|
+
if (lang.code === "es" && test419Country(lang.region)) {
|
|
303
|
+
langs.add("es-419");
|
|
304
|
+
}
|
|
254
305
|
}
|
|
255
306
|
langs.add(lang.code);
|
|
256
307
|
});
|
|
@@ -279,275 +330,132 @@ function isWebCrawler(req) {
|
|
|
279
330
|
const userAgent = userAgentHeader.toLowerCase();
|
|
280
331
|
return userAgent.includes("googlebot") || userAgent.includes("bingbot") || userAgent.includes("twitterbot");
|
|
281
332
|
}
|
|
333
|
+
function test419Country(countryCode) {
|
|
334
|
+
return ES_419_COUNTRIES.includes(countryCode);
|
|
335
|
+
}
|
|
282
336
|
|
|
283
337
|
// src/render/router.ts
|
|
284
338
|
import path from "node:path";
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
let nextNode;
|
|
312
|
-
if (head.startsWith("[") && head.endsWith("]")) {
|
|
313
|
-
if (!this.paramNodes) {
|
|
314
|
-
this.paramNodes = {};
|
|
315
|
-
}
|
|
316
|
-
const paramName = head.slice(1, -1);
|
|
317
|
-
if (!this.paramNodes[paramName]) {
|
|
318
|
-
this.paramNodes[paramName] = new ParamNode(paramName);
|
|
319
|
-
}
|
|
320
|
-
nextNode = this.paramNodes[paramName].trie;
|
|
321
|
-
} else {
|
|
322
|
-
nextNode = this.children[head];
|
|
323
|
-
if (!nextNode) {
|
|
324
|
-
nextNode = new _RouteTrie();
|
|
325
|
-
this.children[head] = nextNode;
|
|
339
|
+
var ROUTES_FILES = import.meta.glob(
|
|
340
|
+
["/routes/*.ts", "/routes/**/*.ts", "/routes/*.tsx", "/routes/**/*.tsx"],
|
|
341
|
+
{ eager: true }
|
|
342
|
+
);
|
|
343
|
+
var Router = class {
|
|
344
|
+
constructor(rootConfig) {
|
|
345
|
+
this.rootConfig = rootConfig;
|
|
346
|
+
this.routeTrie = this.initRouteTrie();
|
|
347
|
+
}
|
|
348
|
+
get(url) {
|
|
349
|
+
return this.routeTrie.get(url);
|
|
350
|
+
}
|
|
351
|
+
async walk(cb) {
|
|
352
|
+
await this.routeTrie.walk(cb);
|
|
353
|
+
}
|
|
354
|
+
initRouteTrie() {
|
|
355
|
+
const locales = this.rootConfig.i18n?.locales || [];
|
|
356
|
+
const basePath = this.rootConfig.base || "/";
|
|
357
|
+
const defaultLocale = this.rootConfig.i18n?.defaultLocale || "en";
|
|
358
|
+
const trie = new RouteTrie();
|
|
359
|
+
Object.keys(ROUTES_FILES).forEach((modulePath) => {
|
|
360
|
+
const src = modulePath.slice(1);
|
|
361
|
+
let relativeRoutePath = modulePath.replace(/^\/routes/, "");
|
|
362
|
+
const parts = path.parse(relativeRoutePath);
|
|
363
|
+
if (parts.name.startsWith("_")) {
|
|
364
|
+
return;
|
|
326
365
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
* Returns a route mapped to the given path and any parameter values from the
|
|
332
|
-
* URL.
|
|
333
|
-
*/
|
|
334
|
-
get(path2) {
|
|
335
|
-
const params = {};
|
|
336
|
-
const route = this.getRoute(path2, params);
|
|
337
|
-
return [route, params];
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Walks the route trie and calls a callback function for each route.
|
|
341
|
-
*/
|
|
342
|
-
walk(cb) {
|
|
343
|
-
const promises = [];
|
|
344
|
-
const addPromise = (promise) => {
|
|
345
|
-
if (promise) {
|
|
346
|
-
promises.push(promise);
|
|
366
|
+
if (parts.name === "index") {
|
|
367
|
+
relativeRoutePath = parts.dir;
|
|
368
|
+
} else {
|
|
369
|
+
relativeRoutePath = path.join(parts.dir, parts.name);
|
|
347
370
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
371
|
+
const urlFormat = "/[base]/[path]";
|
|
372
|
+
const i18nUrlFormat = toSquareBrackets(
|
|
373
|
+
this.rootConfig.i18n?.urlFormat || "/[locale]/[base]/[path]"
|
|
374
|
+
);
|
|
375
|
+
const placeholders = {
|
|
376
|
+
base: removeSlashes(basePath),
|
|
377
|
+
path: removeSlashes(relativeRoutePath)
|
|
378
|
+
};
|
|
379
|
+
const formatUrl = (format) => {
|
|
380
|
+
const url = format.replaceAll("[base]", placeholders.base).replaceAll("[path]", placeholders.path);
|
|
381
|
+
return normalizeUrlPath(url, {
|
|
382
|
+
trailingSlash: this.rootConfig.server?.trailingSlash
|
|
358
383
|
});
|
|
384
|
+
};
|
|
385
|
+
const routePath = formatUrl(urlFormat);
|
|
386
|
+
const localeRoutePath = formatUrl(i18nUrlFormat);
|
|
387
|
+
trie.add(routePath, {
|
|
388
|
+
src,
|
|
389
|
+
module: ROUTES_FILES[modulePath],
|
|
390
|
+
locale: defaultLocale,
|
|
391
|
+
isDefaultLocale: true,
|
|
392
|
+
routePath,
|
|
393
|
+
localeRoutePath
|
|
359
394
|
});
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
return Promise.all(promises).then(() => {
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Removes all routes from the trie.
|
|
380
|
-
*/
|
|
381
|
-
clear() {
|
|
382
|
-
this.children = {};
|
|
383
|
-
this.paramNodes = void 0;
|
|
384
|
-
this.catchAllNodes = void 0;
|
|
385
|
-
this.optCatchAllNodes = void 0;
|
|
386
|
-
this.route = void 0;
|
|
387
|
-
}
|
|
388
|
-
getRoute(urlPath, params) {
|
|
389
|
-
urlPath = this.normalizePath(urlPath);
|
|
390
|
-
if (urlPath === "") {
|
|
391
|
-
if (this.route) {
|
|
392
|
-
return this.route;
|
|
393
|
-
}
|
|
394
|
-
if (this.optCatchAllNodes) {
|
|
395
|
-
if (urlPath) {
|
|
396
|
-
params[this.optCatchAllNodes.name] = urlPath;
|
|
397
|
-
}
|
|
398
|
-
return this.optCatchAllNodes.route;
|
|
399
|
-
}
|
|
400
|
-
return void 0;
|
|
401
|
-
}
|
|
402
|
-
const [head, tail] = this.splitPath(urlPath);
|
|
403
|
-
const child = this.children[head];
|
|
404
|
-
if (child) {
|
|
405
|
-
const route = child.getRoute(tail, params);
|
|
406
|
-
if (route) {
|
|
407
|
-
return route;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
if (this.paramNodes) {
|
|
411
|
-
for (const paramChild of Object.values(this.paramNodes)) {
|
|
412
|
-
const route = paramChild.trie.getRoute(tail, params);
|
|
413
|
-
if (route) {
|
|
414
|
-
params[paramChild.name] = head;
|
|
415
|
-
return route;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
if (this.catchAllNodes) {
|
|
420
|
-
params[this.catchAllNodes.name] = urlPath;
|
|
421
|
-
return this.catchAllNodes.route;
|
|
422
|
-
}
|
|
423
|
-
if (this.optCatchAllNodes) {
|
|
424
|
-
params[this.optCatchAllNodes.name] = urlPath;
|
|
425
|
-
return this.optCatchAllNodes.route;
|
|
426
|
-
}
|
|
427
|
-
return void 0;
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Normalizes a path for inclusion into the route trie.
|
|
431
|
-
*/
|
|
432
|
-
normalizePath(path2) {
|
|
433
|
-
return path2.replace(/^\/+/g, "").replace(/\/+$/g, "");
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Splits the parent directory from its children, e.g.:
|
|
437
|
-
*
|
|
438
|
-
* splitPath("foo/bar/baz") -> ["foo", "bar/baz"]
|
|
439
|
-
*/
|
|
440
|
-
splitPath(path2) {
|
|
441
|
-
const i = path2.indexOf("/");
|
|
442
|
-
if (i === -1) {
|
|
443
|
-
return [path2, ""];
|
|
444
|
-
}
|
|
445
|
-
return [path2.slice(0, i), path2.slice(i + 1)];
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
var ParamNode = class {
|
|
449
|
-
constructor(name) {
|
|
450
|
-
this.trie = new RouteTrie();
|
|
451
|
-
this.name = name;
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
var CatchAllNode = class {
|
|
455
|
-
constructor(name, route) {
|
|
456
|
-
this.name = name;
|
|
457
|
-
this.route = route;
|
|
458
|
-
}
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
// src/render/router.ts
|
|
462
|
-
function getRoutes(config) {
|
|
463
|
-
var _a, _b, _c;
|
|
464
|
-
const locales = ((_a = config.i18n) == null ? void 0 : _a.locales) || [];
|
|
465
|
-
const i18nUrlFormat = ((_b = config.i18n) == null ? void 0 : _b.urlFormat) || "/{locale}/{path}";
|
|
466
|
-
const defaultLocale = ((_c = config.i18n) == null ? void 0 : _c.defaultLocale) || "en";
|
|
467
|
-
const routes = import.meta.glob(
|
|
468
|
-
["/routes/*.ts", "/routes/**/*.ts", "/routes/*.tsx", "/routes/**/*.tsx"],
|
|
469
|
-
{
|
|
470
|
-
eager: true
|
|
471
|
-
}
|
|
472
|
-
);
|
|
473
|
-
const trie = new RouteTrie();
|
|
474
|
-
Object.keys(routes).forEach((modulePath) => {
|
|
475
|
-
const src = modulePath.slice(1);
|
|
476
|
-
let routePath = modulePath.replace(/^\/routes/, "");
|
|
477
|
-
const parts = path.parse(routePath);
|
|
478
|
-
if (parts.name.startsWith("_")) {
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
if (parts.name === "index") {
|
|
482
|
-
routePath = parts.dir;
|
|
483
|
-
} else {
|
|
484
|
-
routePath = path.join(parts.dir, parts.name);
|
|
485
|
-
}
|
|
486
|
-
const localeRoutePath = i18nUrlFormat.replace("{locale}", "[locale]").replace("{path}", routePath.replace(/^\/*/, ""));
|
|
487
|
-
trie.add(routePath, {
|
|
488
|
-
src,
|
|
489
|
-
module: routes[modulePath],
|
|
490
|
-
locale: defaultLocale,
|
|
491
|
-
isDefaultLocale: true,
|
|
492
|
-
routePath: normalizeUrlPath(routePath),
|
|
493
|
-
localeRoutePath: normalizeUrlPath(localeRoutePath)
|
|
494
|
-
});
|
|
495
|
-
locales.forEach((locale) => {
|
|
496
|
-
const localePath = localeRoutePath.replace("[locale]", locale);
|
|
497
|
-
if (localePath !== routePath) {
|
|
498
|
-
trie.add(localePath, {
|
|
499
|
-
src,
|
|
500
|
-
module: routes[modulePath],
|
|
501
|
-
locale,
|
|
502
|
-
isDefaultLocale: false,
|
|
503
|
-
routePath,
|
|
504
|
-
localeRoutePath
|
|
395
|
+
if (i18nUrlFormat.includes("[locale]")) {
|
|
396
|
+
locales.forEach((locale) => {
|
|
397
|
+
const localePath = localeRoutePath.replace("[locale]", locale);
|
|
398
|
+
if (localePath !== relativeRoutePath) {
|
|
399
|
+
trie.add(localePath, {
|
|
400
|
+
src,
|
|
401
|
+
module: ROUTES_FILES[modulePath],
|
|
402
|
+
locale,
|
|
403
|
+
isDefaultLocale: false,
|
|
404
|
+
routePath,
|
|
405
|
+
localeRoutePath
|
|
406
|
+
});
|
|
407
|
+
}
|
|
505
408
|
});
|
|
506
409
|
}
|
|
507
410
|
});
|
|
508
|
-
|
|
509
|
-
return trie;
|
|
510
|
-
}
|
|
511
|
-
async function getAllPathsForRoute(urlPathFormat, route) {
|
|
512
|
-
const routeModule = route.module;
|
|
513
|
-
if (!routeModule.default) {
|
|
514
|
-
return [];
|
|
411
|
+
return trie;
|
|
515
412
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
413
|
+
async getAllPathsForRoute(urlPathFormat, route) {
|
|
414
|
+
const routeModule = route.module;
|
|
415
|
+
if (!routeModule.default) {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
const urlPaths = [];
|
|
419
|
+
if (routeModule.getStaticPaths) {
|
|
420
|
+
const staticPaths = await routeModule.getStaticPaths({
|
|
421
|
+
rootConfig: this.rootConfig
|
|
422
|
+
});
|
|
423
|
+
if (staticPaths.paths) {
|
|
424
|
+
staticPaths.paths.forEach(
|
|
425
|
+
(pathParams) => {
|
|
426
|
+
const urlPath = replaceParams(
|
|
427
|
+
urlPathFormat,
|
|
428
|
+
pathParams.params || {}
|
|
526
429
|
);
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
430
|
+
if (pathContainsPlaceholders(urlPath)) {
|
|
431
|
+
console.warn(
|
|
432
|
+
`path contains placeholders: ${urlPathFormat}, double check getStaticPaths() and ensure all params are returned. more info: https://rootjs.dev/guide/routes#getStaticPaths`
|
|
433
|
+
);
|
|
434
|
+
} else {
|
|
435
|
+
urlPaths.push({
|
|
436
|
+
urlPath: normalizeUrlPath(urlPath),
|
|
437
|
+
params: pathParams.params || {}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
532
440
|
}
|
|
533
|
-
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
} else if (routeModule.getStaticProps && !pathContainsPlaceholders(urlPathFormat)) {
|
|
444
|
+
urlPaths.push({ urlPath: normalizeUrlPath(urlPathFormat), params: {} });
|
|
445
|
+
} else if (!routeModule.handle && !pathContainsPlaceholders(urlPathFormat)) {
|
|
446
|
+
urlPaths.push({ urlPath: normalizeUrlPath(urlPathFormat), params: {} });
|
|
447
|
+
} else if (pathContainsPlaceholders(urlPathFormat) && !routeModule.handle && !routeModule.getStaticPaths) {
|
|
448
|
+
console.warn(
|
|
449
|
+
[
|
|
450
|
+
`warning: path contains placeholders: ${urlPathFormat}.`,
|
|
451
|
+
`define either ssg getStaticPaths() or ssr handle() for route: ${route.src}.`,
|
|
452
|
+
"more info: https://rootjs.dev/guide/routes"
|
|
453
|
+
].join("\n")
|
|
534
454
|
);
|
|
535
455
|
}
|
|
536
|
-
|
|
537
|
-
urlPaths.push({ urlPath: normalizeUrlPath(urlPathFormat), params: {} });
|
|
538
|
-
} else if (!routeModule.handle && !pathContainsPlaceholders(urlPathFormat)) {
|
|
539
|
-
urlPaths.push({ urlPath: normalizeUrlPath(urlPathFormat), params: {} });
|
|
540
|
-
} else if (pathContainsPlaceholders(urlPathFormat) && !routeModule.handle && !routeModule.getStaticPaths) {
|
|
541
|
-
console.warn(
|
|
542
|
-
[
|
|
543
|
-
`warning: path contains placeholders: ${urlPathFormat}.`,
|
|
544
|
-
`define either ssg getStaticPaths() or ssr handle() for route: ${route.src}.`,
|
|
545
|
-
"more info: https://rootjs.dev/guide/routes"
|
|
546
|
-
].join("\n")
|
|
547
|
-
);
|
|
456
|
+
return urlPaths;
|
|
548
457
|
}
|
|
549
|
-
|
|
550
|
-
}
|
|
458
|
+
};
|
|
551
459
|
function replaceParams(urlPathFormat, params) {
|
|
552
460
|
const urlPath = urlPathFormat.replaceAll(
|
|
553
461
|
/\[\[?(\.\.\.)?([\w\-_]*)\]?\]/g,
|
|
@@ -561,10 +469,17 @@ function replaceParams(urlPathFormat, params) {
|
|
|
561
469
|
);
|
|
562
470
|
return urlPath;
|
|
563
471
|
}
|
|
564
|
-
function normalizeUrlPath(urlPath) {
|
|
565
|
-
|
|
472
|
+
function normalizeUrlPath(urlPath, options) {
|
|
473
|
+
urlPath = urlPath.replace(/\/+/g, "/");
|
|
474
|
+
if (options?.trailingSlash === false && urlPath !== "/" && urlPath.endsWith("/")) {
|
|
566
475
|
urlPath = urlPath.replace(/\/*$/g, "");
|
|
567
476
|
}
|
|
477
|
+
if (urlPath.endsWith("/index")) {
|
|
478
|
+
urlPath = urlPath.slice(0, -6);
|
|
479
|
+
}
|
|
480
|
+
if (!urlPath.startsWith("/")) {
|
|
481
|
+
urlPath = `/${urlPath}`;
|
|
482
|
+
}
|
|
568
483
|
return urlPath;
|
|
569
484
|
}
|
|
570
485
|
function pathContainsPlaceholders(urlPath) {
|
|
@@ -573,19 +488,30 @@ function pathContainsPlaceholders(urlPath) {
|
|
|
573
488
|
return segment.startsWith("[") && segment.endsWith("]");
|
|
574
489
|
});
|
|
575
490
|
}
|
|
491
|
+
function removeSlashes(str) {
|
|
492
|
+
return str.replace(/^\/*/g, "").replace(/\/*$/g, "");
|
|
493
|
+
}
|
|
494
|
+
function toSquareBrackets(str) {
|
|
495
|
+
if (str.includes("{") || str.includes("}")) {
|
|
496
|
+
const val = str.replaceAll("{", "[").replaceAll("}", "]");
|
|
497
|
+
console.warn(`"${str}" is a deprecated format, please switch to "${val}"`);
|
|
498
|
+
return val;
|
|
499
|
+
}
|
|
500
|
+
return str;
|
|
501
|
+
}
|
|
576
502
|
|
|
577
503
|
// src/render/render.tsx
|
|
578
504
|
import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
|
|
579
505
|
var Renderer = class {
|
|
580
506
|
constructor(rootConfig, options) {
|
|
581
507
|
this.rootConfig = rootConfig;
|
|
582
|
-
this.routes = getRoutes(this.rootConfig);
|
|
583
508
|
this.assetMap = options.assetMap;
|
|
584
509
|
this.elementGraph = options.elementGraph;
|
|
510
|
+
this.router = new Router(rootConfig);
|
|
585
511
|
}
|
|
586
512
|
async handle(req, res, next) {
|
|
587
|
-
const url = req.path
|
|
588
|
-
const [route, routeParams] = this.
|
|
513
|
+
const url = req.path;
|
|
514
|
+
const [route, routeParams] = this.router.get(url);
|
|
589
515
|
if (!route) {
|
|
590
516
|
next();
|
|
591
517
|
return;
|
|
@@ -595,14 +521,13 @@ var Renderer = class {
|
|
|
595
521
|
}
|
|
596
522
|
const fallbackLocales = route.isDefaultLocale ? getFallbackLocales(req) : [route.locale];
|
|
597
523
|
const getPreferredLocale = (availableLocales) => {
|
|
598
|
-
var _a, _b;
|
|
599
524
|
const lowerLocales = availableLocales.map((l) => l.toLowerCase());
|
|
600
525
|
for (const fallbackLocale of fallbackLocales) {
|
|
601
526
|
if (lowerLocales.includes(fallbackLocale.toLowerCase())) {
|
|
602
527
|
return fallbackLocale;
|
|
603
528
|
}
|
|
604
529
|
}
|
|
605
|
-
return
|
|
530
|
+
return req.rootConfig?.i18n?.defaultLocale || "en";
|
|
606
531
|
};
|
|
607
532
|
const render404 = async () => {
|
|
608
533
|
next();
|
|
@@ -613,15 +538,19 @@ var Renderer = class {
|
|
|
613
538
|
render404();
|
|
614
539
|
return;
|
|
615
540
|
}
|
|
541
|
+
const securityConfig = this.getSecurityConfig();
|
|
542
|
+
const cspEnabled = !!securityConfig.contentSecurityPolicy;
|
|
616
543
|
const currentPath = req.path;
|
|
617
|
-
const locale =
|
|
618
|
-
const translations = options
|
|
544
|
+
const locale = options?.locale || route.locale;
|
|
545
|
+
const translations = options?.translations;
|
|
546
|
+
const nonce = cspEnabled ? this.generateNonce() : void 0;
|
|
619
547
|
const output = await this.renderComponent(route.module.default, props2, {
|
|
620
548
|
currentPath,
|
|
621
549
|
route,
|
|
622
550
|
routeParams,
|
|
623
551
|
locale,
|
|
624
|
-
translations
|
|
552
|
+
translations,
|
|
553
|
+
nonce
|
|
625
554
|
});
|
|
626
555
|
let html = output.html;
|
|
627
556
|
if (this.rootConfig.prettyHtml) {
|
|
@@ -631,6 +560,12 @@ var Renderer = class {
|
|
|
631
560
|
}
|
|
632
561
|
if (req.viteServer) {
|
|
633
562
|
html = await req.viteServer.transformIndexHtml(currentPath, html);
|
|
563
|
+
if (nonce) {
|
|
564
|
+
html = html.replace(
|
|
565
|
+
'<script type="module" src="/@vite/client"></script>',
|
|
566
|
+
`<script type="module" src="/@vite/client" nonce="${nonce}"></script>`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
634
569
|
}
|
|
635
570
|
let statusCode = 200;
|
|
636
571
|
if (route.src === "routes/404.tsx") {
|
|
@@ -639,7 +574,13 @@ var Renderer = class {
|
|
|
639
574
|
statusCode = 500;
|
|
640
575
|
}
|
|
641
576
|
req.hooks.trigger("preRender");
|
|
642
|
-
res.status(statusCode)
|
|
577
|
+
res.status(statusCode);
|
|
578
|
+
res.set({ "Content-Type": "text/html" });
|
|
579
|
+
this.setSecurityHeaders(res, {
|
|
580
|
+
securityConfig,
|
|
581
|
+
nonce
|
|
582
|
+
});
|
|
583
|
+
res.end(html);
|
|
643
584
|
};
|
|
644
585
|
if (route.module.handle) {
|
|
645
586
|
const handlerContext = {
|
|
@@ -669,7 +610,7 @@ var Renderer = class {
|
|
|
669
610
|
await render(props);
|
|
670
611
|
}
|
|
671
612
|
async renderComponent(Component, props, options) {
|
|
672
|
-
const { currentPath, route, routeParams } = options;
|
|
613
|
+
const { currentPath, route, routeParams, nonce } = options;
|
|
673
614
|
const locale = options.locale;
|
|
674
615
|
const translations = {
|
|
675
616
|
...getTranslations(locale),
|
|
@@ -681,7 +622,8 @@ var Renderer = class {
|
|
|
681
622
|
props,
|
|
682
623
|
routeParams,
|
|
683
624
|
locale,
|
|
684
|
-
translations
|
|
625
|
+
translations,
|
|
626
|
+
nonce
|
|
685
627
|
};
|
|
686
628
|
const htmlContext = {
|
|
687
629
|
htmlAttrs: {},
|
|
@@ -691,13 +633,40 @@ var Renderer = class {
|
|
|
691
633
|
scriptDeps: []
|
|
692
634
|
};
|
|
693
635
|
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 }) }) }) });
|
|
694
|
-
const
|
|
636
|
+
const preactHook = preactOptions.vnode;
|
|
637
|
+
let mainHtml;
|
|
638
|
+
try {
|
|
639
|
+
preactOptions.vnode = (vnode) => {
|
|
640
|
+
if (vnode && vnode.type === "script") {
|
|
641
|
+
vnode.props.nonce = nonce;
|
|
642
|
+
}
|
|
643
|
+
if (vnode && vnode.type === "style") {
|
|
644
|
+
vnode.props.nonce = nonce;
|
|
645
|
+
}
|
|
646
|
+
if (vnode && vnode.type === "link" && vnode.props.rel === "stylesheet") {
|
|
647
|
+
vnode.props.nonce = nonce;
|
|
648
|
+
}
|
|
649
|
+
if (preactHook) {
|
|
650
|
+
preactHook(vnode);
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
mainHtml = renderToString(vdom);
|
|
654
|
+
preactOptions.vnode = preactHook;
|
|
655
|
+
} catch (err) {
|
|
656
|
+
preactOptions.vnode = preactHook;
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
695
659
|
const jsDeps = /* @__PURE__ */ new Set();
|
|
696
660
|
const cssDeps = /* @__PURE__ */ new Set();
|
|
697
661
|
const routeAsset = await this.assetMap.get(route.src);
|
|
698
662
|
if (routeAsset) {
|
|
699
663
|
const routeCssDeps = await routeAsset.getCssDeps();
|
|
700
|
-
routeCssDeps.forEach((dep) =>
|
|
664
|
+
routeCssDeps.forEach((dep) => {
|
|
665
|
+
if (dep.endsWith("?inline")) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
cssDeps.add(dep);
|
|
669
|
+
});
|
|
701
670
|
}
|
|
702
671
|
await this.collectElementDeps(mainHtml, jsDeps, cssDeps);
|
|
703
672
|
await Promise.all(
|
|
@@ -715,10 +684,10 @@ var Renderer = class {
|
|
|
715
684
|
})
|
|
716
685
|
);
|
|
717
686
|
const styleTags = Array.from(cssDeps).map((cssUrl) => {
|
|
718
|
-
return /* @__PURE__ */ jsx4("link", { rel: "stylesheet", href: cssUrl });
|
|
687
|
+
return /* @__PURE__ */ jsx4("link", { rel: "stylesheet", href: cssUrl, nonce });
|
|
719
688
|
});
|
|
720
689
|
const scriptTags = Array.from(jsDeps).map((jsUrls) => {
|
|
721
|
-
return /* @__PURE__ */ jsx4("script", { type: "module", src: jsUrls });
|
|
690
|
+
return /* @__PURE__ */ jsx4("script", { type: "module", src: jsUrls, nonce });
|
|
722
691
|
});
|
|
723
692
|
const html = await this.renderHtml(mainHtml, {
|
|
724
693
|
htmlAttrs: htmlContext.htmlAttrs,
|
|
@@ -780,8 +749,8 @@ var Renderer = class {
|
|
|
780
749
|
}
|
|
781
750
|
async getSitemap() {
|
|
782
751
|
const sitemap = {};
|
|
783
|
-
await this.
|
|
784
|
-
const routePaths = await getAllPathsForRoute(urlPath, route);
|
|
752
|
+
await this.router.walk(async (urlPath, route) => {
|
|
753
|
+
const routePaths = await this.router.getAllPathsForRoute(urlPath, route);
|
|
785
754
|
routePaths.forEach((routePath) => {
|
|
786
755
|
sitemap[routePath.urlPath] = {
|
|
787
756
|
route,
|
|
@@ -792,13 +761,13 @@ var Renderer = class {
|
|
|
792
761
|
return sitemap;
|
|
793
762
|
}
|
|
794
763
|
async renderHtml(html, options) {
|
|
795
|
-
const htmlAttrs =
|
|
796
|
-
const headAttrs =
|
|
797
|
-
const bodyAttrs =
|
|
764
|
+
const htmlAttrs = options?.htmlAttrs || {};
|
|
765
|
+
const headAttrs = options?.headAttrs || {};
|
|
766
|
+
const bodyAttrs = options?.bodyAttrs || {};
|
|
798
767
|
const page = /* @__PURE__ */ jsxs4("html", { ...htmlAttrs, children: [
|
|
799
768
|
/* @__PURE__ */ jsxs4("head", { ...headAttrs, children: [
|
|
800
769
|
/* @__PURE__ */ jsx4("meta", { charSet: "utf-8" }),
|
|
801
|
-
options
|
|
770
|
+
options?.headComponents
|
|
802
771
|
] }),
|
|
803
772
|
/* @__PURE__ */ jsx4("body", { ...bodyAttrs, dangerouslySetInnerHTML: { __html: html } })
|
|
804
773
|
] });
|
|
@@ -807,8 +776,8 @@ ${renderToString(page)}
|
|
|
807
776
|
`;
|
|
808
777
|
}
|
|
809
778
|
async render404(options) {
|
|
810
|
-
const currentPath =
|
|
811
|
-
const [route, routeParams] = this.
|
|
779
|
+
const currentPath = options?.currentPath || "/404";
|
|
780
|
+
const [route, routeParams] = this.router.get("/404");
|
|
812
781
|
if (route && route.src === "routes/404.tsx" && route.module.default) {
|
|
813
782
|
const Component = route.module.default;
|
|
814
783
|
return this.renderComponent(
|
|
@@ -843,8 +812,8 @@ ${renderToString(page)}
|
|
|
843
812
|
return { html };
|
|
844
813
|
}
|
|
845
814
|
async renderError(err, options) {
|
|
846
|
-
const currentPath =
|
|
847
|
-
const [route, routeParams] = this.
|
|
815
|
+
const currentPath = options?.currentPath || "/500";
|
|
816
|
+
const [route, routeParams] = this.router.get("/500");
|
|
848
817
|
if (route && route.src === "routes/500.tsx" && route.module.default) {
|
|
849
818
|
const Component = route.module.default;
|
|
850
819
|
return this.renderComponent(
|
|
@@ -889,7 +858,7 @@ ${renderToString(page)}
|
|
|
889
858
|
return { html };
|
|
890
859
|
}
|
|
891
860
|
async renderDevServer500(req, error) {
|
|
892
|
-
const [route, routeParams] = this.
|
|
861
|
+
const [route, routeParams] = this.router.get(req.path);
|
|
893
862
|
const mainHtml = renderToString(
|
|
894
863
|
/* @__PURE__ */ jsx4(
|
|
895
864
|
DevErrorPage,
|
|
@@ -932,12 +901,105 @@ ${renderToString(page)}
|
|
|
932
901
|
const assetJsDeps = await asset.getJsDeps();
|
|
933
902
|
assetJsDeps.forEach((dep) => jsDeps.add(dep));
|
|
934
903
|
const assetCssDeps = await asset.getCssDeps();
|
|
935
|
-
assetCssDeps.forEach((dep) =>
|
|
904
|
+
assetCssDeps.forEach((dep) => {
|
|
905
|
+
if (dep.endsWith("?inline")) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
cssDeps.add(dep);
|
|
909
|
+
});
|
|
936
910
|
})
|
|
937
911
|
);
|
|
938
912
|
return { jsDeps, cssDeps };
|
|
939
913
|
}
|
|
914
|
+
/**
|
|
915
|
+
* Returns the `security` config value with default values inserted wherever
|
|
916
|
+
* a user config value is blank or set to `true`.
|
|
917
|
+
*/
|
|
918
|
+
getSecurityConfig() {
|
|
919
|
+
const userConfig = this.rootConfig.server?.security || {};
|
|
920
|
+
const securityConfig = {};
|
|
921
|
+
if (isTrueOrUndefined(userConfig.contentSecurityPolicy)) {
|
|
922
|
+
securityConfig.contentSecurityPolicy = {
|
|
923
|
+
directives: {
|
|
924
|
+
"base-uri": ["'none'"],
|
|
925
|
+
"object-src": ["'none'"],
|
|
926
|
+
// NOTE: nonce is automatically added to this list.
|
|
927
|
+
"script-src": [
|
|
928
|
+
"'unsafe-inline'",
|
|
929
|
+
"'unsafe-eval'",
|
|
930
|
+
"'strict-dynamic' https: http:"
|
|
931
|
+
]
|
|
932
|
+
},
|
|
933
|
+
reportOnly: true
|
|
934
|
+
};
|
|
935
|
+
} else {
|
|
936
|
+
securityConfig.contentSecurityPolicy = userConfig.contentSecurityPolicy;
|
|
937
|
+
}
|
|
938
|
+
if (isTrueOrUndefined(userConfig.xFrameOptions)) {
|
|
939
|
+
securityConfig.xFrameOptions = "SAMEORIGIN";
|
|
940
|
+
} else {
|
|
941
|
+
securityConfig.xFrameOptions = userConfig.xFrameOptions;
|
|
942
|
+
}
|
|
943
|
+
securityConfig.strictTransportSecurity = userConfig.strictTransportSecurity ?? true;
|
|
944
|
+
securityConfig.xContentTypeOptions = userConfig.xContentTypeOptions ?? true;
|
|
945
|
+
securityConfig.xXssProtection = userConfig.xXssProtection ?? true;
|
|
946
|
+
return securityConfig;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Generates a random string that can be used as the "nonce" value for CSP.
|
|
950
|
+
*/
|
|
951
|
+
generateNonce() {
|
|
952
|
+
return crypto.randomBytes(16).toString("base64");
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Sets security-related HTTP headers.
|
|
956
|
+
*/
|
|
957
|
+
setSecurityHeaders(res, options) {
|
|
958
|
+
const securityConfig = options.securityConfig;
|
|
959
|
+
const contentSecurityPolicy = securityConfig.contentSecurityPolicy;
|
|
960
|
+
if (typeof contentSecurityPolicy === "object") {
|
|
961
|
+
const directives = contentSecurityPolicy.directives || {};
|
|
962
|
+
if (options.nonce) {
|
|
963
|
+
if (!directives["script-src"]) {
|
|
964
|
+
directives["script-src"] = [
|
|
965
|
+
"'unsafe-inline'",
|
|
966
|
+
"'unsafe-eval'",
|
|
967
|
+
"'strict-dynamic' https: http:"
|
|
968
|
+
];
|
|
969
|
+
}
|
|
970
|
+
directives["script-src"].push(`'nonce-${options.nonce}'`);
|
|
971
|
+
}
|
|
972
|
+
const headerSegments = [];
|
|
973
|
+
Object.entries(directives).forEach(([key, values]) => {
|
|
974
|
+
headerSegments.push([key, ...values].join(" "));
|
|
975
|
+
});
|
|
976
|
+
const csp = headerSegments.join("; ");
|
|
977
|
+
if (contentSecurityPolicy.reportOnly === false) {
|
|
978
|
+
res.setHeader("content-security-policy", csp);
|
|
979
|
+
} else {
|
|
980
|
+
res.setHeader("content-security-policy-report-only", csp);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (typeof securityConfig.xFrameOptions === "string") {
|
|
984
|
+
res.setHeader("x-frame-options", securityConfig.xFrameOptions);
|
|
985
|
+
}
|
|
986
|
+
if (securityConfig.strictTransportSecurity) {
|
|
987
|
+
res.setHeader(
|
|
988
|
+
"strict-transport-security",
|
|
989
|
+
"max-age=63072000; includeSubdomains; preload"
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
if (securityConfig.xContentTypeOptions) {
|
|
993
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
994
|
+
}
|
|
995
|
+
if (securityConfig.xXssProtection) {
|
|
996
|
+
res.setHeader("x-xss-protection", "1; mode=block");
|
|
997
|
+
}
|
|
998
|
+
}
|
|
940
999
|
};
|
|
1000
|
+
function isTrueOrUndefined(value) {
|
|
1001
|
+
return value === true || value === void 0;
|
|
1002
|
+
}
|
|
941
1003
|
export {
|
|
942
1004
|
Renderer
|
|
943
1005
|
};
|