@blinkk/root 1.0.0-rc.34 → 1.0.0-rc.36

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/cli.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as Server } from './types-a9rD7sef.js';
1
+ import { S as Server } from './types-403nR8i5.js';
2
2
  import 'express';
3
3
  import 'preact';
4
4
  import 'vite';
@@ -8,7 +8,7 @@ import 'js-beautify';
8
8
  interface BuildOptions {
9
9
  ssrOnly?: boolean;
10
10
  mode?: string;
11
- concurrency?: number;
11
+ concurrency?: string | number;
12
12
  }
13
13
  declare function build(rootProjectDir?: string, options?: BuildOptions): Promise<void>;
14
14
 
@@ -37,4 +37,11 @@ declare function createProdServer(options: {
37
37
  rootDir: string;
38
38
  }): Promise<Server>;
39
39
 
40
- export { build, createDevServer, createPreviewServer, createProdServer, dev, preview, start };
40
+ declare class CliRunner {
41
+ private name;
42
+ private version;
43
+ constructor(name: string, version: string);
44
+ run(argv: string[]): Promise<void>;
45
+ }
46
+
47
+ export { CliRunner, build, createDevServer, createPreviewServer, createProdServer, dev, preview, start };
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ CliRunner,
2
3
  build,
3
4
  createDevServer,
4
5
  createPreviewServer,
@@ -6,12 +7,13 @@ import {
6
7
  dev,
7
8
  preview,
8
9
  start
9
- } from "./chunk-RKCJAQUN.js";
10
- import "./chunk-3XHMMHV7.js";
11
- import "./chunk-JH33OQEK.js";
10
+ } from "./chunk-VDORN4UU.js";
11
+ import "./chunk-TZAHHHA4.js";
12
+ import "./chunk-L2UBQKLP.js";
12
13
  import "./chunk-QKBMWK5B.js";
13
14
  import "./chunk-IUQLRDFW.js";
14
15
  export {
16
+ CliRunner,
15
17
  build,
16
18
  createDevServer,
17
19
  createPreviewServer,
package/dist/core.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as Route } from './types-a9rD7sef.js';
2
- export { C as ConfigureServerHook, h as ConfigureServerOptions, l as GetStaticPaths, G as GetStaticProps, p as Handler, H as HandlerContext, s as HandlerRenderFn, r as HandlerRenderOptions, L as LocaleGroup, M as MultipartFile, N as NextFunction, P as Plugin, n as Request, m as RequestMiddleware, o as Response, b as RootConfig, e as RootHeaderConfig, c as RootI18nConfig, d as RootRedirectConfig, f as RootServerConfig, a as RootUserConfig, q as RouteModule, k as RouteParams, S as Server, i as configureServerPlugins, g as defineConfig, j as getVitePlugins } from './types-a9rD7sef.js';
1
+ import { R as Route } from './types-403nR8i5.js';
2
+ export { i as ConfigureServerHook, j as ConfigureServerOptions, C as ContentSecurityPolicyConfig, n as GetStaticPaths, G as GetStaticProps, r as Handler, H as HandlerContext, u as HandlerRenderFn, t as HandlerRenderOptions, L as LocaleGroup, M as MultipartFile, N as NextFunction, P as Plugin, p as Request, o as RequestMiddleware, q as Response, b as RootConfig, e as RootHeaderConfig, c as RootI18nConfig, d as RootRedirectConfig, f as RootSecurityConfig, g as RootServerConfig, a as RootUserConfig, s as RouteModule, m as RouteParams, S as Server, X as XFrameOptionsConfig, k as configureServerPlugins, h as defineConfig, l as getVitePlugins } from './types-403nR8i5.js';
3
3
  import * as preact$1 from 'preact';
4
4
  import { FunctionalComponent, ComponentChildren } from 'preact';
5
5
  import 'express';
@@ -116,6 +116,8 @@ interface RequestContext {
116
116
  locale: string;
117
117
  /** Translations map for the current locale. */
118
118
  translations: Record<string, string>;
119
+ /** CSP nonce value. */
120
+ nonce?: string;
119
121
  }
