@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/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,291 +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
- }
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
- 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;
410
+ });
411
+ return trie;
509
412
  }
510
- };
511
-
512
- // src/render/router.ts
513
- function getRoutes(rootConfig) {
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 urlFormat = "/[base]/[path]";
539
- const i18nUrlFormat = toSquareBrackets(
540
- ((_a2 = rootConfig.i18n) == null ? void 0 : _a2.urlFormat) || "/[locale]/[base]/[path]"
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
- return trie;
580
- }
581
- async function getAllPathsForRoute(urlPathFormat, route) {
582
- const routeModule = route.module;
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
- } else {
598
- urlPaths.push({
599
- urlPath: normalizeUrlPath(urlPath),
600
- params: pathParams.params || {}
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
- } else if (routeModule.getStaticProps && !pathContainsPlaceholders(urlPathFormat)) {
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
- return urlPaths;
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 ((options == null ? void 0 : options.trailingSlash) === false && urlPath !== "/" && urlPath.endsWith("/")) {
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.toLowerCase();
673
- const [route, routeParams] = this.routes.get(url);
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 ((_b = (_a = req.rootConfig) == null ? void 0 : _a.i18n) == null ? void 0 : _b.defaultLocale) || "en";
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 = (options == null ? void 0 : options.locale) || route.locale;
703
- 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;
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).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);
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 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
+ }
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.routes.walk(async (urlPath, route) => {
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 = (options == null ? void 0 : options.htmlAttrs) || {};
886
- const headAttrs = (options == null ? void 0 : options.headAttrs) || {};
887
- const bodyAttrs = (options == null ? void 0 : options.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 == null ? void 0 : options.headComponents
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 = (options == null ? void 0 : options.currentPath) || "/404";
901
- const [route, routeParams] = this.routes.get("/404");
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 = (options == null ? void 0 : options.currentPath) || "/500";
937
- const [route, routeParams] = this.routes.get("/500");
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.routes.get(req.path);
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
  };