@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/dist/render.js CHANGED
@@ -3,14 +3,19 @@ import {
3
3
  I18N_CONTEXT,
4
4
  REQUEST_CONTEXT,
5
5
  getTranslations
6
- } from "./chunk-MJCIAH6K.js";
6
+ } from "./chunk-7IDJ3PBT.js";
7
7
  import {
8
+ RouteTrie,
8
9
  htmlMinify,
9
10
  htmlPretty,
10
11
  parseTagNames
11
- } from "./chunk-DFBTOMQF.js";
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 ((_a = req.rootConfig) == null ? void 0 : _a.rootDir) {
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: ${(route == null ? void 0 : route.src) || "null"}
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 = ((_b = (_a = req.rootConfig) == null ? void 0 : _a.i18n) == null ? void 0 : _b.defaultLocale) || "en";
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
- // src/render/route-trie.ts
338
- var RouteTrie = class _RouteTrie {
339
- constructor() {
340
- this.children = {};
341
- }
342
- /**
343
- * Adds a route to the trie.
344
- */
345
- add(path2, route) {
346
- path2 = this.normalizePath(path2);
347
- if (path2 === "") {
348
- this.route = route;
349
- return;
350
- }
351
- const [head, tail] = this.splitPath(path2);
352
- if (head.startsWith("[[...") && head.endsWith("]]")) {
353
- const paramName = head.slice(5, -2);
354
- this.optCatchAllNodes = new CatchAllNode(paramName, route);
355
- return;
356
- }
357
- if (head.startsWith("[...") && head.endsWith("]")) {
358
- const paramName = head.slice(4, -1);
359
- this.catchAllNodes = new CatchAllNode(paramName, route);
360
- return;
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
- nextNode.add(tail, route);
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
- if (this.route) {
401
- addPromise(cb("/", this.route));
402
- }
403
- if (this.paramNodes) {
404
- Object.values(this.paramNodes).forEach((paramChild) => {
405
- const param = `[${paramChild.name}]`;
406
- paramChild.trie.walk((childPath, route) => {
407
- const paramUrlPath = `/${param}${childPath}`;
408
- addPromise(cb(paramUrlPath, route));
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
- if (this.catchAllNodes) {
413
- const wildcardUrlPath = `/[...${this.catchAllNodes.name}]`;
414
- addPromise(cb(wildcardUrlPath, this.catchAllNodes.route));
415
- }
416
- if (this.optCatchAllNodes) {
417
- const wildcardUrlPath = `/[[...${this.optCatchAllNodes.name}]]`;
418
- addPromise(cb(wildcardUrlPath, this.optCatchAllNodes.route));
419
- }
420
- for (const subpath of Object.keys(this.children)) {
421
- const childTrie = this.children[subpath];
422
- childTrie.walk((childPath, childRoute) => {
423
- addPromise(cb(`/${subpath}${childPath}`, childRoute));
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
- const urlPaths = [];
568
- if (routeModule.getStaticPaths) {
569
- const staticPaths = await routeModule.getStaticPaths();
570
- if (staticPaths.paths) {
571
- staticPaths.paths.forEach(
572
- (pathParams) => {
573
- const urlPath = replaceParams(urlPathFormat, pathParams.params || {});
574
- if (pathContainsPlaceholders(urlPath)) {
575
- console.warn(
576
- `path contains placeholders: ${urlPathFormat}, double check getStaticPaths() and ensure all params are returned. more info: https://rootjs.dev/guide/routes#getStaticPaths`
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
- } else {
579
- urlPaths.push({
580
- urlPath: normalizeUrlPath(urlPath),
581
- params: pathParams.params || {}
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
- } else if (routeModule.getStaticProps && !pathContainsPlaceholders(urlPathFormat)) {
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
- return urlPaths;
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
- if (urlPath !== "/" && urlPath.endsWith("/")) {
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.toLowerCase();
639
- const [route, routeParams] = this.routes.get(url);
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 ((_b = (_a = req.rootConfig) == null ? void 0 : _a.i18n) == null ? void 0 : _b.defaultLocale) || "en";
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 = (options == null ? void 0 : options.locale) || route.locale;
669
- const translations = options == null ? void 0 : options.translations;
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).set({ "Content-Type": "text/html" }).end(html);
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 mainHtml = renderToString(vdom);
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.routes.walk(async (urlPath, route) => {
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 = (options == null ? void 0 : options.htmlAttrs) || {};
852
- const headAttrs = (options == null ? void 0 : options.headAttrs) || {};
853
- const bodyAttrs = (options == null ? void 0 : options.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 == null ? void 0 : options.headComponents
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 = (options == null ? void 0 : options.currentPath) || "/404";
867
- const [route, routeParams] = this.routes.get("/404");
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 = (options == null ? void 0 : options.currentPath) || "/500";
903
- const [route, routeParams] = this.routes.get("/500");
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.routes.get(req.path);
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
  };