@blinkk/root 1.0.0-rc.4 → 1.0.0-rc.41

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