@blinkk/root 1.0.0-rc.9 → 1.0.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/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-J2ANSYAE.js → chunk-KGPR2CJV.js} +111 -9
- package/dist/chunk-KGPR2CJV.js.map +1 -0
- package/dist/{chunk-G7ZCTWGB.js → chunk-RLFKEIOC.js} +437 -119
- package/dist/chunk-RLFKEIOC.js.map +1 -0
- package/dist/{chunk-WNXIRMFF.js → chunk-TZAHHHA4.js} +27 -7
- package/dist/chunk-TZAHHHA4.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 +11 -2
- package/dist/node.js +7 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.js +274 -307
- package/dist/render.js.map +1 -1
- package/dist/{types-9f7a7695.d.ts → types-403nR8i5.d.ts} +91 -7
- package/package.json +38 -35
- package/dist/chunk-DFBTOMQF.js +0 -61
- package/dist/chunk-DFBTOMQF.js.map +0 -1
- package/dist/chunk-G7ZCTWGB.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
|
}
|
|
@@ -244,11 +248,10 @@ var ES_419_COUNTRIES = [
|
|
|
244
248
|
// Venezuela
|
|
245
249
|
];
|
|
246
250
|
function getFallbackLocales(req) {
|
|
247
|
-
var _a, _b;
|
|
248
251
|
const hl = getFirstQueryParam(req, "hl");
|
|
249
252
|
const countryCode = getCountry(req);
|
|
250
253
|
if (isWebCrawler(req)) {
|
|
251
|
-
const defaultLocale =
|
|
254
|
+
const defaultLocale = req.rootConfig?.i18n?.defaultLocale || "en";
|
|
252
255
|
if (hl && hl !== defaultLocale) {
|
|
253
256
|
return [hl, defaultLocale];
|
|
254
257
|
}
|
|
@@ -268,12 +271,12 @@ function getFallbackLocales(req) {
|
|
|
268
271
|
locales.add(`ALL_${countryCode}`);
|
|
269
272
|
const isEs419Country = test419Country(countryCode);
|
|
270
273
|
langs.forEach((langCode) => {
|
|
271
|
-
locales.add(`${langCode}_ALL`);
|
|
272
|
-
locales.add(langCode);
|
|
273
274
|
if (langCode === "es" && isEs419Country) {
|
|
274
275
|
locales.add("es-419_ALL");
|
|
275
276
|
locales.add("es-419");
|
|
276
277
|
}
|
|
278
|
+
locales.add(`${langCode}_ALL`);
|
|
279
|
+
locales.add(langCode);
|
|
277
280
|
});
|
|
278
281
|
return Array.from(locales);
|
|
279
282
|
}
|
|
@@ -333,291 +336,126 @@ function test419Country(countryCode) {
|
|
|
333
336
|
|
|
334
337
|
// src/render/router.ts
|
|
335
338
|
import path from "node:path";
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
let nextNode;
|
|
363
|
-
if (head.startsWith("[") && head.endsWith("]")) {
|
|
364
|
-
if (!this.paramNodes) {
|
|
365
|
-
this.paramNodes = {};
|
|
366
|
-
}
|
|
367
|
-
const paramName = head.slice(1, -1);
|
|
368
|
-
if (!this.paramNodes[paramName]) {
|
|
369
|
-
this.paramNodes[paramName] = new ParamNode(paramName);
|
|
370
|
-
}
|
|
371
|
-
nextNode = this.paramNodes[paramName].trie;
|
|
372
|
-
} else {
|
|
373
|
-
nextNode = this.children[head];
|
|
374
|
-
if (!nextNode) {
|
|
375
|
-
nextNode = new _RouteTrie();
|
|
376
|
-
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;
|
|
377
365
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
* Returns a route mapped to the given path and any parameter values from the
|
|
383
|
-
* URL.
|
|
384
|
-
*/
|
|
385
|
-
get(path2) {
|
|
386
|
-
const params = {};
|
|
387
|
-
const route = this.getRoute(path2, params);
|
|
388
|
-
return [route, params];
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Walks the route trie and calls a callback function for each route.
|
|
392
|
-
*/
|
|
393
|
-
walk(cb) {
|
|
394
|
-
const promises = [];
|
|
395
|
-
const addPromise = (promise) => {
|
|
396
|
-
if (promise) {
|
|
397
|
-
promises.push(promise);
|
|
366
|
+
if (parts.name === "index") {
|
|
367
|
+
relativeRoutePath = parts.dir;
|
|
368
|
+
} else {
|
|
369
|
+
relativeRoutePath = path.join(parts.dir, parts.name);
|
|
398
370
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
|
409
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
|
|
410
394
|
});
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
return Promise.all(promises).then(() => {
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Removes all routes from the trie.
|
|
431
|
-
*/
|
|
432
|
-
clear() {
|
|
433
|
-
this.children = {};
|
|
434
|
-
this.paramNodes = void 0;
|
|
435
|
-
this.catchAllNodes = void 0;
|
|
436
|
-
this.optCatchAllNodes = void 0;
|
|
437
|
-
this.route = void 0;
|
|
438
|
-
}
|
|
439
|
-
getRoute(urlPath, params) {
|
|
440
|
-
urlPath = this.normalizePath(urlPath);
|
|
441
|
-
if (urlPath === "") {
|
|
442
|
-
if (this.route) {
|
|
443
|
-
return this.route;
|
|
444
|
-
}
|
|
445
|
-
if (this.optCatchAllNodes) {
|
|
446
|
-
if (urlPath) {
|
|
447
|
-
params[this.optCatchAllNodes.name] = urlPath;
|
|
448
|
-
}
|
|
449
|
-
return this.optCatchAllNodes.route;
|
|
450
|
-
}
|
|
451
|
-
return void 0;
|
|
452
|
-
}
|
|
453
|
-
const [head, tail] = this.splitPath(urlPath);
|
|
454
|
-
const child = this.children[head];
|
|
455
|
-
if (child) {
|
|
456
|
-
const route = child.getRoute(tail, params);
|
|
457
|
-
if (route) {
|
|
458
|
-
return route;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
if (this.paramNodes) {
|
|
462
|
-
for (const paramChild of Object.values(this.paramNodes)) {
|
|
463
|
-
const route = paramChild.trie.getRoute(tail, params);
|
|
464
|
-
if (route) {
|
|
465
|
-
params[paramChild.name] = head;
|
|
466
|
-
return route;
|
|
467
|
-
}
|
|
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
|
+
}
|
|
408
|
+
});
|
|
468
409
|
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
params[this.catchAllNodes.name] = urlPath;
|
|
472
|
-
return this.catchAllNodes.route;
|
|
473
|
-
}
|
|
474
|
-
if (this.optCatchAllNodes) {
|
|
475
|
-
params[this.optCatchAllNodes.name] = urlPath;
|
|
476
|
-
return this.optCatchAllNodes.route;
|
|
477
|
-
}
|
|
478
|
-
return void 0;
|
|
479
|
-
}
|
|
480
|
-
/**
|
|
481
|
-
* Normalizes a path for inclusion into the route trie.
|
|
482
|
-
*/
|
|
483
|
-
normalizePath(path2) {
|
|
484
|
-
return path2.replace(/^\/+/g, "").replace(/\/+$/g, "");
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Splits the parent directory from its children, e.g.:
|
|
488
|
-
*
|
|
489
|
-
* splitPath("foo/bar/baz") -> ["foo", "bar/baz"]
|
|
490
|
-
*/
|
|
491
|
-
splitPath(path2) {
|
|
492
|
-
const i = path2.indexOf("/");
|
|
493
|
-
if (i === -1) {
|
|
494
|
-
return [path2, ""];
|
|
495
|
-
}
|
|
496
|
-
return [path2.slice(0, i), path2.slice(i + 1)];
|
|
497
|
-
}
|
|
498
|
-
};
|
|
499
|
-
var ParamNode = class {
|
|
500
|
-
constructor(name) {
|
|
501
|
-
this.trie = new RouteTrie();
|
|
502
|
-
this.name = name;
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
var CatchAllNode = class {
|
|
506
|
-
constructor(name, route) {
|
|
507
|
-
this.name = name;
|
|
508
|
-
this.route = route;
|
|
410
|
+
});
|
|
411
|
+
return trie;
|
|
509
412
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
var _a, _b;
|
|
515
|
-
const locales = ((_a = rootConfig.i18n) == null ? void 0 : _a.locales) || [];
|
|
516
|
-
const basePath = rootConfig.base || "/";
|
|
517
|
-
const defaultLocale = ((_b = rootConfig.i18n) == null ? void 0 : _b.defaultLocale) || "en";
|
|
518
|
-
const routes = import.meta.glob(
|
|
519
|
-
["/routes/*.ts", "/routes/**/*.ts", "/routes/*.tsx", "/routes/**/*.tsx"],
|
|
520
|
-
{
|
|
521
|
-
eager: true
|
|
522
|
-
}
|
|
523
|
-
);
|
|
524
|
-
const trie = new RouteTrie();
|
|
525
|
-
Object.keys(routes).forEach((modulePath) => {
|
|
526
|
-
var _a2;
|
|
527
|
-
const src = modulePath.slice(1);
|
|
528
|
-
let relativeRoutePath = modulePath.replace(/^\/routes/, "");
|
|
529
|
-
const parts = path.parse(relativeRoutePath);
|
|
530
|
-
if (parts.name.startsWith("_")) {
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
if (parts.name === "index") {
|
|
534
|
-
relativeRoutePath = parts.dir;
|
|
535
|
-
} else {
|
|
536
|
-
relativeRoutePath = path.join(parts.dir, parts.name);
|
|
413
|
+
async getAllPathsForRoute(urlPathFormat, route) {
|
|
414
|
+
const routeModule = route.module;
|
|
415
|
+
if (!routeModule.default) {
|
|
416
|
+
return [];
|
|
537
417
|
}
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const placeholders = {
|
|
543
|
-
base: removeSlashes(basePath),
|
|
544
|
-
path: removeSlashes(relativeRoutePath)
|
|
545
|
-
};
|
|
546
|
-
const formatUrl = (format) => {
|
|
547
|
-
var _a3;
|
|
548
|
-
const url = format.replaceAll("[base]", placeholders.base).replaceAll("[path]", placeholders.path);
|
|
549
|
-
return normalizeUrlPath(url, {
|
|
550
|
-
trailingSlash: (_a3 = rootConfig.server) == null ? void 0 : _a3.trailingSlash
|
|
551
|
-
});
|
|
552
|
-
};
|
|
553
|
-
const routePath = formatUrl(urlFormat);
|
|
554
|
-
const localeRoutePath = formatUrl(i18nUrlFormat);
|
|
555
|
-
trie.add(routePath, {
|
|
556
|
-
src,
|
|
557
|
-
module: routes[modulePath],
|
|
558
|
-
locale: defaultLocale,
|
|
559
|
-
isDefaultLocale: true,
|
|
560
|
-
routePath,
|
|
561
|
-
localeRoutePath
|
|
562
|
-
});
|
|
563
|
-
if (i18nUrlFormat.includes("[locale]")) {
|
|
564
|
-
locales.forEach((locale) => {
|
|
565
|
-
const localePath = localeRoutePath.replace("[locale]", locale);
|
|
566
|
-
if (localePath !== relativeRoutePath) {
|
|
567
|
-
trie.add(localePath, {
|
|
568
|
-
src,
|
|
569
|
-
module: routes[modulePath],
|
|
570
|
-
locale,
|
|
571
|
-
isDefaultLocale: false,
|
|
572
|
-
routePath,
|
|
573
|
-
localeRoutePath
|
|
574
|
-
});
|
|
575
|
-
}
|
|
418
|
+
const urlPaths = [];
|
|
419
|
+
if (routeModule.getStaticPaths) {
|
|
420
|
+
const staticPaths = await routeModule.getStaticPaths({
|
|
421
|
+
rootConfig: this.rootConfig
|
|
576
422
|
});
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (!routeModule.default) {
|
|
584
|
-
return [];
|
|
585
|
-
}
|
|
586
|
-
const urlPaths = [];
|
|
587
|
-
if (routeModule.getStaticPaths) {
|
|
588
|
-
const staticPaths = await routeModule.getStaticPaths();
|
|
589
|
-
if (staticPaths.paths) {
|
|
590
|
-
staticPaths.paths.forEach(
|
|
591
|
-
(pathParams) => {
|
|
592
|
-
const urlPath = replaceParams(urlPathFormat, pathParams.params || {});
|
|
593
|
-
if (pathContainsPlaceholders(urlPath)) {
|
|
594
|
-
console.warn(
|
|
595
|
-
`path contains placeholders: ${urlPathFormat}, double check getStaticPaths() and ensure all params are returned. more info: https://rootjs.dev/guide/routes#getStaticPaths`
|
|
423
|
+
if (staticPaths.paths) {
|
|
424
|
+
staticPaths.paths.forEach(
|
|
425
|
+
(pathParams) => {
|
|
426
|
+
const urlPath = replaceParams(
|
|
427
|
+
urlPathFormat,
|
|
428
|
+
pathParams.params || {}
|
|
596
429
|
);
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
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
|
+
}
|
|
602
440
|
}
|
|
603
|
-
|
|
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")
|
|
604
454
|
);
|
|
605
455
|
}
|
|
606
|
-
|
|
607
|
-
urlPaths.push({ urlPath: normalizeUrlPath(urlPathFormat), params: {} });
|
|
608
|
-
} else if (!routeModule.handle && !pathContainsPlaceholders(urlPathFormat)) {
|
|
609
|
-
urlPaths.push({ urlPath: normalizeUrlPath(urlPathFormat), params: {} });
|
|
610
|
-
} else if (pathContainsPlaceholders(urlPathFormat) && !routeModule.handle && !routeModule.getStaticPaths) {
|
|
611
|
-
console.warn(
|
|
612
|
-
[
|
|
613
|
-
`warning: path contains placeholders: ${urlPathFormat}.`,
|
|
614
|
-
`define either ssg getStaticPaths() or ssr handle() for route: ${route.src}.`,
|
|
615
|
-
"more info: https://rootjs.dev/guide/routes"
|
|
616
|
-
].join("\n")
|
|
617
|
-
);
|
|
456
|
+
return urlPaths;
|
|
618
457
|
}
|
|
619
|
-
|
|
620
|
-
}
|
|
458
|
+
};
|
|
621
459
|
function replaceParams(urlPathFormat, params) {
|
|
622
460
|
const urlPath = urlPathFormat.replaceAll(
|
|
623
461
|
/\[\[?(\.\.\.)?([\w\-_]*)\]?\]/g,
|
|
@@ -633,9 +471,12 @@ function replaceParams(urlPathFormat, params) {
|
|
|
633
471
|
}
|
|
634
472
|
function normalizeUrlPath(urlPath, options) {
|
|
635
473
|
urlPath = urlPath.replace(/\/+/g, "/");
|
|
636
|
-
if (
|
|
474
|
+
if (options?.trailingSlash === false && urlPath !== "/" && urlPath.endsWith("/")) {
|
|
637
475
|
urlPath = urlPath.replace(/\/*$/g, "");
|
|
638
476
|
}
|
|
477
|
+
if (urlPath.endsWith("/index")) {
|
|
478
|
+
urlPath = urlPath.slice(0, -6);
|
|
479
|
+
}
|
|
639
480
|
if (!urlPath.startsWith("/")) {
|
|
640
481
|
urlPath = `/${urlPath}`;
|
|
641
482
|
}
|
|
@@ -664,13 +505,13 @@ import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
|
|
|
664
505
|
var Renderer = class {
|
|
665
506
|
constructor(rootConfig, options) {
|
|
666
507
|
this.rootConfig = rootConfig;
|
|
667
|
-
this.routes = getRoutes(this.rootConfig);
|
|
668
508
|
this.assetMap = options.assetMap;
|
|
669
509
|
this.elementGraph = options.elementGraph;
|
|
510
|
+
this.router = new Router(rootConfig);
|
|
670
511
|
}
|
|
671
512
|
async handle(req, res, next) {
|
|
672
|
-
const url = req.path
|
|
673
|
-
const [route, routeParams] = this.
|
|
513
|
+
const url = req.path;
|
|
514
|
+
const [route, routeParams] = this.router.get(url);
|
|
674
515
|
if (!route) {
|
|
675
516
|
next();
|
|
676
517
|
return;
|
|
@@ -680,14 +521,13 @@ var Renderer = class {
|
|
|
680
521
|
}
|
|
681
522
|
const fallbackLocales = route.isDefaultLocale ? getFallbackLocales(req) : [route.locale];
|
|
682
523
|
const getPreferredLocale = (availableLocales) => {
|
|
683
|
-
var _a, _b;
|
|
684
524
|
const lowerLocales = availableLocales.map((l) => l.toLowerCase());
|
|
685
525
|
for (const fallbackLocale of fallbackLocales) {
|
|
686
526
|
if (lowerLocales.includes(fallbackLocale.toLowerCase())) {
|
|
687
527
|
return fallbackLocale;
|
|
688
528
|
}
|
|
689
529
|
}
|
|
690
|
-
return
|
|
530
|
+
return req.rootConfig?.i18n?.defaultLocale || "en";
|
|
691
531
|
};
|
|
692
532
|
const render404 = async () => {
|
|
693
533
|
next();
|
|
@@ -698,15 +538,19 @@ var Renderer = class {
|
|
|
698
538
|
render404();
|
|
699
539
|
return;
|
|
700
540
|
}
|
|
541
|
+
const securityConfig = this.getSecurityConfig();
|
|
542
|
+
const cspEnabled = !!securityConfig.contentSecurityPolicy;
|
|
701
543
|
const currentPath = req.path;
|
|
702
|
-
const locale =
|
|
703
|
-
const translations = options
|
|
544
|
+
const locale = options?.locale || route.locale;
|
|
545
|
+
const translations = options?.translations;
|
|
546
|
+
const nonce = cspEnabled ? this.generateNonce() : void 0;
|
|
704
547
|
const output = await this.renderComponent(route.module.default, props2, {
|
|
705
548
|
currentPath,
|
|
706
549
|
route,
|
|
707
550
|
routeParams,
|
|
708
551
|
locale,
|
|
709
|
-
translations
|
|
552
|
+
translations,
|
|
553
|
+
nonce
|
|
710
554
|
});
|
|
711
555
|
let html = output.html;
|
|
712
556
|
if (this.rootConfig.prettyHtml) {
|
|
@@ -716,6 +560,12 @@ var Renderer = class {
|
|
|
716
560
|
}
|
|
717
561
|
if (req.viteServer) {
|
|
718
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
|
+
}
|
|
719
569
|
}
|
|
720
570
|
let statusCode = 200;
|
|
721
571
|
if (route.src === "routes/404.tsx") {
|
|
@@ -724,7 +574,13 @@ var Renderer = class {
|
|
|
724
574
|
statusCode = 500;
|
|
725
575
|
}
|
|
726
576
|
req.hooks.trigger("preRender");
|
|
727
|
-
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);
|
|
728
584
|
};
|
|
729
585
|
if (route.module.handle) {
|
|
730
586
|
const handlerContext = {
|
|
@@ -754,7 +610,7 @@ var Renderer = class {
|
|
|
754
610
|
await render(props);
|
|
755
611
|
}
|
|
756
612
|
async renderComponent(Component, props, options) {
|
|
757
|
-
const { currentPath, route, routeParams } = options;
|
|
613
|
+
const { currentPath, route, routeParams, nonce } = options;
|
|
758
614
|
const locale = options.locale;
|
|
759
615
|
const translations = {
|
|
760
616
|
...getTranslations(locale),
|
|
@@ -766,7 +622,8 @@ var Renderer = class {
|
|
|
766
622
|
props,
|
|
767
623
|
routeParams,
|
|
768
624
|
locale,
|
|
769
|
-
translations
|
|
625
|
+
translations,
|
|
626
|
+
nonce
|
|
770
627
|
};
|
|
771
628
|
const htmlContext = {
|
|
772
629
|
htmlAttrs: {},
|
|
@@ -776,7 +633,29 @@ var Renderer = class {
|
|
|
776
633
|
scriptDeps: []
|
|
777
634
|
};
|
|
778
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 }) }) }) });
|
|
779
|
-
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
|
+
}
|
|
780
659
|
const jsDeps = /* @__PURE__ */ new Set();
|
|
781
660
|
const cssDeps = /* @__PURE__ */ new Set();
|
|
782
661
|
const routeAsset = await this.assetMap.get(route.src);
|
|
@@ -805,10 +684,10 @@ var Renderer = class {
|
|
|
805
684
|
})
|
|
806
685
|
);
|
|
807
686
|
const styleTags = Array.from(cssDeps).map((cssUrl) => {
|
|
808
|
-
return /* @__PURE__ */ jsx4("link", { rel: "stylesheet", href: cssUrl });
|
|
687
|
+
return /* @__PURE__ */ jsx4("link", { rel: "stylesheet", href: cssUrl, nonce });
|
|
809
688
|
});
|
|
810
689
|
const scriptTags = Array.from(jsDeps).map((jsUrls) => {
|
|
811
|
-
return /* @__PURE__ */ jsx4("script", { type: "module", src: jsUrls });
|
|
690
|
+
return /* @__PURE__ */ jsx4("script", { type: "module", src: jsUrls, nonce });
|
|
812
691
|
});
|
|
813
692
|
const html = await this.renderHtml(mainHtml, {
|
|
814
693
|
htmlAttrs: htmlContext.htmlAttrs,
|
|
@@ -870,8 +749,8 @@ var Renderer = class {
|
|
|
870
749
|
}
|
|
871
750
|
async getSitemap() {
|
|
872
751
|
const sitemap = {};
|
|
873
|
-
await this.
|
|
874
|
-
const routePaths = await getAllPathsForRoute(urlPath, route);
|
|
752
|
+
await this.router.walk(async (urlPath, route) => {
|
|
753
|
+
const routePaths = await this.router.getAllPathsForRoute(urlPath, route);
|
|
875
754
|
routePaths.forEach((routePath) => {
|
|
876
755
|
sitemap[routePath.urlPath] = {
|
|
877
756
|
route,
|
|
@@ -882,13 +761,13 @@ var Renderer = class {
|
|
|
882
761
|
return sitemap;
|
|
883
762
|
}
|
|
884
763
|
async renderHtml(html, options) {
|
|
885
|
-
const htmlAttrs =
|
|
886
|
-
const headAttrs =
|
|
887
|
-
const bodyAttrs =
|
|
764
|
+
const htmlAttrs = options?.htmlAttrs || {};
|
|
765
|
+
const headAttrs = options?.headAttrs || {};
|
|
766
|
+
const bodyAttrs = options?.bodyAttrs || {};
|
|
888
767
|
const page = /* @__PURE__ */ jsxs4("html", { ...htmlAttrs, children: [
|
|
889
768
|
/* @__PURE__ */ jsxs4("head", { ...headAttrs, children: [
|
|
890
769
|
/* @__PURE__ */ jsx4("meta", { charSet: "utf-8" }),
|
|
891
|
-
options
|
|
770
|
+
options?.headComponents
|
|
892
771
|
] }),
|
|
893
772
|
/* @__PURE__ */ jsx4("body", { ...bodyAttrs, dangerouslySetInnerHTML: { __html: html } })
|
|
894
773
|
] });
|
|
@@ -897,8 +776,8 @@ ${renderToString(page)}
|
|
|
897
776
|
`;
|
|
898
777
|
}
|
|
899
778
|
async render404(options) {
|
|
900
|
-
const currentPath =
|
|
901
|
-
const [route, routeParams] = this.
|
|
779
|
+
const currentPath = options?.currentPath || "/404";
|
|
780
|
+
const [route, routeParams] = this.router.get("/404");
|
|
902
781
|
if (route && route.src === "routes/404.tsx" && route.module.default) {
|
|
903
782
|
const Component = route.module.default;
|
|
904
783
|
return this.renderComponent(
|
|
@@ -933,8 +812,8 @@ ${renderToString(page)}
|
|
|
933
812
|
return { html };
|
|
934
813
|
}
|
|
935
814
|
async renderError(err, options) {
|
|
936
|
-
const currentPath =
|
|
937
|
-
const [route, routeParams] = this.
|
|
815
|
+
const currentPath = options?.currentPath || "/500";
|
|
816
|
+
const [route, routeParams] = this.router.get("/500");
|
|
938
817
|
if (route && route.src === "routes/500.tsx" && route.module.default) {
|
|
939
818
|
const Component = route.module.default;
|
|
940
819
|
return this.renderComponent(
|
|
@@ -979,7 +858,7 @@ ${renderToString(page)}
|
|
|
979
858
|
return { html };
|
|
980
859
|
}
|
|
981
860
|
async renderDevServer500(req, error) {
|
|
982
|
-
const [route, routeParams] = this.
|
|
861
|
+
const [route, routeParams] = this.router.get(req.path);
|
|
983
862
|
const mainHtml = renderToString(
|
|
984
863
|
/* @__PURE__ */ jsx4(
|
|
985
864
|
DevErrorPage,
|
|
@@ -1032,7 +911,95 @@ ${renderToString(page)}
|
|
|
1032
911
|
);
|
|
1033
912
|
return { jsDeps, cssDeps };
|
|
1034
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
|
+
}
|
|
1035
999
|
};
|
|
1000
|
+
function isTrueOrUndefined(value) {
|
|
1001
|
+
return value === true || value === void 0;
|
|
1002
|
+
}
|
|
1036
1003
|
export {
|
|
1037
1004
|
Renderer
|
|
1038
1005
|
};
|