120
122
  /**
121
123
  * A hook that returns information about the current route.
package/dist/core.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  getTranslations,
9
9
  useI18nContext,
10
10
  useRequestContext
11
- } from "./chunk-MJCIAH6K.js";
11
+ } from "./chunk-7IDJ3PBT.js";
12
12
 
13
13
  // src/core/config.ts
14
14
  function defineConfig(config) {
package/dist/core.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/config.ts","../src/core/components/Body.tsx","../src/core/components/Head.ts","../src/core/components/Script.ts","../src/core/hooks/useTranslations.ts"],"sourcesContent":["import {UserConfig as ViteUserConfig} from 'vite';\n\nimport {HtmlMinifyOptions} from '../render/html-minify';\nimport {HtmlPrettyOptions} from '../render/html-pretty';\n\nimport {Plugin} from './plugin';\nimport {RequestMiddleware} from './types';\n\nexport interface RootUserConfig {\n /**\n * Canonical domain the website will serve on. Useful for things like the\n * sitemap, SEO tags, etc.\n */\n domain?: string;\n\n /**\n * The base URL path that the site will serve on. Defaults to `/`;\n */\n base?: string;\n\n /**\n * Config for auto-injecting custom element dependencies.\n */\n elements?: {\n /**\n * A list of directories to use to look for custom elements. The dir path\n * should be relative to the project dir, e.g. \"path/to/elements\".\n */\n include?: string[];\n\n /**\n * A list of RegEx patterns to exclude. The string passed to the RegEx is\n * the file URL relative to the project root, e.g. \"/elements/foo/foo.ts\".\n */\n exclude?: RegExp[];\n };\n\n /**\n * Config options for localization and internationalization.\n */\n i18n?: RootI18nConfig;\n\n /**\n * Config options for the Root.js express server.\n */\n server?: RootServerConfig;\n\n /**\n * Vite config.\n * @see {@link https://vitejs.dev/config/} for more information.\n */\n vite?: ViteUserConfig;\n\n /**\n * Whether to automatically minify HTML output. This is enabled by default,\n * in order to disable, pass `minifyHtml: false` to root.config.ts.\n */\n minifyHtml?: boolean;\n\n /**\n * Options to pass to html-minifier-terser.\n */\n minifyHtmlOptions?: HtmlMinifyOptions;\n\n /**\n * Whether to pretty print HTML output.\n */\n prettyHtml?: boolean;\n\n /**\n * Options to pass to js-beautify.\n */\n prettyHtmlOptions?: HtmlPrettyOptions;\n\n /**\n * Whether to include a sitemap.xml file to the build output.\n */\n sitemap?: boolean;\n\n /**\n * Plugins.\n */\n plugins?: Plugin[];\n}\n\nexport type RootConfig = RootUserConfig & {\n rootDir: string;\n};\n\nexport interface LocaleGroup {\n label?: string;\n locales: string[];\n}\n\nexport interface RootI18nConfig {\n /**\n * Locales enabled for the site.\n */\n locales?: string[];\n\n /**\n * The default locale to use. Defaults is `en`.\n */\n defaultLocale?: string;\n\n /**\n * URL format for localized content. Default is `/[locale]/[base]/[path]`.\n */\n urlFormat?: string;\n\n /**\n * Localization groups, to help UIs (like Root.js CMS) logically group\n * locales.\n */\n groups?: Record<string, LocaleGroup>;\n}\n\nexport interface RootRedirectConfig {\n source: string;\n destination: string;\n type?: number;\n}\n\nexport interface RootHeaderConfig {\n /** A glob pattern match (regex not supported yet). */\n source: string;\n headers: Array<{\n key: string;\n value: string;\n }>;\n}\n\nexport interface RootServerConfig {\n /**\n * An array of middleware to add to the express server. These middleware are\n * added to the beginning of the express app.\n */\n middlewares?: RequestMiddleware[];\n\n /**\n * The `trailingSlash` config allows you to control how the server handles\n * trailing slashes. This config only affects URLs that do not have a file\n * extension (i.e. HTML paths).\n *\n * - When `true`, the server redirects URLs to add a trailing slash\n * - When `false`, the server redirects URLs to remove a trailing slash\n * - When unspecified, the server allows URLs with and without trailing slash\n */\n trailingSlash?: boolean;\n\n /**\n * Cookie secret for the session middleware.\n */\n sessionCookieSecret?: string | string[];\n\n /**\n * List of redirects.\n */\n redirects?: RootRedirectConfig[];\n\n /**\n * HTTP headers to add to a response.\n */\n headers?: RootHeaderConfig[];\n}\n\nexport function defineConfig(config: RootUserConfig): RootUserConfig {\n return config;\n}\n","import {ComponentChildren, FunctionalComponent} from 'preact';\nimport {useContext} from 'preact/hooks';\n\nimport {HTML_CONTEXT} from './Html';\n\nexport type BodyProps = preact.JSX.HTMLAttributes<HTMLBodyElement> & {\n children?: ComponentChildren;\n};\n\n/**\n * The `<Body>` component can be used to update attrs in the `<body>` tag.\n *\n * Usage:\n *\n * ```tsx\n * <Body className=\"body\">\n * <h1>Hello world</h1>\n * </Body>\n * ```\n *\n * Output:\n *\n * ```html\n * <body class=\"body\">\n * <h1>Hello world</h1>\n * </body>\n */\nexport const Body: FunctionalComponent<BodyProps> = ({children, ...attrs}) => {\n const context = useContext(HTML_CONTEXT);\n if (!context) {\n throw new Error(\n 'HTML_CONTEXT not found, double-check usage of the <Body> component'\n );\n }\n context.bodyAttrs = attrs;\n return <>{children}</>;\n};\n","import {ComponentChildren, FunctionalComponent} from 'preact';\nimport {useContext} from 'preact/hooks';\n\nimport {HTML_CONTEXT} from './Html';\n\nexport type HeadProps = preact.JSX.HTMLAttributes<HTMLHeadElement> & {\n children?: ComponentChildren;\n};\n\n/**\n * The <Head> component can be used for injecting elements into the HTML head\n * tag from any part of a page. The <Head> can be added via any component or\n * sub-component and will automatically be hoisted to the `<head>` element.\n *\n * Usage:\n *\n * ```tsx\n * <Head>\n * <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n * </Head>\n * ```\n */\nexport const Head: FunctionalComponent<HeadProps> = ({children, ...attrs}) => {\n const context = useContext(HTML_CONTEXT);\n if (!context) {\n throw new Error(\n 'HTML_CONTEXT not found, double-check usage of the <Head> component'\n );\n }\n context.headComponents.push(children);\n context.headAttrs = attrs;\n return null;\n};\n","import {FunctionalComponent} from 'preact';\nimport {useContext} from 'preact/hooks';\n\nimport {HTML_CONTEXT} from './Html';\n\nexport type ScriptProps = preact.JSX.HTMLAttributes<HTMLScriptElement>;\n\n/**\n * The <Script> component is used for rendering any custom script modules. At\n * the moment, the system only pre-renders and bundles files that are in the\n * `/bundles` folder at the root of the project.\n *\n * Usage:\n *\n * ```tsx\n * <Script src=\"/bundles/main.ts\" />\n * ```\n */\nexport const Script: FunctionalComponent<ScriptProps> = (props) => {\n const context = useContext(HTML_CONTEXT);\n if (!context) {\n throw new Error(\n 'HTML_CONTEXT not found, double-check usage of the <Script> component'\n );\n }\n context.scriptDeps.push(props);\n return null;\n};\n","import {useI18nContext} from './useI18nContext';\n\n/**\n * A hook that returns a function that can be used to translate a string, and\n * optionally replace any parameterized values that are surrounded in curly\n * braces.\n *\n * Usage:\n *\n * ```ts\n * const t = useTranslations();\n * t('Hello {name}', {name: 'Bob'});\n * // => 'Bounjour Bob'\n */\nexport function useTranslations() {\n const context = useI18nContext();\n const translations = context.translations || {};\n const t = (str: string, params?: Record<string, string | number>) => {\n const key = normalizeString(str);\n let translation = translations[key] ?? key ?? '';\n if (params) {\n for (const param of Object.keys(params)) {\n const val = String(params[param] ?? '');\n translation = translation.replaceAll(`{${param}}`, val);\n }\n }\n return translation;\n };\n return t;\n}\n\n/**\n * Cleans a string that's used for translations. Performs the following:\n * - Removes any leading/trailing whitespace\n * - Removes spaces at the end of any line\n */\nexport function normalizeString(str: string) {\n const lines = String(str)\n .trim()\n .split('\\n')\n .map((line) => line.trimEnd());\n return lines.join('\\n');\n}\n"],"mappings":";;;;;;;;;;;;;AAsKO,SAAS,aAAa,QAAwC;AACnE,SAAO;AACT;;;ACvKA,SAAQ,kBAAiB;AAkChB;AARF,IAAM,OAAuC,CAAC,EAAC,UAAU,GAAG,MAAK,MAAM;AAC5E,QAAM,UAAU,WAAW,YAAY;AACvC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,UAAQ,YAAY;AACpB,SAAO,gCAAG,UAAS;AACrB;;;ACnCA,SAAQ,cAAAA,mBAAiB;AAqBlB,IAAM,OAAuC,CAAC,EAAC,UAAU,GAAG,MAAK,MAAM;AAC5E,QAAM,UAAUC,YAAW,YAAY;AACvC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,UAAQ,eAAe,KAAK,QAAQ;AACpC,UAAQ,YAAY;AACpB,SAAO;AACT;;;AC/BA,SAAQ,cAAAC,mBAAiB;AAiBlB,IAAM,SAA2C,CAAC,UAAU;AACjE,QAAM,UAAUC,YAAW,YAAY;AACvC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,UAAQ,WAAW,KAAK,KAAK;AAC7B,SAAO;AACT;;;ACbO,SAAS,kBAAkB;AAChC,QAAM,UAAU,eAAe;AAC/B,QAAM,eAAe,QAAQ,gBAAgB,CAAC;AAC9C,QAAM,IAAI,CAAC,KAAa,WAA6C;AACnE,UAAM,MAAM,gBAAgB,GAAG;AAC/B,QAAI,cAAc,aAAa,GAAG,KAAK,OAAO;AAC9C,QAAI,QAAQ;AACV,iBAAW,SAAS,OAAO,KAAK,MAAM,GAAG;AACvC,cAAM,MAAM,OAAO,OAAO,KAAK,KAAK,EAAE;AACtC,sBAAc,YAAY,WAAW,IAAI,KAAK,KAAK,GAAG;AAAA,MACxD;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAOO,SAAS,gBAAgB,KAAa;AAC3C,QAAM,QAAQ,OAAO,GAAG,EACrB,KAAK,EACL,MAAM,IAAI,EACV,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC;AAC/B,SAAO,MAAM,KAAK,IAAI;AACxB;","names":["useContext","useContext","useContext","useContext"]}
1
+ {"version":3,"sources":["../src/core/config.ts","../src/core/components/Body.tsx","../src/core/components/Head.ts","../src/core/components/Script.ts","../src/core/hooks/useTranslations.ts"],"sourcesContent":["import {UserConfig as ViteUserConfig} from 'vite';\n\nimport {HtmlMinifyOptions} from '../render/html-minify';\nimport {HtmlPrettyOptions} from '../render/html-pretty';\n\nimport {Plugin} from './plugin';\nimport {RequestMiddleware} from './types';\n\nexport interface RootUserConfig {\n /**\n * Canonical domain the website will serve on. Useful for things like the\n * sitemap, SEO tags, etc.\n */\n domain?: string;\n\n /**\n * The base URL path that the site will serve on. Defaults to `/`;\n */\n base?: string;\n\n /**\n * Config for auto-injecting custom element dependencies.\n */\n elements?: {\n /**\n * A list of directories to use to look for custom elements. The dir path\n * should be relative to the project dir, e.g. \"path/to/elements\".\n */\n include?: string[];\n\n /**\n * A list of RegEx patterns to exclude. The string passed to the RegEx is\n * the file URL relative to the project root, e.g. \"/elements/foo/foo.ts\".\n */\n exclude?: RegExp[];\n };\n\n /**\n * Config options for localization and internationalization.\n */\n i18n?: RootI18nConfig;\n\n /**\n * Config options for the Root.js express server.\n */\n server?: RootServerConfig;\n\n /**\n * Vite config.\n * @see {@link https://vitejs.dev/config/} for more information.\n */\n vite?: ViteUserConfig;\n\n /**\n * Whether to automatically minify HTML output. This is enabled by default,\n * in order to disable, pass `minifyHtml: false` to root.config.ts.\n */\n minifyHtml?: boolean;\n\n /**\n * Options to pass to html-minifier-terser.\n */\n minifyHtmlOptions?: HtmlMinifyOptions;\n\n /**\n * Whether to pretty print HTML output.\n */\n prettyHtml?: boolean;\n\n /**\n * Options to pass to js-beautify.\n */\n prettyHtmlOptions?: HtmlPrettyOptions;\n\n /**\n * Whether to include a sitemap.xml file to the build output.\n */\n sitemap?: boolean;\n\n /**\n * Plugins.\n */\n plugins?: Plugin[];\n}\n\nexport type RootConfig = RootUserConfig & {\n rootDir: string;\n};\n\nexport interface LocaleGroup {\n label?: string;\n locales: string[];\n}\n\nexport interface RootI18nConfig {\n /**\n * Locales enabled for the site.\n */\n locales?: string[];\n\n /**\n * The default locale to use. Defaults is `en`.\n */\n defaultLocale?: string;\n\n /**\n * URL format for localized content. Default is `/[locale]/[base]/[path]`.\n */\n urlFormat?: string;\n\n /**\n * Localization groups, to help UIs (like Root.js CMS) logically group\n * locales.\n */\n groups?: Record<string, LocaleGroup>;\n}\n\nexport interface RootRedirectConfig {\n source: string;\n destination: string;\n type?: number;\n}\n\nexport interface RootHeaderConfig {\n /** A glob pattern match (regex not supported yet). */\n source: string;\n headers: Array<{\n key: string;\n value: string;\n }>;\n}\n\nexport interface ContentSecurityPolicyConfig {\n directives?: Record<string, string[]>;\n reportOnly?: boolean;\n}\n\nexport interface XFrameOptionsConfig {\n action: 'DENY' | 'SAMEORIGIN';\n}\n\nexport interface RootSecurityConfig {\n /**\n * Content-Security-Policy config. If enabled, a nonce is auto-generated\n * for every request and appended to script and stylesheet tags. You can\n * validate your CSP headers using a tool like {@link https://csp-evaluator.withgoogle.com/}.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP}\n */\n contentSecurityPolicy?: ContentSecurityPolicyConfig | boolean;\n\n /**\n * Strict-Transport-Security config. When enabled, the header value is set\n * to `Strict-Transport-Security: max-age=63072000; includeSubDomains; preload`.\n */\n strictTransportSecurity?: boolean;\n\n /**\n * X-Content-Type-Options config. When enabled, the header value is set to\n * `X-Content-Type-Options: nosniff`.\n */\n xContentTypeOptions?: boolean;\n\n /**\n * X-Frame-Options config. Setting this value to `true` will default the\n * header value to `X-Frame-Options: SAMEORIGIN`.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options}\n */\n xFrameOptions?: 'DENY' | 'SAMEORIGIN' | boolean;\n\n /**\n * X-XSS-Protection config. When enabled, the header value is set to\n * `X-XSS-Protection: 1; mode=block`.\n */\n xXssProtection?: boolean;\n}\n\nexport interface RootServerConfig {\n /**\n * An array of middleware to add to the express server. These middleware are\n * added to the beginning of the express app.\n */\n middlewares?: RequestMiddleware[];\n\n /**\n * The `trailingSlash` config allows you to control how the server handles\n * trailing slashes. This config only affects URLs that do not have a file\n * extension (i.e. HTML paths).\n *\n * - When `true`, the server redirects URLs to add a trailing slash\n * - When `false`, the server redirects URLs to remove a trailing slash\n * - When unspecified, the server allows URLs with and without trailing slash\n */\n trailingSlash?: boolean;\n\n /**\n * Cookie secret for the session middleware.\n */\n sessionCookieSecret?: string | string[];\n\n /**\n * List of redirects.\n */\n redirects?: RootRedirectConfig[];\n\n /**\n * HTTP headers to add to a response.\n */\n headers?: RootHeaderConfig[];\n\n /**\n * HTTP security settings. By default, all security settings are enabled with\n * commonly used default values.\n */\n security?: RootSecurityConfig;\n}\n\nexport function defineConfig(config: RootUserConfig): RootUserConfig {\n return config;\n}\n","import {ComponentChildren, FunctionalComponent} from 'preact';\nimport {useContext} from 'preact/hooks';\n\nimport {HTML_CONTEXT} from './Html';\n\nexport type BodyProps = preact.JSX.HTMLAttributes<HTMLBodyElement> & {\n children?: ComponentChildren;\n};\n\n/**\n * The `<Body>` component can be used to update attrs in the `<body>` tag.\n *\n * Usage:\n *\n * ```tsx\n * <Body className=\"body\">\n * <h1>Hello world</h1>\n * </Body>\n * ```\n *\n * Output:\n *\n * ```html\n * <body class=\"body\">\n * <h1>Hello world</h1>\n * </body>\n */\nexport const Body: FunctionalComponent<BodyProps> = ({children, ...attrs}) => {\n const context = useContext(HTML_CONTEXT);\n if (!context) {\n throw new Error(\n 'HTML_CONTEXT not found, double-check usage of the <Body> component'\n );\n }\n context.bodyAttrs = attrs;\n return <>{children}</>;\n};\n","import {ComponentChildren, FunctionalComponent} from 'preact';\nimport {useContext} from 'preact/hooks';\n\nimport {HTML_CONTEXT} from './Html';\n\nexport type HeadProps = preact.JSX.HTMLAttributes<HTMLHeadElement> & {\n children?: ComponentChildren;\n};\n\n/**\n * The <Head> component can be used for injecting elements into the HTML head\n * tag from any part of a page. The <Head> can be added via any component or\n * sub-component and will automatically be hoisted to the `<head>` element.\n *\n * Usage:\n *\n * ```tsx\n * <Head>\n * <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n * </Head>\n * ```\n */\nexport const Head: FunctionalComponent<HeadProps> = ({children, ...attrs}) => {\n const context = useContext(HTML_CONTEXT);\n if (!context) {\n throw new Error(\n 'HTML_CONTEXT not found, double-check usage of the <Head> component'\n );\n }\n context.headComponents.push(children);\n context.headAttrs = attrs;\n return null;\n};\n","import {FunctionalComponent} from 'preact';\nimport {useContext} from 'preact/hooks';\n\nimport {HTML_CONTEXT} from './Html';\n\nexport type ScriptProps = preact.JSX.HTMLAttributes<HTMLScriptElement>;\n\n/**\n * The <Script> component is used for rendering any custom script modules. At\n * the moment, the system only pre-renders and bundles files that are in the\n * `/bundles` folder at the root of the project.\n *\n * Usage:\n *\n * ```tsx\n * <Script src=\"/bundles/main.ts\" />\n * ```\n */\nexport const Script: FunctionalComponent<ScriptProps> = (props) => {\n const context = useContext(HTML_CONTEXT);\n if (!context) {\n throw new Error(\n 'HTML_CONTEXT not found, double-check usage of the <Script> component'\n );\n }\n context.scriptDeps.push(props);\n return null;\n};\n","import {useI18nContext} from './useI18nContext';\n\n/**\n * A hook that returns a function that can be used to translate a string, and\n * optionally replace any parameterized values that are surrounded in curly\n * braces.\n *\n * Usage:\n *\n * ```ts\n * const t = useTranslations();\n * t('Hello {name}', {name: 'Bob'});\n * // => 'Bounjour Bob'\n */\nexport function useTranslations() {\n const context = useI18nContext();\n const translations = context.translations || {};\n const t = (str: string, params?: Record<string, string | number>) => {\n const key = normalizeString(str);\n let translation = translations[key] ?? key ?? '';\n if (params) {\n for (const param of Object.keys(params)) {\n const val = String(params[param] ?? '');\n translation = translation.replaceAll(`{${param}}`, val);\n }\n }\n return translation;\n };\n return t;\n}\n\n/**\n * Cleans a string that's used for translations. Performs the following:\n * - Removes any leading/trailing whitespace\n * - Removes spaces at the end of any line\n */\nexport function normalizeString(str: string) {\n const lines = String(str)\n .trim()\n .split('\\n')\n .map((line) => line.trimEnd());\n return lines.join('\\n');\n}\n"],"mappings":";;;;;;;;;;;;;AA0NO,SAAS,aAAa,QAAwC;AACnE,SAAO;AACT;;;AC3NA,SAAQ,kBAAiB;AAkChB;AARF,IAAM,OAAuC,CAAC,EAAC,UAAU,GAAG,MAAK,MAAM;AAC5E,QAAM,UAAU,WAAW,YAAY;AACvC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,UAAQ,YAAY;AACpB,SAAO,gCAAG,UAAS;AACrB;;;ACnCA,SAAQ,cAAAA,mBAAiB;AAqBlB,IAAM,OAAuC,CAAC,EAAC,UAAU,GAAG,MAAK,MAAM;AAC5E,QAAM,UAAUC,YAAW,YAAY;AACvC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,UAAQ,eAAe,KAAK,QAAQ;AACpC,UAAQ,YAAY;AACpB,SAAO;AACT;;;AC/BA,SAAQ,cAAAC,mBAAiB;AAiBlB,IAAM,SAA2C,CAAC,UAAU;AACjE,QAAM,UAAUC,YAAW,YAAY;AACvC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,UAAQ,WAAW,KAAK,KAAK;AAC7B,SAAO;AACT;;;ACbO,SAAS,kBAAkB;AAChC,QAAM,UAAU,eAAe;AAC/B,QAAM,eAAe,QAAQ,gBAAgB,CAAC;AAC9C,QAAM,IAAI,CAAC,KAAa,WAA6C;AACnE,UAAM,MAAM,gBAAgB,GAAG;AAC/B,QAAI,cAAc,aAAa,GAAG,KAAK,OAAO;AAC9C,QAAI,QAAQ;AACV,iBAAW,SAAS,OAAO,KAAK,MAAM,GAAG;AACvC,cAAM,MAAM,OAAO,OAAO,KAAK,KAAK,EAAE;AACtC,sBAAc,YAAY,WAAW,IAAI,KAAK,KAAK,GAAG;AAAA,MACxD;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAOO,SAAS,gBAAgB,KAAa;AAC3C,QAAM,QAAQ,OAAO,GAAG,EACrB,KAAK,EACL,MAAM,IAAI,EACV,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC;AAC/B,SAAO,MAAM,KAAK,IAAI;AACxB;","names":["useContext","useContext","useContext","useContext"]}
package/dist/functions.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  createPreviewServer,
3
3
  createProdServer
