@blinkk/root 1.0.0-rc.8 → 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-YF6DPNDK.js → chunk-RLFKEIOC.js} +449 -121
- 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 +290 -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-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/chunk-YF6DPNDK.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,272 +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
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
if (this.catchAllNodes) {
|
|
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;
|
|
509
|
-
}
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
// src/render/router.ts
|
|
513
|
-
function getRoutes(config) {
|
|
514
|
-
var _a, _b, _c;
|
|
515
|
-
const locales = ((_a = config.i18n) == null ? void 0 : _a.locales) || [];
|
|
516
|
-
const i18nUrlFormat = ((_b = config.i18n) == null ? void 0 : _b.urlFormat) || "/{locale}/{path}";
|
|
517
|
-
const defaultLocale = ((_c = config.i18n) == null ? void 0 : _c.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
|
-
const src = modulePath.slice(1);
|
|
527
|
-
let routePath = modulePath.replace(/^\/routes/, "");
|
|
528
|
-
const parts = path.parse(routePath);
|
|
529
|
-
if (parts.name.startsWith("_")) {
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
if (parts.name === "index") {
|
|
533
|
-
routePath = parts.dir;
|
|
534
|
-
} else {
|
|
535
|
-
routePath = path.join(parts.dir, parts.name);
|
|
536
|
-
}
|
|
537
|
-
const localeRoutePath = i18nUrlFormat.replace("{locale}", "[locale]").replace("{path}", routePath.replace(/^\/*/, ""));
|
|
538
|
-
trie.add(routePath, {
|
|
539
|
-
src,
|
|
540
|
-
module: routes[modulePath],
|
|
541
|
-
locale: defaultLocale,
|
|
542
|
-
isDefaultLocale: true,
|
|
543
|
-
routePath: normalizeUrlPath(routePath),
|
|
544
|
-
localeRoutePath: normalizeUrlPath(localeRoutePath)
|
|
545
|
-
});
|
|
546
|
-
locales.forEach((locale) => {
|
|
547
|
-
const localePath = localeRoutePath.replace("[locale]", locale);
|
|
548
|
-
if (localePath !== routePath) {
|
|
549
|
-
trie.add(localePath, {
|
|
550
|
-
src,
|
|
551
|
-
module: routes[modulePath],
|
|
552
|
-
locale,
|
|
553
|
-
isDefaultLocale: false,
|
|
554
|
-
routePath,
|
|
555
|
-
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
|
+
}
|
|
556
408
|
});
|
|
557
409
|
}
|
|
558
410
|
});
|
|
559
|
-
|
|
560
|
-
return trie;
|
|
561
|
-
}
|
|
562
|
-
async function getAllPathsForRoute(urlPathFormat, route) {
|
|
563
|
-
const routeModule = route.module;
|
|
564
|
-
if (!routeModule.default) {
|
|
565
|
-
return [];
|
|
411
|
+
return trie;
|
|
566
412
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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 || {}
|
|
577
429
|
);
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
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
|
+
}
|
|
583
440
|
}
|
|
584
|
-
|
|
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")
|
|
585
454
|
);
|
|
586
455
|
}
|
|
587
|
-
|
|
588
|
-
urlPaths.push({ urlPath: normalizeUrlPath(urlPathFormat), params: {} });
|
|
589
|
-
} else if (!routeModule.handle && !pathContainsPlaceholders(urlPathFormat)) {
|
|
590
|
-
urlPaths.push({ urlPath: normalizeUrlPath(urlPathFormat), params: {} });
|
|
591
|
-
} else if (pathContainsPlaceholders(urlPathFormat) && !routeModule.handle && !routeModule.getStaticPaths) {
|
|
592
|
-
console.warn(
|
|
593
|
-
[
|
|
594
|
-
`warning: path contains placeholders: ${urlPathFormat}.`,
|
|
595
|
-
`define either ssg getStaticPaths() or ssr handle() for route: ${route.src}.`,
|
|
596
|
-
"more info: https://rootjs.dev/guide/routes"
|
|
597
|
-
].join("\n")
|
|
598
|
-
);
|
|
456
|
+
return urlPaths;
|
|
599
457
|
}
|
|
600
|
-
|
|
601
|
-
}
|
|
458
|
+
};
|
|
602
459
|
function replaceParams(urlPathFormat, params) {
|
|
603
460
|
const urlPath = urlPathFormat.replaceAll(
|
|
604
461
|
/\[\[?(\.\.\.)?([\w\-_]*)\]?\]/g,
|
|
@@ -612,10 +469,17 @@ function replaceParams(urlPathFormat, params) {
|
|
|
612
469
|
);
|
|
613
470
|
return urlPath;
|
|
614
471
|
}
|
|
615
|
-
function normalizeUrlPath(urlPath) {
|
|
616
|
-
|
|
472
|
+
function normalizeUrlPath(urlPath, options) {
|
|
473
|
+
urlPath = urlPath.replace(/\/+/g, "/");
|
|
474
|
+
if (options?.trailingSlash === false && urlPath !== "/" && urlPath.endsWith("/")) {
|
|
617
475
|
urlPath = urlPath.replace(/\/*$/g, "");
|
|
618
476
|
}
|
|
477
|
+
if (urlPath.endsWith("/index")) {
|
|
478
|
+
urlPath = urlPath.slice(0, -6);
|
|
479
|
+
}
|
|
480
|
+
if (!urlPath.startsWith("/")) {
|
|
481
|
+
urlPath = `/${urlPath}`;
|
|
482
|
+
}
|
|
619
483
|
return urlPath;
|
|
620
484
|
}
|
|
621
485
|
function pathContainsPlaceholders(urlPath) {
|
|
@@ -624,19 +488,30 @@ function pathContainsPlaceholders(urlPath) {
|
|
|
624
488
|
return segment.startsWith("[") && segment.endsWith("]");
|
|
625
489
|
});
|
|
626
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
|
+
}
|
|
627
502
|
|
|
628
503
|
// src/render/render.tsx
|
|
629
504
|
import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
|
|
630
505
|
var Renderer = class {
|
|
631
506
|
constructor(rootConfig, options) {
|
|
632
507
|
this.rootConfig = rootConfig;
|
|
633
|
-
this.routes = getRoutes(this.rootConfig);
|
|
634
508
|
this.assetMap = options.assetMap;
|
|
635
509
|
this.elementGraph = options.elementGraph;
|
|
510
|
+
this.router = new Router(rootConfig);
|
|
636
511
|
}
|
|
637
512
|
async handle(req, res, next) {
|
|
638
|
-
const url = req.path
|
|
639
|
-
const [route, routeParams] = this.
|
|
513
|
+
const url = req.path;
|
|
514
|
+
const [route, routeParams] = this.router.get(url);
|
|
640
515
|
if (!route) {
|
|
641
516
|
next();
|
|
642
517
|
return;
|
|
@@ -646,14 +521,13 @@ var Renderer = class {
|
|
|
646
521
|
}
|
|
647
522
|
const fallbackLocales = route.isDefaultLocale ? getFallbackLocales(req) : [route.locale];
|
|
648
523
|
const getPreferredLocale = (availableLocales) => {
|
|
649
|
-
var _a, _b;
|
|
650
524
|
const lowerLocales = availableLocales.map((l) => l.toLowerCase());
|
|
651
525
|
for (const fallbackLocale of fallbackLocales) {
|
|
652
526
|
if (lowerLocales.includes(fallbackLocale.toLowerCase())) {
|
|
653
527
|
return fallbackLocale;
|
|
654
528
|
}
|
|
655
529
|
}
|
|
656
|
-
return
|
|
530
|
+
return req.rootConfig?.i18n?.defaultLocale || "en";
|
|
657
531
|
};
|
|
658
532
|
const render404 = async () => {
|
|
659
533
|
next();
|
|
@@ -664,15 +538,19 @@ var Renderer = class {
|
|
|
664
538
|
render404();
|
|
665
539
|
return;
|
|
666
540
|
}
|
|
541
|
+
const securityConfig = this.getSecurityConfig();
|
|
542
|
+
const cspEnabled = !!securityConfig.contentSecurityPolicy;
|
|
667
543
|
const currentPath = req.path;
|
|
668
|
-
const locale =
|
|
669
|
-
const translations = options
|
|
544
|
+
const locale = options?.locale || route.locale;
|
|
545
|
+
const translations = options?.translations;
|
|
546
|
+
const nonce = cspEnabled ? this.generateNonce() : void 0;
|
|
670
547
|
const output = await this.renderComponent(route.module.default, props2, {
|
|
671
548
|
currentPath,
|
|
672
549
|
route,
|
|
673
550
|
routeParams,
|
|
674
551
|
locale,
|
|
675
|
-
translations
|
|
552
|
+
translations,
|
|
553
|
+
nonce
|
|
676
554
|
});
|
|
677
555
|
let html = output.html;
|
|
678
556
|
if (this.rootConfig.prettyHtml) {
|
|
@@ -682,6 +560,12 @@ var Renderer = class {
|
|
|
682
560
|
}
|
|
683
561
|
if (req.viteServer) {
|
|
684
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
|
+
}
|
|
685
569
|
}
|
|
686
570
|
let statusCode = 200;
|
|
687
571
|
if (route.src === "routes/404.tsx") {
|
|
@@ -690,7 +574,13 @@ var Renderer = class {
|
|
|
690
574
|
statusCode = 500;
|
|
691
575
|
}
|
|
692
576
|
req.hooks.trigger("preRender");
|
|
693
|
-
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);
|
|
694
584
|
};
|
|
695
585
|
if (route.module.handle) {
|
|
696
586
|
const handlerContext = {
|
|
@@ -720,7 +610,7 @@ var Renderer = class {
|
|
|
720
610
|
await render(props);
|
|
721
611
|
}
|
|
722
612
|
async renderComponent(Component, props, options) {
|
|
723
|
-
const { currentPath, route, routeParams } = options;
|
|
613
|
+
const { currentPath, route, routeParams, nonce } = options;
|
|
724
614
|
const locale = options.locale;
|
|
725
615
|
const translations = {
|
|
726
616
|
...getTranslations(locale),
|
|
@@ -732,7 +622,8 @@ var Renderer = class {
|
|
|
732
622
|
props,
|
|
733
623
|
routeParams,
|
|
734
624
|
locale,
|
|
735
|
-
translations
|
|
625
|
+
translations,
|
|
626
|
+
nonce
|
|
736
627
|
};
|
|
737
628
|
const htmlContext = {
|
|
738
629
|
htmlAttrs: {},
|
|
@@ -742,7 +633,29 @@ var Renderer = class {
|
|
|
742
633
|
scriptDeps: []
|
|
743
634
|
};
|
|
744
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 }) }) }) });
|
|
745
|
-
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
|
+
}
|
|
746
659
|
const jsDeps = /* @__PURE__ */ new Set();
|
|
747
660
|
const cssDeps = /* @__PURE__ */ new Set();
|
|
748
661
|
const routeAsset = await this.assetMap.get(route.src);
|
|
@@ -771,10 +684,10 @@ var Renderer = class {
|
|
|
771
684
|
})
|
|
772
685
|
);
|
|
773
686
|
const styleTags = Array.from(cssDeps).map((cssUrl) => {
|
|
774
|
-
return /* @__PURE__ */ jsx4("link", { rel: "stylesheet", href: cssUrl });
|
|
687
|
+
return /* @__PURE__ */ jsx4("link", { rel: "stylesheet", href: cssUrl, nonce });
|
|
775
688
|
});
|
|
776
689
|
const scriptTags = Array.from(jsDeps).map((jsUrls) => {
|
|
777
|
-
return /* @__PURE__ */ jsx4("script", { type: "module", src: jsUrls });
|
|
690
|
+
return /* @__PURE__ */ jsx4("script", { type: "module", src: jsUrls, nonce });
|
|
778
691
|
});
|
|
779
692
|
const html = await this.renderHtml(mainHtml, {
|
|
780
693
|
htmlAttrs: htmlContext.htmlAttrs,
|
|
@@ -836,8 +749,8 @@ var Renderer = class {
|
|
|
836
749
|
}
|
|
837
750
|
async getSitemap() {
|
|
838
751
|
const sitemap = {};
|
|
839
|
-
await this.
|
|
840
|
-
const routePaths = await getAllPathsForRoute(urlPath, route);
|
|
752
|
+
await this.router.walk(async (urlPath, route) => {
|
|
753
|
+
const routePaths = await this.router.getAllPathsForRoute(urlPath, route);
|
|
841
754
|
routePaths.forEach((routePath) => {
|
|
842
755
|
sitemap[routePath.urlPath] = {
|
|
843
756
|
route,
|
|
@@ -848,13 +761,13 @@ var Renderer = class {
|
|
|
848
761
|
return sitemap;
|
|
849
762
|
}
|
|
850
763
|
async renderHtml(html, options) {
|
|
851
|
-
const htmlAttrs =
|
|
852
|
-
const headAttrs =
|
|
853
|
-
const bodyAttrs =
|
|
764
|
+
const htmlAttrs = options?.htmlAttrs || {};
|
|
765
|
+
const headAttrs = options?.headAttrs || {};
|
|
766
|
+
const bodyAttrs = options?.bodyAttrs || {};
|
|
854
767
|
const page = /* @__PURE__ */ jsxs4("html", { ...htmlAttrs, children: [
|
|
855
768
|
/* @__PURE__ */ jsxs4("head", { ...headAttrs, children: [
|
|
856
769
|
/* @__PURE__ */ jsx4("meta", { charSet: "utf-8" }),
|
|
857
|
-
options
|
|
770
|
+
options?.headComponents
|
|
858
771
|
] }),
|
|
859
772
|
/* @__PURE__ */ jsx4("body", { ...bodyAttrs, dangerouslySetInnerHTML: { __html: html } })
|
|
860
773
|
] });
|
|
@@ -863,8 +776,8 @@ ${renderToString(page)}
|
|
|
863
776
|
`;
|
|
864
777
|
}
|
|
865
778
|
async render404(options) {
|
|
866
|
-
const currentPath =
|
|
867
|
-
const [route, routeParams] = this.
|
|
779
|
+
const currentPath = options?.currentPath || "/404";
|
|
780
|
+
const [route, routeParams] = this.router.get("/404");
|
|
868
781
|
if (route && route.src === "routes/404.tsx" && route.module.default) {
|
|
869
782
|
const Component = route.module.default;
|
|
870
783
|
return this.renderComponent(
|
|
@@ -899,8 +812,8 @@ ${renderToString(page)}
|
|
|
899
812
|
return { html };
|
|
900
813
|
}
|
|
901
814
|
async renderError(err, options) {
|
|
902
|
-
const currentPath =
|
|
903
|
-
const [route, routeParams] = this.
|
|
815
|
+
const currentPath = options?.currentPath || "/500";
|
|
816
|
+
const [route, routeParams] = this.router.get("/500");
|
|
904
817
|
if (route && route.src === "routes/500.tsx" && route.module.default) {
|
|
905
818
|
const Component = route.module.default;
|
|
906
819
|
return this.renderComponent(
|
|
@@ -945,7 +858,7 @@ ${renderToString(page)}
|
|
|
945
858
|
return { html };
|
|
946
859
|
}
|
|
947
860
|
async renderDevServer500(req, error) {
|
|
948
|
-
const [route, routeParams] = this.
|
|
861
|
+
const [route, routeParams] = this.router.get(req.path);
|
|
949
862
|
const mainHtml = renderToString(
|
|
950
863
|
/* @__PURE__ */ jsx4(
|
|
951
864
|
DevErrorPage,
|
|
@@ -998,7 +911,95 @@ ${renderToString(page)}
|
|
|
998
911
|
);
|
|
999
912
|
return { jsDeps, cssDeps };
|
|
1000
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
|
+
}
|
|
1001
999
|
};
|
|
1000
|
+
function isTrueOrUndefined(value) {
|
|
1001
|
+
return value === true || value === void 0;
|
|
1002
|
+
}
|
|
1002
1003
|
export {
|
|
1003
1004
|
Renderer
|
|
1004
1005
|
};
|