@blinkk/root 3.5.3 → 4.0.0-alpha.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.
@@ -79,8 +79,8 @@ export interface RootUserConfig {
79
79
  };
80
80
  /**
81
81
  * Whether to auto-inject `<link rel="modulepreload">` tags for the JS chunks
82
- * imported by the page's `<script type="module">` tags. Disabled by default;
83
- * pass `modulePreload: true` to opt in.
82
+ * imported by the page's `<script type="module">` tags. Enabled by default;
83
+ * pass `modulePreload: false` to opt out.
84
84
  *
85
85
  * Root injects a `<script type="module">` tag for every custom element and
86
86
  * bundle used on the page, but the shared chunks those scripts import are
package/dist/core.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/core/config.ts", "../src/core/components/Body.tsx", "../src/core/components/StyleDeps.ts", "../src/core/components/Head.ts", "../src/core/components/Script.ts", "../src/core/hooks/useStringParams.tsx", "../src/core/hooks/useTranslationsMiddleware.tsx", "../src/core/hooks/useTranslations.ts", "../src/core/pod.ts"],
4
- "sourcesContent": ["import {UserConfig as ViteUserConfig} from 'vite';\nimport {JsxRenderOptions} from '../jsx/jsx-render.js';\nimport {HtmlMinifyOptions} from '../render/html-minify.js';\nimport {HtmlPrettyOptions} from '../render/html-pretty.js';\nimport {Plugin} from './plugin.js';\nimport {PodConfig} from './pod.js';\nimport {RequestMiddleware} from './types.js';\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 * Custom elements to exclude from the SSG output. Matching elements are\n * not bundled, their assets are not emitted to `dist/html`, and their\n * `<script>` and `<link rel=\"stylesheet\">` tags are not auto-injected into\n * pre-rendered pages.\n *\n * This only applies to the SSG phase of `root build`. The dev server and\n * `root build --ssr-only` continue to serve and auto-inject the excluded\n * elements, which makes this useful for elements that only make sense when\n * a page is rendered on demand (help overlays, internal tools, etc.) and\n * shouldn't ship to a static site.\n *\n * Note that this excludes the element's assets, not its HTML. A\n * `<debug-panel>` tag rendered by a route or template is still present in\n * the SSG output, it just never upgrades into a custom element.\n *\n * Unlike `exclude`, which matches file paths and applies to every command,\n * this is matched against the element's tag name.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * elements: {\n * excludeFromSsg: ['help-overlay', /^debug-/],\n * },\n * });\n * ```\n */\n excludeFromSsg?: ElementTagNameMatcher;\n };\n\n /**\n * Config for manually injecting stylesheet entries as\n * `<link rel=\"stylesheet\">` tags.\n */\n styles?: {\n /**\n * Project-root-relative stylesheet files to include on every rendered\n * page, e.g. `styles/index.css`.\n *\n * Entries are normalized as URL paths, so both `styles/index.css` and\n * `/styles/index.css` resolve to `/styles/index.css`.\n *\n * Manual entries are injected first, then auto-collected CSS\n * dependencies are merged after. Duplicate URLs are de-duped so each\n * stylesheet is only injected once.\n */\n entries?: string[];\n };\n\n /**\n * Whether to auto-inject `<link rel=\"modulepreload\">` tags for the JS chunks\n * imported by the page's `<script type=\"module\">` tags. Disabled by default;\n * pass `modulePreload: true` to opt in.\n *\n * Root injects a `<script type=\"module\">` tag for every custom element and\n * bundle used on the page, but the shared chunks those scripts import are\n * only discovered once the browser has downloaded and parsed the script.\n * Preloading them removes that request waterfall.\n *\n * Chunks that are already injected as `<script>` tags are skipped, and no\n * tags are injected by the dev server (where modules are served unbundled).\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/modulepreload}\n */\n modulePreload?: boolean;\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 * Build options for the `root build` command.\n */\n build?: RootBuildConfig;\n\n /**\n * Vite config.\n * @see {@link https://vitejs.dev/config/} for more information.\n */\n vite?: ViteUserConfig;\n\n /**\n * Config for the built-in JSX-to-HTML renderer.\n *\n * - `mode: 'pretty'` (default) \u2014 block-level elements render on their own\n * line with no indentation.\n * - `mode: 'minimal'` \u2014 compact output with no extra whitespace.\n *\n * Use `blockElements` to specify additional custom element tag names that\n * should be treated as block-level in pretty mode.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * jsxRenderer: {\n * mode: 'pretty',\n * blockElements: ['my-card', 'my-section'],\n * },\n * });\n * ```\n */\n jsxRenderer?: JsxRenderOptions;\n\n /**\n * Whether to minify HTML output via html-minifier-terser. Disabled by\n * default; pass `minifyHtml: true` to opt in.\n *\n * Ignored when the built-in JSX renderer (`jsxRenderer`) is enabled, since\n * that renderer controls its own output formatting via its `mode` option.\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 via js-beautify. Disabled by default;\n * pass `prettyHtml: true` to opt in. When both `prettyHtml` and `minifyHtml`\n * are set, `prettyHtml` takes precedence.\n *\n * Ignored when the built-in JSX renderer (`jsxRenderer`) is enabled, since\n * that renderer controls its own output formatting via its `mode` option.\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 /**\n * Per-pod user-level overrides. The key is the pod name as declared in\n * Pod.name.\n */\n pods?: Record<string, PodConfig>;\n\n /**\n * Experimental config options. Note: these are subject to change at any time.\n */\n experiments?: {\n /** Whether to render `<script>` tags with `async`. */\n enableScriptAsync?: boolean;\n };\n}\n\nexport type RootConfig = RootUserConfig & {\n rootDir: string;\n};\n\n/**\n * Matches custom elements by tag name (e.g. `debug-panel`).\n *\n * Accepts a list of strings (matched exactly) and RegEx patterns (tested\n * against the tag name), or a predicate function that receives the tag name\n * and returns true to match. The function form is useful for logic that a\n * pattern can't express, e.g. allowlisting a known set of elements.\n *\n * @example\n * ```ts\n * const ssrOnly = new Set(['help-overlay', 'debug-panel']);\n * const matcher: ElementTagNameMatcher = (tagName) => ssrOnly.has(tagName);\n * ```\n */\nexport type ElementTagNameMatcher =\n Array<string | RegExp> | ((tagName: string) => boolean);\n\nexport interface LocaleGroup {\n label?: string;\n locales: RootLocale[];\n}\n\n/**\n * A site-defined locale identifier, as configured in `i18n.locales`.\n * Root is agnostic to its format.\n */\nexport type RootLocale = string;\n\n/**\n * The language identifier used by translation systems (CSV/Sheets columns,\n * translation services, CMS translations pages), mapped from a root locale\n * via `i18n.translationLanguages`. Defaults to the root locale id.\n */\nexport type TranslationLanguage = string;\n\nexport interface RootI18nConfig {\n /**\n * Locales enabled for the site.\n */\n locales?: RootLocale[];\n\n /**\n * The default locale to use. Defaults is `en`.\n */\n defaultLocale?: RootLocale;\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 /**\n * Maps a root locale to the \"translation language\" used by translation\n * systems. Translation systems often use different language identifiers\n * than root locales, and multiple root locales may share the same\n * translations. For example:\n *\n * ```\n * i18n: {\n * locales: ['en', 'es_mx', 'es_co', 'en_gb', 'en_ca', 'fr_ca'],\n * translationLanguages: {\n * es_mx: 'es-419',\n * es_co: 'es-419',\n * en_gb: 'en-GB',\n * en_ca: 'en-GB',\n * fr_ca: 'fr-CA',\n * },\n * }\n * ```\n *\n * With the config above, translations for the `es_mx` and `es_co` locales\n * are imported and exported using a single `es-419` language. The\n * conversion applies anywhere translations are used, e.g. CSV and Google\n * Sheets import/export, translation services, and the CMS translations\n * pages. Locales not listed here use the locale id as the translation\n * language.\n */\n translationLanguages?: Record<RootLocale, TranslationLanguage>;\n\n /**\n * Locale fallback chains for translations. When a translation is missing\n * for a locale, each fallback locale is checked (in order) before falling\n * back to `defaultLocale` and finally the source string. Fallbacks are\n * resolved recursively, e.g. with the config below, `en-CA` resolves to\n * `['en-CA', 'en-GB', 'en']`. For example:\n *\n * ```\n * i18n: {\n * locales: ['en', 'en-GB', 'en-CA'],\n * fallbacks: {\n * 'en-CA': ['en-GB'],\n * },\n * }\n * ```\n */\n fallbacks?: Record<RootLocale, RootLocale[]>;\n}\n\nexport interface RootBuildConfig {\n /**\n * Excludes the `/intl/{defaultLocale}/...` path from the SSG build.\n */\n excludeDefaultLocaleFromIntlPaths?: boolean;\n}\n\nexport interface RootRedirectConfig {\n /**\n * The source path to redirect. Accepts placeholders in the format\n * `[key]` or `[...key]`. Use `[key]` for single segments and\n * `[...key]` for multi-segment wildcards.\n * @example \"/old-path/[id]\" or \"/old-path/[...wildcard]\"\n */\n source: string;\n\n /**\n * The destination to redirect to. Placeholders from the source can\n * optionally be inserted into the destination using the same\n * placeholder format.\n * @example \"/new-path/[id]\" or \"/new-path/[...wildcard]\"\n */\n destination: string;\n\n /**\n * The redirect type (`301` = permanent, `302` = temporary). If unspecified,\n * defaults to `302` (temporary).\n */\n type?: 301 | 302;\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 * Generate a secure secret with:\n * ```\n * node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"\n * ```\n */\n sessionCookieSecret?: string | string[];\n\n /**\n * List of redirects. Supports optional wildcards.\n *\n * @example\n * ```ts\n * redirects: [\n * {\n * source: '/old-path/[id]',\n * destination: '/new-path/[id]',\n * type: 301,\n * },\n * {\n * source: '/old-path/[...wildcard]',\n * destination: '/new-path/[...wildcard]',\n * },\n * ]\n * ```\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 /**\n * Home page URL path, which is printed when the dev server starts.\n */\n homePagePath?: string;\n}\n\nexport function defineConfig(config: RootUserConfig): RootUserConfig {\n return config;\n}\n", "import {ComponentChildren, FunctionalComponent} from 'preact';\nimport {useContext} from 'preact/hooks';\nimport {HTML_CONTEXT} from './Html.js';\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 {FunctionalComponent} from 'preact';\nimport {useContext} from 'preact/hooks';\nimport {HTML_CONTEXT} from './Html.js';\n\nexport interface StyleDepsProps {\n /**\n * Source path of a component whose CSS deps should be injected into the page,\n * relative to the project root (e.g. `templates/Foo/Foo.tsx`).\n */\n src: string;\n}\n\n/**\n * The `<StyleDeps>` component registers a component's CSS deps for injection\n * into the page `<head>`, even when the component is imported dynamically.\n *\n * Root normally collects CSS by walking a route's *static* import graph, so the\n * `.module.scss` of a dynamically-imported (`import()`) component is never\n * linked. Rendering `<StyleDeps src=\"...\">` alongside such a component tells\n * Root to resolve that source file's CSS deps (via the asset map) and inject\n * them, so a page loads only the CSS for the components it actually renders.\n *\n * Usage:\n *\n * ```tsx\n * <StyleDeps src=\"templates/Foo/Foo.tsx\" />\n * ```\n */\nexport const StyleDeps: FunctionalComponent<StyleDepsProps> = (props) => {\n const context = useContext(HTML_CONTEXT);\n if (!context) {\n throw new Error(\n 'HTML_CONTEXT not found, double-check usage of the <StyleDeps> component'\n );\n }\n if (props.src) {\n context.styleDeps.push(props.src);\n }\n return null;\n};\n", "import {ComponentChildren, FunctionalComponent} from 'preact';\nimport {useContext} from 'preact/hooks';\nimport {HTML_CONTEXT} from './Html.js';\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';\nimport {HTML_CONTEXT} from './Html.js';\n\nexport type ScriptProps = preact.JSX.HTMLAttributes<HTMLScriptElement> & {\n // Cast the \"src\" attr to string. See:\n // https://github.com/blinkk/rootjs/issues/519\n src?: string;\n};\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 {ComponentChildren, FunctionalComponent, createContext} from 'preact';\nimport {useContext} from 'preact/hooks';\n\nexport type StringParamsContext = Record<string, string>;\n\nexport const STRING_PARAMS_CONTEXT = createContext<StringParamsContext | null>(\n null\n);\n\nexport interface StringParamsProviderProps {\n value?: StringParamsContext;\n children?: ComponentChildren;\n}\n\nexport const StringParamsProvider: FunctionalComponent<\n StringParamsProviderProps\n> = (props) => {\n // Allow for nested param values from parent content providers.\n const parent = useContext(STRING_PARAMS_CONTEXT) || {};\n const merged = {...parent, ...props.value};\n return (\n <STRING_PARAMS_CONTEXT.Provider value={merged}>\n {props.children}\n </STRING_PARAMS_CONTEXT.Provider>\n );\n};\n\n/**\n * A hook that returns a map of string params, configured via the\n * `StringParamsProvider` context provider. These params are automatically\n * applied to the `useTranslations()` hook.\n *\n *\n * Usage:\n *\n * ```\n * export default function Page() {\n * return (\n * <StringParamsProvider value={{name: 'Alice'}}>\n * <SayHello />\n * </StringParamsProvider>\n * );\n * }\n *\n * function SayHello() {\n * const t = useTranslations();\n * return <h1>{t('Hello, {name}!')}</h1>;\n * }\n * ```\n *\n * This should render `<h1>Hello, Alice!</h1>`.\n */\nexport function useStringParams() {\n return useContext(STRING_PARAMS_CONTEXT) || {};\n}\n", "import {ComponentChildren, FunctionalComponent, createContext} from 'preact';\nimport {useContext} from 'preact/hooks';\n\ntype TransformFn = (str: string) => string;\n\nexport interface TranslationMiddleware {\n /** Transform the string before translation lookup. */\n beforeTranslate?: TransformFn;\n /** Transform the string after translation lookup. */\n afterTranslate?: TransformFn;\n /** Transform the string before `{param}` values are replaced. */\n beforeReplaceParams?: TransformFn;\n /** Transform the string after `{param}` values are replaced. */\n afterReplaceParams?: TransformFn;\n}\n\nexport interface TranslationMiddlewareContext {\n beforeTranslateFns: TransformFn[];\n afterTranslateFns: TransformFn[];\n beforeReplaceParamsFns: TransformFn[];\n afterReplaceParamsFns: TransformFn[];\n}\n\nconst TRANSLATION_MIDDLEWARE_CONTEXT =\n createContext<TranslationMiddlewareContext | null>(null);\n\nexport interface TranslationMiddlewareProviderProps {\n value?: TranslationMiddleware;\n children?: ComponentChildren;\n}\n\nexport const TranslationMiddlewareProvider: FunctionalComponent<\n TranslationMiddlewareProviderProps\n> = (props) => {\n const parent = useContext(TRANSLATION_MIDDLEWARE_CONTEXT) || {\n beforeTranslateFns: [],\n afterTranslateFns: [],\n beforeReplaceParamsFns: [],\n afterReplaceParamsFns: [],\n };\n const merged: TranslationMiddlewareContext = {\n beforeTranslateFns: [...parent.beforeTranslateFns],\n afterTranslateFns: [...parent.afterTranslateFns],\n beforeReplaceParamsFns: [...parent.beforeReplaceParamsFns],\n afterReplaceParamsFns: [...parent.afterReplaceParamsFns],\n };\n if (props.value?.beforeTranslate) {\n merged.beforeTranslateFns.push(props.value.beforeTranslate);\n }\n if (props.value?.afterTranslate) {\n merged.afterTranslateFns.push(props.value.afterTranslate);\n }\n if (props.value?.beforeReplaceParams) {\n merged.beforeReplaceParamsFns.push(props.value.beforeReplaceParams);\n }\n if (props.value?.afterReplaceParams) {\n merged.afterReplaceParamsFns.push(props.value.afterReplaceParams);\n }\n return (\n <TRANSLATION_MIDDLEWARE_CONTEXT.Provider value={merged}>\n {props.children}\n </TRANSLATION_MIDDLEWARE_CONTEXT.Provider>\n );\n};\n\nexport function useTranslationMiddleware(): TranslationMiddlewareContext {\n return (\n useContext(TRANSLATION_MIDDLEWARE_CONTEXT) || {\n beforeTranslateFns: [],\n afterTranslateFns: [],\n beforeReplaceParamsFns: [],\n afterReplaceParamsFns: [],\n }\n );\n}\n", "import {I18nContext, useI18nContext} from './useI18nContext.js';\nimport {useStringParams} from './useStringParams.js';\nimport {useTranslationMiddleware} from './useTranslationsMiddleware.js';\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 * ```\n */\nexport function useTranslations() {\n // Ignore I18nContext not found error when used with client-side rehydration.\n let i18nContext: I18nContext | null = null;\n try {\n i18nContext = useI18nContext();\n } catch {\n console.warn('I18nContext not found');\n }\n const translations = i18nContext?.translations || {};\n const stringParams = useStringParams();\n const middleware = useTranslationMiddleware();\n\n function t(str: string, params?: Record<string, string | number>): string;\n function t(\n str: string | null | undefined,\n params?: Record<string, string | number>\n ): string | undefined {\n // Suppress verbatim `'undefined'` and `'null'` output, which can occur when\n // using `useTranslations` with undefined values.\n if (typeof str === 'undefined' || str === null) {\n // Return `undefined` to suppress empty props, e.g.\n // <a title={t(undefined)}>Learn more</a>\n return undefined;\n }\n let input = normalizeString(str);\n middleware.beforeTranslateFns.forEach((fn) => {\n input = fn(input);\n });\n let translation = translations[input] ?? input ?? '';\n middleware.afterTranslateFns.forEach((fn) => {\n translation = fn(translation);\n });\n\n // Replace string params, e.g. \"Hello, {name}\".\n middleware.beforeReplaceParamsFns.forEach((fn) => {\n translation = fn(translation);\n });\n if (testHasStringParams(translation)) {\n translation = replaceStringParams(translation, {\n ...stringParams,\n ...params,\n });\n }\n middleware.afterReplaceParamsFns.forEach((fn) => {\n translation = fn(translation);\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) => removeTrailingWhitespace(line));\n return lines.join('\\n');\n}\n\nfunction removeTrailingWhitespace(str: string) {\n return String(str)\n .trimEnd()\n .replace(/&nbsp;$/, '');\n}\n\nfunction testHasStringParams(str: string) {\n return str.includes('{') && str.includes('}');\n}\n\n/**\n * Replaces string placeholder params, e.g.\n *\n * ```\n * replaceStringParams('Hello, {name}!', {name: 'Joe'})\n * // => 'Hello, Joe!'\n * ```\n */\nfunction replaceStringParams(\n str: string,\n params: Record<string, string | number>\n): string {\n return str.replace(/{([^}]+)}/g, (match, key) => {\n if (key in params) {\n return String(params[key]);\n }\n return match;\n });\n}\n", "import {PluginOption as VitePlugin} from 'vite';\nimport {RootConfig} from './config.js';\n\nexport interface Pod {\n /** Unique pod name, e.g. '@blinkk/root-docs-pod'. */\n name: string;\n\n /**\n * URL prefix for pod routes. Routes within the pod are served under this\n * mount path. Defaults to '/'.\n */\n mount?: string;\n\n /**\n * Priority for route conflict resolution. Higher values win when multiple\n * pods register the same URL path. User-site routes always take precedence\n * regardless of priority. Defaults to 0.\n */\n priority?: number;\n\n /** Absolute path to the pod's routes/ directory. */\n routesDir?: string;\n\n /** Absolute path(s) to the pod's elements/ directory(s). */\n elementsDirs?: string[];\n\n /** Absolute path to the pod's bundles/ directory. */\n bundlesDir?: string;\n\n /** Absolute path to the pod's collections/ directory (root-cms only). */\n collectionsDir?: string;\n\n /** Absolute path to the pod's translations/ directory. */\n translationsDir?: string;\n\n /** Extra Vite plugins contributed by the pod. */\n vitePlugins?: VitePlugin[];\n}\n\nexport type PodFactory = (ctx: {rootConfig: RootConfig}) => Pod | Promise<Pod>;\n\nexport interface PodConfig {\n /** Whether the pod is enabled. Defaults to true. */\n enabled?: boolean;\n\n /** Override the pod's mount path. */\n mount?: string;\n\n /** Override the pod's priority. */\n priority?: number;\n\n /** Filter pod routes. */\n routes?: {\n exclude?: (string | RegExp)[];\n };\n\n /** Configure pod collections. */\n collections?: {\n exclude?: string[];\n /** Rename collection ids, e.g. {Posts: 'DocsPosts'}. */\n rename?: Record<string, string>;\n };\n}\n\n/**\n * Helper to define a pod with type-checking.\n */\nexport function definePod(pod: Pod): Pod {\n return pod;\n}\n"],
4
+ "sourcesContent": ["import {UserConfig as ViteUserConfig} from 'vite';\nimport {JsxRenderOptions} from '../jsx/jsx-render.js';\nimport {HtmlMinifyOptions} from '../render/html-minify.js';\nimport {HtmlPrettyOptions} from '../render/html-pretty.js';\nimport {Plugin} from './plugin.js';\nimport {PodConfig} from './pod.js';\nimport {RequestMiddleware} from './types.js';\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 * Custom elements to exclude from the SSG output. Matching elements are\n * not bundled, their assets are not emitted to `dist/html`, and their\n * `<script>` and `<link rel=\"stylesheet\">` tags are not auto-injected into\n * pre-rendered pages.\n *\n * This only applies to the SSG phase of `root build`. The dev server and\n * `root build --ssr-only` continue to serve and auto-inject the excluded\n * elements, which makes this useful for elements that only make sense when\n * a page is rendered on demand (help overlays, internal tools, etc.) and\n * shouldn't ship to a static site.\n *\n * Note that this excludes the element's assets, not its HTML. A\n * `<debug-panel>` tag rendered by a route or template is still present in\n * the SSG output, it just never upgrades into a custom element.\n *\n * Unlike `exclude`, which matches file paths and applies to every command,\n * this is matched against the element's tag name.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * elements: {\n * excludeFromSsg: ['help-overlay', /^debug-/],\n * },\n * });\n * ```\n */\n excludeFromSsg?: ElementTagNameMatcher;\n };\n\n /**\n * Config for manually injecting stylesheet entries as\n * `<link rel=\"stylesheet\">` tags.\n */\n styles?: {\n /**\n * Project-root-relative stylesheet files to include on every rendered\n * page, e.g. `styles/index.css`.\n *\n * Entries are normalized as URL paths, so both `styles/index.css` and\n * `/styles/index.css` resolve to `/styles/index.css`.\n *\n * Manual entries are injected first, then auto-collected CSS\n * dependencies are merged after. Duplicate URLs are de-duped so each\n * stylesheet is only injected once.\n */\n entries?: string[];\n };\n\n /**\n * Whether to auto-inject `<link rel=\"modulepreload\">` tags for the JS chunks\n * imported by the page's `<script type=\"module\">` tags. Enabled by default;\n * pass `modulePreload: false` to opt out.\n *\n * Root injects a `<script type=\"module\">` tag for every custom element and\n * bundle used on the page, but the shared chunks those scripts import are\n * only discovered once the browser has downloaded and parsed the script.\n * Preloading them removes that request waterfall.\n *\n * Chunks that are already injected as `<script>` tags are skipped, and no\n * tags are injected by the dev server (where modules are served unbundled).\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/modulepreload}\n */\n modulePreload?: boolean;\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 * Build options for the `root build` command.\n */\n build?: RootBuildConfig;\n\n /**\n * Vite config.\n * @see {@link https://vitejs.dev/config/} for more information.\n */\n vite?: ViteUserConfig;\n\n /**\n * Config for the built-in JSX-to-HTML renderer.\n *\n * - `mode: 'pretty'` (default) \u2014 block-level elements render on their own\n * line with no indentation.\n * - `mode: 'minimal'` \u2014 compact output with no extra whitespace.\n *\n * Use `blockElements` to specify additional custom element tag names that\n * should be treated as block-level in pretty mode.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * jsxRenderer: {\n * mode: 'pretty',\n * blockElements: ['my-card', 'my-section'],\n * },\n * });\n * ```\n */\n jsxRenderer?: JsxRenderOptions;\n\n /**\n * Whether to minify HTML output via html-minifier-terser. Disabled by\n * default; pass `minifyHtml: true` to opt in.\n *\n * Ignored when the built-in JSX renderer (`jsxRenderer`) is enabled, since\n * that renderer controls its own output formatting via its `mode` option.\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 via js-beautify. Disabled by default;\n * pass `prettyHtml: true` to opt in. When both `prettyHtml` and `minifyHtml`\n * are set, `prettyHtml` takes precedence.\n *\n * Ignored when the built-in JSX renderer (`jsxRenderer`) is enabled, since\n * that renderer controls its own output formatting via its `mode` option.\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 /**\n * Per-pod user-level overrides. The key is the pod name as declared in\n * Pod.name.\n */\n pods?: Record<string, PodConfig>;\n\n /**\n * Experimental config options. Note: these are subject to change at any time.\n */\n experiments?: {\n /** Whether to render `<script>` tags with `async`. */\n enableScriptAsync?: boolean;\n };\n}\n\nexport type RootConfig = RootUserConfig & {\n rootDir: string;\n};\n\n/**\n * Matches custom elements by tag name (e.g. `debug-panel`).\n *\n * Accepts a list of strings (matched exactly) and RegEx patterns (tested\n * against the tag name), or a predicate function that receives the tag name\n * and returns true to match. The function form is useful for logic that a\n * pattern can't express, e.g. allowlisting a known set of elements.\n *\n * @example\n * ```ts\n * const ssrOnly = new Set(['help-overlay', 'debug-panel']);\n * const matcher: ElementTagNameMatcher = (tagName) => ssrOnly.has(tagName);\n * ```\n */\nexport type ElementTagNameMatcher =\n Array<string | RegExp> | ((tagName: string) => boolean);\n\nexport interface LocaleGroup {\n label?: string;\n locales: RootLocale[];\n}\n\n/**\n * A site-defined locale identifier, as configured in `i18n.locales`.\n * Root is agnostic to its format.\n */\nexport type RootLocale = string;\n\n/**\n * The language identifier used by translation systems (CSV/Sheets columns,\n * translation services, CMS translations pages), mapped from a root locale\n * via `i18n.translationLanguages`. Defaults to the root locale id.\n */\nexport type TranslationLanguage = string;\n\nexport interface RootI18nConfig {\n /**\n * Locales enabled for the site.\n */\n locales?: RootLocale[];\n\n /**\n * The default locale to use. Defaults is `en`.\n */\n defaultLocale?: RootLocale;\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 /**\n * Maps a root locale to the \"translation language\" used by translation\n * systems. Translation systems often use different language identifiers\n * than root locales, and multiple root locales may share the same\n * translations. For example:\n *\n * ```\n * i18n: {\n * locales: ['en', 'es_mx', 'es_co', 'en_gb', 'en_ca', 'fr_ca'],\n * translationLanguages: {\n * es_mx: 'es-419',\n * es_co: 'es-419',\n * en_gb: 'en-GB',\n * en_ca: 'en-GB',\n * fr_ca: 'fr-CA',\n * },\n * }\n * ```\n *\n * With the config above, translations for the `es_mx` and `es_co` locales\n * are imported and exported using a single `es-419` language. The\n * conversion applies anywhere translations are used, e.g. CSV and Google\n * Sheets import/export, translation services, and the CMS translations\n * pages. Locales not listed here use the locale id as the translation\n * language.\n */\n translationLanguages?: Record<RootLocale, TranslationLanguage>;\n\n /**\n * Locale fallback chains for translations. When a translation is missing\n * for a locale, each fallback locale is checked (in order) before falling\n * back to `defaultLocale` and finally the source string. Fallbacks are\n * resolved recursively, e.g. with the config below, `en-CA` resolves to\n * `['en-CA', 'en-GB', 'en']`. For example:\n *\n * ```\n * i18n: {\n * locales: ['en', 'en-GB', 'en-CA'],\n * fallbacks: {\n * 'en-CA': ['en-GB'],\n * },\n * }\n * ```\n */\n fallbacks?: Record<RootLocale, RootLocale[]>;\n}\n\nexport interface RootBuildConfig {\n /**\n * Excludes the `/intl/{defaultLocale}/...` path from the SSG build.\n */\n excludeDefaultLocaleFromIntlPaths?: boolean;\n}\n\nexport interface RootRedirectConfig {\n /**\n * The source path to redirect. Accepts placeholders in the format\n * `[key]` or `[...key]`. Use `[key]` for single segments and\n * `[...key]` for multi-segment wildcards.\n * @example \"/old-path/[id]\" or \"/old-path/[...wildcard]\"\n */\n source: string;\n\n /**\n * The destination to redirect to. Placeholders from the source can\n * optionally be inserted into the destination using the same\n * placeholder format.\n * @example \"/new-path/[id]\" or \"/new-path/[...wildcard]\"\n */\n destination: string;\n\n /**\n * The redirect type (`301` = permanent, `302` = temporary). If unspecified,\n * defaults to `302` (temporary).\n */\n type?: 301 | 302;\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 * Generate a secure secret with:\n * ```\n * node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"\n * ```\n */\n sessionCookieSecret?: string | string[];\n\n /**\n * List of redirects. Supports optional wildcards.\n *\n * @example\n * ```ts\n * redirects: [\n * {\n * source: '/old-path/[id]',\n * destination: '/new-path/[id]',\n * type: 301,\n * },\n * {\n * source: '/old-path/[...wildcard]',\n * destination: '/new-path/[...wildcard]',\n * },\n * ]\n * ```\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 /**\n * Home page URL path, which is printed when the dev server starts.\n */\n homePagePath?: string;\n}\n\nexport function defineConfig(config: RootUserConfig): RootUserConfig {\n return config;\n}\n", "import {ComponentChildren, FunctionalComponent} from 'preact';\nimport {useContext} from 'preact/hooks';\nimport {HTML_CONTEXT} from './Html.js';\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 {FunctionalComponent} from 'preact';\nimport {useContext} from 'preact/hooks';\nimport {HTML_CONTEXT} from './Html.js';\n\nexport interface StyleDepsProps {\n /**\n * Source path of a component whose CSS deps should be injected into the page,\n * relative to the project root (e.g. `templates/Foo/Foo.tsx`).\n */\n src: string;\n}\n\n/**\n * The `<StyleDeps>` component registers a component's CSS deps for injection\n * into the page `<head>`, even when the component is imported dynamically.\n *\n * Root normally collects CSS by walking a route's *static* import graph, so the\n * `.module.scss` of a dynamically-imported (`import()`) component is never\n * linked. Rendering `<StyleDeps src=\"...\">` alongside such a component tells\n * Root to resolve that source file's CSS deps (via the asset map) and inject\n * them, so a page loads only the CSS for the components it actually renders.\n *\n * Usage:\n *\n * ```tsx\n * <StyleDeps src=\"templates/Foo/Foo.tsx\" />\n * ```\n */\nexport const StyleDeps: FunctionalComponent<StyleDepsProps> = (props) => {\n const context = useContext(HTML_CONTEXT);\n if (!context) {\n throw new Error(\n 'HTML_CONTEXT not found, double-check usage of the <StyleDeps> component'\n );\n }\n if (props.src) {\n context.styleDeps.push(props.src);\n }\n return null;\n};\n", "import {ComponentChildren, FunctionalComponent} from 'preact';\nimport {useContext} from 'preact/hooks';\nimport {HTML_CONTEXT} from './Html.js';\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';\nimport {HTML_CONTEXT} from './Html.js';\n\nexport type ScriptProps = preact.JSX.HTMLAttributes<HTMLScriptElement> & {\n // Cast the \"src\" attr to string. See:\n // https://github.com/blinkk/rootjs/issues/519\n src?: string;\n};\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 {ComponentChildren, FunctionalComponent, createContext} from 'preact';\nimport {useContext} from 'preact/hooks';\n\nexport type StringParamsContext = Record<string, string>;\n\nexport const STRING_PARAMS_CONTEXT = createContext<StringParamsContext | null>(\n null\n);\n\nexport interface StringParamsProviderProps {\n value?: StringParamsContext;\n children?: ComponentChildren;\n}\n\nexport const StringParamsProvider: FunctionalComponent<\n StringParamsProviderProps\n> = (props) => {\n // Allow for nested param values from parent content providers.\n const parent = useContext(STRING_PARAMS_CONTEXT) || {};\n const merged = {...parent, ...props.value};\n return (\n <STRING_PARAMS_CONTEXT.Provider value={merged}>\n {props.children}\n </STRING_PARAMS_CONTEXT.Provider>\n );\n};\n\n/**\n * A hook that returns a map of string params, configured via the\n * `StringParamsProvider` context provider. These params are automatically\n * applied to the `useTranslations()` hook.\n *\n *\n * Usage:\n *\n * ```\n * export default function Page() {\n * return (\n * <StringParamsProvider value={{name: 'Alice'}}>\n * <SayHello />\n * </StringParamsProvider>\n * );\n * }\n *\n * function SayHello() {\n * const t = useTranslations();\n * return <h1>{t('Hello, {name}!')}</h1>;\n * }\n * ```\n *\n * This should render `<h1>Hello, Alice!</h1>`.\n */\nexport function useStringParams() {\n return useContext(STRING_PARAMS_CONTEXT) || {};\n}\n", "import {ComponentChildren, FunctionalComponent, createContext} from 'preact';\nimport {useContext} from 'preact/hooks';\n\ntype TransformFn = (str: string) => string;\n\nexport interface TranslationMiddleware {\n /** Transform the string before translation lookup. */\n beforeTranslate?: TransformFn;\n /** Transform the string after translation lookup. */\n afterTranslate?: TransformFn;\n /** Transform the string before `{param}` values are replaced. */\n beforeReplaceParams?: TransformFn;\n /** Transform the string after `{param}` values are replaced. */\n afterReplaceParams?: TransformFn;\n}\n\nexport interface TranslationMiddlewareContext {\n beforeTranslateFns: TransformFn[];\n afterTranslateFns: TransformFn[];\n beforeReplaceParamsFns: TransformFn[];\n afterReplaceParamsFns: TransformFn[];\n}\n\nconst TRANSLATION_MIDDLEWARE_CONTEXT =\n createContext<TranslationMiddlewareContext | null>(null);\n\nexport interface TranslationMiddlewareProviderProps {\n value?: TranslationMiddleware;\n children?: ComponentChildren;\n}\n\nexport const TranslationMiddlewareProvider: FunctionalComponent<\n TranslationMiddlewareProviderProps\n> = (props) => {\n const parent = useContext(TRANSLATION_MIDDLEWARE_CONTEXT) || {\n beforeTranslateFns: [],\n afterTranslateFns: [],\n beforeReplaceParamsFns: [],\n afterReplaceParamsFns: [],\n };\n const merged: TranslationMiddlewareContext = {\n beforeTranslateFns: [...parent.beforeTranslateFns],\n afterTranslateFns: [...parent.afterTranslateFns],\n beforeReplaceParamsFns: [...parent.beforeReplaceParamsFns],\n afterReplaceParamsFns: [...parent.afterReplaceParamsFns],\n };\n if (props.value?.beforeTranslate) {\n merged.beforeTranslateFns.push(props.value.beforeTranslate);\n }\n if (props.value?.afterTranslate) {\n merged.afterTranslateFns.push(props.value.afterTranslate);\n }\n if (props.value?.beforeReplaceParams) {\n merged.beforeReplaceParamsFns.push(props.value.beforeReplaceParams);\n }\n if (props.value?.afterReplaceParams) {\n merged.afterReplaceParamsFns.push(props.value.afterReplaceParams);\n }\n return (\n <TRANSLATION_MIDDLEWARE_CONTEXT.Provider value={merged}>\n {props.children}\n </TRANSLATION_MIDDLEWARE_CONTEXT.Provider>\n );\n};\n\nexport function useTranslationMiddleware(): TranslationMiddlewareContext {\n return (\n useContext(TRANSLATION_MIDDLEWARE_CONTEXT) || {\n beforeTranslateFns: [],\n afterTranslateFns: [],\n beforeReplaceParamsFns: [],\n afterReplaceParamsFns: [],\n }\n );\n}\n", "import {I18nContext, useI18nContext} from './useI18nContext.js';\nimport {useStringParams} from './useStringParams.js';\nimport {useTranslationMiddleware} from './useTranslationsMiddleware.js';\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 * ```\n */\nexport function useTranslations() {\n // Ignore I18nContext not found error when used with client-side rehydration.\n let i18nContext: I18nContext | null = null;\n try {\n i18nContext = useI18nContext();\n } catch {\n console.warn('I18nContext not found');\n }\n const translations = i18nContext?.translations || {};\n const stringParams = useStringParams();\n const middleware = useTranslationMiddleware();\n\n function t(str: string, params?: Record<string, string | number>): string;\n function t(\n str: string | null | undefined,\n params?: Record<string, string | number>\n ): string | undefined {\n // Suppress verbatim `'undefined'` and `'null'` output, which can occur when\n // using `useTranslations` with undefined values.\n if (typeof str === 'undefined' || str === null) {\n // Return `undefined` to suppress empty props, e.g.\n // <a title={t(undefined)}>Learn more</a>\n return undefined;\n }\n let input = normalizeString(str);\n middleware.beforeTranslateFns.forEach((fn) => {\n input = fn(input);\n });\n let translation = translations[input] ?? input ?? '';\n middleware.afterTranslateFns.forEach((fn) => {\n translation = fn(translation);\n });\n\n // Replace string params, e.g. \"Hello, {name}\".\n middleware.beforeReplaceParamsFns.forEach((fn) => {\n translation = fn(translation);\n });\n if (testHasStringParams(translation)) {\n translation = replaceStringParams(translation, {\n ...stringParams,\n ...params,\n });\n }\n middleware.afterReplaceParamsFns.forEach((fn) => {\n translation = fn(translation);\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) => removeTrailingWhitespace(line));\n return lines.join('\\n');\n}\n\nfunction removeTrailingWhitespace(str: string) {\n return String(str)\n .trimEnd()\n .replace(/&nbsp;$/, '');\n}\n\nfunction testHasStringParams(str: string) {\n return str.includes('{') && str.includes('}');\n}\n\n/**\n * Replaces string placeholder params, e.g.\n *\n * ```\n * replaceStringParams('Hello, {name}!', {name: 'Joe'})\n * // => 'Hello, Joe!'\n * ```\n */\nfunction replaceStringParams(\n str: string,\n params: Record<string, string | number>\n): string {\n return str.replace(/{([^}]+)}/g, (match, key) => {\n if (key in params) {\n return String(params[key]);\n }\n return match;\n });\n}\n", "import {PluginOption as VitePlugin} from 'vite';\nimport {RootConfig} from './config.js';\n\nexport interface Pod {\n /** Unique pod name, e.g. '@blinkk/root-docs-pod'. */\n name: string;\n\n /**\n * URL prefix for pod routes. Routes within the pod are served under this\n * mount path. Defaults to '/'.\n */\n mount?: string;\n\n /**\n * Priority for route conflict resolution. Higher values win when multiple\n * pods register the same URL path. User-site routes always take precedence\n * regardless of priority. Defaults to 0.\n */\n priority?: number;\n\n /** Absolute path to the pod's routes/ directory. */\n routesDir?: string;\n\n /** Absolute path(s) to the pod's elements/ directory(s). */\n elementsDirs?: string[];\n\n /** Absolute path to the pod's bundles/ directory. */\n bundlesDir?: string;\n\n /** Absolute path to the pod's collections/ directory (root-cms only). */\n collectionsDir?: string;\n\n /** Absolute path to the pod's translations/ directory. */\n translationsDir?: string;\n\n /** Extra Vite plugins contributed by the pod. */\n vitePlugins?: VitePlugin[];\n}\n\nexport type PodFactory = (ctx: {rootConfig: RootConfig}) => Pod | Promise<Pod>;\n\nexport interface PodConfig {\n /** Whether the pod is enabled. Defaults to true. */\n enabled?: boolean;\n\n /** Override the pod's mount path. */\n mount?: string;\n\n /** Override the pod's priority. */\n priority?: number;\n\n /** Filter pod routes. */\n routes?: {\n exclude?: (string | RegExp)[];\n };\n\n /** Configure pod collections. */\n collections?: {\n exclude?: string[];\n /** Rename collection ids, e.g. {Posts: 'DocsPosts'}. */\n rename?: Record<string, string>;\n };\n}\n\n/**\n * Helper to define a pod with type-checking.\n */\nexport function definePod(pod: Pod): Pod {\n return pod;\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": ["useContext", "useContext", "useContext", "useContext", "useContext", "useContext", "useContext", "jsx", "createContext", "useContext", "jsx"]
7
7
  }
@@ -19,7 +19,7 @@ export interface Asset {
19
19
  * Recursively walks all deps and returns a list of JS chunk URLs that the
20
20
  * browser loads as a result of importing this asset. The asset's own URL is
21
21
  * excluded. These URLs are injected as `<link rel="modulepreload">` tags
22
- * when `modulePreload` is enabled in `root.config.ts`.
22
+ * unless `modulePreload: false` is set in `root.config.ts`.
23
23
  */
24
24
  getModulePreloadDeps(): Promise<string[]>;
25
25
  }
package/dist/render.js CHANGED
@@ -13236,7 +13236,7 @@ var Renderer = class {
13236
13236
  }
13237
13237
  const jsDeps = /* @__PURE__ */ new Set();
13238
13238
  const cssDeps = /* @__PURE__ */ new Set();
13239
- const preloadDeps = this.rootConfig.modulePreload ? /* @__PURE__ */ new Set() : null;
13239
+ const preloadDeps = this.rootConfig.modulePreload !== false ? /* @__PURE__ */ new Set() : null;
13240
13240
  const routeAsset = await this.assetMap.get(route.src);
13241
13241
  if (routeAsset) {
13242
13242
  const routeCssDeps = await routeAsset.getCssDeps();