4
- } from "./chunk-RKCJAQUN.js";
5
- import "./chunk-3XHMMHV7.js";
6
- import "./chunk-JH33OQEK.js";
4
+ } from "./chunk-VDORN4UU.js";
5
+ import "./chunk-TZAHHHA4.js";
6
+ import "./chunk-L2UBQKLP.js";
7
7
  import "./chunk-QKBMWK5B.js";
8
8
  import "./chunk-IUQLRDFW.js";
9
9
 
@@ -12,10 +12,10 @@ import path from "node:path";
12
12
  import { onRequest } from "firebase-functions/v2/https";
13
13
  function server(options) {
14
14
  let rootServer;
15
- const rootDir = path.resolve((options == null ? void 0 : options.rootDir) || process.cwd());
16
- return onRequest((options == null ? void 0 : options.httpsOptions) || {}, async (req, res) => {
15
+ const rootDir = path.resolve(options?.rootDir || process.cwd());
16
+ return onRequest(options?.httpsOptions || {}, async (req, res) => {
17
17
  if (!rootServer) {
18
- if ((options == null ? void 0 : options.mode) === "preview") {
18
+ if (options?.mode === "preview") {
19
19
  rootServer = await createPreviewServer({ rootDir });
20
20
  } else {
21
21
  rootServer = await createProdServer({ rootDir });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/functions/server.ts"],"sourcesContent":["import path from 'node:path';\nimport {HttpsOptions, onRequest} from 'firebase-functions/v2/https';\nimport {createPreviewServer, createProdServer} from '../cli/cli';\nimport {Server} from '../core/types';\n\nexport interface ProdServerOptions {\n rootDir?: string;\n mode?: 'preview' | 'production';\n httpsOptions?: HttpsOptions;\n}\n\n/**\n * Firebase Function that runs a Root.js server running in SSR mode.\n */\nexport function server(options?: ProdServerOptions) {\n let rootServer: Server;\n const rootDir = path.resolve(options?.rootDir || process.cwd());\n return onRequest(options?.httpsOptions || {}, async (req, res) => {\n if (!rootServer) {\n if (options?.mode === 'preview') {\n rootServer = await createPreviewServer({rootDir});\n } else {\n rootServer = await createProdServer({rootDir});\n }\n }\n await rootServer(req, res);\n });\n}\n"],"mappings":";;;;;;;;;;AAAA,OAAO,UAAU;AACjB,SAAsB,iBAAgB;AAa/B,SAAS,OAAO,SAA6B;AAClD,MAAI;AACJ,QAAM,UAAU,KAAK,SAAQ,mCAAS,YAAW,QAAQ,IAAI,CAAC;AAC9D,SAAO,WAAU,mCAAS,iBAAgB,CAAC,GAAG,OAAO,KAAK,QAAQ;AAChE,QAAI,CAAC,YAAY;AACf,WAAI,mCAAS,UAAS,WAAW;AAC/B,qBAAa,MAAM,oBAAoB,EAAC,QAAO,CAAC;AAAA,MAClD,OAAO;AACL,qBAAa,MAAM,iBAAiB,EAAC,QAAO,CAAC;AAAA,MAC/C;AAAA,IACF;AACA,UAAM,WAAW,KAAK,GAAG;AAAA,EAC3B,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/functions/server.ts"],"sourcesContent":["import path from 'node:path';\nimport {HttpsOptions, onRequest} from 'firebase-functions/v2/https';\nimport {createPreviewServer, createProdServer} from '../cli/cli';\nimport {Server} from '../core/types';\n\nexport interface ProdServerOptions {\n rootDir?: string;\n mode?: 'preview' | 'production';\n httpsOptions?: HttpsOptions;\n}\n\n/**\n * Firebase Function that runs a Root.js server running in SSR mode.\n */\nexport function server(options?: ProdServerOptions) {\n let rootServer: Server;\n const rootDir = path.resolve(options?.rootDir || process.cwd());\n return onRequest(options?.httpsOptions || {}, async (req, res) => {\n if (!rootServer) {\n if (options?.mode === 'preview') {\n rootServer = await createPreviewServer({rootDir});\n } else {\n rootServer = await createProdServer({rootDir});\n }\n }\n await rootServer(req, res);\n });\n}\n"],"mappings":";;;;;;;;;;AAAA,OAAO,UAAU;AACjB,SAAsB,iBAAgB;AAa/B,SAAS,OAAO,SAA6B;AAClD,MAAI;AACJ,QAAM,UAAU,KAAK,QAAQ,SAAS,WAAW,QAAQ,IAAI,CAAC;AAC9D,SAAO,UAAU,SAAS,gBAAgB,CAAC,GAAG,OAAO,KAAK,QAAQ;AAChE,QAAI,CAAC,YAAY;AACf,UAAI,SAAS,SAAS,WAAW;AAC/B,qBAAa,MAAM,oBAAoB,EAAC,QAAO,CAAC;AAAA,MAClD,OAAO;AACL,qBAAa,MAAM,iBAAiB,EAAC,QAAO,CAAC;AAAA,MAC/C;AAAA,IACF;AACA,UAAM,WAAW,KAAK,GAAG;AAAA,EAC3B,CAAC;AACH;","names":[]}
@@ -1,5 +1,5 @@
1
- import { b as RootConfig, n as Request, o as Response, N as NextFunction } from './types-a9rD7sef.js';
2
- export { t as SESSION_COOKIE, v as SaveSessionOptions, x as Session, u as SessionMiddlewareOptions, w as sessionMiddleware } from './types-a9rD7sef.js';
1
+ import { b as RootConfig, p as Request, q as Response, N as NextFunction } from './types-403nR8i5.js';
2
+ export { v as SESSION_COOKIE, x as SaveSessionOptions, z as Session, w as SessionMiddlewareOptions, y as sessionMiddleware } from './types-403nR8i5.js';
3
3
  import { RequestHandler } from 'express';
4
4
  import 'preact';
5
5
  import 'vite';
@@ -6,7 +6,7 @@ import {
6
6
  rootProjectMiddleware,
7
7
  sessionMiddleware,
8
8
  trailingSlashMiddleware
9
- } from "./chunk-3XHMMHV7.js";
9
+ } from "./chunk-TZAHHHA4.js";
10
10
  export {
11
11
  SESSION_COOKIE,
12
12
  Session,
package/dist/node.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { b as RootConfig } from './types-a9rD7sef.js';
1
+ import { b as RootConfig } from './types-403nR8i5.js';
2
2
  import { ViteDevServer } from 'vite';
3
3
  import 'express';
4
4
  import 'preact';
package/dist/node.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  loadBundledConfig,
5
5
  loadRootConfig,
6
6
  viteSsrLoadModule
7
- } from "./chunk-JH33OQEK.js";
7
+ } from "./chunk-L2UBQKLP.js";
8
8
  import "./chunk-QKBMWK5B.js";
9
9
  export {
10
10
  bundleRootConfig,
package/dist/render.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { y as Renderer } from './types-a9rD7sef.js';
1
+ export { A as Renderer } from './types-403nR8i5.js';
2
2
  import 'express';
3
3
  import 'preact';
4
4
  import 'vite';
package/dist/render.js CHANGED
@@ -3,7 +3,7 @@ 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
8
  RouteTrie,
9
9
  htmlMinify,
@@ -12,6 +12,10 @@ import {
12
12
  } from "./chunk-IUQLRDFW.js";
13
13
 
14
14
  // src/render/render.tsx
15
+ import crypto from "node:crypto";
16
+ import {
17
+ options as preactOptions
18
+ } from "preact";
15
19
  import renderToString from "preact-render-to-string";
16
20
 
17
21
  // src/core/pages/ErrorPage.tsx
@@ -92,7 +96,6 @@ function ErrorPage(props) {
92
96
  // src/core/pages/DevErrorPage.tsx
93
97
  import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
94
98
  function DevErrorPage(props) {
95
- var _a;
96
99
  const req = props.req;
97
100
  const err = props.error;
98
101
  const route = props.route;
@@ -100,7 +103,7 @@ function DevErrorPage(props) {
100
103
  let errMsg = String(err);
101
104
  if (err && err.stack) {
102
105
  errMsg = err.stack.replace(/\(.*node_modules/g, "(node_modules").replace(/at \/.*node_modules/g, "at node_modules");
103
- if ((_a = req.rootConfig) == null ? void 0 : _a.rootDir) {
106
+ if (req.rootConfig?.rootDir) {
104
107
  errMsg = errMsg.replaceAll(req.rootConfig.rootDir, "<root>");
105
108
  }
106
109
  if (process.env.HOME) {
@@ -114,7 +117,7 @@ function DevErrorPage(props) {
114
117
  ] }),
115
118
  /* @__PURE__ */ jsx2("h2", { children: "Debug Info" }),
116
119
  /* @__PURE__ */ jsx2("pre", { className: "box", children: /* @__PURE__ */ jsx2("code", { children: `url: ${req.originalUrl}
117
- route: ${(route == null ? void 0 : route.src) || "null"}
120
+ route: ${route?.src || "null"}
118
121
  routeParams: ${routeParams && JSON.stringify(routeParams) || "null"}` }) })
119
122
  ] });
120
123
  }
@@ -245,11 +248,10 @@ var ES_419_COUNTRIES = [
245
248
  // Venezuela
246
249
  ];
247
250
  function getFallbackLocales(req) {
248
- var _a, _b;
249
251
  const hl = getFirstQueryParam(req, "hl");
250
252
  const countryCode = getCountry(req);
251
253
  if (isWebCrawler(req)) {
252
- 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";
253
255
  if (hl && hl !== defaultLocale) {
254
256
  return [hl, defaultLocale];
255
257
  }
@@ -350,13 +352,11 @@ var Router = class {
350
352
  await this.routeTrie.walk(cb);
351
353
  }
352
354
  initRouteTrie() {
353
- var _a, _b;
354
- const locales = ((_a = this.rootConfig.i18n) == null ? void 0 : _a.locales) || [];
355
+ const locales = this.rootConfig.i18n?.locales || [];
355
356
  const basePath = this.rootConfig.base || "/";
356
- const defaultLocale = ((_b = this.rootConfig.i18n) == null ? void 0 : _b.defaultLocale) || "en";
357
+ const defaultLocale = this.rootConfig.i18n?.defaultLocale || "en";
357
358
  const trie = new RouteTrie();
358
359
  Object.keys(ROUTES_FILES).forEach((modulePath) => {
359
- var _a2;
360
360
  const src = modulePath.slice(1);
361
361
  let relativeRoutePath = modulePath.replace(/^\/routes/, "");
362
362
  const parts = path.parse(relativeRoutePath);
@@ -370,17 +370,16 @@ var Router = class {
370
370
  }
371
371
  const urlFormat = "/[base]/[path]";
372
372
  const i18nUrlFormat = toSquareBrackets(
373
- ((_a2 = this.rootConfig.i18n) == null ? void 0 : _a2.urlFormat) || "/[locale]/[base]/[path]"
373
+ this.rootConfig.i18n?.urlFormat || "/[locale]/[base]/[path]"
374
374
  );
375
375
  const placeholders = {
376
376
  base: removeSlashes(basePath),
377
377
  path: removeSlashes(relativeRoutePath)
378
378
  };
379
379
  const formatUrl = (format) => {
380
- var _a3;
381
380
  const url = format.replaceAll("[base]", placeholders.base).replaceAll("[path]", placeholders.path);
382
381
  return normalizeUrlPath(url, {
383
- trailingSlash: (_a3 = this.rootConfig.server) == null ? void 0 : _a3.trailingSlash
382
+ trailingSlash: this.rootConfig.server?.trailingSlash
384
383
  });
385
384
  };
386
385
  const routePath = formatUrl(urlFormat);
@@ -472,7 +471,7 @@ function replaceParams(urlPathFormat, params) {
472
471
  }
473
472
  function normalizeUrlPath(urlPath, options) {
474
473
  urlPath = urlPath.replace(/\/+/g, "/");
475
- if ((options == null ? void 0 : options.trailingSlash) === false && urlPath !== "/" && urlPath.endsWith("/")) {
474
+ if (options?.trailingSlash === false && urlPath !== "/" && urlPath.endsWith("/")) {
476
475
  urlPath = urlPath.replace(/\/*$/g, "");
477
476
  }
478
477
  if (urlPath.endsWith("/index")) {
@@ -522,14 +521,13 @@ var Renderer = class {
522
521
  }
523
522
  const fallbackLocales = route.isDefaultLocale ? getFallbackLocales(req) : [route.locale];
524
523
  const getPreferredLocale = (availableLocales) => {
525
- var _a, _b;
526
524
  const lowerLocales = availableLocales.map((l) => l.toLowerCase());
527
525
  for (const fallbackLocale of fallbackLocales) {
528
526
  if (lowerLocales.includes(fallbackLocale.toLowerCase())) {
529
527
  return fallbackLocale;
530
528
  }
531
529
  }
532
- return ((_b = (_a = req.rootConfig) == null ? void 0 : _a.i18n) == null ? void 0 : _b.defaultLocale) || "en";
530
+ return req.rootConfig?.i18n?.defaultLocale || "en";
533
531
  };
534
532
  const render404 = async () => {
535
533
  next();
@@ -540,15 +538,19 @@ var Renderer = class {
540
538
  render404();
541
539
  return;
542
540
  }
541
+ const securityConfig = this.getSecurityConfig();
542
+ const cspEnabled = !!securityConfig.contentSecurityPolicy;
543
543
  const currentPath = req.path;
544
- const locale = (options == null ? void 0 : options.locale) || route.locale;
545
- 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;
546
547
  const output = await this.renderComponent(route.module.default, props2, {
547
548
  currentPath,
548
549
  route,
549
550
  routeParams,
550
551
  locale,
551
- translations
552
+ translations,
553
+ nonce
552
554
  });
553
555
  let html = output.html;
554
556
  if (this.rootConfig.prettyHtml) {
@@ -566,7 +568,13 @@ var Renderer = class {
566
568
  statusCode = 500;
567
569
  }
568
570
  req.hooks.trigger("preRender");
569
- res.status(statusCode).set({ "Content-Type": "text/html" }).end(html);
571
+ res.status(statusCode);
572
+ res.set({ "Content-Type": "text/html" });
573
+ this.setSecurityHeaders(res, {
574
+ securityConfig,
575
+ nonce
576
+ });
577
+ res.end(html);
570
578
  };
571
579
  if (route.module.handle) {
572
580
  const handlerContext = {
@@ -596,7 +604,7 @@ var Renderer = class {
596
604
  await render(props);
597
605
  }
598
606
  async renderComponent(Component, props, options) {
599
- const { currentPath, route, routeParams } = options;
607
+ const { currentPath, route, routeParams, nonce } = options;
600
608
  const locale = options.locale;
601
609
  const translations = {
602
610
  ...getTranslations(locale),
@@ -608,7 +616,8 @@ var Renderer = class {
608
616
  props,
609
617
  routeParams,
610
618
  locale,
611
- translations
619
+ translations,
620
+ nonce
612
621
  };
613
622
  const htmlContext = {
614
623
  htmlAttrs: {},
@@ -618,7 +627,26 @@ var Renderer = class {
618
627
  scriptDeps: []
619
628
  };
620
629
  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 }) }) }) });
621
- const mainHtml = renderToString(vdom);
630
+ const preactHook = preactOptions.vnode;
631
+ let mainHtml;
632
+ try {
633
+ preactOptions.vnode = (vnode) => {
634
+ if (vnode && vnode.type === "script") {
635
+ vnode.props.nonce = nonce;
636
+ }
637
+ if (vnode && vnode.type === "link" && vnode.props.rel === "stylesheet") {
638
+ vnode.props.nonce = nonce;
639
+ }
640
+ if (preactHook) {
641
+ preactHook(vnode);
642
+ }
643
+ };
644
+ mainHtml = renderToString(vdom);
645
+ preactOptions.vnode = preactHook;
646
+ } catch (err) {
647
+ preactOptions.vnode = preactHook;
648
+ throw err;
649
+ }
622
650
  const jsDeps = /* @__PURE__ */ new Set();
623
651
  const cssDeps = /* @__PURE__ */ new Set();
624
652
  const routeAsset = await this.assetMap.get(route.src);
@@ -647,10 +675,10 @@ var Renderer = class {
647
675
  })
648
676
  );
649
677
  const styleTags = Array.from(cssDeps).map((cssUrl) => {
650
- return /* @__PURE__ */ jsx4("link", { rel: "stylesheet", href: cssUrl });
678
+ return /* @__PURE__ */ jsx4("link", { rel: "stylesheet", href: cssUrl, nonce });
651
679
  });
652
680
  const scriptTags = Array.from(jsDeps).map((jsUrls) => {
653
- return /* @__PURE__ */ jsx4("script", { type: "module", src: jsUrls });
681
+ return /* @__PURE__ */ jsx4("script", { type: "module", src: jsUrls, nonce });
654
682
  });
655
683
  const html = await this.renderHtml(mainHtml, {
656
684
  htmlAttrs: htmlContext.htmlAttrs,
@@ -724,13 +752,13 @@ var Renderer = class {
724
752
  return sitemap;
725
753
  }
726
754
  async renderHtml(html, options) {
727
- const htmlAttrs = (options == null ? void 0 : options.htmlAttrs) || {};
728
- const headAttrs = (options == null ? void 0 : options.headAttrs) || {};
729
- const bodyAttrs = (options == null ? void 0 : options.bodyAttrs) || {};
755
+ const htmlAttrs = options?.htmlAttrs || {};
756
+ const headAttrs = options?.headAttrs || {};
757
+ const bodyAttrs = options?.bodyAttrs || {};
730
758
  const page = /* @__PURE__ */ jsxs4("html", { ...htmlAttrs, children: [
731
759
  /* @__PURE__ */ jsxs4("head", { ...headAttrs, children: [
732
760
  /* @__PURE__ */ jsx4("meta", { charSet: "utf-8" }),
733
- options == null ? void 0 : options.headComponents
761
+ options?.headComponents
734
762
  ] }),
735
763
  /* @__PURE__ */ jsx4("body", { ...bodyAttrs, dangerouslySetInnerHTML: { __html: html } })
736
764
  ] });
@@ -739,7 +767,7 @@ ${renderToString(page)}
739
767
  `;
740
768
  }
741
769
  async render404(options) {
742
- const currentPath = (options == null ? void 0 : options.currentPath) || "/404";
770
+ const currentPath = options?.currentPath || "/404";
743
771
  const [route, routeParams] = this.router.get("/404");
744
772
  if (route && route.src === "routes/404.tsx" && route.module.default) {
745
773
  const Component = route.module.default;
@@ -775,7 +803,7 @@ ${renderToString(page)}
775
803
  return { html };
776
804
  }
777
805
  async renderError(err, options) {
778
- const currentPath = (options == null ? void 0 : options.currentPath) || "/500";
806
+ const currentPath = options?.currentPath || "/500";
779
807
  const [route, routeParams] = this.router.get("/500");
780
808
  if (route && route.src === "routes/500.tsx" && route.module.default) {
781
809
  const Component = route.module.default;
@@ -874,7 +902,86 @@ ${renderToString(page)}
874
902
  );
875
903
  return { jsDeps, cssDeps };
876
904
  }
905
+ /**
906
+ * Returns the `security` config value with default values inserted wherever
907
+ * a user config value is blank or set to `true`.
908
+ */
909
+ getSecurityConfig() {
910
+ const userConfig = this.rootConfig.server?.security || {};
911
+ const securityConfig = {};
912
+ if (isTrueOrUndefined(userConfig.contentSecurityPolicy)) {
913
+ securityConfig.contentSecurityPolicy = {
914
+ directives: {
915
+ "base-uri": ["'none'"],
916
+ "object-src": ["'none'"],
917
+ "script-src": ["'self'"]
918
+ },
919
+ reportOnly: true
920
+ };
921
+ } else {
922
+ securityConfig.contentSecurityPolicy = userConfig.contentSecurityPolicy;
923
+ }
924
+ if (isTrueOrUndefined(userConfig.xFrameOptions)) {
925
+ securityConfig.xFrameOptions = "SAMEORIGIN";
926
+ } else {
927
+ securityConfig.xFrameOptions = userConfig.xFrameOptions;
928
+ }
929
+ securityConfig.strictTransportSecurity = userConfig.strictTransportSecurity ?? true;
930
+ securityConfig.xContentTypeOptions = userConfig.xContentTypeOptions ?? true;
931
+ securityConfig.xXssProtection = userConfig.xXssProtection ?? true;
932
+ return securityConfig;
933
+ }
934
+ /**
935
+ * Generates a random string that can be used as the "nonce" value for CSP.
936
+ */
937
+ generateNonce() {
938
+ return crypto.randomBytes(16).toString("base64");
939
+ }
940
+ /**
941
+ * Sets security-related HTTP headers.
942
+ */
943
+ setSecurityHeaders(res, options) {
944
+ const securityConfig = options.securityConfig;
945
+ const contentSecurityPolicy = securityConfig.contentSecurityPolicy;
946
+ if (typeof contentSecurityPolicy === "object") {
947
+ const directives = contentSecurityPolicy.directives || {};
948
+ if (options.nonce) {
949
+ if (!directives["script-src"]) {
950
+ directives["script-src"] = ["'self'"];
951
+ }
952
+ directives["script-src"].push(`'nonce-${options.nonce}'`);
953
+ }
954
+ const headerSegments = [];
955
+ Object.entries(directives).forEach(([key, values]) => {
956
+ headerSegments.push([key, ...values].join(" "));
957
+ });
958
+ const csp = headerSegments.join("; ");
959
+ if (contentSecurityPolicy.reportOnly === false) {
960
+ res.setHeader("content-security-policy", csp);
961
+ } else {
962
+ res.setHeader("content-security-policy-report-only", csp);
963
+ }
964
+ }
965
+ if (typeof securityConfig.xFrameOptions === "string") {
966
+ res.setHeader("x-frame-options", securityConfig.xFrameOptions);
967
+ }
968
+ if (securityConfig.strictTransportSecurity) {
969
+ res.setHeader(
970
+ "strict-transport-security",
971
+ "max-age=63072000; includeSubdomains; preload"
972
+ );
973
+ }
974
+ if (securityConfig.xContentTypeOptions) {
975
+ res.setHeader("x-content-type-options", "nosniff");
976
+ }
977
+ if (securityConfig.xXssProtection) {
978
+ res.setHeader("x-xss-protection", "1; mode=block");
979
+ }
980
+ }
877
981
  };
982
+ function isTrueOrUndefined(value) {
983
+ return value === true || value === void 0;
984
+ }
878
985
  export {
879
986
  Renderer
880
987
  };