reroute-js 1.0.0-alpha.2 → 1.0.0-alpha.21
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/README.md +7 -3
- package/cli/bin.d.ts +1 -1
- package/cli/bin.js +1618 -958
- package/cli/bin.js.map +38 -35
- package/cli/index.d.ts +1 -1
- package/cli/index.js +628 -40
- package/cli/index.js.map +53 -5
- package/cli/src/cli.d.ts +1 -1
- package/cli/src/commands/analyze.d.ts +1 -1
- package/cli/src/commands/analyze.d.ts.map +1 -1
- package/cli/src/commands/build.d.ts +2 -2
- package/cli/src/commands/dev.d.ts +6 -6
- package/cli/src/commands/dev.d.ts.map +1 -1
- package/cli/src/commands/gen.d.ts +4 -1
- package/cli/src/commands/gen.d.ts.map +1 -1
- package/cli/src/commands/index.d.ts +1 -1
- package/cli/src/commands/init.d.ts +1 -1
- package/cli/src/commands/lib/assets.d.ts +1 -1
- package/cli/src/commands/lib/bundler.d.ts +7 -1
- package/cli/src/commands/lib/bundler.d.ts.map +1 -1
- package/cli/src/commands/lib/command.d.ts +1 -1
- package/cli/src/commands/lib/env.d.ts +1 -1
- package/cli/src/commands/lib/index.d.ts +1 -1
- package/cli/src/commands/lib/log.d.ts +1 -1
- package/cli/src/commands/lib/markdown/availability.d.ts +1 -1
- package/cli/src/commands/lib/markdown/index.d.ts +1 -1
- package/cli/src/commands/lib/markdown/processor.d.ts +1 -1
- package/cli/src/commands/lib/production.d.ts +1 -1
- package/cli/src/commands/lib/server.d.ts +2 -1
- package/cli/src/commands/lib/server.d.ts.map +1 -1
- package/cli/src/commands/lib/streaming/analyzer.d.ts +1 -1
- package/cli/src/commands/lib/streaming/suspense.d.ts +2 -2
- package/cli/src/commands/lib/streaming/suspense.d.ts.map +1 -1
- package/cli/src/commands/lib/terminal-ui.d.ts +2 -2
- package/cli/src/commands/lib/terminal-ui.d.ts.map +1 -1
- package/cli/src/commands/lib/theme.d.ts +7 -4
- package/cli/src/commands/lib/theme.d.ts.map +1 -1
- package/cli/src/commands/lib/version.d.ts +1 -1
- package/cli/src/commands/lib/watch.d.ts +26 -0
- package/cli/src/commands/lib/watch.d.ts.map +1 -0
- package/cli/src/commands/og.d.ts +1 -1
- package/cli/src/commands/og.d.ts.map +1 -1
- package/cli/src/commands/start.d.ts +2 -2
- package/cli/src/commands/start.d.ts.map +1 -1
- package/cli/src/index.d.ts +1 -1
- package/core/index.d.ts +1 -1
- package/core/index.js +712 -279
- package/core/index.js.map +25 -23
- package/core/src/app-paths.d.ts +32 -0
- package/core/src/app-paths.d.ts.map +1 -0
- package/core/src/bundler/hash.d.ts +1 -1
- package/core/src/bundler/index.d.ts +1 -1
- package/core/src/config.d.ts +51 -7
- package/core/src/config.d.ts.map +1 -1
- package/core/src/content/discovery.d.ts +1 -1
- package/core/src/content/index.d.ts +1 -1
- package/core/src/content/metadata.d.ts +1 -1
- package/core/src/i18n/catalog.d.ts +1 -1
- package/core/src/i18n/discover.d.ts +1 -1
- package/core/src/i18n/index.d.ts +1 -1
- package/core/src/i18n/messages.d.ts +1 -1
- package/core/src/i18n/negotiate.d.ts +1 -1
- package/core/src/i18n/pathname.d.ts +1 -1
- package/core/src/i18n/resolve.d.ts +4 -3
- package/core/src/i18n/resolve.d.ts.map +1 -1
- package/core/src/i18n/types.d.ts +5 -1
- package/core/src/i18n/types.d.ts.map +1 -1
- package/core/src/index.d.ts +2 -1
- package/core/src/index.d.ts.map +1 -1
- package/core/src/llms/discovery.d.ts +35 -0
- package/core/src/llms/discovery.d.ts.map +1 -0
- package/core/src/llms/extractor.d.ts +6 -1
- package/core/src/llms/extractor.d.ts.map +1 -1
- package/core/src/llms/formatter.d.ts +1 -1
- package/core/src/llms/formatter.d.ts.map +1 -1
- package/core/src/llms/full-generator.d.ts +3 -1
- package/core/src/llms/full-generator.d.ts.map +1 -1
- package/core/src/llms/index-generator.d.ts +6 -3
- package/core/src/llms/index-generator.d.ts.map +1 -1
- package/core/src/llms/index.d.ts +5 -2
- package/core/src/llms/index.d.ts.map +1 -1
- package/core/src/og/discovery.d.ts +1 -1
- package/core/src/og/index.d.ts +1 -1
- package/core/src/og/meta.d.ts +1 -1
- package/core/src/og/render.d.ts +1 -1
- package/core/src/og/types.d.ts +1 -1
- package/core/src/robots/discovery.d.ts +1 -1
- package/core/src/robots/generator.d.ts +1 -1
- package/core/src/robots/generator.d.ts.map +1 -1
- package/core/src/robots/index.d.ts +1 -1
- package/core/src/robots/policies.d.ts +1 -1
- package/core/src/rss/discovery.d.ts +2 -2
- package/core/src/rss/discovery.d.ts.map +1 -1
- package/core/src/rss/generator.d.ts +1 -1
- package/core/src/rss/index.d.ts +1 -1
- package/core/src/sitemap/discovery.d.ts +7 -2
- package/core/src/sitemap/discovery.d.ts.map +1 -1
- package/core/src/sitemap/generator.d.ts +1 -1
- package/core/src/sitemap/index.d.ts +1 -1
- package/core/src/ssr/index.d.ts +1 -1
- package/core/src/ssr/lib/cache.d.ts +1 -1
- package/core/src/ssr/lib/cleanup.d.ts +1 -1
- package/core/src/ssr/lib/collections.d.ts +17 -1
- package/core/src/ssr/lib/collections.d.ts.map +1 -1
- package/core/src/ssr/lib/compression.d.ts +1 -1
- package/core/src/ssr/lib/compute/content.d.ts +1 -1
- package/core/src/ssr/lib/compute/index.d.ts +1 -1
- package/core/src/ssr/lib/compute/layouts.d.ts +1 -1
- package/core/src/ssr/lib/compute/routes.d.ts +1 -1
- package/core/src/ssr/lib/content-path.d.ts +1 -1
- package/core/src/ssr/lib/data.d.ts +1 -1
- package/core/src/ssr/lib/html.d.ts +1 -1
- package/core/src/ssr/lib/imports.d.ts +8 -3
- package/core/src/ssr/lib/imports.d.ts.map +1 -1
- package/core/src/ssr/lib/index.d.ts +1 -1
- package/core/src/ssr/lib/layouts.d.ts +1 -1
- package/core/src/ssr/lib/metadata.d.ts +1 -1
- package/core/src/ssr/lib/mime.d.ts +1 -1
- package/core/src/ssr/lib/modules.d.ts +1 -1
- package/core/src/ssr/lib/path.d.ts +1 -1
- package/core/src/ssr/lib/preload.d.ts +1 -1
- package/core/src/ssr/lib/route-match.d.ts +1 -1
- package/core/src/ssr/lib/scripts/collections.d.ts +2 -2
- package/core/src/ssr/lib/scripts/collections.d.ts.map +1 -1
- package/core/src/ssr/lib/scripts/data.d.ts +1 -1
- package/core/src/ssr/lib/scripts/escape.d.ts +1 -1
- package/core/src/ssr/lib/scripts/feeds.d.ts +1 -1
- package/core/src/ssr/lib/scripts/index.d.ts +1 -1
- package/core/src/ssr/lib/seed.d.ts +6 -1
- package/core/src/ssr/lib/seed.d.ts.map +1 -1
- package/core/src/ssr/lib/serialize.d.ts +1 -1
- package/core/src/ssr/lib/setup.d.ts +2 -2
- package/core/src/ssr/lib/setup.d.ts.map +1 -1
- package/core/src/ssr/lib/styles.d.ts +5 -4
- package/core/src/ssr/lib/styles.d.ts.map +1 -1
- package/core/src/ssr/lib/template.d.ts +1 -1
- package/core/src/ssr/lib/types.d.ts +15 -1
- package/core/src/ssr/lib/types.d.ts.map +1 -1
- package/core/src/ssr/render.d.ts +1 -1
- package/core/src/ssr/render.d.ts.map +1 -1
- package/core/src/ssr/stream.d.ts +1 -1
- package/core/src/ssr/stream.d.ts.map +1 -1
- package/elysia/index.d.ts +1 -1
- package/elysia/index.js +971 -432
- package/elysia/index.js.map +38 -33
- package/elysia/src/index.d.ts +1 -1
- package/elysia/src/libs/assets.d.ts +1 -1
- package/elysia/src/libs/cache.d.ts +6 -1
- package/elysia/src/libs/cache.d.ts.map +1 -1
- package/elysia/src/libs/caching.d.ts +1 -1
- package/elysia/src/libs/http.d.ts +1 -1
- package/elysia/src/libs/i18n.d.ts +2 -2
- package/elysia/src/libs/i18n.d.ts.map +1 -1
- package/elysia/src/libs/image.d.ts +1 -1
- package/elysia/src/libs/index.d.ts +1 -1
- package/elysia/src/libs/llms.d.ts +1 -1
- package/elysia/src/libs/response.d.ts +1 -1
- package/elysia/src/libs/serving.d.ts +1 -1
- package/elysia/src/plugin.d.ts +1 -1
- package/elysia/src/plugin.d.ts.map +1 -1
- package/elysia/src/routes/artifacts.d.ts +1 -1
- package/elysia/src/routes/artifacts.d.ts.map +1 -1
- package/elysia/src/routes/content.d.ts +2 -2
- package/elysia/src/routes/content.d.ts.map +1 -1
- package/elysia/src/routes/image.d.ts +1 -1
- package/elysia/src/routes/index.d.ts +1 -1
- package/elysia/src/routes/internal.d.ts +1 -1
- package/elysia/src/routes/internal.d.ts.map +1 -1
- package/elysia/src/routes/liveReloadClient.d.ts +16 -0
- package/elysia/src/routes/liveReloadClient.d.ts.map +1 -0
- package/elysia/src/routes/llms.d.ts +1 -1
- package/elysia/src/routes/llms.d.ts.map +1 -1
- package/elysia/src/routes/og.d.ts +1 -1
- package/elysia/src/routes/redirects.d.ts +1 -1
- package/elysia/src/routes/robots.d.ts +3 -1
- package/elysia/src/routes/robots.d.ts.map +1 -1
- package/elysia/src/routes/rss.d.ts +1 -1
- package/elysia/src/routes/search.d.ts +1 -1
- package/elysia/src/routes/sitemap.d.ts +3 -1
- package/elysia/src/routes/sitemap.d.ts.map +1 -1
- package/elysia/src/routes/ssr.d.ts +4 -2
- package/elysia/src/routes/ssr.d.ts.map +1 -1
- package/elysia/src/routes/static.d.ts +1 -1
- package/elysia/src/routes/themeCssRefresh.d.ts +17 -0
- package/elysia/src/routes/themeCssRefresh.d.ts.map +1 -0
- package/elysia/src/types.d.ts +1 -1
- package/package.json +1 -1
- package/react/index.d.ts +1 -1
- package/react/index.js +203 -107
- package/react/index.js.map +8 -7
- package/react/src/components/ClientOnly.d.ts +1 -1
- package/react/src/components/ContentRoute.d.ts +1 -1
- package/react/src/components/Image.d.ts +1 -1
- package/react/src/components/LazyRoute.d.ts +1 -1
- package/react/src/components/Link.d.ts +1 -1
- package/react/src/components/Markdown.d.ts +1 -1
- package/react/src/components/Outlet.d.ts +1 -1
- package/react/src/components/index.d.ts +1 -1
- package/react/src/hooks/index.d.ts +1 -1
- package/react/src/hooks/useContent.d.ts +1 -1
- package/react/src/hooks/useContent.d.ts.map +1 -1
- package/react/src/hooks/useData.d.ts +1 -1
- package/react/src/hooks/useFeed.d.ts +1 -1
- package/react/src/hooks/useLayoutData.d.ts +1 -1
- package/react/src/hooks/useLlms.d.ts +1 -1
- package/react/src/hooks/useNavigate.d.ts +1 -1
- package/react/src/hooks/useParams.d.ts +1 -1
- package/react/src/hooks/useRouter.d.ts +1 -1
- package/react/src/hooks/useSearch.d.ts +1 -1
- package/react/src/hooks/useSearchParams.d.ts +1 -1
- package/react/src/hooks/useToc.d.ts +1 -1
- package/react/src/i18n/LocaleProvider.d.ts +1 -1
- package/react/src/i18n/LocaleProvider.d.ts.map +1 -1
- package/react/src/i18n/helpers.d.ts +1 -1
- package/react/src/i18n/index.d.ts +1 -1
- package/react/src/index.d.ts +1 -1
- package/react/src/lib/bundle.d.ts +27 -0
- package/react/src/lib/bundle.d.ts.map +1 -0
- package/react/src/lib/collection.d.ts +1 -1
- package/react/src/lib/content.d.ts +1 -1
- package/react/src/lib/head.d.ts +1 -1
- package/react/src/lib/index.d.ts +2 -1
- package/react/src/lib/index.d.ts.map +1 -1
- package/react/src/lib/lazy-route.d.ts +7 -21
- package/react/src/lib/lazy-route.d.ts.map +1 -1
- package/react/src/lib/route-loader.d.ts +1 -1
- package/react/src/providers/ContentProvider.d.ts +1 -1
- package/react/src/providers/RerouteProvider.d.ts +2 -2
- package/react/src/providers/RerouteProvider.d.ts.map +1 -1
- package/react/src/providers/RouterProvider.d.ts +1 -1
- package/react/src/providers/index.d.ts +1 -1
- package/react/src/types/any.d.ts +1 -1
- package/react/src/types/index.d.ts +1 -1
- package/react/src/types/router.d.ts +1 -1
- package/telemetry/react.d.ts +1 -1
- package/telemetry/react.js +16 -16
- package/telemetry/react.js.map +2 -2
- package/telemetry/server.d.ts +1 -1
- package/telemetry/server.js +60 -228
- package/telemetry/server.js.map +20 -16
- package/telemetry/src/react/api.d.ts +1 -1
- package/telemetry/src/react/context.d.ts +1 -1
- package/telemetry/src/react/index.d.ts +1 -1
- package/telemetry/src/react/telemetry.d.ts +1 -1
- package/telemetry/src/server/context.d.ts +1 -1
- package/telemetry/src/server/headers/extractor.d.ts +1 -1
- package/telemetry/src/server/headers/index.d.ts +1 -1
- package/telemetry/src/server/headers/presets.d.ts +1 -1
- package/telemetry/src/server/index.d.ts +1 -1
- package/telemetry/src/server/instrumentation.d.ts +1 -1
- package/telemetry/src/server/plugin.d.ts +1 -1
- package/telemetry/src/server/sourcemap.d.ts +1 -1
package/cli/index.js.map
CHANGED
|
@@ -1,17 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../packages/cli/src/commands/lib/command.ts", "../../packages/cli/src/commands/lib/log.ts", "../../packages/cli/src/commands/lib/markdown/availability.ts", "../../packages/cli/src/commands/lib/markdown/processor.ts", "../../packages/cli/src/commands/lib/production.ts", "../../packages/cli/src/commands/lib/server.ts", "../../packages/cli/src/commands/lib/theme.ts", "../../packages/cli/src/commands/lib/version.ts"],
|
|
3
|
+
"sources": ["../../packages/core/src/app-paths.ts", "../../packages/core/src/config.ts", "../../packages/core/src/content/index.ts", "../../packages/core/src/i18n/messages.ts", "../../packages/core/src/i18n/resolve.ts", "../../packages/core/src/i18n/index.ts", "../../packages/telemetry/src/server/context.ts", "../../packages/telemetry/src/server/headers/index.ts", "../../packages/telemetry/src/server/instrumentation.ts", "../../packages/telemetry/src/server/sourcemap.ts", "../../packages/telemetry/src/server/plugin.ts", "../../packages/telemetry/src/server/index.ts", "../../packages/telemetry/server.ts", "../../packages/core/src/ssr/lib/cache.ts", "../../packages/core/src/ssr/lib/imports.ts", "../../packages/core/src/ssr/lib/collections.ts", "../../packages/core/src/ssr/lib/content-path.ts", "../../packages/core/src/ssr/lib/compute/content.ts", "../../packages/core/src/ssr/lib/compute/layouts.ts", "../../packages/core/src/ssr/lib/compute/routes.ts", "../../packages/core/src/ssr/lib/compute/index.ts", "../../packages/core/src/ssr/lib/modules.ts", "../../packages/core/src/ssr/lib/seed.ts", "../../packages/core/src/ssr/lib/data.ts", "../../packages/core/src/og/meta.ts", "../../packages/core/src/ssr/lib/metadata.ts", "../../packages/core/src/ssr/lib/preload.ts", "../../packages/core/src/ssr/lib/scripts/data.ts", "../../packages/core/src/ssr/lib/scripts/index.ts", "../../packages/core/src/ssr/lib/setup.ts", "../../packages/core/src/ssr/lib/index.ts", "../../packages/core/src/ssr/render.ts", "../../packages/core/src/llms/extractor.ts", "../../packages/core/src/llms/discovery.ts", "../../packages/core/src/llms/full-generator.ts", "../../packages/core/src/llms/index-generator.ts", "../../packages/core/src/llms/index.ts", "../../packages/core/src/og/render.ts", "../../packages/core/src/og/index.ts", "../../packages/core/src/robots/generator.ts", "../../packages/core/src/robots/index.ts", "../../packages/core/src/rss/discovery.ts", "../../packages/core/src/rss/index.ts", "../../packages/core/src/sitemap/index.ts", "../../packages/core/src/ssr/stream.ts", "../../packages/core/src/ssr/index.ts", "../../packages/core/src/index.ts", "../../packages/core/index.ts", "../../packages/cli/src/commands/lib/command.ts", "../../packages/cli/src/commands/lib/log.ts", "../../packages/cli/src/commands/lib/markdown/availability.ts", "../../packages/cli/src/commands/lib/markdown/processor.ts", "../../packages/cli/src/commands/lib/production.ts", "../../packages/cli/src/commands/lib/server.ts", "../../packages/cli/src/commands/lib/theme.ts", "../../packages/cli/src/commands/lib/version.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
+
"import { posix } from 'node:path';\nimport type { RerouteConfig } from './config';\n\nconst DEFAULT_APP_SRC = './src/client/App';\nconst DEFAULT_APP_SERVER = './src/index.ts';\n\ntype AppPaths = {\n\t/** App component, e.g. `./src/client/App` */\n\tsrc: string;\n\t/** Directory containing App, routes, index.css, and assets */\n\tclientDir: string;\n\troutesDir: string;\n\tcssEntry: string;\n\tassetsDir: string;\n\t/** Server entry, e.g. `./src/index.ts` */\n\tserver: string;\n};\n\nfunction toPosix(p: string): string {\n\treturn p.replace(/\\\\/g, '/');\n}\n\n/**\n * Resolve client/server roots from `app.src` and `app.server`.\n * Client dir is the parent of the App component.\n */\nfunction resolveAppPaths(config: RerouteConfig = {}): AppPaths {\n\tconst src = config.app?.src ?? DEFAULT_APP_SRC;\n\tconst server = config.app?.server ?? DEFAULT_APP_SERVER;\n\tconst clientDir = toPosix(posix.dirname(src));\n\n\treturn {\n\t\tsrc,\n\t\tclientDir,\n\t\troutesDir: toPosix(posix.join(clientDir, 'routes')),\n\t\tcssEntry: toPosix(posix.join(clientDir, 'index.css')),\n\t\tassetsDir: toPosix(posix.join(clientDir, 'assets')),\n\t\tserver,\n\t};\n}\n\n/** Import specifier from a `.reroute/*.ts` file to a project-relative path. */\nfunction importFromReroute(relPath: string): string {\n\tconst cleaned = toPosix(relPath)\n\t\t.replace(/^\\.\\//, '')\n\t\t.replace(/\\.(tsx|ts|jsx|js)$/, '');\n\treturn `../${cleaned}`;\n}\n\nexport {\n\tDEFAULT_APP_SERVER,\n\tDEFAULT_APP_SRC,\n\timportFromReroute,\n\tresolveAppPaths,\n\ttype AppPaths,\n};\n",
|
|
6
|
+
"import { pathToFileURL } from 'node:url';\nimport type { ComponentType } from 'react';\nimport type { I18nOptions } from './i18n/types';\n\n// biome-ignore lint/suspicious/noExplicitAny: The user document\ntype Doc = any;\ninterface StreamingOptions {\n\tenabled?: boolean;\n\tdefaultSkeleton?: ComponentType | string;\n\tdefaultLayoutSkeleton?: ComponentType | string;\n\ttimeout?: number;\n}\n\ninterface StaticOptions {\n\t/**\n\t * Patterns to ignore when serving static files\n\t * @default ['.DS_Store', '.git', '.env']\n\t */\n\tignorePatterns?: (string | RegExp)[];\n\t/**\n\t * Custom headers to add to static file responses\n\t */\n\theaders?: Record<string, string>;\n\t/**\n\t * Cache duration in seconds for static assets\n\t * @default 3600 (1 hour)\n\t */\n\tmaxAge?: number;\n\t/**\n\t * Cache-Control directive\n\t * @default 'public'\n\t */\n\tdirective?: string;\n}\n\ninterface ImageOptions {\n\t/**\n\t * Cache duration in seconds for optimized images\n\t * @default 31536000 (1 year)\n\t */\n\tmaxAge?: number;\n\t/**\n\t * Allowed external domains for image optimization\n\t * @default []\n\t */\n\tallowedDomains?: string[];\n\t/**\n\t * Maximum image width in pixels\n\t * @default 3840\n\t */\n\tmaxWidth?: number;\n\t/**\n\t * Maximum image height in pixels\n\t * @default 2160\n\t */\n\tmaxHeight?: number;\n}\n\n/**\n * Sitemap configuration options\n */\ninterface SitemapOptions {\n\t/** Enable sitemap generation (default: false) */\n\tenabled?: boolean;\n\n\t/** Base URL for sitemap generation (e.g., 'https://example.com') - required when enabled */\n\tbaseUrl?: string;\n\n\t/**\n\t * Maximum URLs per sitemap file before pagination (default: 50000)\n\t * When exceeded, creates multiple sitemap files with a sitemap index\n\t * Set lower for testing (e.g., 10) to verify pagination works\n\t */\n\tmaxUrlsPerPage?: number;\n\n\t/**\n\t * Completely exclude specific routes from the sitemap\n\t * Routes listed here will not appear in the sitemap at all\n\t * @example ['/admin', '/api']\n\t */\n\texcludeRoutes?: string[];\n\n\t/**\n\t * Exclude specific routes from SSR data array discovery\n\t * Routes listed here will still appear in the sitemap as single entries,\n\t * but won't generate multiple URLs from arrays found in their SSR data\n\t * @example ['/download', '/pricing'] - these routes appear once, not per array item\n\t */\n\texcludeDiscovery?: string[];\n\n\t/**\n\t * Extract urls from ssr.data result (full control over data shape)\n\t * If provided, this takes precedence over automatic array discovery\n\t * @param data - Full ssr.data result\n\t * @param routePattern - The route pattern (e.g., '/changelog')\n\t * @returns Array of items to process, or null/undefined to skip this route\n\t */\n\textractUrls?: (data: Doc, routePattern: string) => Doc[] | null | undefined;\n\n\t/**\n\t * Extract URL segment from ssr.data items\n\t * @param item - Data item from ssr.data result\n\t * @param routePattern - The route pattern (e.g., '/changelog')\n\t * @returns URL segment to append to routePattern (e.g., '0.1.0' -> '/changelog/0.1.0'),\n\t * null/undefined to use default extraction, or false to skip this item entirely\n\t */\n\textractUrl?: (\n\t\titem: Doc,\n\t\troutePattern: string,\n\t) => string | null | undefined | false;\n\n\t/**\n\t * Extract lastmod date from ssr.data items\n\t * @param item - Data item from ssr.data result\n\t * @returns ISO date string or undefined\n\t */\n\textractLastmod?: (item: Doc) => string | undefined;\n\n\t/** Default changefreq for URLs */\n\tchangefreq?:\n\t\t| 'always'\n\t\t| 'hourly'\n\t\t| 'daily'\n\t\t| 'weekly'\n\t\t| 'monthly'\n\t\t| 'yearly'\n\t\t| 'never';\n\n\t/** Default priority for URLs */\n\tpriority?: number;\n}\n\n/**\n * RSS feed configuration options\n */\ninterface RSSOptions {\n\t/** Enable RSS feed generation (default: false) */\n\tenabled?: boolean;\n\n\t/** Base URL for RSS feeds (e.g., 'https://example.com') - required when enabled */\n\tbaseUrl?: string;\n\n\t/** Feed title (e.g., 'My Blog') */\n\ttitle?: string;\n\n\t/** Feed description */\n\tdescription?: string;\n\n\t/** Maximum number of items per feed (default: 50) */\n\tlimit?: number;\n\n\t/** Feed format: 'rss' or 'atom' (default: 'rss') */\n\tformat?: 'rss' | 'atom';\n\n\t/**\n\t * Completely exclude specific routes from the RSS feed\n\t * Routes listed here will not appear in the feed at all\n\t * @example ['/admin', '/api']\n\t */\n\texcludeRoutes?: string[];\n\n\t/**\n\t * Exclude specific routes from SSR data array discovery\n\t * Routes listed here won't generate multiple feed items from arrays in their SSR data,\n\t * but may still generate a single feed item if appropriate\n\t * @example ['/download', '/pricing'] - won't generate items per array entry\n\t */\n\texcludeDiscovery?: string[];\n\n\t/**\n\t * Extract items from ssr.data result (full control over data shape)\n\t * If provided, this takes precedence over automatic array discovery\n\t * @param data - Full ssr.data result\n\t * @param routePattern - The route pattern (e.g., '/changelog')\n\t * @returns Array of items to process, or null/undefined to skip this route\n\t */\n\textractUrls?: (data: Doc, routePattern: string) => Doc[] | null | undefined;\n\n\t/**\n\t * Extract URL segment from ssr.data items (same as sitemap)\n\t * @returns URL segment, null/undefined for default extraction, or false to skip item\n\t */\n\textractUrl?: (\n\t\titem: Doc,\n\t\troutePattern: string,\n\t) => string | null | undefined | false;\n\n\t/**\n\t * Extract publication date from items\n\t */\n\textractPubDate?: (item: Doc) => string | undefined;\n\n\t/**\n\t * Extract author from items\n\t */\n\textractAuthor?: (item: Doc) => string | undefined;\n\n\t/**\n\t * Extract content/body from items\n\t */\n\textractContent?: (item: Doc) => string | undefined;\n\n\t/**\n\t * Extract description/summary from items\n\t * Falls back to item.description or item.excerpt if not provided\n\t */\n\textractDescription?: (item: Doc) => string | undefined;\n}\n\n/**\n * Markdown configuration options\n */\ninterface MarkdownOptions {\n\t/**\n\t * Custom components to use when rendering markdown\n\t * Path to a module that exports a components object compatible with Streamdown\n\t * @example './src/client/lib/markdown-components'\n\t */\n\tcomponents?: string;\n\n\t/** Enable GitHub Flavored Markdown (default: true if remark-gfm is installed) */\n\tenableGfm?: boolean;\n\n\t/** Shiki theme for syntax highlighting (default: 'github-dark') */\n\ttheme?: string;\n}\n\n/**\n * Search index configuration options\n */\ninterface SearchOptions {\n\t/** Enable search index generation (default: false) */\n\tenabled?: boolean;\n\n\t/**\n\t * Collections to include in search index\n\t * @default [] - all collections will be indexed\n\t * @example ['blog', 'docs']\n\t */\n\tcollections?: string[];\n\n\t/**\n\t * Include full content text in search (increases index size significantly)\n\t * @default false - only index headings and metadata\n\t */\n\tincludeContent?: boolean;\n\n\t/**\n\t * Maximum content length to index per item (characters)\n\t * @default 5000 - prevents huge content from bloating index\n\t */\n\tcontentLimit?: number;\n\n\t/**\n\t * Fields to index from frontmatter metadata\n\t * @default ['title', 'description', 'excerpt', 'tags']\n\t */\n\tmetaFields?: string[];\n\n\t/**\n\t * Minimum heading level to index (1-6)\n\t * @default 1 - index all headings\n\t */\n\tminHeadingLevel?: number;\n\n\t/**\n\t * Maximum heading level to index (1-6)\n\t * @default 6 - index all headings\n\t */\n\tmaxHeadingLevel?: number;\n\n\t/**\n\t * Default page size for paginated results\n\t * @default 20\n\t */\n\tdefaultPageSize?: number;\n\n\t/**\n\t * Maximum allowed page size (prevents excessive results)\n\t * @default 100\n\t */\n\tmaxPageSize?: number;\n}\n\ninterface OGImageFont {\n\t/** Font family name (e.g., 'Inter') */\n\tname: string;\n\t/** Path to font file (TTF, OTF, or WOFF) relative to project root */\n\tpath: string;\n\t/** Font weight (e.g., 400, 700, 900) */\n\tweight?: number;\n\t/** Font style (default: 'normal') */\n\tstyle?:\n\t\t| 'normal'\n\t\t| 'italic'\n\t\t| 'bold'\n\t\t| 'bold-italic'\n\t\t| 'medium'\n\t\t| 'medium-italic'\n\t\t| 'semibold'\n\t\t| 'semibold-italic'\n\t\t| 'light'\n\t\t| 'light-italic'\n\t\t| 'thin'\n\t\t| 'thin-italic'\n\t\t| 'black'\n\t\t| 'black-italic'\n\t\t| 'extra-bold'\n\t\t| 'extra-bold-italic'\n\t\t| 'extra-light'\n\t\t| 'extra-light-italic'\n\t\t| 'extra-medium'\n\t\t| 'extra-medium-italic'\n\t\t| 'extra-semibold'\n\t\t| 'extra-semibold-italic'\n\t\t| 'extra-light'\n\t\t| 'extra-light-italic'\n\t\t| 'extra-medium'\n\t\t| 'extra-medium-italic'\n\t\t| 'extra-semibold'\n\t\t| 'extra-semibold-italic';\n\tdata?: ArrayBuffer | undefined;\n}\n\n/**\n * Open Graph and social meta tags configuration\n */\ninterface OGOptions {\n\t/**\n\t * Site name used for og:site_name meta tag\n\t * This appears as the website name in social media shares\n\t * @example 'My Awesome Site'\n\t */\n\tsiteName?: string;\n\n\t/**\n\t * Default site description used as fallback for og:description\n\t * when a page doesn't define its own description\n\t * @example 'A modern web application built with Reroute'\n\t */\n\tsiteDescription?: string;\n\n\t/**\n\t * Default Open Graph type for pages without explicit og:type\n\t * Common types: 'website', 'article', 'product', 'profile'\n\t * @default 'website'\n\t * @see https://ogp.me/#types\n\t */\n\tdefaultType?: string;\n\n\t/**\n\t * Twitter/X handle for twitter:site meta tag (with or without @)\n\t * @example '@mysite' or 'mysite'\n\t */\n\ttwitterSite?: string;\n\n\t/**\n\t * Twitter/X card type (default: 'summary_large_image')\n\t * @default 'summary_large_image'\n\t */\n\ttwitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player';\n\n\t/**\n\t * Default locale for og:locale meta tag\n\t * @example 'en_US', 'en_GB', 'es_ES'\n\t */\n\tlocale?: string;\n}\n\n/**\n * OG Image generation configuration options\n */\ninterface OGImageOptions {\n\t/** Enable OG image generation (default: false) */\n\tenabled?: boolean;\n\n\t/** Base URL for absolute OG image URLs (e.g., 'https://example.com') - required when enabled */\n\tbaseUrl?: string;\n\n\t/** Path to default template component or 'default' for built-in template */\n\tdefaultTemplate?: string;\n\n\t/** Image width in pixels (default: 1200) */\n\twidth?: number;\n\n\t/** Image height in pixels (default: 630) */\n\theight?: number;\n\n\t/** Cache duration in seconds (default: 3600 = 1 hour) */\n\tmaxAge?: number;\n\n\t/** Default avatar/logo URL (can be external or local path) */\n\tavatar?: string;\n\n\t/** Site name to display */\n\tsiteName?: string;\n\n\t/** Custom fonts to load for OG images */\n\tfonts?: OGImageFont[];\n}\n\n/**\n * Preferences for how fetched content may be used (Content Signals).\n * Omitted keys default to yes.\n */\ninterface ContentSignalPolicy {\n\t/** Use in search results */\n\tsearch?: boolean;\n\n\t/** Use as live context for an AI answer */\n\taiInput?: boolean;\n\n\t/** Use to train or fine-tune a model */\n\taiTrain?: boolean;\n}\n\n/**\n * LLMs content configuration options\n */\ninterface LLMsOptions {\n\t/** Enable LLM content features (default: false) */\n\tenabled?: boolean;\n\n\t/** Base URL for absolute URLs in llms.txt index (e.g., 'https://example.com') */\n\tbaseUrl?: string;\n\n\t/** Enable .txt extension for routes (default: true when enabled) */\n\tenableTextExtension?: boolean;\n\n\t/** Enable .md extension for routes (default: true when enabled) */\n\tenableMarkdownExtension?: boolean;\n\n\t/** Enable Accept header content negotiation (default: true when enabled) */\n\tenableContentNegotiation?: boolean;\n\n\t/** Enable /llms.txt index generation (default: true when enabled) */\n\tenableIndex?: boolean;\n\n\t/** Enable /llms-full.txt generation (default: true when enabled) */\n\tenableFullContent?: boolean;\n\n\t/**\n\t * Content-Signal header on HTML and markdown/text responses.\n\t * Default when enabled: `ai-train=yes, search=yes, ai-input=yes`.\n\t * Set `false` to omit the header.\n\t */\n\tcontentSignal?: ContentSignalPolicy | false;\n\n\t/** Cache duration in seconds for LLM content (default: 86400 = 24 hours, or falls back to options.maxAge) */\n\tmaxAge?: number;\n\n\t/** Cache duration in seconds for llms-full.txt specifically (default: 604800 = 7 days, or falls back to llms.maxAge) */\n\tmaxAgeFull?: number;\n\n\t/** Maximum items in full content bundle (default: 10000) */\n\tmaxItems?: number;\n\n\t/** Maximum file size for full content in bytes (default: 50MB) */\n\tmaxSize?: number;\n\n\t/**\n\t * Exclude specific routes from LLM features\n\t * Supports strings (exact match or substring), RegExp, or custom functions\n\t * @example ['/admin', '/api', /^\\/private/, (path) => path.includes('draft')]\n\t */\n\texcludeRoutes?: (string | RegExp | ((pathname: string) => boolean))[];\n\n\t/**\n\t * Exclude collections from LLM features\n\t * @example ['drafts', 'private']\n\t */\n\texcludeCollections?: string[];\n\n\t/** Site name for index header */\n\tsiteName?: string;\n\n\t/** Site description for index header (brief context for LLMs) */\n\tsiteDescription?: string;\n\n\t/**\n\t * Include each page's description next to the index link\n\t * (`- [Title](url.txt): description`). Default: true.\n\t * Set false for compact title+link only.\n\t */\n\tindexSummaries?: boolean;\n\n\t/**\n\t * Per-collection customization for titles and descriptions\n\t * @example\n\t * ```ts\n\t * collections: {\n\t * blog: {\n\t * title: 'My Awesome Blog',\n\t * description: 'Insights and tutorials about web development'\n\t * },\n\t * docs: {\n\t * title: 'Documentation',\n\t * description: 'Complete API reference and guides'\n\t * }\n\t * }\n\t * ```\n\t */\n\tcollections?: Record<\n\t\tstring,\n\t\t{\n\t\t\t/** Custom title for this collection's LLMS index */\n\t\t\ttitle?: string;\n\t\t\t/** Custom description for this collection's LLMS index */\n\t\t\tdescription?: string;\n\t\t}\n\t>;\n}\n\n/**\n * Robots policy for specific user-agents\n */\ninterface RobotsPolicy {\n\t/** User-Agent (e.g., '*', 'Googlebot', 'GPTBot') */\n\tuserAgent: string;\n\n\t/** Paths to explicitly allow */\n\tallow?: string[];\n\n\t/** Paths to disallow */\n\tdisallow?: string[];\n\n\t/** Crawl delay for this user-agent (seconds) */\n\tcrawlDelay?: number;\n}\n\n/**\n * Robots.txt generation configuration options\n */\ninterface RobotsOptions {\n\t/** Enable robots.txt generation (default: false) */\n\tenabled?: boolean;\n\n\t/** Base URL for sitemap reference (e.g., 'https://example.com') - required when enabled */\n\tbaseUrl?: string;\n\n\t/** Cache duration in seconds (default: 86400 = 24 hours) */\n\tmaxAge?: number;\n\n\t/** Custom policies per user-agent (default: permissive) */\n\tpolicies?: RobotsPolicy[];\n\n\t/** Auto-reference sitemap.xml if sitemap is enabled (default: true) */\n\treferenceSitemap?: boolean;\n\n\t/** Auto-disallow routes excluded from sitemap (default: true) */\n\tsitemapExclusion?: boolean;\n\n\t/** Auto-disallow old redirect source paths (default: true) */\n\tredirectExclusion?: boolean;\n\n\t/**\n\t * Content-Signal line on the `User-agent: *` group.\n\t * When omitted, no line is written (the plugin copies `llms.contentSignal`\n\t * when both features are enabled).\n\t */\n\tcontentSignal?: ContentSignalPolicy | false;\n\n\t/** Additional raw text appended to robots.txt */\n\tcustomRules?: string;\n\n\t/** Global crawl delay in seconds (optional) */\n\tcrawlDelay?: number;\n}\n\n/**\n * Telemetry proxy configuration for forwarding browser telemetry\n */\ninterface TelemetryProxyOptions {\n\t/**\n\t * Enable browser telemetry proxy (default: true when proxy config exists)\n\t * Creates endpoints to forward browser traces/metrics/logs to OTLP backend\n\t * This avoids CORS issues and ad blockers\n\t */\n\tenabled?: boolean;\n\n\t/**\n\t * Base path for proxy endpoints (default: '/__reroute_telemetry')\n\t * Creates:\n\t * - {pathname}/v1/traces\n\t * - {pathname}/v1/metrics\n\t * - {pathname}/v1/logs\n\t * - {pathname}/health\n\t */\n\tpathname?: string;\n\n\t/**\n\t * Enable verbose logging for proxy requests\n\t * @default false\n\t */\n\tverbose?: boolean;\n\n\t/**\n\t * Custom handler for proxy requests (advanced)\n\t * If provided, replaces default OTLP forwarding logic\n\t *\n\t * @example\n\t * ```ts\n\t * handler: async ({ body, endpoint, headers }) => {\n\t * // Custom logic: filter, transform, or route to different backends\n\t * await fetch(`https://custom-backend${endpoint}`, {\n\t * method: 'POST',\n\t * headers,\n\t * body: JSON.stringify(body),\n\t * });\n\t * return { status: 200 };\n\t * }\n\t * ```\n\t */\n\thandler?: (params: {\n\t\tbody: unknown;\n\t\tendpoint: string;\n\t\theaders: Record<string, string>;\n\t\totlpEndpoint: string;\n\t}) => Promise<{ status: number }>;\n}\n\n/**\n * Browser telemetry configuration options\n */\ninterface BrowserTelemetryOptions {\n\t/**\n\t * Enable browser telemetry\n\t * @default true\n\t */\n\tenabled?: boolean;\n\n\t/**\n\t * Service name for browser telemetry\n\t * @default 'reroute-app-browser'\n\t */\n\tserviceName?: string;\n\n\t/**\n\t * Service version\n\t * @default '1.0.0'\n\t */\n\tserviceVersion?: string;\n\n\t/**\n\t * OTLP endpoint URL (use relative URL for same-origin proxy)\n\t * @default '/__reroute_telemetry'\n\t */\n\totlpEndpoint?: string;\n\n\t/**\n\t * API key for authentication (optional)\n\t */\n\tapiKey?: string;\n\n\t/**\n\t * Deployment environment (e.g., 'production', 'staging', 'development')\n\t * @default 'development'\n\t */\n\tenvironment?: string;\n\n\t/**\n\t * Enable console.error capture\n\t * @default true\n\t */\n\tenableConsoleCapture?: boolean;\n\n\t/**\n\t * Ignore specific routes/URLs from breadcrumbs and network tracking\n\t *\n\t * Note: Functions are NOT supported for browser telemetry (closures cannot be serialized)\n\t * Use strings or regex patterns instead\n\t *\n\t * @example\n\t * ```ts\n\t * ignoreRoutes: [\n\t * '/bundles/', // String prefix match\n\t * /\\.(js|css|png)$/, // Regex pattern\n\t * /^\\/assets\\//, // Regex for path prefix\n\t * ]\n\t * ```\n\t */\n\tignoreRoutes?: Array<string | RegExp>;\n}\n\n/**\n * Telemetry configuration options for OpenTelemetry integration\n */\ninterface TelemetryOptions {\n\t/**\n\t * Enable server telemetry collection (default: false)\n\t * Must be explicitly enabled to start collecting traces, metrics, and logs\n\t */\n\tenabled?: boolean;\n\n\t/**\n\t * Browser telemetry configuration\n\t * Configure browser-side telemetry separately from server\n\t */\n\tbrowser?: BrowserTelemetryOptions;\n\n\t/**\n\t * Service name for server telemetry\n\t * @default 'reroute-app'\n\t */\n\tserviceName?: string;\n\n\t/**\n\t * Service version\n\t * @default process.env.npm_package_version || '1.0.0'\n\t */\n\tserviceVersion?: string;\n\n\t/**\n\t * OTLP endpoint URL\n\t * @default process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318'\n\t */\n\totlpEndpoint?: string;\n\n\t/**\n\t * API key for authentication (optional, only needed for SigNoz Cloud)\n\t */\n\tapiKey?: string;\n\n\t/**\n\t * Deployment environment (e.g., 'production', 'staging', 'development')\n\t * Used as a deployment.environment attribute on all telemetry\n\t * @default process.env.NODE_ENV || 'development'\n\t */\n\tenvironment?: string;\n\n\t/**\n\t * Metric export interval in milliseconds\n\t * @default 5000\n\t */\n\tmetricsInterval?: number;\n\n\t/**\n\t * Browser telemetry proxy configuration\n\t * When configured, creates endpoints to forward browser telemetry to OTLP backend\n\t * This avoids CORS issues and ad blockers\n\t */\n\tproxy?: TelemetryProxyOptions;\n\n\t/**\n\t * Ignore specific routes from telemetry collection\n\t * Supports flexible pattern matching:\n\t * - String: Matches if pathname contains the string (e.g., '/assets' matches '/assets/logo.png')\n\t * - RegExp: Tests pathname against regex (e.g., /\\.(js|css)$/ matches file extensions)\n\t * - Function: Custom logic with pathname and method (server-side only)\n\t *\n\t * Note: For browser telemetry, only strings and regex are supported (functions use closures that cannot be serialized)\n\t *\n\t * @example\n\t * ```ts\n\t * ignoreRoutes: [\n\t * '/health', // String match\n\t * /\\.(js|css|png|jpg|svg|ico|woff2?)$/, // Regex for static assets\n\t * (pathname) => pathname.startsWith('/assets/'), // Function (server-side only)\n\t * (pathname, method) => method === 'OPTIONS', // Access to HTTP method (server-side only)\n\t * ]\n\t * ```\n\t */\n\tignoreRoutes?: (\n\t\t| string\n\t\t| RegExp\n\t\t| ((pathname: string, method: string) => boolean)\n\t)[];\n\n\t/**\n\t * Enable distributed tracing (default: true)\n\t * Captures request flows across services with OpenTelemetry traces\n\t */\n\tenableTraces?: boolean;\n\n\t/**\n\t * Enable metrics collection (default: true)\n\t * Captures counters, gauges, and histograms for request volume, duration, etc.\n\t */\n\tenableMetrics?: boolean;\n\n\t/**\n\t * Enable structured logging (default: true)\n\t * Captures log events with structured attributes for better querying\n\t */\n\tenableLogs?: boolean;\n\n\t/**\n\t * Enable system metrics collection (default: true)\n\t * Captures CPU usage, memory consumption, and process metrics\n\t */\n\tenableSystemMetrics?: boolean;\n\n\t/**\n\t * Enable console.error and console.warn capture (default: true)\n\t * Automatically creates telemetry spans for console.error/warn calls\n\t * Useful for catching errors that are logged but not thrown\n\t */\n\tenableConsoleCapture?: boolean;\n\n\t/**\n\t * Sample rate for regular requests (default: 1.0)\n\t * Controls what percentage of requests to track (0.0 = 0%, 1.0 = 100%)\n\t *\n\t * Use sampling to reduce telemetry volume and costs on high-traffic applications:\n\t * - 1.0 = Track every request (100%)\n\t * - 0.5 = Track half of requests (50%)\n\t * - 0.1 = Track 10% of requests\n\t * - 0.01 = Track 1% of requests\n\t *\n\t * Sampling uses random selection, so a 10% sample of 100,000 requests\n\t * still provides ~10,000 data points for statistical analysis\n\t *\n\t * @example sampleRate: 0.1 // Track 10% of requests\n\t */\n\tsampleRate?: number;\n\n\t/**\n\t * Sample rate for error requests (default: 1.0)\n\t * Controls what percentage of errors to track (0.0 = 0%, 1.0 = 100%)\n\t *\n\t * Typically set higher than sampleRate since errors are more important to track.\n\t * Common pattern: low sample rate for normal requests, 100% for errors\n\t *\n\t * @example\n\t * ```ts\n\t * sampleRate: 0.1, // Track 10% of normal requests\n\t * errorSampleRate: 1.0, // Track 100% of errors\n\t * ```\n\t */\n\terrorSampleRate?: number;\n\n\t/**\n\t * Capture and map HTTP headers to custom telemetry attributes\n\t * Provides fine-grained control over header extraction and attribute naming\n\t *\n\t * Use presets for common scenarios or define custom mappings for any header\n\t *\n\t * @example\n\t * ```ts\n\t * import { RerouteHeaders, CloudflareHeaders } from 'reroute-js/telemetry/server';\n\t *\n\t * capture: [\n\t * ...RerouteHeaders.http(), // user-agent, referer, accept-language, etc.\n\t * ...CloudflareHeaders.geo(), // Cloudflare geolocation (IP, country, city, etc.)\n\t * { header: 'x-custom', as: 'custom.value' }, // Custom mapping\n\t * ]\n\t * ```\n\t */\n\tcapture?: Array<{ header: string; as: string }>;\n\n\t/**\n\t * Custom attributes to add to all spans and logs\n\t * Useful for tagging telemetry with environment, region, version, etc.\n\t *\n\t * @example\n\t * ```ts\n\t * customAttributes: {\n\t * environment: 'production',\n\t * region: 'us-east-1',\n\t * version: '1.2.3',\n\t * }\n\t * ```\n\t */\n\tcustomAttributes?: Record<string, string | number | boolean>;\n\n\t/**\n\t * Source map configuration for production error tracking\n\t * When enabled, error stack traces are mapped to original source files\n\t * instead of compiled/minified code\n\t *\n\t * @example\n\t * ```ts\n\t * sourcemaps: {\n\t * enabled: true, // Enable sourcemap resolution\n\t * }\n\t * ```\n\t */\n\tsourcemaps?: {\n\t\t/**\n\t\t * Enable sourcemap resolution\n\t\t * @default true in production, false in development\n\t\t */\n\t\tenabled?: boolean;\n\t};\n}\n\n/**\n * Redirect handler function that can dynamically determine destination\n * @param url - The original URL object\n * @returns The destination path/url or null to skip redirect\n */\ntype RedirectHandler = (url: URL) => string | null | Promise<string | null>;\n\n/**\n * Individual redirect rule\n */\ninterface RedirectRule {\n\t/** Enable or disable this redirect rule (default: true) */\n\tenabled?: boolean;\n\t/** Source path (string or regex pattern) */\n\tsource: string | RegExp;\n\t/** Destination path or handler function */\n\tdestination: string | RedirectHandler;\n\t/** HTTP status code (301 = permanent, 302 = temporary, 307 = preserve method, 308 = permanent preserve method) */\n\tstatus?: 301 | 302 | 307 | 308;\n\t/** Whether source is a regex pattern (auto-detected if source is RegExp) */\n\tregex?: boolean;\n}\n\n/**\n * Server-side caching configuration.\n * Controls in-process HTML, `ssr.data()`, and `/__reroute_data` caches.\n */\ninterface ServerOptions {\n\t/**\n\t * Enable server-side caching.\n\t * `false` recomputes `ssr.data()` on every origin request.\n\t * @default true\n\t */\n\tcache?: boolean;\n\n\t/**\n\t * Cache duration in seconds for server-side cache\n\t * @default 3600 (1 hour)\n\t */\n\tmaxAge?: number;\n}\n\n/**\n * Browser caching configuration\n * Controls Cache-Control headers sent to browsers\n */\ninterface BrowserOptions {\n\t/**\n\t * Enable sending Cache-Control headers to browsers\n\t * Set to false when running behind a CDN like Cloudflare that handles caching\n\t * @default true\n\t */\n\tcache?: boolean;\n\n\t/**\n\t * Cache duration in seconds for browser caching\n\t * @default 300 (5 minutes)\n\t */\n\tmaxAge?: number;\n\n\t/**\n\t * Cache-Control directive ('public' | 'private')\n\t * @default 'public'\n\t */\n\tdirective?: 'public' | 'private';\n}\n\ninterface AppOptions {\n\t/**\n\t * App component path. The parent directory is the client root\n\t * (routes, index.css, assets).\n\t * @default './src/client/App'\n\t * @example './src/client/App'\n\t * @example './client/App'\n\t */\n\tsrc?: string;\n\n\t/**\n\t * Server entry used by `reroute dev`, `reroute start`, and compiled binaries.\n\t * @default './src/index.ts'\n\t * @example './src/index.ts'\n\t * @example './index.ts'\n\t */\n\tserver?: string;\n\n\t/**\n\t * Base URL for the application (e.g., 'https://example.com')\n\t * Used for canonical URLs, social meta tags, and other absolute URLs\n\t */\n\tbaseUrl?: string;\n\n\t/** Custom HTML to inject into <head> */\n\thead?: string;\n\n\t/** HTML lang attribute @default 'en' */\n\tlang?: string;\n\n\t/** Root element ID @default 'root' */\n\tappId?: string;\n\n\t/** Enable automatic canonical URL generation @default true when baseUrl is set */\n\tautoCanonical?: boolean;\n\n\t/**\n\t * Suffix to append to all page titles (e.g., ' | My Site')\n\t * Applied automatically to page titles and og:title meta tags\n\t */\n\ttitleSuffix?: string;\n\n\t/** Minify HTML output */\n\tminify?: boolean;\n\n\t/** Enable compression for SSR responses @default true */\n\tcompression?: boolean;\n\n\t/** Enable detailed performance logging */\n\tdebug?: boolean;\n\n\t/** Cache duration for JS bundles in seconds @default 31536000 (1 year) */\n\tbundleMaxAge?: number;\n}\n\ninterface RerouteConfig {\n\tapp?: AppOptions;\n\tstatic?: StaticOptions;\n\tstreaming?: StreamingOptions;\n\tredirects?: RedirectRule[];\n\timage?: ImageOptions;\n\tog?: OGOptions;\n\togImage?: OGImageOptions;\n\tsitemap?: SitemapOptions;\n\trss?: RSSOptions;\n\tmarkdown?: MarkdownOptions;\n\tsearch?: SearchOptions;\n\tllms?: LLMsOptions;\n\trobots?: RobotsOptions;\n\ttelemetry?: TelemetryOptions;\n\t/** Optional i18n overrides. Feature still requires a `locales/` directory. */\n\ti18n?: I18nOptions;\n\n\t/**\n\t * Server-side caching configuration\n\t * Controls internal LRU cache for rendered HTML\n\t *\n\t * @example\n\t * ```ts\n\t * // Disable in-process HTML and ssr.data() caches (use behind a CDN)\n\t * server: { cache: false }\n\t *\n\t * // Custom server cache TTL\n\t * server: { cache: true, maxAge: 3600 }\n\t * ```\n\t */\n\tserver?: ServerOptions;\n\n\t/**\n\t * Browser caching configuration\n\t * Controls Cache-Control headers sent to browsers\n\t *\n\t * @example\n\t * ```ts\n\t * // Disable browser caching (recommended when using Cloudflare)\n\t * browser: { cache: false }\n\t *\n\t * // Custom browser cache settings\n\t * browser: { cache: true, maxAge: 60, directive: 'private' }\n\t * ```\n\t */\n\tbrowser?: BrowserOptions;\n}\n\nfunction defineConfig(config: RerouteConfig): RerouteConfig {\n\treturn config;\n}\n\nasync function loadConfig(cwd: string): Promise<RerouteConfig> {\n\t// Check for bundled config first (compiled binary support)\n\tconst bundledConfig = (globalThis as Record<string, unknown>)\n\t\t.__REROUTE_CONFIG__ as RerouteConfig | undefined;\n\tif (bundledConfig) {\n\t\treturn bundledConfig;\n\t}\n\n\t// Fallback to filesystem read (dev mode)\n\tconst configPath = `${cwd}/reroute.config.ts`;\n\tconst configFile = Bun.file(configPath);\n\n\tif (!(await configFile.exists())) {\n\t\treturn {};\n\t}\n\n\ttry {\n\t\tconst mod = await import(\n\t\t\t`${pathToFileURL(configPath).href}?t=${Date.now()}`\n\t\t);\n\t\treturn (mod.default || mod) as RerouteConfig;\n\t} catch (error) {\n\t\tconsole.warn('[reroute] Failed to load reroute.config.ts:', error);\n\t\treturn {};\n\t}\n}\n\nexport {\n\tdefineConfig,\n\tloadConfig,\n\ttype AppOptions,\n\ttype BrowserOptions,\n\ttype ContentSignalPolicy,\n\ttype I18nOptions,\n\ttype ImageOptions,\n\ttype LLMsOptions,\n\ttype MarkdownOptions,\n\ttype OGOptions,\n\ttype OGImageOptions,\n\ttype OGImageFont,\n\ttype RedirectRule,\n\ttype RerouteConfig,\n\ttype RobotsOptions,\n\ttype RobotsPolicy,\n\ttype RSSOptions,\n\ttype SearchOptions,\n\ttype ServerOptions,\n\ttype SitemapOptions,\n\ttype StaticOptions,\n\ttype StreamingOptions,\n\ttype TelemetryOptions,\n\ttype TelemetryProxyOptions,\n\ttype BrowserTelemetryOptions,\n};\n",
|
|
7
|
+
"export * from './discovery';\nexport * from './metadata';\n",
|
|
8
|
+
"import { IntlMessageFormat } from 'intl-messageformat';\nimport { lookupMessage } from './catalog';\nimport type { MessageCatalog } from './types';\n\ntype MessageValues = Record<\n\tstring,\n\tstring | number | boolean | Date | null | undefined\n>;\n\nconst formatterCache = new Map<string, IntlMessageFormat>();\n\nfunction formatMessage(\n\tmessage: string,\n\tlocale: string,\n\tvalues?: MessageValues,\n): string {\n\tif (!values || Object.keys(values).length === 0) {\n\t\t// Fast path: no values — still format if message has ICU syntax without args needed\n\t\tif (!/\\{[^}]+\\}/.test(message)) return message;\n\t}\n\n\tconst cacheKey = `${locale}\\0${message}`;\n\tlet formatter = formatterCache.get(cacheKey);\n\tif (!formatter) {\n\t\tformatter = new IntlMessageFormat(message, locale);\n\t\tformatterCache.set(cacheKey, formatter);\n\t}\n\n\ttry {\n\t\tconst result = formatter.format(values as Record<string, unknown>);\n\t\treturn typeof result === 'string' ? result : String(result);\n\t} catch {\n\t\treturn message;\n\t}\n}\n\n/**\n * Translate a key using catalogs. Missing keys return the key itself.\n */\nfunction createTranslator(\n\tcatalogs: Record<string, MessageCatalog>,\n\tlocale: string,\n\tdefaultLocale: string,\n): (key: string, values?: MessageValues) => string {\n\treturn (key: string, values?: MessageValues) => {\n\t\tconst message = lookupMessage(catalogs, locale, defaultLocale, key);\n\t\tif (message == null) return key;\n\t\treturn formatMessage(message, locale, values);\n\t};\n}\n\nexport { createTranslator, formatMessage, type MessageValues };\n",
|
|
9
|
+
"import { DEFAULT_COOKIE } from './discover';\nimport { negotiateLocale, parseCookieValue } from './negotiate';\nimport { localizeContentHref, parseContentPath } from './pathname';\nimport type { LocaleResolution, LocaleResolutionInput } from './types';\n\nfunction canonicalLocale(\n\tvalue: string,\n\tavailableLocales: string[],\n): string {\n\treturn (\n\t\tavailableLocales.find((l) => l.toLowerCase() === value.toLowerCase()) ||\n\t\tvalue\n\t);\n}\n\nfunction parseLangParam(\n\tsearch: string | null | undefined,\n\tisAvailable: (locale: string | undefined) => locale is string,\n\tavailableLocales: string[],\n): string | undefined {\n\tif (!search) return undefined;\n\tconst params = new URLSearchParams(\n\t\tsearch.startsWith('?') ? search.slice(1) : search,\n\t);\n\tconst raw = params.get('lang') ?? undefined;\n\tif (!isAvailable(raw)) return undefined;\n\treturn canonicalLocale(raw, availableLocales);\n}\n\nfunction stripLangParam(search: string | null | undefined): string {\n\tif (!search) return '';\n\tconst params = new URLSearchParams(\n\t\tsearch.startsWith('?') ? search.slice(1) : search,\n\t);\n\tparams.delete('lang');\n\tconst rest = params.toString();\n\treturn rest ? `?${rest}` : '';\n}\n\n/**\n * Resolve active locale: `?lang=` → content URL → cookie → Accept-Language → default.\n * `?lang=` is an explicit override (language switcher). A content URL locale\n * always beats cookie / Accept-Language.\n */\nfunction resolveLocale(input: LocaleResolutionInput): LocaleResolution {\n\tconst {\n\t\tpathname,\n\t\tsearch,\n\t\tcookieHeader,\n\t\tacceptLanguage,\n\t\tavailableLocales,\n\t\tdefaultLocale,\n\t\tcookieName = DEFAULT_COOKIE,\n\t\tcollections,\n\t} = input;\n\n\tconst parsed = parseContentPath(pathname, availableLocales, collections);\n\tconst availableLower = new Set(availableLocales.map((l) => l.toLowerCase()));\n\n\tconst isAvailable = (locale: string | undefined): locale is string =>\n\t\t!!locale && availableLower.has(locale.toLowerCase());\n\n\tconst routingPathname = parsed?.routingPathname ?? pathname.split('?')[0];\n\tconst restSearch = stripLangParam(search);\n\n\t// 1) Explicit ?lang= override — then 302 to the canonical URL without the param\n\tconst queryLocale = parseLangParam(search, isAvailable, availableLocales);\n\tif (queryLocale) {\n\t\tconst target = parsed\n\t\t\t? localizeContentHref(\n\t\t\t\t\tparsed.collection,\n\t\t\t\t\tparsed.name,\n\t\t\t\t\tqueryLocale,\n\t\t\t\t\tdefaultLocale,\n\t\t\t\t)\n\t\t\t: routingPathname;\n\t\treturn {\n\t\t\tlocale: queryLocale,\n\t\t\troutingPathname,\n\t\t\tfromPath: false,\n\t\t\tfromQuery: true,\n\t\t\tredirectTo: `${target}${restSearch}`,\n\t\t\tparsed: parsed\n\t\t\t\t? {\n\t\t\t\t\t\t...parsed,\n\t\t\t\t\t\tlocale: queryLocale === defaultLocale ? undefined : queryLocale,\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t\t};\n\t}\n\n\t// 2) Locale from content URL — URL wins over cookie / Accept-Language\n\tif (parsed?.locale && isAvailable(parsed.locale)) {\n\t\tconst locale = canonicalLocale(parsed.locale, availableLocales);\n\n\t\tif (\n\t\t\tlocale === defaultLocale &&\n\t\t\tparsed.pathname !== parsed.routingPathname\n\t\t) {\n\t\t\treturn {\n\t\t\t\tlocale,\n\t\t\t\troutingPathname: parsed.routingPathname,\n\t\t\t\tfromPath: true,\n\t\t\t\tfromQuery: false,\n\t\t\t\tredirectTo: `${parsed.routingPathname}${restSearch}`,\n\t\t\t\tparsed: { ...parsed, locale },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tlocale,\n\t\t\troutingPathname: parsed.routingPathname,\n\t\t\tfromPath: true,\n\t\t\tfromQuery: false,\n\t\t\tparsed: { ...parsed, locale },\n\t\t};\n\t}\n\n\t// 3) Cookie\n\tconst cookieLocale = parseCookieValue(cookieHeader, cookieName);\n\tif (isAvailable(cookieLocale)) {\n\t\tconst locale = canonicalLocale(cookieLocale, availableLocales);\n\t\treturn {\n\t\t\tlocale,\n\t\t\troutingPathname,\n\t\t\tfromPath: false,\n\t\t\tfromQuery: false,\n\t\t\tparsed: parsed ? { ...parsed, locale: undefined } : undefined,\n\t\t};\n\t}\n\n\t// 4) Accept-Language\n\tconst negotiated = negotiateLocale(\n\t\tacceptLanguage,\n\t\tavailableLocales,\n\t\tdefaultLocale,\n\t);\n\n\treturn {\n\t\tlocale: negotiated,\n\t\troutingPathname,\n\t\tfromPath: false,\n\t\tfromQuery: false,\n\t\tparsed: parsed ? { ...parsed, locale: undefined } : undefined,\n\t};\n}\n\n/**\n * If content negotiation wants a non-default locale on a default content URL,\n * return the localized path to redirect to.\n */\nfunction contentNegotiateRedirect(\n\tresolution: LocaleResolution,\n\tdefaultLocale: string,\n\tenabled: boolean,\n): string | undefined {\n\tif (!enabled) return undefined;\n\tif (resolution.fromPath || resolution.fromQuery) return undefined;\n\tif (resolution.locale === defaultLocale) return undefined;\n\tconst parsed = resolution.parsed;\n\tif (!parsed) return undefined;\n\treturn localizeContentHref(\n\t\tparsed.collection,\n\t\tparsed.name,\n\t\tresolution.locale,\n\t\tdefaultLocale,\n\t);\n}\n\nexport { contentNegotiateRedirect, resolveLocale };\n",
|
|
10
|
+
"export { getMessage, lookupMessage } from './catalog';\nexport {\n\tDEFAULT_COOKIE,\n\tDEFAULT_LOCALE,\n\tDEFAULT_LOCALES_DIR,\n\tdiscoverI18n,\n} from './discover';\nexport {\n\tcreateTranslator,\n\tformatMessage,\n\ttype MessageValues,\n} from './messages';\nexport {\n\tbuildLocaleCookie,\n\tnegotiateLocale,\n\tparseCookieValue,\n} from './negotiate';\nexport {\n\tcontentKey,\n\threflangTags,\n\tlocalizeContentHref,\n\tlocalizeHref,\n\tlooksLikeLocale,\n\tnormalizePathname,\n\tparseContentPath,\n\ttoOgLocale,\n} from './pathname';\nexport { contentNegotiateRedirect, resolveLocale } from './resolve';\nexport type {\n\tI18nOptions,\n\tLocaleResolution,\n\tLocaleResolutionInput,\n\tMessageCatalog,\n\tParsedContentPath,\n\tResolvedI18n,\n} from './types';\n",
|
|
11
|
+
"import { AsyncLocalStorage } from 'node:async_hooks';\n\n/**\n * Context attributes storage using AsyncLocalStorage for request-scoped context\n * This ensures concurrent requests don't mix context data\n */\nconst contextStorage = new AsyncLocalStorage<Record<string, string>>();\n\n/**\n * Set context attributes that will be automatically attached to all telemetry\n *\n * All attributes are prefixed with 'context.' when attached to spans/logs.\n *\n * **Privacy-first:** You control what data to track. No assumptions about PII.\n *\n * @example\n * ```typescript\n * // In your auth middleware\n * setContext({\n * 'user_id': user.id,\n * 'plan': user.subscription,\n * 'region': user.region,\n * });\n *\n * // Later, when an error happens, telemetry will include:\n * // context.user_id, context.plan, context.region\n * ```\n *\n * @param attributes - Key-value pairs to attach to telemetry (values must be strings)\n */\nexport function setContext(attributes: Record<string, string>): void {\n\tconst current = contextStorage.getStore() || {};\n\tconst updated = { ...current, ...attributes };\n\n\t// Store the updated attributes in the current async context\n\t// Note: This only works within an async context (e.g., inside a request handler)\n\t// For server-wide context (rare), use a global Map with request IDs instead\n\ttry {\n\t\t// If we're already in an async context, merge\n\t\tif (contextStorage.getStore()) {\n\t\t\t// AsyncLocalStorage doesn't have a way to update in place\n\t\t\t// So we need to be called from within the context that was created\n\t\t\t// This is typically fine since it's called from request handlers\n\t\t}\n\t} catch {\n\t\t// Not in async context - will be set when request starts\n\t}\n\n\t// Store globally as fallback for non-async contexts\n\tglobalContextAttributes = updated;\n}\n\n/**\n * Clear all context attributes\n *\n * Call this when a user logs out or when you want to reset context\n *\n * @example\n * ```typescript\n * app.post('/logout', async () => {\n * clearContext();\n * // ... logout logic\n * });\n * ```\n */\nexport function clearContext(): void {\n\tglobalContextAttributes = {};\n}\n\n/**\n * Get current context attributes\n * Returns attributes from async context if available, otherwise from global fallback\n *\n * @internal\n */\nexport function getContextAttr(): Record<string, string> {\n\treturn contextStorage.getStore() || globalContextAttributes || {};\n}\n\n// Global fallback for non-async contexts\nlet globalContextAttributes: Record<string, string> = {};\n\n/**\n * Get context attributes formatted for OpenTelemetry spans/logs\n * Prefixes all keys with 'context.'\n *\n * @internal\n */\nexport function getContextAttributesForTelemetry(): Record<string, string> {\n\tconst attrs = getContextAttr();\n\tconst prefixed: Record<string, string> = {};\n\n\tfor (const [key, value] of Object.entries(attrs)) {\n\t\tprefixed[`context.${key}`] = value;\n\t}\n\n\treturn prefixed;\n}\n",
|
|
12
|
+
"export * from './extractor';\nexport * from './presets';\n",
|
|
13
|
+
"/**\n * Telemetry utilities for instrumenting operations\n *\n * This module provides helpers to create spans for custom instrumentation:\n * - API endpoints\n * - Database queries\n * - External service calls\n * - Business logic operations\n */\n\nimport { context, type Span, SpanStatusCode, trace } from '@opentelemetry/api';\n\n// Check if OpenTelemetry is available (lazy loaded to avoid requiring it)\nlet otelAvailable: boolean | undefined;\n\nfunction isOtelAvailable(): boolean {\n\tif (otelAvailable !== undefined) return otelAvailable;\n\n\ttry {\n\t\t// Check if @opentelemetry/api is available\n\t\trequire('@opentelemetry/api');\n\t\totelAvailable = true;\n\t\treturn true;\n\t} catch {\n\t\totelAvailable = false;\n\t\treturn false;\n\t}\n}\n\n/**\n * No-op span implementation when OpenTelemetry is not available\n * Implements the Span interface but all methods do nothing\n */\nclass NoopSpan implements Span {\n\tspanContext() {\n\t\treturn {\n\t\t\ttraceId: '',\n\t\t\tspanId: '',\n\t\t\ttraceFlags: 0,\n\t\t};\n\t}\n\tsetAttribute() {\n\t\treturn this;\n\t}\n\tsetAttributes() {\n\t\treturn this;\n\t}\n\taddEvent() {\n\t\treturn this;\n\t}\n\taddLink() {\n\t\treturn this;\n\t}\n\taddLinks() {\n\t\treturn this;\n\t}\n\tsetStatus() {\n\t\treturn this;\n\t}\n\tupdateName() {\n\t\treturn this;\n\t}\n\tend() {}\n\tisRecording() {\n\t\treturn false;\n\t}\n\trecordException() {}\n}\n\n// Singleton instance\nconst noopSpan = new NoopSpan();\n\n/**\n * Execute a function within a named span for distributed tracing\n *\n * @example\n * ```ts\n * const result = await withSpan('database.query', async (span) => {\n * span.setAttribute('query', 'SELECT * FROM users');\n * return await db.query('SELECT * FROM users');\n * });\n * ```\n */\nexport async function withSpan<T>(\n\tname: string,\n\tfn: (span: Span) => Promise<T>,\n\tattributes?: Record<string, string | number | boolean>,\n): Promise<T> {\n\tif (!isOtelAvailable()) {\n\t\treturn fn(noopSpan);\n\t}\n\n\tconst tracer = trace.getTracer('reroute');\n\treturn tracer.startActiveSpan(name, { attributes }, async (span) => {\n\t\ttry {\n\t\t\tconst result = await fn(span);\n\t\t\tspan.end();\n\t\t\treturn result;\n\t\t} catch (error) {\n\t\t\tspan.recordException(error as Error);\n\t\t\tspan.setStatus({\n\t\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t\tspan.end();\n\t\t\tthrow error;\n\t\t}\n\t});\n}\n\n/**\n * Execute a synchronous function within a named span\n */\nexport function withSpanSync<T>(\n\tname: string,\n\tfn: (span: Span) => T,\n\tattributes?: Record<string, string | number | boolean>,\n): T {\n\tif (!isOtelAvailable()) {\n\t\treturn fn(noopSpan);\n\t}\n\n\tconst tracer = trace.getTracer('reroute');\n\treturn tracer.startActiveSpan(name, { attributes }, (span) => {\n\t\ttry {\n\t\t\tconst result = fn(span);\n\t\t\tspan.end();\n\t\t\treturn result;\n\t\t} catch (error) {\n\t\t\tspan.recordException(error as Error);\n\t\t\tspan.setStatus({\n\t\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t\tspan.end();\n\t\t\tthrow error;\n\t\t}\n\t});\n}\n\n/**\n * Add attributes to the current active span (if available)\n */\nexport function setSpanAttributes(\n\tattributes: Record<string, string | number | boolean>,\n) {\n\tif (!isOtelAvailable()) return;\n\n\tconst span = trace.getActiveSpan();\n\tif (span) {\n\t\tspan.setAttributes(attributes);\n\t}\n}\n\n/**\n * Record an event in the current active span\n */\nexport function addSpanEvent(\n\tname: string,\n\tattributes?: Record<string, string | number | boolean>,\n) {\n\tif (!isOtelAvailable()) return;\n\n\tconst span = trace.getActiveSpan();\n\tif (span) {\n\t\tspan.addEvent(name, attributes);\n\t}\n}\n\n/**\n * Execute a function within the context of a parent span\n * This ensures child spans created inside the function are properly nested\n *\n * @example\n * ```ts\n * const parentSpan = store.telemetrySpan;\n * await withParentSpan(parentSpan, async () => {\n * // Any spans created here will be children of parentSpan\n * await withSpan('child.operation', async () => {...});\n * });\n * ```\n */\nexport async function withParentSpan<T>(\n\tparentSpan: Span | undefined | null,\n\tfn: () => Promise<T>,\n): Promise<T> {\n\tif (!(isOtelAvailable() && parentSpan)) {\n\t\treturn fn();\n\t}\n\n\t// Set the parent span as active in the context\n\tconst ctx = trace.setSpan(context.active(), parentSpan);\n\treturn context.with(ctx, fn);\n}\n\n/**\n * Execute a synchronous function within the context of a parent span\n */\nexport function withParentSpanSync<T>(\n\tparentSpan: Span | undefined | null,\n\tfn: () => T,\n): T {\n\tif (!(isOtelAvailable() && parentSpan)) {\n\t\treturn fn();\n\t}\n\n\tconst ctx = trace.setSpan(context.active(), parentSpan);\n\treturn context.with(ctx, fn);\n}\n\n/**\n * Extract W3C Trace Context from incoming HTTP request headers\n * Returns the extracted context or undefined if not available\n *\n * @example\n * ```ts\n * const traceContext = extractTraceContext(request.headers);\n * ```\n */\nexport function extractTraceContext(\n\theaders: Headers | Record<string, string | undefined>,\n): ReturnType<typeof context.active> | undefined {\n\tif (!isOtelAvailable()) return undefined;\n\n\ttry {\n\t\tconst { propagation } = require('@opentelemetry/api');\n\n\t\t// Create a carrier object that matches the W3C Trace Context format\n\t\tconst carrier: Record<string, string | undefined> = {};\n\n\t\tif (headers instanceof Headers) {\n\t\t\tcarrier.traceparent = headers.get('traceparent') || undefined;\n\t\t\tcarrier.tracestate = headers.get('tracestate') || undefined;\n\t\t} else {\n\t\t\tcarrier.traceparent = headers.traceparent;\n\t\t\tcarrier.tracestate = headers.tracestate;\n\t\t}\n\n\t\t// If no traceparent header, return undefined\n\t\tif (!carrier.traceparent) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// Extract context from carrier\n\t\treturn propagation.extract(context.active(), carrier);\n\t} catch (error) {\n\t\tconsole.error('[telemetry] Failed to extract trace context:', error);\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Get the trace context from a span for injection into browser\n * Returns an object with traceparent and tracestate (if available)\n *\n * @example\n * ```ts\n * const span = store.telemetrySpan;\n * const traceContext = getTraceContextForInjection(span);\n * // Inject into HTML: <script>window.__TRACE_CONTEXT__ = {...traceContext};</script>\n * ```\n */\nexport function getTraceContextForInjection(\n\tspan: Span | undefined | null,\n): { traceparent?: string; tracestate?: string } | undefined {\n\tif (!(isOtelAvailable() && span)) return undefined;\n\n\ttry {\n\t\tconst { propagation } = require('@opentelemetry/api');\n\t\tconst carrier: Record<string, string | undefined> = {};\n\n\t\t// Set the span as active and inject context\n\t\tconst ctx = trace.setSpan(context.active(), span);\n\t\tpropagation.inject(ctx, carrier);\n\n\t\treturn {\n\t\t\ttraceparent: carrier.traceparent,\n\t\t\ttracestate: carrier.tracestate,\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\n\t\t\t'[telemetry] Failed to get trace context for injection:',\n\t\t\terror,\n\t\t);\n\t\treturn undefined;\n\t}\n}\n",
|
|
14
|
+
"import { promises as fs } from 'node:fs';\nimport { join } from 'node:path';\nimport { SourceMapConsumer } from 'source-map';\n\ninterface SourceMappedPosition {\n\tsource: string | null;\n\tline: number | null;\n\tcolumn: number | null;\n\tname: string | null;\n}\n\ninterface StackFrame {\n\traw: string;\n\tfunctionName?: string;\n\tfileName?: string;\n\tlineNumber?: number;\n\tcolumnNumber?: number;\n\tmapped?: SourceMappedPosition;\n}\n\n/**\n * Cache for loaded sourcemaps\n */\nconst sourcemapCache = new Map<string, SourceMapConsumer>();\n\n/**\n * Parse a stack trace line into a structured frame\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Stack frame parsing requires many conditions\nfunction parseStackFrame(line: string): StackFrame {\n\t// Common stack trace patterns:\n\t// at functionName (file:line:column)\n\t// at file:line:column\n\t// at <anonymous>\n\n\tconst patterns = [\n\t\t// Chrome/V8: at functionName (file:line:column)\n\t\t/^\\s*at\\s+(.+?)\\s+\\((.+?):(\\d+):(\\d+)\\)/,\n\t\t// Chrome/V8: at file:line:column\n\t\t/^\\s*at\\s+(.+?):(\\d+):(\\d+)/,\n\t\t// Firefox: functionName@file:line:column\n\t\t/^(.+?)@(.+?):(\\d+):(\\d+)/,\n\t];\n\n\tfor (const pattern of patterns) {\n\t\tconst match = line.match(pattern);\n\t\tif (match) {\n\t\t\tif (match.length === 5 && match[2]) {\n\t\t\t\t// Pattern with function name\n\t\t\t\treturn {\n\t\t\t\t\traw: line,\n\t\t\t\t\tfunctionName: match[1].trim(),\n\t\t\t\t\tfileName: match[2],\n\t\t\t\t\tlineNumber: Number.parseInt(match[3], 10),\n\t\t\t\t\tcolumnNumber: Number.parseInt(match[4], 10),\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (match.length === 4 && match[1]) {\n\t\t\t\t// Pattern without function name or Firefox pattern\n\t\t\t\tconst hasAt = line.includes('@');\n\t\t\t\treturn {\n\t\t\t\t\traw: line,\n\t\t\t\t\tfunctionName: hasAt ? match[1] : undefined,\n\t\t\t\t\tfileName: hasAt ? match[2] : match[1],\n\t\t\t\t\tlineNumber: Number.parseInt(hasAt ? match[3] : match[2], 10),\n\t\t\t\t\tcolumnNumber: Number.parseInt(hasAt ? match[4] : match[3], 10),\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { raw: line };\n}\n\n/**\n * Find and load a sourcemap for a given file\n */\nasync function loadSourceMap(\n\tfileName: string,\n\trerouteDir?: string,\n): Promise<SourceMapConsumer | null> {\n\t// Check cache first\n\tif (sourcemapCache.has(fileName)) {\n\t\treturn sourcemapCache.get(fileName) || null;\n\t}\n\n\ttry {\n\t\t// Common locations to check for sourcemaps\n\t\tconst possiblePaths: string[] = [];\n\n\t\t// If we have a .reroute directory, look there\n\t\tif (rerouteDir) {\n\t\t\t// Extract just the filename from the path\n\t\t\tconst baseName = fileName.split('/').pop() || fileName;\n\t\t\tpossiblePaths.push(join(rerouteDir, 'bundles', `${baseName}.map`));\n\t\t}\n\n\t\t// Also try adjacent to the file\n\t\tpossiblePaths.push(`${fileName}.map`);\n\n\t\tfor (const mapPath of possiblePaths) {\n\t\t\ttry {\n\t\t\t\tconst mapContent = await fs.readFile(mapPath, 'utf-8');\n\t\t\t\tconst consumer = await new SourceMapConsumer(mapContent);\n\t\t\t\tsourcemapCache.set(fileName, consumer);\n\t\t\t\treturn consumer;\n\t\t\t} catch {}\n\t\t}\n\n\t\treturn null;\n\t} catch (error) {\n\t\tconsole.error(\n\t\t\t`[sourcemap] Failed to load sourcemap for ${fileName}:`,\n\t\t\terror,\n\t\t);\n\t\treturn null;\n\t}\n}\n\n/**\n * Map a position using a sourcemap consumer\n */\nfunction mapPosition(\n\tconsumer: SourceMapConsumer,\n\tline: number,\n\tcolumn: number,\n): SourceMappedPosition {\n\tconst pos = consumer.originalPositionFor({ line, column });\n\treturn {\n\t\tsource: pos.source,\n\t\tline: pos.line,\n\t\tcolumn: pos.column,\n\t\tname: pos.name,\n\t};\n}\n\n/**\n * Process a stack trace and resolve source positions\n */\nexport async function resolveStackTrace(\n\tstack: string,\n\toptions: {\n\t\trerouteDir?: string;\n\t\tcwd?: string;\n\t} = {},\n): Promise<{\n\toriginal: string;\n\tframes: StackFrame[];\n\tmapped: string;\n}> {\n\tconst lines = stack.split('\\n');\n\tconst frames: StackFrame[] = [];\n\n\tfor (const line of lines) {\n\t\tconst frame = parseStackFrame(line);\n\t\tif (frame?.fileName && frame.lineNumber && frame.columnNumber) {\n\t\t\ttry {\n\t\t\t\tconst consumer = await loadSourceMap(\n\t\t\t\t\tframe.fileName,\n\t\t\t\t\toptions.rerouteDir,\n\t\t\t\t);\n\t\t\t\tif (consumer) {\n\t\t\t\t\tframe.mapped = mapPosition(\n\t\t\t\t\t\tconsumer,\n\t\t\t\t\t\tframe.lineNumber,\n\t\t\t\t\t\tframe.columnNumber,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch (_error) {\n\t\t\t\t// Keep frame without mapping\n\t\t\t}\n\t\t}\n\t\tframes.push(frame);\n\t}\n\n\t// Generate mapped stack trace\n\tconst mappedLines = frames.map((frame) => {\n\t\tif (frame.mapped?.source && frame.mapped.line !== null) {\n\t\t\tconst location = `${frame.mapped.source}:${frame.mapped.line}:${frame.mapped.column ?? 0}`;\n\t\t\tif (frame.mapped.name || frame.functionName) {\n\t\t\t\treturn ` at ${frame.mapped.name || frame.functionName} (${location})`;\n\t\t\t}\n\t\t\treturn ` at ${location}`;\n\t\t}\n\t\treturn frame.raw;\n\t});\n\n\treturn {\n\t\toriginal: stack,\n\t\tframes,\n\t\tmapped: mappedLines.join('\\n'),\n\t};\n}\n\n/**\n * Enhance error with source-mapped stack trace\n * Adds a `sourceMappedStack` property to the error object\n */\nexport async function enhanceError(\n\terror: Error,\n\toptions: {\n\t\trerouteDir?: string;\n\t\tcwd?: string;\n\t} = {},\n): Promise<Error & { sourceMappedStack?: string; stackFrames?: StackFrame[] }> {\n\tif (!error.stack) return error;\n\n\ttry {\n\t\tconst result = await resolveStackTrace(error.stack, options);\n\t\t// Attach enhanced info to the error object\n\t\t(\n\t\t\terror as Error & {\n\t\t\t\tsourceMappedStack?: string;\n\t\t\t\tstackFrames?: StackFrame[];\n\t\t\t}\n\t\t).sourceMappedStack = result.mapped;\n\t\t(error as Error & { stackFrames?: StackFrame[] }).stackFrames =\n\t\t\tresult.frames;\n\t} catch {\n\t\t// Silently fail - source mapping is optional\n\t}\n\n\treturn error;\n}\n\n/**\n * Clear the sourcemap cache (useful for development)\n */\nexport function clearSourceMapCache(): void {\n\tfor (const consumer of sourcemapCache.values()) {\n\t\tconsumer.destroy();\n\t}\n\tsourcemapCache.clear();\n}\n\n/**\n * Format stack frames for display\n */\nexport function formatStackFrames(frames: StackFrame[]): string {\n\treturn frames\n\t\t.map((frame) => {\n\t\t\tif (frame.mapped?.source && frame.mapped.line !== null) {\n\t\t\t\tconst location = `${frame.mapped.source}:${frame.mapped.line}:${frame.mapped.column ?? 0}`;\n\t\t\t\tif (frame.mapped.name || frame.functionName) {\n\t\t\t\t\treturn ` at ${frame.mapped.name || frame.functionName} (${location})`;\n\t\t\t\t}\n\t\t\t\treturn ` at ${location}`;\n\t\t\t}\n\t\t\treturn frame.raw;\n\t\t})\n\t\t.join('\\n');\n}\n",
|
|
15
|
+
"import os from 'node:os';\nimport {\n\ttype Counter,\n\tcontext,\n\ttype Histogram,\n\tmetrics,\n\ttype Span,\n\tSpanStatusCode,\n\ttrace,\n\ttype UpDownCounter,\n} from '@opentelemetry/api';\nimport { logs, SeverityNumber } from '@opentelemetry/api-logs';\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto';\nimport { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';\nimport { resourceFromAttributes } from '@opentelemetry/resources';\nimport {\n\tBatchLogRecordProcessor,\n\tLoggerProvider,\n} from '@opentelemetry/sdk-logs';\nimport {\n\tMeterProvider,\n\tPeriodicExportingMetricReader,\n} from '@opentelemetry/sdk-metrics';\nimport {\n\tBatchSpanProcessor,\n\tNodeTracerProvider,\n} from '@opentelemetry/sdk-trace-node';\nimport {\n\tATTR_SERVICE_NAME,\n\tATTR_SERVICE_VERSION,\n} from '@opentelemetry/semantic-conventions';\nimport { ATTR_HOST_NAME } from '@opentelemetry/semantic-conventions/incubating';\nimport type { Elysia } from 'elysia';\nimport { loadConfig, type TelemetryOptions } from 'reroute-js/core';\nimport { getContextAttributesForTelemetry } from './context';\nimport { extractHeaders } from './headers/extractor';\nimport { extractTraceContext } from './instrumentation';\nimport { enhanceError } from './sourcemap';\n\n/**\n * Server telemetry options\n * Extends TelemetryOptions from config - all options can be set in reroute.config.ts\n * or passed directly here (direct options override config values)\n */\nexport interface ServerTelemetryOptions extends TelemetryOptions {}\n\n// Helper to extract error details\nfunction getErrorDetails(error: unknown): {\n\tname: string;\n\tmessage: string;\n\tstack: string;\n} {\n\tif (error instanceof Error) {\n\t\treturn {\n\t\t\tname: error.name,\n\t\t\tmessage: error.message,\n\t\t\tstack: error.stack || '',\n\t\t};\n\t}\n\n\tif (typeof error === 'object' && error !== null) {\n\t\tconst err = error as Record<string, unknown>;\n\t\treturn {\n\t\t\tname: String(err.name || err.code || 'UnknownError'),\n\t\t\tmessage: String(err.message || err.error || JSON.stringify(error)),\n\t\t\tstack: String(err.stack || ''),\n\t\t};\n\t}\n\n\treturn {\n\t\tname: 'UnknownError',\n\t\tmessage: String(error),\n\t\tstack: '',\n\t};\n}\n\n/**\n * Check if a route should be ignored based on ignore patterns\n * Supports strings, RegExp, and custom functions\n */\nfunction shouldIgnoreRoute(\n\tpatterns: (\n\t\t| string\n\t\t| RegExp\n\t\t| ((pathname: string, method: string) => boolean)\n\t)[],\n\tpathname: string,\n\tmethod: string,\n): boolean {\n\treturn patterns.some((pattern) => {\n\t\tif (typeof pattern === 'string') {\n\t\t\treturn pathname.includes(pattern);\n\t\t}\n\t\tif (pattern instanceof RegExp) {\n\t\t\treturn pattern.test(pathname);\n\t\t}\n\t\tif (typeof pattern === 'function') {\n\t\t\treturn pattern(pathname, method);\n\t\t}\n\t\treturn false;\n\t});\n}\n\n/**\n * Determine if a request should be sampled based on sample rate\n * Uses deterministic sampling based on trace ID for consistency\n * @param rate - Sample rate between 0.0 and 1.0\n * @param traceId - Trace ID for deterministic sampling (optional)\n * @returns true if request should be sampled\n */\nfunction shouldSample(rate: number, traceId?: string): boolean {\n\tif (rate >= 1.0) return true;\n\tif (rate <= 0.0) return false;\n\n\t// Use deterministic sampling if trace ID provided\n\tif (traceId) {\n\t\t// Convert first 8 chars of trace ID to number for consistent sampling\n\t\tconst hash = Number.parseInt(traceId.slice(0, 8), 16);\n\t\treturn hash % 100 < rate * 100;\n\t}\n\n\t// Fall back to random sampling\n\treturn Math.random() < rate;\n}\n\n/**\n * Server-side telemetry plugin for Elysia\n *\n * Automatically captures:\n * - HTTP requests and responses\n * - Errors with full stack traces\n * - System metrics (CPU, memory)\n * - Request/response sizes\n * - Active connections\n *\n * Configuration is loaded from reroute.config.ts automatically.\n * Direct options override config values.\n *\n * @example\n * ```typescript\n * import { Elysia } from 'elysia';\n * import { telemetry } from 'reroute-js/telemetry/server';\n *\n * new Elysia()\n * .use(telemetry({\n * serviceName: 'My App',\n * }))\n * .listen(3000);\n * ```\n */\nexport function telemetry(options: ServerTelemetryOptions = {}) {\n\t// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Telemetry initialization with config loading and feature toggles\n\treturn async (app: Elysia) => {\n\t\t// Load reroute.config.ts automatically\n\t\tconst cwd =\n\t\t\ttypeof process !== 'undefined' && typeof process.cwd === 'function'\n\t\t\t\t? process.cwd()\n\t\t\t\t: '/';\n\t\tconst config = await loadConfig(cwd);\n\n\t\t// Merge config with direct options (direct options take precedence)\n\t\tconst merged = {\n\t\t\t...config.telemetry,\n\t\t\t...options,\n\t\t};\n\n\t\t// Check if telemetry is enabled (default: false)\n\t\tconst TELEMETRY_ENABLED = merged.enabled ?? false;\n\t\tif (!TELEMETRY_ENABLED) {\n\t\t\tconsole.log(\n\t\t\t\t'[telemetry] Disabled - set enabled: true in config or options to enable',\n\t\t\t);\n\t\t\t// Return app unchanged when disabled\n\t\t\treturn app;\n\t\t}\n\n\t\t// Extract configuration values\n\t\tconst SERVICE_NAME = merged.serviceName || 'reroute-app';\n\t\tconst SERVICE_VERSION =\n\t\t\tmerged.serviceVersion || process.env.npm_package_version || '1.0.0';\n\t\tconst OTLP_ENDPOINT =\n\t\t\tmerged.otlpEndpoint ||\n\t\t\tprocess.env.OTEL_EXPORTER_OTLP_ENDPOINT ||\n\t\t\t'http://localhost:4318';\n\t\tconst METRICS_INTERVAL = merged.metricsInterval ?? 60000; // Default: 60s (was 5s, caused memory leaks)\n\n\t\t// Handle proxy configuration\n\t\tconst proxyConfig = merged.proxy;\n\t\tconst ENABLE_PROXY = proxyConfig?.enabled ?? !!proxyConfig;\n\t\tconst PROXY_PATHNAME = proxyConfig?.pathname ?? '/__reroute_telemetry';\n\t\tconst PROXY_VERBOSE = proxyConfig?.verbose ?? false;\n\t\tconst PROXY_HANDLER = proxyConfig?.handler;\n\n\t\t// Feature toggles (default: true when telemetry is enabled)\n\t\tconst ENABLE_TRACES = merged.enableTraces ?? true;\n\t\tconst ENABLE_METRICS = merged.enableMetrics ?? true;\n\t\tconst ENABLE_LOGS = merged.enableLogs ?? true;\n\t\tconst ENABLE_SYSTEM_METRICS = merged.enableSystemMetrics ?? true;\n\n\t\t// Sampling configuration\n\t\t// Default: Sample 20% of normal traffic, 100% of errors\n\t\t// This reduces telemetry volume by 80% while keeping all errors\n\t\tconst SAMPLE_RATE = merged.sampleRate ?? 0.2;\n\t\tconst ERROR_SAMPLE_RATE = merged.errorSampleRate ?? 1.0;\n\n\t\t// Route filtering\n\t\tconst IGNORE_ROUTES = merged.ignoreRoutes || [];\n\n\t\t// Header capture and custom attributes\n\t\tconst CAPTURE_MAPPINGS = merged.capture || [];\n\t\tconst CUSTOM_ATTRIBUTES = merged.customAttributes || {};\n\n\t\tconsole.log(`[telemetry] Initializing with endpoint: ${OTLP_ENDPOINT}`);\n\t\tconsole.log(\n\t\t\t`[telemetry] Features - Traces: ${ENABLE_TRACES}, Metrics: ${ENABLE_METRICS}, Logs: ${ENABLE_LOGS}`,\n\t\t);\n\t\tif (SAMPLE_RATE < 1.0) {\n\t\t\tconsole.log(\n\t\t\t\t`[telemetry] Sampling - Requests: ${SAMPLE_RATE * 100}%, Errors: ${ERROR_SAMPLE_RATE * 100}%`,\n\t\t\t);\n\t\t}\n\t\tif (ENABLE_PROXY) {\n\t\t\tconsole.log(\n\t\t\t\t`[telemetry] Browser telemetry proxy enabled at ${PROXY_PATHNAME}/*`,\n\t\t\t);\n\t\t}\n\n\t\t// Secure headers for API authentication (optional)\n\t\tconst otlpHeaders: Record<string, string> = {};\n\t\tif (merged.apiKey || process.env.SIGNOZ_API_KEY) {\n\t\t\totlpHeaders.Authorization = `Bearer ${merged.apiKey || process.env.SIGNOZ_API_KEY}`;\n\t\t}\n\n\t\t// Configure OTLP exporters with timeouts (conditionally based on feature toggles)\n\t\tconst traceExporter = ENABLE_TRACES\n\t\t\t? new OTLPTraceExporter({\n\t\t\t\t\turl: `${OTLP_ENDPOINT}/v1/traces`,\n\t\t\t\t\theaders:\n\t\t\t\t\t\tObject.keys(otlpHeaders).length > 0 ? otlpHeaders : undefined,\n\t\t\t\t\ttimeoutMillis: 5000, // 5 second timeout\n\t\t\t\t})\n\t\t\t: null;\n\n\t\tconst logExporter = ENABLE_LOGS\n\t\t\t? new OTLPLogExporter({\n\t\t\t\t\turl: `${OTLP_ENDPOINT}/v1/logs`,\n\t\t\t\t\theaders:\n\t\t\t\t\t\tObject.keys(otlpHeaders).length > 0 ? otlpHeaders : undefined,\n\t\t\t\t\ttimeoutMillis: 5000, // 5 second timeout\n\t\t\t\t})\n\t\t\t: null;\n\n\t\tconst metricExporter = ENABLE_METRICS\n\t\t\t? new OTLPMetricExporter({\n\t\t\t\t\turl: `${OTLP_ENDPOINT}/v1/metrics`,\n\t\t\t\t\theaders:\n\t\t\t\t\t\tObject.keys(otlpHeaders).length > 0 ? otlpHeaders : undefined,\n\t\t\t\t\ttimeoutMillis: 5000, // 5 second timeout\n\t\t\t\t})\n\t\t\t: null;\n\n\t\t// Create resource with service information\n\t\t// In Docker, prefer env var over os.hostname() (which returns container ID)\n\t\tconst HOSTNAME =\n\t\t\tprocess.env.OTEL_RESOURCE_ATTRIBUTES?.match(/host\\.name=([^,]+)/)?.[1] ||\n\t\t\tprocess.env.HOSTNAME ||\n\t\t\tos.hostname();\n\n\t\tconst resource = resourceFromAttributes({\n\t\t\t[ATTR_SERVICE_NAME]: SERVICE_NAME,\n\t\t\t[ATTR_SERVICE_VERSION]: SERVICE_VERSION,\n\t\t\t[ATTR_HOST_NAME]: HOSTNAME,\n\t\t});\n\n\t\t// Set up Traces with error handling (only if enabled)\n\t\tif (ENABLE_TRACES && traceExporter) {\n\t\t\ttry {\n\t\t\t\tconst tracerProvider = new NodeTracerProvider({\n\t\t\t\t\tresource,\n\t\t\t\t\t// Note: We use middleware-level sampling instead of SDK sampling\n\t\t\t\t\t// This allows us to preserve 100% of errors while sampling normal traffic\n\t\t\t\t\tspanProcessors: [new BatchSpanProcessor(traceExporter)],\n\t\t\t\t});\n\t\t\t\ttracerProvider.register();\n\n\t\t\t\tif (SAMPLE_RATE < 1.0 || ERROR_SAMPLE_RATE < 1.0) {\n\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t`[telemetry] Middleware sampling: ${SAMPLE_RATE * 100}% normal, ${ERROR_SAMPLE_RATE * 100}% errors`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('[telemetry] Failed to initialize tracer:', error);\n\t\t\t}\n\t\t}\n\n\t\t// Set up Logs with error handling (only if enabled)\n\t\tlet logger: ReturnType<LoggerProvider['getLogger']> | null = null;\n\t\tif (ENABLE_LOGS && logExporter) {\n\t\t\ttry {\n\t\t\t\tconst loggerProvider = new LoggerProvider({\n\t\t\t\t\tresource,\n\t\t\t\t\tprocessors: [new BatchLogRecordProcessor(logExporter)],\n\t\t\t\t});\n\t\t\t\tlogs.setGlobalLoggerProvider(loggerProvider);\n\t\t\t\tlogger = loggerProvider.getLogger(SERVICE_NAME, SERVICE_VERSION);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('[telemetry] Failed to initialize logger:', error);\n\t\t\t}\n\t\t}\n\n\t\t// Set up Metrics with error handling (only if enabled)\n\t\tlet meter: ReturnType<MeterProvider['getMeter']> | null = null;\n\t\tif (ENABLE_METRICS && metricExporter) {\n\t\t\ttry {\n\t\t\t\tconst meterProvider = new MeterProvider({\n\t\t\t\t\tresource,\n\t\t\t\t\treaders: [\n\t\t\t\t\t\tnew PeriodicExportingMetricReader({\n\t\t\t\t\t\t\texporter: metricExporter,\n\t\t\t\t\t\t\texportIntervalMillis: METRICS_INTERVAL,\n\t\t\t\t\t\t}),\n\t\t\t\t\t],\n\t\t\t\t});\n\t\t\t\tmetrics.setGlobalMeterProvider(meterProvider);\n\t\t\t\tmeter = meterProvider.getMeter(SERVICE_NAME, SERVICE_VERSION);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('[telemetry] Failed to initialize meter:', error);\n\t\t\t}\n\t\t}\n\n\t\t// Create HTTP metrics with null checks\n\t\tlet httpRequestCounter: Counter | null = null;\n\t\tlet httpRequestDuration: Histogram | null = null;\n\t\tlet httpErrorCounter: Counter | null = null;\n\t\tlet httpActiveRequests: UpDownCounter | null = null;\n\t\tlet activeConnections: UpDownCounter | null = null;\n\n\t\tif (meter) {\n\t\t\ttry {\n\t\t\t\thttpRequestCounter = meter.createCounter('http.server.request.count', {\n\t\t\t\t\tdescription: 'Total HTTP requests',\n\t\t\t\t\tunit: 'requests',\n\t\t\t\t});\n\n\t\t\t\thttpRequestDuration = meter.createHistogram(\n\t\t\t\t\t'http.server.request.duration',\n\t\t\t\t\t{\n\t\t\t\t\t\tdescription: 'HTTP request duration',\n\t\t\t\t\t\tunit: 'milliseconds',\n\t\t\t\t\t},\n\t\t\t\t);\n\n\t\t\t\thttpErrorCounter = meter.createCounter('http.server.error.count', {\n\t\t\t\t\tdescription: 'Total HTTP errors',\n\t\t\t\t\tunit: 'errors',\n\t\t\t\t});\n\n\t\t\t\thttpActiveRequests = meter.createUpDownCounter(\n\t\t\t\t\t'http.server.active_requests',\n\t\t\t\t\t{\n\t\t\t\t\t\tdescription: 'Active HTTP requests',\n\t\t\t\t\t\tunit: 'requests',\n\t\t\t\t\t},\n\t\t\t\t);\n\n\t\t\t\tactiveConnections = meter.createUpDownCounter(\n\t\t\t\t\t'app.connections.active',\n\t\t\t\t\t{\n\t\t\t\t\t\tdescription: 'Active connections',\n\t\t\t\t\t\tunit: 'connections',\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('[telemetry] Failed to create HTTP metrics:', error);\n\t\t\t}\n\t\t}\n\n\t\t// Create system metrics (optional) with error handling\n\t\tif (ENABLE_SYSTEM_METRICS && meter) {\n\t\t\ttry {\n\t\t\t\t// Reuse HOSTNAME from resource (already computed above)\n\n\t\t\t\tconst memoryUsage = meter.createObservableGauge('app.memory.usage', {\n\t\t\t\t\tdescription: 'Process memory usage',\n\t\t\t\t\tunit: 'bytes',\n\t\t\t\t});\n\n\t\t\t\tmemoryUsage.addCallback((result) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst usage = process.memoryUsage();\n\t\t\t\t\t\tresult.observe(usage.heapUsed, {\n\t\t\t\t\t\t\ttype: 'heap',\n\t\t\t\t\t\t\t'host.name': HOSTNAME,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tresult.observe(usage.rss, { type: 'rss', 'host.name': HOSTNAME });\n\t\t\t\t\t\tresult.observe(usage.external, {\n\t\t\t\t\t\t\ttype: 'external',\n\t\t\t\t\t\t\t'host.name': HOSTNAME,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error('[telemetry] Memory usage callback failed:', error);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\tconst systemCpuUsage = meter.createObservableGauge(\n\t\t\t\t\t'system.cpu.utilization',\n\t\t\t\t\t{\n\t\t\t\t\t\tdescription: 'System CPU utilization',\n\t\t\t\t\t\tunit: '1',\n\t\t\t\t\t},\n\t\t\t\t);\n\n\t\t\t\tsystemCpuUsage.addCallback((result) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst cpus = os.cpus();\n\t\t\t\t\t\tlet totalIdle = 0;\n\t\t\t\t\t\tlet totalTick = 0;\n\n\t\t\t\t\t\tcpus.forEach((cpu) => {\n\t\t\t\t\t\t\tconst times = cpu.times;\n\t\t\t\t\t\t\ttotalIdle += times.idle;\n\t\t\t\t\t\t\ttotalTick +=\n\t\t\t\t\t\t\t\ttimes.user + times.nice + times.sys + times.idle + times.irq;\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tconst usage = 1 - totalIdle / totalTick;\n\t\t\t\t\t\tresult.observe(usage, {\n\t\t\t\t\t\t\t'service.name': SERVICE_NAME,\n\t\t\t\t\t\t\t'host.name': HOSTNAME,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error('[telemetry] CPU usage callback failed:', error);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\tconst systemMemoryUsage = meter.createObservableGauge(\n\t\t\t\t\t'system.memory.utilization',\n\t\t\t\t\t{\n\t\t\t\t\t\tdescription: 'System memory utilization',\n\t\t\t\t\t\tunit: '1',\n\t\t\t\t\t},\n\t\t\t\t);\n\n\t\t\t\tsystemMemoryUsage.addCallback((result) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst totalMem = os.totalmem();\n\t\t\t\t\t\tconst freeMem = os.freemem();\n\t\t\t\t\t\tconst usage = (totalMem - freeMem) / totalMem;\n\n\t\t\t\t\t\tresult.observe(usage, {\n\t\t\t\t\t\t\t'service.name': SERVICE_NAME,\n\t\t\t\t\t\t\t'host.name': HOSTNAME,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error('[telemetry] System memory callback failed:', error);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('[telemetry] Failed to create system metrics:', error);\n\t\t\t}\n\t\t}\n\n\t\t// Graceful shutdown with error handling\n\t\tconst shutdown = async () => {\n\t\t\tconsole.log('[telemetry] Shutting down...');\n\t\t\ttry {\n\t\t\t\t// Note: We can't reference loggerProvider/meterProvider here since they're in try blocks\n\t\t\t\t// The providers will be cleaned up automatically\n\t\t\t\tawait logs.disable();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('[telemetry] Shutdown error:', error);\n\t\t\t}\n\t\t};\n\n\t\tprocess.on('SIGTERM', async () => {\n\t\t\tawait shutdown();\n\t\t\tprocess.exit(0);\n\t\t});\n\n\t\t// Initialize tracer for manual span creation\n\t\tlet tracer: ReturnType<typeof trace.getTracer> | null = null;\n\t\tif (ENABLE_TRACES && traceExporter) {\n\t\t\ttracer = trace.getTracer(SERVICE_NAME, SERVICE_VERSION);\n\t\t}\n\n\t\t// Setup global console.error capture for automatic error telemetry\n\t\t// This captures errors that are caught and logged but not thrown\n\t\t// Default to false - console capture creates spans for every error which\n\t\t// can cause significant memory growth in apps with many logged errors\n\t\tconst ENABLE_CONSOLE_CAPTURE = merged.enableConsoleCapture ?? false;\n\t\tif (ENABLE_CONSOLE_CAPTURE && tracer) {\n\t\t\tconst originalError = console.error;\n\t\t\tconst originalWarn = console.warn;\n\n\t\t\t// Don't override if already captured (e.g., in tests)\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: Console capture\n\t\t\tif (!(originalError as any).__telemetry_captured__) {\n\t\t\t\tconst errorTracer = trace.getTracer('console-error');\n\n\t\t\t\t/** biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Console error capture */\n\t\t\t\tconsole.error = (...args: unknown[]) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Create a span for this console.error\n\t\t\t\t\t\tconst span = errorTracer.startSpan('server.console.error');\n\n\t\t\t\t\t\t// Try to extract error information\n\t\t\t\t\t\tconst firstArg = args[0];\n\t\t\t\t\t\tlet errorMessage = args.map(String).join(' ');\n\t\t\t\t\t\tlet errorStack = '';\n\t\t\t\t\t\tlet errorType = 'ConsoleError';\n\n\t\t\t\t\t\tif (firstArg instanceof Error) {\n\t\t\t\t\t\t\terrorMessage = firstArg.message;\n\t\t\t\t\t\t\terrorStack = firstArg.stack || '';\n\t\t\t\t\t\t\terrorType = firstArg.name;\n\n\t\t\t\t\t\t\t// Enhance error with source maps if available\n\t\t\t\t\t\t\tconst rerouteDir = '.reroute';\n\t\t\t\t\t\t\tif (rerouteDir && firstArg.stack) {\n\t\t\t\t\t\t\t\tenhanceError(firstArg, { rerouteDir })\n\t\t\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\t\t\tconst enhancedError = firstArg as Error & {\n\t\t\t\t\t\t\t\t\t\t\tsourceMappedStack?: string;\n\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t\tif (enhancedError.sourceMappedStack) {\n\t\t\t\t\t\t\t\t\t\t\terrorStack = enhancedError.sourceMappedStack;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t.catch(() => {\n\t\t\t\t\t\t\t\t\t\t// Silently fail sourcemap enhancement\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tspan.setStatus({\n\t\t\t\t\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\t\t\t\t\tmessage: errorMessage,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tspan.setAttributes({\n\t\t\t\t\t\t\t'log.level': 'error',\n\t\t\t\t\t\t\t'log.message': errorMessage,\n\t\t\t\t\t\t\t'error.type': errorType,\n\t\t\t\t\t\t\t...(errorStack ? { 'error.stack': errorStack } : {}),\n\t\t\t\t\t\t\t'service.name': SERVICE_NAME,\n\t\t\t\t\t\t\t...CUSTOM_ATTRIBUTES,\n\t\t\t\t\t\t\t...getContextAttributesForTelemetry(),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (firstArg instanceof Error) {\n\t\t\t\t\t\t\tspan.recordException(firstArg);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tspan.end();\n\n\t\t\t\t\t\t// Also emit as log if logger is available\n\t\t\t\t\t\tif (logger) {\n\t\t\t\t\t\t\tlogger.emit({\n\t\t\t\t\t\t\t\tseverityNumber: SeverityNumber.ERROR,\n\t\t\t\t\t\t\t\tseverityText: 'ERROR',\n\t\t\t\t\t\t\t\tbody: errorMessage,\n\t\t\t\t\t\t\t\tattributes: {\n\t\t\t\t\t\t\t\t\t'log.level': 'error',\n\t\t\t\t\t\t\t\t\t'error.type': errorType,\n\t\t\t\t\t\t\t\t\t...(errorStack ? { 'error.stack': errorStack } : {}),\n\t\t\t\t\t\t\t\t\t'service.name': SERVICE_NAME,\n\t\t\t\t\t\t\t\t\t...CUSTOM_ATTRIBUTES,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Silently fail - don't break console.error\n\t\t\t\t\t}\n\n\t\t\t\t\t// Call original console.error\n\t\t\t\t\toriginalError.apply(console, args);\n\t\t\t\t};\n\n\t\t\t\t// Mark as captured to prevent double-wrapping\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: Console capture\n\t\t\t\t(console.error as any).__telemetry_captured__ = true;\n\n\t\t\t\tconsole.log('[telemetry] Console error capture enabled');\n\t\t\t}\n\n\t\t\t// Also capture console.warn for completeness\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: Console capture\n\t\t\tif (!(originalWarn as any).__telemetry_captured__) {\n\t\t\t\tconst warnTracer = trace.getTracer('console-warn');\n\n\t\t\t\tconsole.warn = (...args: unknown[]) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst span = warnTracer.startSpan('server.console.warn');\n\t\t\t\t\t\tconst message = args.map(String).join(' ');\n\n\t\t\t\t\t\tspan.setAttributes({\n\t\t\t\t\t\t\t'log.level': 'warn',\n\t\t\t\t\t\t\t'log.message': message,\n\t\t\t\t\t\t\t'service.name': SERVICE_NAME,\n\t\t\t\t\t\t\t...CUSTOM_ATTRIBUTES,\n\t\t\t\t\t\t\t...getContextAttributesForTelemetry(),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tspan.end();\n\n\t\t\t\t\t\tif (logger) {\n\t\t\t\t\t\t\tlogger.emit({\n\t\t\t\t\t\t\t\tseverityNumber: SeverityNumber.WARN,\n\t\t\t\t\t\t\t\tseverityText: 'WARN',\n\t\t\t\t\t\t\t\tbody: message,\n\t\t\t\t\t\t\t\tattributes: {\n\t\t\t\t\t\t\t\t\t'log.level': 'warn',\n\t\t\t\t\t\t\t\t\t'service.name': SERVICE_NAME,\n\t\t\t\t\t\t\t\t\t...CUSTOM_ATTRIBUTES,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Silently fail\n\t\t\t\t\t}\n\n\t\t\t\t\toriginalWarn.apply(console, args);\n\t\t\t\t};\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: Console capture\n\t\t\t\t(console.warn as any).__telemetry_captured__ = true;\n\t\t\t}\n\t\t}\n\n\t\tapp = app\n\t\t\t// Track incoming requests\n\t\t\t// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Request tracking with filtering and sampling\n\t\t\t.onBeforeHandle(({ request, store }) => {\n\t\t\t\tconst startTime = Date.now();\n\t\t\t\tconst url = new URL(request.url);\n\t\t\t\tconst route = url.pathname;\n\t\t\t\tconst method = request.method;\n\n\t\t\t\t(store as Record<string, unknown>).startTime = startTime;\n\t\t\t\t(store as Record<string, unknown>).route = route;\n\n\t\t\t\t// Check if route should be ignored\n\t\t\t\tconst shouldIgnore =\n\t\t\t\t\tIGNORE_ROUTES.length > 0 &&\n\t\t\t\t\tshouldIgnoreRoute(IGNORE_ROUTES, route, method);\n\t\t\t\t(store as Record<string, unknown>).shouldIgnoreTelemetry = shouldIgnore;\n\n\t\t\t\t// Extract incoming trace context from browser (if present)\n\t\t\t\t// This allows browser spans to link to server spans\n\t\t\t\tconst incomingContext = extractTraceContext(request.headers);\n\n\t\t\t\t// Apply sampling (skip if ignored or not sampled)\n\t\t\t\t// Use trace ID for deterministic sampling if available\n\t\t\t\tconst traceId = incomingContext\n\t\t\t\t\t? (trace.getSpan(incomingContext)?.spanContext().traceId ?? undefined)\n\t\t\t\t\t: undefined;\n\n\t\t\t\tif (!shouldIgnore) {\n\t\t\t\t\tconst sampled = shouldSample(SAMPLE_RATE, traceId);\n\t\t\t\t\t(store as Record<string, unknown>).isSampled = sampled;\n\t\t\t\t\tif (!sampled) {\n\t\t\t\t\t\t(store as Record<string, unknown>).shouldIgnoreTelemetry = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Create manual span for request if traces enabled and not ignored\n\t\t\t\tif (\n\t\t\t\t\ttracer &&\n\t\t\t\t\t!(store as Record<string, unknown>).shouldIgnoreTelemetry\n\t\t\t\t) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Extract mapped headers early for span creation\n\t\t\t\t\t\tconst mappedHeaders =\n\t\t\t\t\t\t\tCAPTURE_MAPPINGS.length > 0\n\t\t\t\t\t\t\t\t? extractHeaders(request.headers, CAPTURE_MAPPINGS)\n\t\t\t\t\t\t\t\t: {};\n\n\t\t\t\t\t\t// Create span within extracted context (if available)\n\t\t\t\t\t\tconst createSpan = () => {\n\t\t\t\t\t\t\treturn tracer.startSpan(`${method} ${route}`, {\n\t\t\t\t\t\t\t\tattributes: {\n\t\t\t\t\t\t\t\t\t'http.request.method': method,\n\t\t\t\t\t\t\t\t\t'http.route': route,\n\t\t\t\t\t\t\t\t\t'http.request.url': request.url,\n\t\t\t\t\t\t\t\t\t'http.request.scheme': url.protocol.replace(':', ''),\n\t\t\t\t\t\t\t\t\t'host.address': url.host,\n\t\t\t\t\t\t\t\t\t'service.name': SERVICE_NAME,\n\t\t\t\t\t\t\t\t\t...CUSTOM_ATTRIBUTES,\n\t\t\t\t\t\t\t\t\t...mappedHeaders,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tconst span = incomingContext\n\t\t\t\t\t\t\t? context.with(incomingContext, createSpan)\n\t\t\t\t\t\t\t: createSpan();\n\n\t\t\t\t\t\t// Add session attributes if any\n\t\t\t\t\t\tconst sessionAttrs = getContextAttributesForTelemetry();\n\t\t\t\t\t\tif (Object.keys(sessionAttrs).length > 0) {\n\t\t\t\t\t\t\tspan.setAttributes(sessionAttrs);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t(store as Record<string, unknown>).telemetrySpan = span;\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error('[telemetry] Failed to create span:', error);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Always track active connections for metrics accuracy\n\t\t\t\ttry {\n\t\t\t\t\tactiveConnections?.add(1);\n\t\t\t\t\thttpActiveRequests?.add(1, {\n\t\t\t\t\t\t'http.request.method': method,\n\t\t\t\t\t\troute,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error('[telemetry] Failed to track active requests:', error);\n\t\t\t\t}\n\n\t\t\t\t// Skip detailed telemetry if ignored or not sampled\n\t\t\t\tif ((store as Record<string, unknown>).shouldIgnoreTelemetry) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Capture mapped headers\n\t\t\t\tconst mappedHeaders =\n\t\t\t\t\tCAPTURE_MAPPINGS.length > 0\n\t\t\t\t\t\t? extractHeaders(request.headers, CAPTURE_MAPPINGS)\n\t\t\t\t\t\t: {};\n\n\t\t\t\t// Build attributes with custom attributes and captured headers\n\t\t\t\tconst baseAttributes = {\n\t\t\t\t\t'http.request.method': method,\n\t\t\t\t\t'http.request.url': request.url,\n\t\t\t\t\t'http.route': route,\n\t\t\t\t\t'http.request.scheme': url.protocol.replace(':', ''),\n\t\t\t\t\t'host.address': url.host,\n\t\t\t\t\t'service.name': SERVICE_NAME,\n\t\t\t\t\t...CUSTOM_ATTRIBUTES,\n\t\t\t\t\t...mappedHeaders,\n\t\t\t\t};\n\n\t\t\t\t// Log incoming request - wrapped in try/catch to prevent blocking\n\t\t\t\ttry {\n\t\t\t\t\tlogger?.emit({\n\t\t\t\t\t\tseverityNumber: SeverityNumber.INFO,\n\t\t\t\t\t\tseverityText: 'INFO',\n\t\t\t\t\t\tbody: `→ ${method} ${route}`,\n\t\t\t\t\t\tattributes: baseAttributes,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error('[telemetry] Failed to log incoming request:', error);\n\t\t\t\t}\n\n\t\t\t\t// Increment request counter - wrapped in try/catch to prevent blocking\n\t\t\t\ttry {\n\t\t\t\t\thttpRequestCounter?.add(1, {\n\t\t\t\t\t\t'http.request.method': method,\n\t\t\t\t\t\t'http.route': route,\n\t\t\t\t\t\t'http.request.scheme': url.protocol.replace(':', ''),\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t'[telemetry] Failed to increment request counter:',\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t})\n\t\t\t// Handle errors with full context\n\t\t\t// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Error handling with context\n\t\t\t.onError(async ({ error, request, set, store }) => {\n\t\t\t\tconst errorDetails = getErrorDetails(error);\n\t\t\t\tconst duration =\n\t\t\t\t\tDate.now() -\n\t\t\t\t\t(((store as Record<string, unknown>).startTime as number) ||\n\t\t\t\t\t\tDate.now());\n\n\t\t\t\t// Check if this error should be sampled (errors have their own sample rate)\n\t\t\t\tconst url = new URL(request.url);\n\t\t\t\tconst route = url.pathname;\n\t\t\t\tconst method = request.method;\n\t\t\t\tconst shouldIgnore =\n\t\t\t\t\tIGNORE_ROUTES.length > 0 &&\n\t\t\t\t\tshouldIgnoreRoute(IGNORE_ROUTES, route, method);\n\n\t\t\t\t// Apply error sampling (separate from regular request sampling)\n\t\t\t\t// Always sample errors if ERROR_SAMPLE_RATE = 1.0 (default)\n\t\t\t\tconst span = (store as Record<string, unknown>).telemetrySpan as\n\t\t\t\t\t| Span\n\t\t\t\t\t| undefined;\n\t\t\t\tconst traceId = span?.spanContext().traceId;\n\t\t\t\tconst errorSampled = shouldSample(ERROR_SAMPLE_RATE, traceId);\n\t\t\t\tconst skipTelemetry = shouldIgnore || !errorSampled;\n\n\t\t\t\t// Record exception in manual span - wrapped in try/catch to prevent blocking\n\t\t\t\tif (!skipTelemetry) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst span = (store as Record<string, unknown>).telemetrySpan as\n\t\t\t\t\t\t\t| Span\n\t\t\t\t\t\t\t| undefined;\n\t\t\t\t\t\tif (span) {\n\t\t\t\t\t\t\tconst errorObj =\n\t\t\t\t\t\t\t\terror instanceof Error\n\t\t\t\t\t\t\t\t\t? error\n\t\t\t\t\t\t\t\t\t: new Error(errorDetails.message);\n\n\t\t\t\t\t\t\t// Enhance error with source maps if available\n\t\t\t\t\t\t\tconst rerouteDir = '.reroute';\n\t\t\t\t\t\t\tif (rerouteDir && errorObj.stack) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tawait enhanceError(errorObj, { rerouteDir });\n\t\t\t\t\t\t\t\t} catch (smError) {\n\t\t\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t\t\t'[telemetry] Sourcemap enhancement failed:',\n\t\t\t\t\t\t\t\t\t\tsmError,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tspan.recordException(errorObj);\n\t\t\t\t\t\t\tspan.setStatus({\n\t\t\t\t\t\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\t\t\t\t\t\tmessage: `${errorDetails.name}: ${errorDetails.message}`,\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\tconst enhancedError = errorObj as Error & {\n\t\t\t\t\t\t\t\tsourceMappedStack?: string;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tconst stackToUse =\n\t\t\t\t\t\t\t\tenhancedError.sourceMappedStack || errorDetails.stack;\n\n\t\t\t\t\t\t\tspan.setAttributes({\n\t\t\t\t\t\t\t\terror: true,\n\t\t\t\t\t\t\t\t'error.type': errorDetails.name,\n\t\t\t\t\t\t\t\t'error.message': errorDetails.message,\n\t\t\t\t\t\t\t\t'error.stack': errorDetails.stack,\n\t\t\t\t\t\t\t\t...(enhancedError.sourceMappedStack\n\t\t\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t\t\t'error.stack.decoded': enhancedError.sourceMappedStack,\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\t\t\t'http.response.status_code':\n\t\t\t\t\t\t\t\t\ttypeof set.status === 'number' ? set.status : 500,\n\t\t\t\t\t\t\t\t'http.duration_ms': duration,\n\t\t\t\t\t\t\t\t...CUSTOM_ATTRIBUTES,\n\t\t\t\t\t\t\t\t...getContextAttributesForTelemetry(),\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\tspan.addEvent('exception', {\n\t\t\t\t\t\t\t\t'exception.type': errorDetails.name,\n\t\t\t\t\t\t\t\t'exception.message': errorDetails.message,\n\t\t\t\t\t\t\t\t'exception.stacktrace': stackToUse,\n\t\t\t\t\t\t\t\t'exception.escaped': 'false',\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t// End the span here since onAfterResponse won't handle errors\n\t\t\t\t\t\t\tspan.end();\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (spanError) {\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t'[telemetry] Failed to record exception in span:',\n\t\t\t\t\t\t\tspanError,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Log error - wrapped in try/catch to prevent blocking\n\t\t\t\t\ttry {\n\t\t\t\t\t\tlogger?.emit({\n\t\t\t\t\t\t\tseverityNumber: SeverityNumber.ERROR,\n\t\t\t\t\t\t\tseverityText: 'ERROR',\n\t\t\t\t\t\t\tbody: `${errorDetails.name}: ${errorDetails.message}`,\n\t\t\t\t\t\t\tattributes: {\n\t\t\t\t\t\t\t\t'error.type': errorDetails.name,\n\t\t\t\t\t\t\t\t'error.message': errorDetails.message,\n\t\t\t\t\t\t\t\t'error.stack': errorDetails.stack,\n\t\t\t\t\t\t\t\t'http.request.method': method,\n\t\t\t\t\t\t\t\t'http.request.url': request.url,\n\t\t\t\t\t\t\t\t'http.response.status_code':\n\t\t\t\t\t\t\t\t\ttypeof set.status === 'number' ? set.status : 500,\n\t\t\t\t\t\t\t\t'service.name': SERVICE_NAME,\n\t\t\t\t\t\t\t\t...CUSTOM_ATTRIBUTES,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (logError) {\n\t\t\t\t\t\tconsole.error('[telemetry] Failed to log error:', logError);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Increment error counter - wrapped in try/catch to prevent blocking\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst statusCode =\n\t\t\t\t\t\t\ttypeof set.status === 'number' ? set.status : 500;\n\t\t\t\t\t\thttpErrorCounter?.add(1, {\n\t\t\t\t\t\t\t'http.request.method': method,\n\t\t\t\t\t\t\t'http.response.status_code': statusCode,\n\t\t\t\t\t\t\t'error.type': errorDetails.name,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\thttpRequestDuration?.record(duration, {\n\t\t\t\t\t\t\t'http.request.method': method,\n\t\t\t\t\t\t\t'http.response.status_code': statusCode,\n\t\t\t\t\t\t\terror: true,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (metricsError) {\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t'[telemetry] Failed to record error metrics:',\n\t\t\t\t\t\t\tmetricsError,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t\t// Record response metrics\n\t\t\t// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Response metrics recording\n\t\t\t.onAfterResponse(({ request, set, store }) => {\n\t\t\t\tconst url = new URL(request.url);\n\t\t\t\tconst pathname = url.pathname;\n\t\t\t\tconst method = request.method;\n\t\t\t\tconst statusCode = typeof set.status === 'number' ? set.status : 200;\n\t\t\t\tconst duration =\n\t\t\t\t\tDate.now() -\n\t\t\t\t\t(((store as Record<string, unknown>).startTime as number) ||\n\t\t\t\t\t\tDate.now());\n\t\t\t\tconst route =\n\t\t\t\t\t((store as Record<string, unknown>).route as string) || pathname;\n\n\t\t\t\t// Always decrement active connections for metrics accuracy\n\t\t\t\ttry {\n\t\t\t\t\tactiveConnections?.add(-1);\n\t\t\t\t\thttpActiveRequests?.add(-1, {\n\t\t\t\t\t\t'http.request.method': method,\n\t\t\t\t\t\troute,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t'[telemetry] Failed to decrement active requests:',\n\t\t\t\t\t\terror,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t// Skip detailed telemetry if ignored or not sampled (set in onBeforeHandle)\n\t\t\t\tconst shouldIgnoreTelemetry = (store as Record<string, unknown>)\n\t\t\t\t\t.shouldIgnoreTelemetry;\n\t\t\t\tif (shouldIgnoreTelemetry) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// End manual span with final attributes\n\t\t\t\ttry {\n\t\t\t\t\tconst span = (store as Record<string, unknown>).telemetrySpan as\n\t\t\t\t\t\t| Span\n\t\t\t\t\t\t| undefined;\n\t\t\t\t\tif (span) {\n\t\t\t\t\t\tspan.setAttributes({\n\t\t\t\t\t\t\t'http.response.status_code': statusCode,\n\t\t\t\t\t\t\t'http.duration_ms': duration,\n\t\t\t\t\t\t\t...CUSTOM_ATTRIBUTES,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (statusCode >= 400) {\n\t\t\t\t\t\t\tspan.setStatus({\n\t\t\t\t\t\t\t\tcode: SpanStatusCode.ERROR,\n\t\t\t\t\t\t\t\tmessage: `HTTP ${statusCode}`,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tspan.setAttribute('error', true);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tspan.end();\n\t\t\t\t\t}\n\t\t\t\t} catch (spanError) {\n\t\t\t\t\tconsole.error('[telemetry] Failed to end span:', spanError);\n\t\t\t\t}\n\n\t\t\t\t// Log response - wrapped in try/catch to prevent blocking\n\t\t\t\ttry {\n\t\t\t\t\tconst isError = statusCode >= 400;\n\t\t\t\t\tlogger?.emit({\n\t\t\t\t\t\tseverityNumber: isError\n\t\t\t\t\t\t\t? SeverityNumber.ERROR\n\t\t\t\t\t\t\t: SeverityNumber.INFO,\n\t\t\t\t\t\tseverityText: isError ? 'ERROR' : 'INFO',\n\t\t\t\t\t\tbody: `← ${method} ${route} → ${statusCode} (${duration}ms)`,\n\t\t\t\t\t\tattributes: {\n\t\t\t\t\t\t\t'http.request.method': method,\n\t\t\t\t\t\t\t'http.request.url': request.url,\n\t\t\t\t\t\t\t'http.route': route,\n\t\t\t\t\t\t\t'http.response.status_code': statusCode,\n\t\t\t\t\t\t\t'http.duration_ms': duration,\n\t\t\t\t\t\t\t'http.request.scheme': url.protocol.replace(':', ''),\n\t\t\t\t\t\t\t'host.address': url.host,\n\t\t\t\t\t\t\t'service.name': SERVICE_NAME,\n\t\t\t\t\t\t\t...CUSTOM_ATTRIBUTES,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t} catch (logError) {\n\t\t\t\t\tconsole.error('[telemetry] Failed to log response:', logError);\n\t\t\t\t}\n\n\t\t\t\t// Record metrics - wrapped in try/catch to prevent blocking\n\t\t\t\ttry {\n\t\t\t\t\thttpRequestDuration?.record(duration, {\n\t\t\t\t\t\t'http.request.method': method,\n\t\t\t\t\t\t'http.route': route,\n\t\t\t\t\t\t'http.response.status_code': statusCode,\n\t\t\t\t\t\t'http.request.scheme': url.protocol.replace(':', ''),\n\t\t\t\t\t});\n\t\t\t\t} catch (metricsError) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t'[telemetry] Failed to record request duration:',\n\t\t\t\t\t\tmetricsError,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t});\n\n\t\t// Optionally add error inspector endpoint for source-mapped stack traces\n\t\tif (merged.sourcemaps?.enabled !== false) {\n\t\t\tconst rerouteDir = '.reroute';\n\n\t\t\tapp.post('/__reroute_telemetry/inspect', async ({ body }) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst { stack } = body as { stack?: string };\n\t\t\t\t\tif (!stack) {\n\t\t\t\t\t\treturn { error: 'Missing stack trace' };\n\t\t\t\t\t}\n\n\t\t\t\t\tconst { resolveStackTrace } = await import('./sourcemap');\n\t\t\t\t\tconst result = await resolveStackTrace(stack, { rerouteDir });\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\toriginal: result.original,\n\t\t\t\t\t\tmapped: result.mapped,\n\t\t\t\t\t\tframes: result.frames,\n\t\t\t\t\t};\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error('[telemetry] Error inspection failed:', error);\n\t\t\t\t\treturn {\n\t\t\t\t\t\terror: error instanceof Error ? error.message : 'Unknown error',\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t// Optionally add browser telemetry proxy\n\t\tif (ENABLE_PROXY) {\n\t\t\tconst API_KEY = merged.apiKey || process.env.SIGNOZ_API_KEY;\n\n\t\t\t// Proxy handler function\n\t\t\tconst handleProxy = async (\n\t\t\t\tendpoint: string,\n\t\t\t\trequestBody: unknown,\n\t\t\t\t// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Proxy forwarding with error handling\n\t\t\t): Promise<{ status: number }> => {\n\t\t\t\tconst headers: Record<string, string> = {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t};\n\n\t\t\t\tif (API_KEY) {\n\t\t\t\t\theaders.Authorization = `Bearer ${API_KEY}`;\n\t\t\t\t}\n\n\t\t\t\t// Use custom handler if provided\n\t\t\t\tif (PROXY_HANDLER) {\n\t\t\t\t\treturn await PROXY_HANDLER({\n\t\t\t\t\t\tbody: requestBody,\n\t\t\t\t\t\tendpoint,\n\t\t\t\t\t\theaders,\n\t\t\t\t\t\totlpEndpoint: OTLP_ENDPOINT,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\t// Default handler: forward to OTLP endpoint\n\t\t\t\ttry {\n\t\t\t\t\tconst controller = new AbortController();\n\t\t\t\t\tconst timeoutId = setTimeout(() => controller.abort(), 5000);\n\n\t\t\t\t\tconst response = await fetch(`${OTLP_ENDPOINT}${endpoint}`, {\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\theaders,\n\t\t\t\t\t\tbody: JSON.stringify(requestBody),\n\t\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\t});\n\n\t\t\t\t\tclearTimeout(timeoutId);\n\n\t\t\t\t\tif (PROXY_VERBOSE) {\n\t\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t\t`[telemetry-proxy]${endpoint} forwarded: ${response.status}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!response.ok && PROXY_VERBOSE) {\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t`[telemetry-proxy] OTLP error: ${response.status} ${response.statusText}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\treturn { status: 200 };\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (PROXY_VERBOSE) {\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t`[telemetry-proxy] Failed to forward${endpoint}:`,\n\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn { status: 200 };\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tapp\n\t\t\t\t.post(\n\t\t\t\t\t`${PROXY_PATHNAME}/v1/traces`,\n\t\t\t\t\tasync function forwardBrowserTraces({ body }) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst result = await handleProxy('/v1/traces', body);\n\t\t\t\t\t\t\treturn new Response(null, { status: result.status });\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconsole.error('[telemetry-proxy] Handler error:', error);\n\t\t\t\t\t\t\treturn new Response(null, { status: 200 });\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\t.post(\n\t\t\t\t\t`${PROXY_PATHNAME}/v1/metrics`,\n\t\t\t\t\tasync function forwardBrowserMetrics({ body }) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst result = await handleProxy('/v1/metrics', body);\n\t\t\t\t\t\t\treturn new Response(null, { status: result.status });\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconsole.error('[telemetry-proxy] Handler error:', error);\n\t\t\t\t\t\t\treturn new Response(null, { status: 200 });\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\t.post(\n\t\t\t\t\t`${PROXY_PATHNAME}/v1/logs`,\n\t\t\t\t\tasync function forwardBrowserLogs({ body }) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst result = await handleProxy('/v1/logs', body);\n\t\t\t\t\t\t\treturn new Response(null, { status: result.status });\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconsole.error('[telemetry-proxy] Handler error:', error);\n\t\t\t\t\t\t\treturn new Response(null, { status: 200 });\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\t.get(`${PROXY_PATHNAME}/health`, () => {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: 'ok',\n\t\t\t\t\t\totlpEndpoint: OTLP_ENDPOINT,\n\t\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\t};\n\t\t\t\t});\n\t\t}\n\n\t\treturn app;\n\t};\n}\n",
|
|
16
|
+
"export * from './context';\nexport * from './headers';\nexport * from './instrumentation';\nexport * from './plugin';\nexport * from './sourcemap';\n",
|
|
17
|
+
"export * from './src/server';\n",
|
|
18
|
+
"import type { CacheEntry } from './types';\n\n/**\n * Simple in-memory cache for SSR data.\n * Thread-safe for single-process usage.\n */\nclass SSRDataCache {\n\tprivate cache: Map<string, CacheEntry<unknown>> = new Map();\n\tprivate static cleanupInterval: NodeJS.Timeout | null = null;\n\n\tconstructor() {\n\t\t// Run cleanup every 60 seconds to remove expired entries\n\t\tif (!SSRDataCache.cleanupInterval) {\n\t\t\tSSRDataCache.cleanupInterval = setInterval(() => {\n\t\t\t\tthis.cleanup();\n\t\t\t}, 60_000);\n\t\t}\n\t}\n\n\t/**\n\t * Get cached data if available and not expired\n\t */\n\tget<T>(key: string): T | undefined {\n\t\tconst entry = this.cache.get(key);\n\t\tif (!entry) return undefined;\n\n\t\tconst age = Date.now() - entry.timestamp;\n\t\tif (age > entry.maxAge) {\n\t\t\tthis.cache.delete(key);\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn entry.data as T;\n\t}\n\n\t/**\n\t * Set data in cache with maxAge (in milliseconds)\n\t */\n\tset<T>(key: string, data: T, maxAge: number): void {\n\t\tif (maxAge <= 0) return;\n\n\t\tthis.cache.set(key, {\n\t\t\tdata,\n\t\t\ttimestamp: Date.now(),\n\t\t\tmaxAge,\n\t\t});\n\t}\n\n\t/**\n\t * Check if key exists and is not expired\n\t */\n\thas(key: string): boolean {\n\t\treturn this.get(key) !== undefined;\n\t}\n\n\t/**\n\t * Clear all cached data\n\t */\n\tclear(): void {\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Remove expired entries\n\t */\n\tprivate cleanup(): void {\n\t\tconst now = Date.now();\n\t\tfor (const [key, entry] of this.cache.entries()) {\n\t\t\tif (now - entry.timestamp > entry.maxAge) {\n\t\t\t\tthis.cache.delete(key);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get cache stats (useful for debugging)\n\t */\n\tstats(): { size: number; keys: string[] } {\n\t\treturn {\n\t\t\tsize: this.cache.size,\n\t\t\tkeys: Array.from(this.cache.keys()),\n\t\t};\n\t}\n\n\t/**\n\t * Cleanup interval on shutdown\n\t */\n\tdestroy(): void {\n\t\tif (SSRDataCache.cleanupInterval) {\n\t\t\tclearInterval(SSRDataCache.cleanupInterval);\n\t\t\tSSRDataCache.cleanupInterval = null;\n\t\t}\n\t\tthis.clear();\n\t}\n}\n\n/**\n * Simple LRU Cache implementation with TTL support.\n * Used for caching responses and data across requests.\n * Automatically evicts expired entries to prevent memory leaks.\n */\nexport class LRUCache<K, V> {\n\tprivate cache: Map<K, V> = new Map();\n\tprivate maxSize: number;\n\tprivate cleanupInterval: NodeJS.Timeout | null = null;\n\n\tconstructor(maxSize = 100) {\n\t\tthis.maxSize = maxSize;\n\n\t\t// Periodic cleanup of expired entries (every 60 seconds)\n\t\tthis.cleanupInterval = setInterval(() => {\n\t\t\tthis.cleanup();\n\t\t}, 60_000);\n\n\t\t// Prevent interval from keeping process alive\n\t\tif (this.cleanupInterval.unref) {\n\t\t\tthis.cleanupInterval.unref();\n\t\t}\n\t}\n\n\tget(key: K): V | undefined {\n\t\tconst value = this.cache.get(key);\n\t\tif (value !== undefined) {\n\t\t\t// Check if entry has expired (if it has `exp` field)\n\t\t\tconst entry = value as { exp?: number };\n\t\t\tif (entry.exp !== undefined && entry.exp <= Date.now()) {\n\t\t\t\tthis.cache.delete(key);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// Move to end (most recently used)\n\t\t\tthis.cache.delete(key);\n\t\t\tthis.cache.set(key, value);\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * Remove all expired entries\n\t */\n\tprivate cleanup(): void {\n\t\tconst now = Date.now();\n\t\tfor (const [key, value] of this.cache.entries()) {\n\t\t\tconst entry = value as { exp?: number };\n\t\t\tif (entry.exp !== undefined && entry.exp <= now) {\n\t\t\t\tthis.cache.delete(key);\n\t\t\t}\n\t\t}\n\t}\n\n\tset(key: K, value: V): void {\n\t\tthis.cache.delete(key);\n\n\t\tif (this.cache.size >= this.maxSize) {\n\t\t\tconst firstKey = this.cache.keys().next().value;\n\t\t\tthis.cache.delete(firstKey as K);\n\t\t}\n\n\t\tthis.cache.set(key, value);\n\t}\n\n\thas(key: K): boolean {\n\t\treturn this.cache.has(key);\n\t}\n\n\tclear(): void {\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Stop cleanup interval and clear cache (call on shutdown)\n\t */\n\tdestroy(): void {\n\t\tif (this.cleanupInterval) {\n\t\t\tclearInterval(this.cleanupInterval);\n\t\t\tthis.cleanupInterval = null;\n\t\t}\n\t\tthis.clear();\n\t}\n}\n\n/**\n * TTL-based cache with automatic expiration and size limits.\n * Use this for content caching where entries should expire after a set time.\n */\nexport class TTLCache<K, V> {\n\tprivate cache: Map<K, { value: V; exp: number; size?: number }> = new Map();\n\tprivate maxSize: number;\n\tprivate defaultTtlMs: number;\n\tprivate cleanupInterval: NodeJS.Timeout | null = null;\n\tprivate maxMemoryBytes: number | null = null;\n\tprivate currentMemoryBytes = 0;\n\n\t/**\n\t * @param maxSize Maximum number of entries (default: 100)\n\t * @param defaultTtlMs Default TTL in milliseconds (default: 300000 = 5 min)\n\t * @param cleanupIntervalMs How often to run cleanup (default: 60000 = 1 min)\n\t * @param maxMemoryMB Optional maximum memory in MB (e.g., 1000 = 1GB)\n\t */\n\tconstructor(\n\t\tmaxSize = 100,\n\t\tdefaultTtlMs = 300_000,\n\t\tcleanupIntervalMs = 60_000,\n\t\tmaxMemoryMB?: number,\n\t) {\n\t\tthis.maxSize = maxSize;\n\t\tthis.defaultTtlMs = defaultTtlMs;\n\t\tthis.maxMemoryBytes = maxMemoryMB ? maxMemoryMB * 1024 * 1024 : null;\n\n\t\t// Periodic cleanup of expired entries\n\t\tthis.cleanupInterval = setInterval(() => {\n\t\t\tthis.cleanup();\n\t\t}, cleanupIntervalMs);\n\n\t\t// Prevent interval from keeping process alive\n\t\tif (this.cleanupInterval.unref) {\n\t\t\tthis.cleanupInterval.unref();\n\t\t}\n\t}\n\n\t/**\n\t * Get value if exists and not expired\n\t */\n\tget(key: K): V | undefined {\n\t\tconst entry = this.cache.get(key);\n\t\tif (!entry) return undefined;\n\n\t\tif (entry.exp <= Date.now()) {\n\t\t\tthis.cache.delete(key);\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn entry.value;\n\t}\n\n\t/**\n\t * Estimate size of value in bytes\n\t */\n\tprivate estimateSize(value: V): number {\n\t\tif (value instanceof Buffer || value instanceof ArrayBuffer) {\n\t\t\treturn value.byteLength;\n\t\t}\n\t\tif (typeof value === 'string') {\n\t\t\treturn value.length * 2; // UTF-16 encoding\n\t\t}\n\t\t// For objects, use rough estimate\n\t\ttry {\n\t\t\treturn JSON.stringify(value).length * 2;\n\t\t} catch {\n\t\t\treturn 1024; // Default estimate\n\t\t}\n\t}\n\n\t/**\n\t * Set value with optional custom TTL\n\t * @param key Cache key\n\t * @param value Value to cache\n\t * @param ttlMs Optional TTL in milliseconds (uses default if not provided)\n\t */\n\tset(key: K, value: V, ttlMs?: number): void {\n\t\t// Calculate size of new entry\n\t\tconst entrySize = this.estimateSize(value);\n\n\t\t// Remove existing entry and update memory tracking\n\t\tconst existing = this.cache.get(key);\n\t\tif (existing) {\n\t\t\tthis.cache.delete(key);\n\t\t\tif (existing.size) {\n\t\t\t\tthis.currentMemoryBytes -= existing.size;\n\t\t\t}\n\t\t}\n\n\t\t// Evict entries if over memory limit\n\t\tif (\n\t\t\tthis.maxMemoryBytes &&\n\t\t\tthis.currentMemoryBytes + entrySize > this.maxMemoryBytes\n\t\t) {\n\t\t\tthis.evictForMemory(entrySize);\n\t\t}\n\n\t\t// Evict oldest entry if at capacity\n\t\tif (this.cache.size >= this.maxSize) {\n\t\t\tconst firstKey = this.cache.keys().next().value;\n\t\t\tif (firstKey !== undefined) {\n\t\t\t\tconst evicted = this.cache.get(firstKey);\n\t\t\t\tthis.cache.delete(firstKey);\n\t\t\t\tif (evicted?.size) {\n\t\t\t\t\tthis.currentMemoryBytes -= evicted.size;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.cache.set(key, {\n\t\t\tvalue,\n\t\t\texp: Date.now() + (ttlMs ?? this.defaultTtlMs),\n\t\t\tsize: entrySize,\n\t\t});\n\t\tthis.currentMemoryBytes += entrySize;\n\t}\n\n\t/**\n\t * Check if key exists and is not expired\n\t */\n\thas(key: K): boolean {\n\t\treturn this.get(key) !== undefined;\n\t}\n\n\t/**\n\t * Evict entries to free up memory\n\t */\n\tprivate evictForMemory(neededBytes: number): void {\n\t\tif (!this.maxMemoryBytes) return;\n\t\tconst targetBytes = this.maxMemoryBytes * 0.7; // Evict down to 70%\n\t\tconst keys = Array.from(this.cache.keys());\n\n\t\tfor (const key of keys) {\n\t\t\tif (this.currentMemoryBytes + neededBytes <= targetBytes) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst entry = this.cache.get(key);\n\t\t\tif (entry) {\n\t\t\t\tthis.cache.delete(key);\n\t\t\t\tif (entry.size) {\n\t\t\t\t\tthis.currentMemoryBytes -= entry.size;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Delete a specific key\n\t */\n\tdelete(key: K): boolean {\n\t\tconst entry = this.cache.get(key);\n\t\tconst deleted = this.cache.delete(key);\n\t\tif (deleted && entry?.size) {\n\t\t\tthis.currentMemoryBytes -= entry.size;\n\t\t}\n\t\treturn deleted;\n\t}\n\n\t/**\n\t * Clear all entries\n\t */\n\tclear(): void {\n\t\tthis.cache.clear();\n\t\tthis.currentMemoryBytes = 0;\n\t}\n\n\t/**\n\t * Get current cache size (including potentially expired entries)\n\t */\n\tget size(): number {\n\t\treturn this.cache.size;\n\t}\n\n\t/**\n\t * Get cache statistics including memory usage\n\t */\n\tstats(): {\n\t\tentries: number;\n\t\tmemoryMB: number;\n\t\tmaxMemoryMB: number | null;\n\t\tmemoryUsagePercent: number | null;\n\t} {\n\t\tconst memoryMB = this.currentMemoryBytes / (1024 * 1024);\n\t\tconst maxMemoryMB = this.maxMemoryBytes\n\t\t\t? this.maxMemoryBytes / (1024 * 1024)\n\t\t\t: null;\n\t\tconst memoryUsagePercent = maxMemoryMB\n\t\t\t? (memoryMB / maxMemoryMB) * 100\n\t\t\t: null;\n\n\t\treturn {\n\t\t\tentries: this.cache.size,\n\t\t\tmemoryMB: Math.round(memoryMB * 100) / 100,\n\t\t\tmaxMemoryMB: maxMemoryMB ? Math.round(maxMemoryMB) : null,\n\t\t\tmemoryUsagePercent:\n\t\t\t\tmemoryUsagePercent !== null ? Math.round(memoryUsagePercent) : null,\n\t\t};\n\t}\n\n\t/**\n\t * Remove expired entries\n\t */\n\tprivate cleanup(): void {\n\t\tconst now = Date.now();\n\t\tfor (const [key, entry] of this.cache.entries()) {\n\t\t\tif (entry.exp <= now) {\n\t\t\t\tthis.cache.delete(key);\n\t\t\t\tif (entry.size) {\n\t\t\t\t\tthis.currentMemoryBytes -= entry.size;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Stop cleanup interval (call on shutdown)\n\t */\n\tdestroy(): void {\n\t\tif (this.cleanupInterval) {\n\t\t\tclearInterval(this.cleanupInterval);\n\t\t\tthis.cleanupInterval = null;\n\t\t}\n\t\tthis.clear();\n\t}\n}\n\n// Global singleton instances using globalThis for hot reload survival\nconst getGlobalCache = (): SSRDataCache => {\n\tconst g = globalThis as typeof globalThis & {\n\t\t__REROUTE_SSR_CACHE__?: SSRDataCache;\n\t};\n\n\tif (!g.__REROUTE_SSR_CACHE__) {\n\t\tg.__REROUTE_SSR_CACHE__ = new SSRDataCache();\n\t}\n\n\treturn g.__REROUTE_SSR_CACHE__;\n};\n\nconst getGlobalStatusCache = (): SSRDataCache => {\n\tconst g = globalThis as typeof globalThis & {\n\t\t__REROUTE_STATUS_CACHE__?: SSRDataCache;\n\t};\n\n\tif (!g.__REROUTE_STATUS_CACHE__) {\n\t\tg.__REROUTE_STATUS_CACHE__ = new SSRDataCache();\n\t}\n\n\treturn g.__REROUTE_STATUS_CACHE__;\n};\n\nconst ssrCache = getGlobalCache();\nconst statusCache = getGlobalStatusCache();\n\nexport { ssrCache, statusCache, SSRDataCache };\n",
|
|
19
|
+
"import { copyFile, realpath, stat, unlink } from 'node:fs/promises';\nimport { basename, dirname, extname, join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { Doc } from './types';\n\ntype CachedWatchImport = {\n\tmtimeMs: number;\n\tmod: unknown;\n};\n\nconst watchImportCache = new Map<string, CachedWatchImport>();\n\n/**\n * Drop in-process watch-mode import results so the next `dynamicImport`\n * re-reads from disk. Called when the browser is told to reload.\n */\nexport function clearWatchImportCache(): void {\n\twatchImportCache.clear();\n}\n\n/**\n * Sibling copy Bun has never resolved. Query strings on `import()` are\n * ignored — the first path sticks for the process lifetime.\n */\nfunction bustCopyPath(absolutePath: string): string {\n\tconst dir = dirname(absolutePath);\n\tconst ext = extname(absolutePath);\n\tconst base = basename(absolutePath, ext);\n\tconst token = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n\treturn join(dir, `${base}.bust-${token}${ext}`);\n}\n\n/**\n * Dynamically import a module. In watch mode, copy to a unique sibling so\n * Bun cannot reuse a stale module after gen rewrites the file.\n */\nexport async function dynamicImport<T = Doc>(\n\tabsolutePath: string,\n\tisWatchMode: boolean,\n): Promise<T> {\n\tconst href = pathToFileURL(absolutePath).href;\n\tif (!isWatchMode) {\n\t\treturn (await import(href)) as T;\n\t}\n\n\tconst resolved = await realpath(absolutePath);\n\tconst mtimeMs = (await stat(resolved)).mtimeMs;\n\tconst cached = watchImportCache.get(resolved);\n\tif (cached && cached.mtimeMs === mtimeMs) {\n\t\treturn cached.mod as T;\n\t}\n\n\tconst tmp = bustCopyPath(resolved);\n\tawait copyFile(resolved, tmp);\n\ttry {\n\t\tconst mod = (await import(pathToFileURL(tmp).href)) as T;\n\t\twatchImportCache.set(resolved, { mtimeMs, mod });\n\t\treturn mod;\n\t} finally {\n\t\tawait unlink(tmp).catch(() => {});\n\t}\n}\n\n/**\n * Check if a value is a thenable (Promise-like object)\n */\nexport function isThenable(value: unknown): value is Promise<unknown> {\n\treturn (\n\t\ttypeof value === 'object' &&\n\t\tvalue !== null &&\n\t\ttypeof (value as { then?: unknown }).then === 'function'\n\t);\n}\n\n/**\n * Convert a value to a Promise if it isn't already\n */\nexport function toPromise<T>(input: T | Promise<T>): Promise<T> {\n\treturn input instanceof Promise ? input : Promise.resolve(input);\n}\n",
|
|
20
|
+
"import { dynamicImport } from './imports';\nimport { join } from './path';\nimport type { Doc } from './types';\n\n/**\n * Load content collections for SSR rendering\n */\nexport async function loadCollections(\n\tcwd: string,\n\tisWatchMode: boolean,\n): Promise<Record<string, unknown[]>> {\n\ttry {\n\t\tconst p = join(cwd, '.reroute', 'content.ts');\n\t\tconst mod = await dynamicImport(p, isWatchMode);\n\t\treturn (mod?.byCollection || {}) as Record<string, unknown[]>;\n\t} catch {\n\t\treturn {};\n\t}\n}\n\n/**\n * Set global collections for SSR\n */\nexport function setGlobalCollections(\n\tbyCollectionForSSR: Record<string, unknown[]>,\n): void {\n\ttry {\n\t\t(globalThis as Doc).__REROUTE_COLLECTIONS__ = byCollectionForSSR;\n\t} catch {\n\t\t// Ignore errors in restricted environments\n\t}\n}\n\n/**\n * Discard content results captured by `useContent` during SSR.\n *\n * The capture buffer is request scoped: it holds the exact items a request\n * rendered, already filtered by the active locale. Leaving it in place across\n * requests serializes another request's locale into this response, which\n * hydrates to the wrong language.\n */\nexport function resetSSRContentResults(): void {\n\ttry {\n\t\t(globalThis as Doc).__REROUTE_SSR_RESULTS__ = {};\n\t} catch {\n\t\t// Ignore errors in restricted environments\n\t}\n}\n\n/**\n * Read the captured content results and clear the buffer.\n *\n * Call this synchronously after rendering so an interleaved request cannot\n * append its own items before they are serialized.\n */\nexport function takeSSRContentResults(): Record<string, unknown[]> | undefined {\n\ttry {\n\t\tconst g = globalThis as Doc;\n\t\tconst results = g.__REROUTE_SSR_RESULTS__ as\n\t\t\t| Record<string, unknown[]>\n\t\t\t| undefined;\n\t\tg.__REROUTE_SSR_RESULTS__ = {};\n\t\treturn results;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n",
|
|
21
|
+
"import {\n\tcontentKey,\n\tlooksLikeLocale,\n\tparseContentPath,\n} from '../../i18n/pathname';\n\n/**\n * Resolve collection/name/locale from a request pathname.\n * Supports `/collection/name` and `/collection/locale/name`.\n */\nfunction resolveContentPathParts(pathname: string): {\n\tcollection: string;\n\tname: string;\n\tlocale?: string;\n\tkey: string;\n\troutingPathname: string;\n} | null {\n\tconst pathOnly = pathname.split('?')[0].split('#')[0];\n\tconst parts = pathOnly.split('/').filter(Boolean);\n\tif (parts.length < 2) return null;\n\n\t// Prefer structured parse (locale in the middle)\n\tconst g = globalThis as {\n\t\t__REROUTE_I18N__?: { locales?: string[]; defaultLocale?: string };\n\t};\n\tconst locales = g.__REROUTE_I18N__?.locales || [];\n\tconst defaultLocale = g.__REROUTE_I18N__?.defaultLocale || 'en-US';\n\n\tconst parsed = parseContentPath(pathOnly, locales);\n\tif (parsed) {\n\t\tconst locale = parsed.locale || defaultLocale;\n\t\treturn {\n\t\t\tcollection: parsed.collection,\n\t\t\tname: parsed.name,\n\t\t\tlocale: parsed.locale,\n\t\t\tkey: contentKey(parsed.collection, parsed.name, locale, defaultLocale),\n\t\t\troutingPathname: parsed.routingPathname,\n\t\t};\n\t}\n\n\t// Fallback without known locales list\n\tif (parts.length >= 3 && looksLikeLocale(parts[1])) {\n\t\tconst locale = parts[1];\n\t\treturn {\n\t\t\tcollection: parts[0],\n\t\t\tname: parts[2],\n\t\t\tlocale,\n\t\t\tkey: contentKey(parts[0], parts[2], locale, defaultLocale),\n\t\t\troutingPathname: `/${parts[0]}/${parts[2]}`,\n\t\t};\n\t}\n\n\treturn {\n\t\tcollection: parts[0],\n\t\tname: parts[1],\n\t\tkey: `${parts[0]}:${parts[1]}`,\n\t\troutingPathname: `/${parts[0]}/${parts[1]}`,\n\t};\n}\n\nexport { resolveContentPathParts };\n",
|
|
22
|
+
"import { ssrCache, statusCache } from '../cache';\nimport { resolveContentPathParts } from '../content-path';\nimport { toPromise } from '../imports';\nimport type { ComputeOptions, Doc, SSR, SSRContext } from '../types';\n\ntype ContentComputeResult = {\n\tdata: Record<string, unknown>;\n\terror?: string;\n\tstatusOverride?: number;\n\tpendingTasks: Promise<unknown>[];\n};\n\nfunction checkStatusCache(\n\tstatusCacheKey: string,\n\tmaxAgeMs: number,\n): number | undefined {\n\tif (maxAgeMs <= 0) return undefined;\n\treturn statusCache.get<number>(statusCacheKey);\n}\n\nfunction checkDataCache(cacheKey: string, maxAgeMs: number): unknown {\n\tif (maxAgeMs <= 0) return undefined;\n\treturn ssrCache.get(cacheKey);\n}\n\nfunction createDataTask(\n\tdataFn: NonNullable<SSR['data']>,\n\tpathname: string,\n\tparts: string[],\n\tsearchParams: URLSearchParams | undefined,\n\tset: { status: number },\n\theaders?: Record<string, string>,\n): Promise<unknown> {\n\treturn toPromise(\n\t\tdataFn({\n\t\t\tpathname,\n\t\t\tparams: { name: parts[1] },\n\t\t\tset,\n\t\t\tsearchParams,\n\t\t\theaders,\n\t\t} satisfies SSRContext),\n\t).catch((err) => {\n\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\tconst error = `SSR data() failed: ${msg}`;\n\t\tconsole.error(`[reroute] ${error}`);\n\t\tthrow err;\n\t});\n}\n\nfunction handleStreamingMode(\n\tssrData: Record<string, unknown>,\n\tpathname: string,\n\ttask: Promise<unknown>,\n\tset: { status: number },\n\tcacheKey: string,\n\tstatusCacheKey: string,\n\tmaxAgeMs: number,\n\tpendingTasks: Promise<unknown>[],\n): void {\n\tssrData[pathname] = task;\n\n\tconst storeTask = task.then((value) => {\n\t\tif (set.status !== 200) {\n\t\t\t// statusOverride handled by caller\n\t\t}\n\t\tif (maxAgeMs > 0) {\n\t\t\tstatusCache.set(statusCacheKey, set.status, maxAgeMs);\n\t\t}\n\t\tif (maxAgeMs > 0 && set.status === 200) {\n\t\t\tssrCache.set(cacheKey, value, maxAgeMs);\n\t\t}\n\t\treturn value;\n\t});\n\n\tpendingTasks.push(\n\t\tstoreTask\n\t\t\t.then((value) => {\n\t\t\t\tssrData[pathname] = value;\n\t\t\t})\n\t\t\t.catch(() => {}),\n\t);\n}\n\nasync function handleNonStreamingMode(\n\tssrData: Record<string, unknown>,\n\tpathname: string,\n\ttask: Promise<unknown>,\n\tcacheKey: string,\n\tstatusCacheKey: string,\n\tmaxAgeMs: number,\n): Promise<unknown> {\n\tconst result = await task;\n\tssrData[pathname] = result;\n\n\tif (maxAgeMs > 0) {\n\t\tstatusCache.set(\n\t\t\tstatusCacheKey,\n\t\t\t(result as { status?: number }).status || 200,\n\t\t\tmaxAgeMs,\n\t\t);\n\t}\n\tif (maxAgeMs > 0) {\n\t\tssrCache.set(cacheKey, result, maxAgeMs);\n\t}\n\n\treturn result;\n}\n\n/**\n * Compute SSR data for content pages (e.g., /blog/my-post).\n * Content pages export ssr.data via __REROUTE_SSR_EXPORTS__.\n *\n * @param sharedSsrData - In streaming mode, the shared ssrData object that gets passed to React.\n * Promise resolution callbacks must update THIS object directly.\n */\nasync function runContentDataTask(\n\tstreaming: boolean,\n\tssrData: Record<string, unknown>,\n\tpathname: string,\n\ttask: Promise<unknown>,\n\tset: { status: number },\n\tcacheKey: string,\n\tstatusCacheKey: string,\n\tmaxAgeMs: number,\n\tpendingTasks: Promise<unknown>[],\n): Promise<number | undefined> {\n\tif (streaming) {\n\t\thandleStreamingMode(\n\t\t\tssrData,\n\t\t\tpathname,\n\t\t\ttask,\n\t\t\tset,\n\t\t\tcacheKey,\n\t\t\tstatusCacheKey,\n\t\t\tmaxAgeMs,\n\t\t\tpendingTasks,\n\t\t);\n\t} else {\n\t\tawait handleNonStreamingMode(\n\t\t\tssrData,\n\t\t\tpathname,\n\t\t\ttask,\n\t\t\tcacheKey,\n\t\t\tstatusCacheKey,\n\t\t\tmaxAgeMs,\n\t\t);\n\t}\n\treturn set.status !== 200 ? set.status : undefined;\n}\n\nexport async function computeContentData(\n\tpathname: string,\n\toptions: ComputeOptions,\n\tsearchParams?: URLSearchParams,\n\tsharedSsrData?: Record<string, unknown>,\n): Promise<ContentComputeResult> {\n\tconst streaming = options.streaming === true;\n\tconst ssrData: Record<string, unknown> =\n\t\tstreaming && sharedSsrData ? sharedSsrData : {};\n\tconst pendingTasks: Promise<unknown>[] = [];\n\tlet error: string | undefined;\n\tlet statusOverride: number | undefined;\n\n\tconst maxAge = options.maxAge || 0;\n\tconst maxAgeMs = maxAge * 1000;\n\n\ttry {\n\t\tconst resolved = resolveContentPathParts(pathname);\n\t\tif (!resolved) {\n\t\t\treturn { data: ssrData, pendingTasks };\n\t\t}\n\n\t\tconst parts = [resolved.collection, resolved.name];\n\t\tif (resolved.locale) parts.splice(1, 0, resolved.locale);\n\n\t\tconst g = globalThis as Doc;\n\t\tconst exp = g.__REROUTE_SSR_EXPORTS__?.[resolved.key] as\n\t\t\t| { ssr?: SSR }\n\t\t\t| undefined;\n\t\tconst dataFn = exp?.ssr?.data;\n\n\t\tif (typeof dataFn !== 'function') {\n\t\t\treturn { data: ssrData, pendingTasks };\n\t\t}\n\n\t\tconst cacheKey = `content:${pathname}:${resolved.name}:${resolved.locale || ''}`;\n\t\tconst statusCacheKey = `status:${cacheKey}`;\n\n\t\tconst cachedStatus = checkStatusCache(statusCacheKey, maxAgeMs);\n\t\tif (cachedStatus && cachedStatus !== 200) {\n\t\t\tstatusOverride = cachedStatus;\n\t\t}\n\n\t\tconst cached = checkDataCache(cacheKey, maxAgeMs);\n\t\tif (cached !== undefined) {\n\t\t\tssrData[pathname] = cached;\n\t\t\treturn { data: ssrData, statusOverride, pendingTasks };\n\t\t}\n\n\t\tconst set = { status: 200 };\n\t\tconst task = createDataTask(\n\t\t\tdataFn,\n\t\t\tpathname,\n\t\t\tparts,\n\t\t\tsearchParams,\n\t\t\tset,\n\t\t\toptions.headers,\n\t\t);\n\n\t\tstatusOverride =\n\t\t\t(await runContentDataTask(\n\t\t\t\tstreaming,\n\t\t\t\tssrData,\n\t\t\t\tpathname,\n\t\t\t\ttask,\n\t\t\t\tset,\n\t\t\t\tcacheKey,\n\t\t\t\tstatusCacheKey,\n\t\t\t\tmaxAgeMs,\n\t\t\t\tpendingTasks,\n\t\t\t)) ?? statusOverride;\n\t} catch {\n\t\t// Return what we have on error\n\t}\n\n\treturn { data: ssrData, error, statusOverride, pendingTasks };\n}\n",
|
|
23
|
+
"import { ssrCache, statusCache } from '../cache';\nimport { dynamicImport, toPromise } from '../imports';\nimport {\n\tcreateLayoutCacheKey,\n\tcreateLayoutDataKey,\n\tgetMatchingLayouts,\n} from '../layouts';\nimport { join } from '../path';\nimport type { ComputeOptions, Doc, SSR, SSRContext } from '../types';\n\ntype LayoutComputeResult = {\n\tdata: Record<string, unknown>;\n\tstatusOverride?: number;\n\tpendingTasks: Promise<unknown>[];\n};\n\nasync function loadLayoutSSRExport(\n\tlayout: Doc,\n\tclientDir: string,\n\tisWatchMode: boolean,\n): Promise<SSR | undefined> {\n\tlet ssrExport = (layout as Doc)?.ssr as SSR | undefined;\n\n\tif (!ssrExport) {\n\t\tconst abs = join(clientDir, 'routes', String(layout.path));\n\t\tconst mod = await dynamicImport(abs, isWatchMode);\n\t\tssrExport = (mod as Doc)?.ssr as SSR | undefined;\n\t}\n\n\treturn ssrExport;\n}\n\nfunction checkLayoutCache(\n\tcacheKey: string,\n\tstatusCacheKey: string,\n\tmaxAgeMs: number,\n): { cachedData?: unknown; cachedStatus?: number } {\n\tconst cachedStatus =\n\t\tmaxAgeMs > 0 ? statusCache.get<number>(statusCacheKey) : undefined;\n\tconst cachedData = maxAgeMs > 0 ? ssrCache.get(cacheKey) : undefined;\n\n\treturn { cachedData, cachedStatus };\n}\n\nfunction createLayoutDataTask(\n\tdataFn: NonNullable<SSR['data']>,\n\tpathname: string,\n\tsearchParams: URLSearchParams | undefined,\n\tlayoutPattern: string,\n\theaders?: Record<string, string>,\n): Promise<unknown> {\n\tconst set = { status: 200 };\n\treturn toPromise(\n\t\tdataFn({\n\t\t\tpathname,\n\t\t\tparams: {},\n\t\t\tset,\n\t\t\tsearchParams,\n\t\t\theaders,\n\t\t} satisfies SSRContext),\n\t).catch((err) => {\n\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\tconsole.error(\n\t\t\t`[reroute] Layout SSR data() failed for ${layoutPattern}:`,\n\t\t\tmsg,\n\t\t);\n\t\tthrow err;\n\t});\n}\n\nfunction handleLayoutStreamingMode(\n\tssrData: Record<string, unknown>,\n\tlayoutKey: string,\n\ttask: Promise<unknown>,\n\tset: { status: number },\n\tcacheKey: string,\n\tstatusCacheKey: string,\n\tmaxAgeMs: number,\n\tpendingTasks: Promise<unknown>[],\n): void {\n\tssrData[layoutKey] = task;\n\n\t// Ensure global store mirrors streaming promises so useLayoutData() can suspend\n\t// and later resume once the promise resolves.\n\tconst g = globalThis as unknown as Record<string, Record<string, unknown>>;\n\tg.__REROUTE_DATA__ = g.__REROUTE_DATA__ || {};\n\tg.__REROUTE_DATA__[layoutKey] = task;\n\n\tconst storeTask = task.then((value) => {\n\t\tif (maxAgeMs > 0) {\n\t\t\tstatusCache.set(statusCacheKey, set.status, maxAgeMs);\n\t\t}\n\t\tif (maxAgeMs > 0 && set.status === 200) {\n\t\t\tssrCache.set(cacheKey, value, maxAgeMs);\n\t\t}\n\t\treturn value;\n\t});\n\n\tpendingTasks.push(\n\t\tstoreTask\n\t\t\t.then((value) => {\n\t\t\t\tssrData[layoutKey] = value;\n\t\t\t\t// Keep global store in sync after resolution for Suspense retries\n\t\t\t\tif (g.__REROUTE_DATA__) {\n\t\t\t\t\tg.__REROUTE_DATA__[layoutKey] = value;\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}),\n\t);\n}\n\nasync function handleLayoutNonStreamingMode(\n\tssrData: Record<string, unknown>,\n\tlayoutKey: string,\n\ttask: Promise<unknown>,\n\tcacheKey: string,\n\tstatusCacheKey: string,\n\tmaxAgeMs: number,\n): Promise<unknown> {\n\tconst result = await task;\n\tssrData[layoutKey] = result;\n\n\tif (maxAgeMs > 0) {\n\t\tstatusCache.set(\n\t\t\tstatusCacheKey,\n\t\t\t(result as { status?: number }).status || 200,\n\t\t\tmaxAgeMs,\n\t\t);\n\t}\n\tif (maxAgeMs > 0) {\n\t\tssrCache.set(cacheKey, result, maxAgeMs);\n\t}\n\n\treturn result;\n}\n\nasync function processLayout(\n\tlayout: Doc,\n\tpathname: string,\n\tclientDir: string,\n\tisWatchMode: boolean,\n\tstreaming: boolean,\n\tmaxAgeMs: number,\n\tsearchParams: URLSearchParams | undefined,\n\tssrData: Record<string, unknown>,\n\tpendingTasks: Promise<unknown>[],\n\theaders?: Record<string, string>,\n): Promise<number | undefined> {\n\tif (typeof layout?.path !== 'string') return undefined;\n\n\tconst ssrExport = await loadLayoutSSRExport(layout, clientDir, isWatchMode);\n\tconst dataFn = ssrExport?.data;\n\n\tif (typeof dataFn !== 'function') return undefined;\n\n\tconst pattern = String(layout.pattern || '/');\n\tconst layoutKey = createLayoutDataKey(pattern);\n\tconst cacheKey = createLayoutCacheKey(pattern, pathname);\n\tconst statusCacheKey = `status:${cacheKey}`;\n\n\tconst { cachedData, cachedStatus } = checkLayoutCache(\n\t\tcacheKey,\n\t\tstatusCacheKey,\n\t\tmaxAgeMs,\n\t);\n\n\tif (cachedStatus && cachedStatus !== 200) {\n\t\treturn cachedStatus;\n\t}\n\n\tif (cachedData !== undefined) {\n\t\tssrData[layoutKey] = cachedData;\n\t\treturn undefined;\n\t}\n\n\tconst set = { status: 200 };\n\tconst task = createLayoutDataTask(\n\t\tdataFn,\n\t\tpathname,\n\t\tsearchParams,\n\t\tpattern,\n\t\theaders,\n\t);\n\n\tif (streaming) {\n\t\thandleLayoutStreamingMode(\n\t\t\tssrData,\n\t\t\tlayoutKey,\n\t\t\ttask,\n\t\t\tset,\n\t\t\tcacheKey,\n\t\t\tstatusCacheKey,\n\t\t\tmaxAgeMs,\n\t\t\tpendingTasks,\n\t\t);\n\t} else {\n\t\tawait handleLayoutNonStreamingMode(\n\t\t\tssrData,\n\t\t\tlayoutKey,\n\t\t\ttask,\n\t\t\tcacheKey,\n\t\t\tstatusCacheKey,\n\t\t\tmaxAgeMs,\n\t\t);\n\t}\n\n\treturn set.status !== 200 ? set.status : undefined;\n}\n\n/**\n * Compute SSR data from all matching layouts for a pathname.\n * Executes layout ssr.data() functions in hierarchical order (root → nested).\n *\n * @param sharedSsrData - In streaming mode, the shared ssrData object that gets passed to React.\n * Promise resolution callbacks must update THIS object directly.\n */\nexport async function computeLayoutData(\n\tpathname: string,\n\tclientDir: string,\n\t_cwd: string,\n\tisWatchMode: boolean,\n\toptions: ComputeOptions,\n\tsearchParams?: URLSearchParams,\n\troutesModule?: Doc,\n\tsharedSsrData?: Record<string, unknown>,\n): Promise<LayoutComputeResult> {\n\tconst streaming = options.streaming === true;\n\tconst ssrData: Record<string, unknown> =\n\t\tstreaming && sharedSsrData ? sharedSsrData : {};\n\tconst pendingTasks: Promise<unknown>[] = [];\n\tlet statusOverride: number | undefined;\n\n\tconst maxAge = options.maxAge || 0;\n\tconst maxAgeMs = maxAge * 1000;\n\n\ttry {\n\t\tconst layouts = routesModule?.layouts as Doc[] | undefined;\n\t\tconst matchingLayouts = getMatchingLayouts(pathname, layouts);\n\n\t\tfor (const layout of matchingLayouts) {\n\t\t\ttry {\n\t\t\t\tconst layoutStatus = await processLayout(\n\t\t\t\t\tlayout,\n\t\t\t\t\tpathname,\n\t\t\t\t\tclientDir,\n\t\t\t\t\tisWatchMode,\n\t\t\t\t\tstreaming,\n\t\t\t\t\tmaxAgeMs,\n\t\t\t\t\tsearchParams,\n\t\t\t\t\tssrData,\n\t\t\t\t\tpendingTasks,\n\t\t\t\t\toptions.headers,\n\t\t\t\t);\n\n\t\t\t\tif (layoutStatus && !statusOverride) {\n\t\t\t\t\tstatusOverride = layoutStatus;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Continue to next layout on error\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Return empty data on failure\n\t}\n\n\treturn { data: ssrData, statusOverride, pendingTasks };\n}\n\n/**\n * Merge layout data into route data for non-streaming mode.\n * Users can call useData() without knowing about layout keys.\n */\nexport function mergeLayoutData(\n\tssrData: Record<string, unknown>,\n\tpathname: string,\n): void {\n\tconst layoutKeys = Object.keys(ssrData).filter((key) =>\n\t\tkey.startsWith('__layout:'),\n\t);\n\n\tconst layoutData: Record<string, unknown> = {};\n\tfor (const key of layoutKeys) {\n\t\tObject.assign(layoutData, ssrData[key] as Record<string, unknown>);\n\t}\n\n\tif (pathname in ssrData) {\n\t\tssrData[pathname] = {\n\t\t\t...layoutData,\n\t\t\t...(typeof ssrData[pathname] === 'object' && ssrData[pathname] !== null\n\t\t\t\t? (ssrData[pathname] as Record<string, unknown>)\n\t\t\t\t: {}),\n\t\t};\n\t} else if (Object.keys(layoutData).length > 0) {\n\t\tssrData[pathname] = layoutData;\n\t}\n}\n",
|
|
24
|
+
"import { ssrCache, statusCache } from '../cache';\nimport { resolveContentPathParts } from '../content-path';\nimport { dynamicImport, isThenable, toPromise } from '../imports';\nimport { join } from '../path';\nimport type {\n\tComputeOptions,\n\tDoc,\n\tRouteMatch,\n\tSSR,\n\tSSRContext,\n} from '../types';\n\ntype RouteComputeResult = {\n\tdata: Record<string, unknown>;\n\terror?: string;\n\tstatusOverride?: number;\n\tpendingTasks: Promise<unknown>[];\n};\n\n/**\n * Compute SSR data for route-level ssr.data() functions.\n * Handles both streaming and non-streaming modes with promise flattening.\n *\n * @param sharedSsrData - In streaming mode, the shared ssrData object that gets passed to React.\n * Promise resolution callbacks must update THIS object directly.\n */\nfunction checkCache(\n\tcacheKey: string,\n\tstatusCacheKey: string,\n\tmaxAgeMs: number,\n\tpathname: string,\n\tssrData: Record<string, unknown>,\n): { cached: boolean; statusOverride?: number } {\n\t// Check status cache first\n\tconst cachedStatus =\n\t\tmaxAgeMs > 0 ? statusCache.get<number>(statusCacheKey) : undefined;\n\tconst statusOverride =\n\t\tcachedStatus && cachedStatus !== 200 ? cachedStatus : undefined;\n\n\t// Check data cache\n\tconst cached = maxAgeMs > 0 ? ssrCache.get(cacheKey) : undefined;\n\n\tif (cached !== undefined) {\n\t\tssrData[pathname] = cached;\n\t\treturn { cached: true, statusOverride };\n\t}\n\n\treturn { cached: false, statusOverride };\n}\n\nasync function loadRouteSSRExport(\n\troute: Doc,\n\tclientDir: string,\n\tisWatchMode: boolean,\n): Promise<SSR | undefined> {\n\t// Try to use bundled ssr export first (compiled binary)\n\tlet ssrExport = (route as Doc)?.ssr as SSR | undefined;\n\n\t// Fallback to dynamic import if not bundled (dev mode)\n\tif (!ssrExport) {\n\t\tconst routePath = String(route.path);\n\t\tconst cwd = join(clientDir, '..');\n\n\t\t// Try streaming wrapper first (for routes with ssr.data())\n\t\tlet abs = join(cwd, '.reroute', 'streaming', routePath);\n\t\tlet mod = await dynamicImport(abs, isWatchMode).catch(() => null);\n\n\t\t// Fallback to source if streaming doesn't exist\n\t\tif (!mod) {\n\t\t\tabs = join(clientDir, 'routes', routePath);\n\t\t\tmod = await dynamicImport(abs, isWatchMode);\n\t\t}\n\n\t\tssrExport = (mod as Doc)?.ssr as SSR | undefined;\n\t}\n\n\treturn ssrExport;\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Route data computation handles caching, streaming, and promise flattening\nexport async function computeRouteData(\n\tpathname: string,\n\tclientDir: string,\n\t_cwd: string,\n\tisWatchMode: boolean,\n\toptions: ComputeOptions,\n\tsearchParams?: URLSearchParams,\n\troutesModule?: Doc,\n\tsharedSsrData?: Record<string, unknown>,\n): Promise<RouteComputeResult> {\n\tconst streaming = options.streaming === true;\n\tconst ssrData: Record<string, unknown> =\n\t\tstreaming && sharedSsrData ? sharedSsrData : {};\n\tconst pendingTasks: Promise<unknown>[] = [];\n\tlet error: string | undefined;\n\tlet statusOverride: number | undefined;\n\n\tconst maxAge = options.maxAge || 0;\n\tconst maxAgeMs = maxAge * 1000;\n\n\tconst storeResult = (key: string, task: Promise<unknown>) => {\n\t\tpendingTasks.push(\n\t\t\ttask\n\t\t\t\t.then((value) => {\n\t\t\t\t\tssrData[key] = value;\n\t\t\t\t})\n\t\t\t\t.catch(() => {}),\n\t\t);\n\t};\n\n\ttry {\n\t\t// Strip query string and collection-scoped locale for route matching\n\t\t// (`/blog/pt-BR/hello` → `/blog/hello`)\n\t\tconst resolved = resolveContentPathParts(pathname);\n\t\tconst pathnameOnly = (resolved?.routingPathname || pathname).split('?')[0];\n\t\tconst match: RouteMatch =\n\t\t\ttypeof routesModule?.matchRoute === 'function'\n\t\t\t\t? routesModule.matchRoute(pathnameOnly)\n\t\t\t\t: null;\n\n\t\tconst route = match?.route;\n\t\tconst params = match?.params || {};\n\n\t\tif (!route || typeof route.path !== 'string') {\n\t\t\treturn { data: ssrData, pendingTasks };\n\t\t}\n\n\t\t// Load SSR export\n\t\tconst ssrExport = await loadRouteSSRExport(route, clientDir, isWatchMode);\n\n\t\tconst dataFn = ssrExport?.data;\n\n\t\tif (typeof dataFn !== 'function') {\n\t\t\treturn { data: ssrData, pendingTasks };\n\t\t}\n\n\t\t// Include search params in cache key\n\t\tconst searchQuery = searchParams ? `?${searchParams.toString()}` : '';\n\t\tconst cacheKey = `route:${pathname}${searchQuery}:${JSON.stringify(params)}`;\n\t\tconst statusCacheKey = `status:${cacheKey}`;\n\n\t\tconst cacheResult = checkCache(\n\t\t\tcacheKey,\n\t\t\tstatusCacheKey,\n\t\t\tmaxAgeMs,\n\t\t\tpathname,\n\t\t\tssrData,\n\t\t);\n\n\t\tif (cacheResult.cached) {\n\t\t\treturn {\n\t\t\t\tdata: ssrData,\n\t\t\t\tstatusOverride: cacheResult.statusOverride,\n\t\t\t\tpendingTasks,\n\t\t\t};\n\t\t}\n\n\t\tif (cacheResult.statusOverride) {\n\t\t\tstatusOverride = cacheResult.statusOverride;\n\t\t}\n\n\t\t// Execute data function\n\t\tconst set = { status: 200 };\n\t\tconst rawResult = dataFn({\n\t\t\tpathname,\n\t\t\tparams,\n\t\t\tset,\n\t\t\tsearchParams,\n\t\t\theaders: options.headers,\n\t\t} satisfies SSRContext);\n\t\tconst isPromise = rawResult instanceof Promise;\n\n\t\tif (streaming) {\n\t\t\tconst result = await handleStreamingRouteData(\n\t\t\t\trawResult,\n\t\t\t\tisPromise,\n\t\t\t\tpathname,\n\t\t\t\tset,\n\t\t\t\t{ ssrData, pendingTasks, storeResult },\n\t\t\t\t{ cacheKey, statusCacheKey, maxAgeMs },\n\t\t\t\t(msg) => {\n\t\t\t\t\terror = msg;\n\t\t\t\t},\n\t\t\t\t(val) => {\n\t\t\t\t\tif (!statusOverride) statusOverride = val;\n\t\t\t\t},\n\t\t\t);\n\t\t\tObject.assign(ssrData, result);\n\t\t} else {\n\t\t\tconst task = toPromise(rawResult).catch((err) => {\n\t\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\t\terror = `SSR data() failed: ${msg}`;\n\t\t\t\tconsole.error(`[reroute] ${error}`);\n\t\t\t\tthrow err;\n\t\t\t});\n\n\t\t\tconst result = await task;\n\t\t\tif (set.status !== 200 && !statusOverride) {\n\t\t\t\tstatusOverride = set.status;\n\t\t\t}\n\t\t\tssrData[pathname] = result;\n\t\t\tif (maxAgeMs > 0) {\n\t\t\t\tstatusCache.set(statusCacheKey, set.status, maxAgeMs);\n\t\t\t}\n\t\t\tif (maxAgeMs > 0 && set.status === 200) {\n\t\t\t\tssrCache.set(cacheKey, result, maxAgeMs);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Return what we have on error\n\t}\n\n\treturn { data: ssrData, error, statusOverride, pendingTasks };\n}\n\ntype StreamingContext = {\n\tssrData: Record<string, unknown>;\n\tpendingTasks: Promise<unknown>[];\n\tstoreResult: (key: string, task: Promise<unknown>) => void;\n};\n\ntype CacheContext = {\n\tcacheKey: string;\n\tstatusCacheKey: string;\n\tmaxAgeMs: number;\n};\n\nfunction shouldFlattenResult(\n\trawResult: unknown,\n\tisPromise: boolean,\n): { should: boolean; entries?: [string, unknown][] } {\n\tif (\n\t\ttypeof rawResult === 'object' &&\n\t\trawResult !== null &&\n\t\t!Array.isArray(rawResult) &&\n\t\t!isPromise\n\t) {\n\t\tconst entries = Object.entries(rawResult as Record<string, unknown>);\n\t\tconst hasMultipleKeys = entries.length > 1;\n\t\tconst hasPromises = entries.some(([_, v]) => isThenable(v));\n\n\t\tif (hasMultipleKeys || hasPromises) {\n\t\t\treturn { should: true, entries };\n\t\t}\n\t}\n\n\treturn { should: false };\n}\n\nfunction handleCaching(\n\tvalue: unknown,\n\tset: { status: number },\n\tcache: CacheContext,\n\tsetStatus: (val: number) => void,\n) {\n\tconst { cacheKey, statusCacheKey, maxAgeMs } = cache;\n\n\tif (set.status !== 200) {\n\t\tsetStatus(set.status);\n\t}\n\tif (maxAgeMs > 0) {\n\t\tstatusCache.set(statusCacheKey, set.status, maxAgeMs);\n\t}\n\tif (maxAgeMs > 0 && set.status === 200) {\n\t\tssrCache.set(cacheKey, value, maxAgeMs);\n\t}\n}\n\nasync function handleStreamingRouteData(\n\trawResult: unknown,\n\tisPromise: boolean,\n\tpathname: string,\n\tset: { status: number },\n\tctx: StreamingContext,\n\tcache: CacheContext,\n\tsetError: (msg: string) => void,\n\tsetStatus: (val: number) => void,\n): Promise<Record<string, unknown>> {\n\tconst { ssrData, storeResult } = ctx;\n\n\t// Check if this is a plain object with promises (flattening candidate)\n\tconst flattenCheck = shouldFlattenResult(rawResult, isPromise);\n\tif (flattenCheck.should && flattenCheck.entries) {\n\t\treturn handleFlattenedResult(\n\t\t\tflattenCheck.entries,\n\t\t\tset,\n\t\t\tctx,\n\t\t\tcache,\n\t\t\tsetError,\n\t\t\tsetStatus,\n\t\t);\n\t}\n\n\t// Single value, promise, or array - handle normally\n\tconst task = toPromise(rawResult).catch((err) => {\n\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\tsetError(`SSR data() failed: ${msg}`);\n\t\tconsole.error(`[reroute] SSR data() failed: ${msg}`);\n\t\tthrow err;\n\t});\n\n\t// Store Promise in both ssrData AND globalThis to handle concurrent requests\n\t// The global is what useData reads from during SSR\n\tssrData[pathname] = task;\n\tconst g = globalThis as unknown as Record<string, Record<string, unknown>>;\n\tif (!g.__REROUTE_DATA__) g.__REROUTE_DATA__ = {};\n\tg.__REROUTE_DATA__[pathname] = task;\n\n\tstoreResult(\n\t\tpathname,\n\t\ttask.then((value) => {\n\t\t\thandleCaching(value, set, cache, setStatus);\n\t\t\t// Update BOTH the local ssrData AND the global\n\t\t\tssrData[pathname] = value;\n\t\t\tif (g.__REROUTE_DATA__) {\n\t\t\t\tg.__REROUTE_DATA__[pathname] = value;\n\t\t\t}\n\t\t\treturn value;\n\t\t}),\n\t);\n\n\treturn {};\n}\n\n/**\n * Handle flattened object result where each key streams independently.\n */\nfunction handleFlattenedResult(\n\tentries: [string, unknown][],\n\tset: { status: number },\n\tctx: StreamingContext,\n\tcache: CacheContext,\n\tsetError: (msg: string) => void,\n\tsetStatus: (val: number) => void,\n): Record<string, unknown> {\n\tconst { ssrData, storeResult } = ctx;\n\tconst { cacheKey, statusCacheKey, maxAgeMs } = cache;\n\n\t// Ensure globalThis.__REROUTE_DATA__ exists for useData to read from\n\tconst g = globalThis as unknown as Record<string, Record<string, unknown>>;\n\tif (!g.__REROUTE_DATA__) g.__REROUTE_DATA__ = {};\n\n\tconst promisesToResolve: Promise<[string, unknown]>[] = [];\n\n\tfor (const [key, val] of entries) {\n\t\tif (isThenable(val)) {\n\t\t\tconst wrappedPromise = (val as Promise<unknown>)\n\t\t\t\t.then((resolved) => {\n\t\t\t\t\tif (set.status !== 200) {\n\t\t\t\t\t\tsetStatus(set.status);\n\t\t\t\t\t}\n\t\t\t\t\t// CRITICAL: Update both ssrData and globalThis with resolved value\n\t\t\t\t\t// This allows React's Suspense retry to find the resolved data\n\t\t\t\t\tssrData[key] = resolved;\n\t\t\t\t\tg.__REROUTE_DATA__[key] = resolved;\n\t\t\t\t\treturn resolved;\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tsetError(`SSR data() failed for key ${key}: ${msg}`);\n\t\t\t\t\tconsole.error(`[reroute] SSR data() failed for key ${key}: ${msg}`);\n\t\t\t\t\tthrow err;\n\t\t\t\t});\n\n\t\t\t// Store Promise initially - useData will throw it for Suspense\n\t\t\tssrData[key] = wrappedPromise;\n\t\t\tg.__REROUTE_DATA__[key] = wrappedPromise;\n\t\t\tstoreResult(key, wrappedPromise);\n\t\t\tpromisesToResolve.push(\n\t\t\t\twrappedPromise.then((resolved) => [key, resolved] as [string, unknown]),\n\t\t\t);\n\t\t} else {\n\t\t\tssrData[key] = val;\n\t\t\tg.__REROUTE_DATA__[key] = val;\n\t\t\tpromisesToResolve.push(Promise.resolve([key, val] as [string, unknown]));\n\t\t}\n\t}\n\n\t// Cache when all promises resolve\n\tif (maxAgeMs > 0) {\n\t\tPromise.all(promisesToResolve)\n\t\t\t.then((resolvedEntries) => {\n\t\t\t\tstatusCache.set(statusCacheKey, set.status, maxAgeMs);\n\t\t\t\tif (set.status === 200) {\n\t\t\t\t\tconst resolvedResult: Record<string, unknown> = {};\n\t\t\t\t\tfor (const [key, value] of resolvedEntries) {\n\t\t\t\t\t\tresolvedResult[key] = value;\n\t\t\t\t\t}\n\t\t\t\t\tssrCache.set(cacheKey, resolvedResult, maxAgeMs);\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {\n\t\t\t\t// Don't cache on failure\n\t\t\t});\n\t}\n\n\treturn {};\n}\n",
|
|
25
|
+
"import { dynamicImport } from '../imports';\nimport { join } from '../path';\nimport { matchRoutes } from '../route-match';\nimport type { ComputeOptions, ComputeResult, Doc } from '../types';\nimport { computeContentData } from './content';\nimport { computeLayoutData, mergeLayoutData } from './layouts';\nimport { computeRouteData } from './routes';\n\nexport { computeContentData } from './content';\nexport { computeLayoutData, mergeLayoutData } from './layouts';\nexport { computeRouteData } from './routes';\nexport { loadRoutesModule };\n\nfunction createBundledRoutesModule(\n\tbundledRoutes: unknown,\n\tbundledLayouts: unknown,\n): Doc {\n\tconst bundledNotFoundRoutes = (globalThis as Record<string, unknown>)\n\t\t.__REROUTE_NOT_FOUND_ROUTES__;\n\n\treturn {\n\t\troutes: bundledRoutes,\n\t\tlayouts: bundledLayouts,\n\t\tnotFoundRoutes: bundledNotFoundRoutes,\n\t\tmatchRoute: (pathname: string) =>\n\t\t\tmatchRoutes(bundledRoutes as Doc[], pathname),\n\t};\n}\n\nasync function loadRoutesModule(\n\tcwd: string,\n\tisWatchMode: boolean,\n): Promise<Doc | null> {\n\ttry {\n\t\tconst bundledRoutes = (globalThis as Record<string, unknown>)\n\t\t\t.__REROUTE_ROUTES__;\n\t\tconst bundledLayouts = (globalThis as Record<string, unknown>)\n\t\t\t.__REROUTE_LAYOUTS__;\n\n\t\tif (bundledRoutes && bundledLayouts) {\n\t\t\treturn createBundledRoutesModule(bundledRoutes, bundledLayouts);\n\t\t}\n\n\t\tconst routesPath = join(cwd, '.reroute', 'routes.ts');\n\t\treturn await dynamicImport(routesPath, isWatchMode);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction mergeComputeResult(\n\tresult: {\n\t\tdata: Record<string, unknown>;\n\t\terror?: string;\n\t\tstatusOverride?: number;\n\t\tpendingTasks: Promise<unknown>[];\n\t},\n\tssrData: Record<string, unknown>,\n\tstatusContainer: { value: number | undefined },\n\tallPendingTasks: Promise<unknown>[],\n\tstreaming: boolean,\n): string | undefined {\n\tif (!streaming) {\n\t\tObject.assign(ssrData, result.data);\n\t}\n\tif (result.statusOverride && !statusContainer.value) {\n\t\tstatusContainer.value = result.statusOverride;\n\t}\n\tallPendingTasks.push(...result.pendingTasks);\n\treturn result.error;\n}\n\n/**\n * Compute SSR data by executing ssr.data() from layouts, routes, and content modules.\n * Orchestrates the full data computation pipeline for a request.\n */\nexport async function computeSSRData(\n\tpathname: string,\n\tclientDir: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n\toptions: ComputeOptions = {},\n\tsearchParams?: URLSearchParams,\n): Promise<ComputeResult> {\n\tconst ssrData: Record<string, unknown> = {};\n\tlet error: string | undefined;\n\tconst statusContainer = { value: undefined as number | undefined };\n\tconst allPendingTasks: Promise<unknown>[] = [];\n\tconst streaming = options.streaming === true;\n\n\tconst routesModule = await loadRoutesModule(cwd, isWatchMode);\n\tif (!routesModule) {\n\t\treturn {\n\t\t\tdata: ssrData,\n\t\t\terror: 'Failed to load routes module',\n\t\t\tstatusContainer,\n\t\t};\n\t}\n\n\ttry {\n\t\tconst layoutResult = await computeLayoutData(\n\t\t\tpathname,\n\t\t\tclientDir,\n\t\t\tcwd,\n\t\t\tisWatchMode,\n\t\t\toptions,\n\t\t\tsearchParams,\n\t\t\troutesModule,\n\t\t\tssrData,\n\t\t);\n\t\tmergeComputeResult(\n\t\t\tlayoutResult,\n\t\t\tssrData,\n\t\t\tstatusContainer,\n\t\t\tallPendingTasks,\n\t\t\tstreaming,\n\t\t);\n\t} catch {\n\t\t// Continue with other data sources\n\t}\n\n\ttry {\n\t\tconst contentResult = await computeContentData(\n\t\t\tpathname,\n\t\t\toptions,\n\t\t\tsearchParams,\n\t\t\tssrData,\n\t\t);\n\t\terror = mergeComputeResult(\n\t\t\tcontentResult,\n\t\t\tssrData,\n\t\t\tstatusContainer,\n\t\t\tallPendingTasks,\n\t\t\tstreaming,\n\t\t);\n\t} catch {\n\t\t// Continue with route data\n\t}\n\n\ttry {\n\t\tconst routeResult = await computeRouteData(\n\t\t\tpathname,\n\t\t\tclientDir,\n\t\t\tcwd,\n\t\t\tisWatchMode,\n\t\t\toptions,\n\t\t\tsearchParams,\n\t\t\troutesModule,\n\t\t\tssrData,\n\t\t);\n\t\tconst routeError = mergeComputeResult(\n\t\t\trouteResult,\n\t\t\tssrData,\n\t\t\tstatusContainer,\n\t\t\tallPendingTasks,\n\t\t\tstreaming,\n\t\t);\n\t\tif (routeError) error = routeError;\n\t} catch {\n\t\t// Return what we have\n\t}\n\n\tif (!streaming) {\n\t\tmergeLayoutData(ssrData, pathname);\n\t}\n\n\tconst pending =\n\t\tstreaming && allPendingTasks.length\n\t\t\t? Promise.allSettled(allPendingTasks).then(() => undefined)\n\t\t\t: undefined;\n\n\treturn { data: ssrData, error, pending, statusContainer };\n}\n\n/**\n * Set global SSR data for client hydration\n * MERGES into existing data to support concurrent streaming requests\n */\nexport function setGlobalSSRData(data: Record<string, unknown>): void {\n\ttry {\n\t\tconst g = globalThis as Doc;\n\t\tif (!g.__REROUTE_DATA__) {\n\t\t\tg.__REROUTE_DATA__ = {};\n\t\t}\n\t\t// Merge new data into existing - don't replace to avoid race conditions\n\t\tObject.assign(g.__REROUTE_DATA__, data);\n\t} catch {\n\t\t// Ignore errors in restricted environments\n\t}\n}\n",
|
|
26
|
+
"import { readdir, stat } from 'node:fs/promises';\nimport { resolveContentPathParts } from './content-path';\nimport { dynamicImport } from './imports';\nimport { join } from './path';\nimport type { Doc } from './types';\n\n/**\n * Import a content module for a given pathname like \"/collection/name\"\n * or \"/collection/locale/name\".\n */\nexport async function importContentModuleForPath(\n\tpathname: string,\n\tclientDir: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n): Promise<Doc | null> {\n\ttry {\n\t\tconst resolved = resolveContentPathParts(pathname);\n\t\tif (!resolved) return null;\n\n\t\tconst { collection, name, locale } = resolved;\n\n\t\t// 1) Try registry mapping for exact chunk path\n\t\tconst registryModule = await tryRegistryImport(\n\t\t\tcollection,\n\t\t\tname,\n\t\t\tcwd,\n\t\t\tisWatchMode,\n\t\t\tlocale,\n\t\t);\n\t\tif (registryModule) return registryModule;\n\n\t\t// 2) Try latest prebuilt chunk from .reroute\n\t\tconst chunkModule = await tryChunkImport(\n\t\t\tcollection,\n\t\t\tname,\n\t\t\tcwd,\n\t\t\tisWatchMode,\n\t\t);\n\t\tif (chunkModule) return chunkModule;\n\n\t\t// 3) Fallback to source files\n\t\tconst sourceModule = await trySourceImport(\n\t\t\tcollection,\n\t\t\tname,\n\t\t\tclientDir,\n\t\t\tisWatchMode,\n\t\t\tlocale,\n\t\t);\n\t\tif (sourceModule) return sourceModule;\n\t} catch {\n\t\t// Return null on any error\n\t}\n\n\treturn null;\n}\n\nasync function tryRegistryImport(\n\tcollection: string,\n\tname: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n\tlocale?: string,\n): Promise<Doc | null> {\n\ttry {\n\t\tconst registryPath = join(cwd, '.reroute', 'content.ts');\n\t\tconst reg = await dynamicImport(registryPath, isWatchMode);\n\n\t\tconst get = reg?.getContentEntry as\n\t\t\t| ((c: string, n: string, l?: string) => { module?: string } | undefined)\n\t\t\t| undefined;\n\t\tconst entry =\n\t\t\ttypeof get === 'function' ? get(collection, name, locale) : undefined;\n\t\tconst moduleUrl = (entry as Doc)?.module as string | undefined;\n\n\t\tif (!moduleUrl?.endsWith('.js')) return null;\n\n\t\tconst raw = moduleUrl.split('?')[0] || '';\n\t\tconst normalized = raw.replace(/^\\/+/, '');\n\n\t\tconst candidates: string[] = [];\n\t\tif (normalized) {\n\t\t\tcandidates.push(join(cwd, normalized));\n\t\t\tif (!normalized.startsWith('.reroute/')) {\n\t\t\t\tcandidates.push(join(cwd, '.reroute', normalized));\n\t\t\t}\n\t\t}\n\n\t\tfor (const abs of candidates) {\n\t\t\ttry {\n\t\t\t\tif (!(await Bun.file(abs).exists())) continue;\n\t\t\t\treturn await dynamicImport(abs, isWatchMode);\n\t\t\t} catch {\n\t\t\t\t// Try next candidate\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Registry unavailable\n\t}\n\n\treturn null;\n}\n\nasync function tryChunkImport(\n\tcollection: string,\n\tname: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n): Promise<Doc | null> {\n\ttry {\n\t\tconst chunkDir = join(cwd, '.reroute', 'chunks', collection);\n\t\tconst files = await readdir(chunkDir);\n\t\tconst matches = files.filter(\n\t\t\t(n) => n.startsWith(`${name}.`) && n.endsWith('.js'),\n\t\t);\n\n\t\tif (!matches.length) return null;\n\n\t\t// Find latest by modification time\n\t\tlet latest = matches[0];\n\t\tlet latestM = 0;\n\t\tfor (const f of matches) {\n\t\t\ttry {\n\t\t\t\tconst s = await stat(join(chunkDir, f));\n\t\t\t\tif (s.mtimeMs >= latestM) {\n\t\t\t\t\tlatestM = s.mtimeMs;\n\t\t\t\t\tlatest = f;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip inaccessible files\n\t\t\t}\n\t\t}\n\n\t\tconst absChunk = join(chunkDir, latest);\n\t\treturn await dynamicImport(absChunk, isWatchMode);\n\t} catch {\n\t\t// Chunk directory doesn't exist\n\t}\n\n\treturn null;\n}\n\nasync function trySourceImport(\n\tcollection: string,\n\tname: string,\n\tclientDir: string,\n\tisWatchMode: boolean,\n\tlocale?: string,\n): Promise<Doc | null> {\n\ttry {\n\t\tconst candidates: string[] = [];\n\t\tif (locale) {\n\t\t\tcandidates.push(\n\t\t\t\tjoin(clientDir, 'routes', collection, 'content', locale, `${name}.tsx`),\n\t\t\t\tjoin(clientDir, 'routes', collection, 'content', locale, `${name}.ts`),\n\t\t\t);\n\t\t}\n\t\tcandidates.push(\n\t\t\tjoin(clientDir, 'routes', collection, 'content', `${name}.tsx`),\n\t\t\tjoin(clientDir, 'routes', collection, 'content', `${name}.ts`),\n\t\t);\n\n\t\tfor (const absSrc of candidates) {\n\t\t\tif (await Bun.file(absSrc).exists()) {\n\t\t\t\treturn await dynamicImport(absSrc, isWatchMode);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Source files don't exist\n\t}\n\n\treturn null;\n}\n",
|
|
27
|
+
"import { resolveContentPathParts } from './content-path';\nimport { importContentModuleForPath } from './modules';\nimport type { Doc } from './types';\n\n// LRU cache for SSR modules to prevent unbounded memory growth\n// Max 200 modules (each ~3-5MB) = ~1GB max instead of infinite growth\nconst MAX_SSR_MODULES = 200;\nconst moduleAccessOrder: string[] = [];\n\n/**\n * Evict least recently used modules when cache is full\n */\nfunction evictLRUModule(): void {\n\tif (moduleAccessOrder.length === 0) return;\n\n\tconst keyToEvict = moduleAccessOrder.shift(); // Remove oldest\n\tif (!keyToEvict) return;\n\n\tconst g = globalThis as Doc;\n\tif (g.__REROUTE_SSR_MODULES__) {\n\t\tdelete g.__REROUTE_SSR_MODULES__[keyToEvict];\n\t}\n\tif (g.__REROUTE_SSR_EXPORTS__) {\n\t\tdelete g.__REROUTE_SSR_EXPORTS__[keyToEvict];\n\t}\n}\n\n/**\n * Mark module as recently used (move to end of LRU queue)\n */\nfunction touchModule(key: string): void {\n\t// Remove from current position\n\tconst index = moduleAccessOrder.indexOf(key);\n\tif (index > -1) {\n\t\tmoduleAccessOrder.splice(index, 1);\n\t}\n\t// Add to end (most recently used)\n\tmoduleAccessOrder.push(key);\n\n\t// Evict if over limit\n\twhile (moduleAccessOrder.length > MAX_SSR_MODULES) {\n\t\tevictLRUModule();\n\t}\n}\n\n/**\n * Drop SSR module/export stores so the next request re-imports content.\n * Watch-mode reloads must not keep the previous markdown module.\n */\nexport function clearSSRModuleStores(): void {\n\tconst g = globalThis as Doc;\n\tif (g.__REROUTE_SSR_MODULES__) {\n\t\tg.__REROUTE_SSR_MODULES__ = {};\n\t}\n\tif (g.__REROUTE_SSR_EXPORTS__) {\n\t\tg.__REROUTE_SSR_EXPORTS__ = {};\n\t}\n\tmoduleAccessOrder.length = 0;\n}\n\n/**\n * Seed SSR module store for a content path.\n * Makes the content module available for server-side rendering.\n * Uses LRU eviction to prevent unbounded memory growth.\n */\nexport async function seedSSRModuleForPath(\n\tpathname: string,\n\tclientDir: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n): Promise<void> {\n\ttry {\n\t\tconst resolved = resolveContentPathParts(pathname);\n\n\t\tif (!resolved) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst { key } = resolved;\n\n\t\tconst mod = await importContentModuleForPath(\n\t\t\tpathname,\n\t\t\tclientDir,\n\t\t\tcwd,\n\t\t\tisWatchMode,\n\t\t);\n\n\t\tif (!mod) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Store full module for hooks like useToc\n\t\t(globalThis as Doc).__REROUTE_SSR_MODULES__ =\n\t\t\t(globalThis as Doc).__REROUTE_SSR_MODULES__ || {};\n\t\t(globalThis as Doc).__REROUTE_SSR_MODULES__[key] = mod;\n\n\t\t// Capture exports for head injection\n\t\ttry {\n\t\t\t(globalThis as Doc).__REROUTE_SSR_EXPORTS__ =\n\t\t\t\t(globalThis as Doc).__REROUTE_SSR_EXPORTS__ || {};\n\t\t\t(globalThis as Doc).__REROUTE_SSR_EXPORTS__[key] = {\n\t\t\t\tmeta: (mod as Doc).meta || {},\n\t\t\t\tssr: (mod as Doc).ssr || {},\n\t\t\t\ttoc: (mod as Doc).toc || [],\n\t\t\t};\n\t\t} catch {\n\t\t\t// Ignore export capture errors\n\t\t}\n\n\t\t// Mark as recently used for LRU eviction\n\t\ttouchModule(key);\n\t} catch {\n\t\t// Ignore seeding errors\n\t}\n}\n",
|
|
28
|
+
"import { resolveContentPathParts } from './content-path';\nimport { dynamicImport } from './imports';\nimport { getMatchingLayouts } from './layouts';\nimport { join } from './path';\nimport { matchRoutes } from './route-match';\nimport { seedSSRModuleForPath } from './seed';\nimport type { ComputeParams, Doc, SSR } from './types';\n\n/**\n * Compute SSR data for client-side navigation.\n * Returns lazy markers for promises to support progressive loading.\n */\nfunction loadContentData(\n\tpathname: string,\n\tsearchParams?: URLSearchParams,\n): { key: string; data: unknown } | null {\n\ttry {\n\t\tconst resolved = resolveContentPathParts(pathname);\n\t\tif (!resolved) return null;\n\n\t\tconst key = resolved.key;\n\t\tconst g = globalThis as Doc;\n\t\tconst exp = g.__REROUTE_SSR_EXPORTS__?.[key];\n\t\tconst dataFn = (exp as Doc)?.ssr?.data as SSR['data'] | undefined;\n\n\t\tif (typeof dataFn === 'function') {\n\t\t\tconst rawResult = dataFn({\n\t\t\t\tpathname,\n\t\t\t\tparams: { name: resolved.name },\n\t\t\t\tsearchParams,\n\t\t\t\t// Note: headers are not available in client-side navigation\n\t\t\t});\n\n\t\t\tconst result = processDataResult(rawResult, pathname, pathname);\n\t\t\treturn { key: pathname, data: result };\n\t\t}\n\t} catch {\n\t\t// Content data unavailable\n\t}\n\n\treturn null;\n}\n\nfunction loadMatchedRouteData(\n\tpathname: string,\n\tclientDir: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n\tsearchParams?: URLSearchParams,\n): Promise<{ key: string; data: unknown } | null> {\n\treturn loadRouteDataInternal(\n\t\tpathname,\n\t\tclientDir,\n\t\tcwd,\n\t\tisWatchMode,\n\t\tsearchParams,\n\t);\n}\n\nasync function loadRouteDataInternal(\n\tpathname: string,\n\tclientDir: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n\tsearchParams?: URLSearchParams,\n): Promise<{ key: string; data: unknown } | null> {\n\ttry {\n\t\t// Check for bundled routes first (compiled binary support)\n\t\tlet routes = (globalThis as Record<string, unknown>).__REROUTE_ROUTES__ as\n\t\t\t| Doc[]\n\t\t\t| undefined;\n\t\tlet matchRouteFn: ((pathname: string) => Doc | null) | undefined;\n\n\t\t// Fallback to dynamic import (dev mode)\n\t\tif (!routes) {\n\t\t\tconst routesPath = join(cwd, '.reroute', 'routes.ts');\n\t\t\tconst m = await dynamicImport(routesPath, isWatchMode);\n\t\t\troutes = m?.routes as Doc[] | undefined;\n\t\t\tmatchRouteFn = (m as Doc)?.matchRoute as\n\t\t\t\t| ((pathname: string) => Doc | null)\n\t\t\t\t| undefined;\n\t\t} else {\n\t\t\t// In bundled mode, we need to match manually\n\t\t\tmatchRouteFn = (pathname: string) =>\n\t\t\t\tmatchRoutes((routes || []) as Doc[], pathname);\n\t\t}\n\n\t\tconst pathnameOnly = pathname.split('?')[0];\n\t\tconst match =\n\t\t\ttypeof matchRouteFn === 'function' ? matchRouteFn(pathnameOnly) : null;\n\n\t\tconst r = (match as Doc | null)?.route;\n\t\tconst paramsValue = ((match as Doc | null)?.params || {}) as Record<\n\t\t\tstring,\n\t\t\tstring\n\t\t>;\n\n\t\tif (r && typeof (r as Doc).path === 'string') {\n\t\t\t// Try to use bundled ssr export first (compiled binary)\n\t\t\tlet ssrExport = (r as Doc)?.ssr as Doc | undefined;\n\n\t\t\t// Fallback to dynamic import if not bundled (dev mode)\n\t\t\tif (!ssrExport) {\n\t\t\t\t// In dev mode, routes might be from .reroute/routes.ts which imports from streaming wrappers\n\t\t\t\t// The streaming wrapper re-exports ssr, so we should try loading from there first\n\t\t\t\tconst routePath = String((r as Doc).path);\n\t\t\t\tlet abs = join(cwd, '.reroute', 'streaming', routePath);\n\n\t\t\t\tlet mod = await dynamicImport(abs, isWatchMode).catch(() => null);\n\n\t\t\t\t// If streaming wrapper doesn't exist or fails, load from source\n\t\t\t\tif (!mod) {\n\t\t\t\t\tabs = join(clientDir, 'routes', routePath);\n\t\t\t\t\tmod = await dynamicImport(abs, isWatchMode);\n\t\t\t\t}\n\n\t\t\t\tssrExport = (mod as Doc)?.ssr as SSR | undefined;\n\t\t\t}\n\n\t\t\tconst dataFn = ssrExport?.data;\n\n\t\t\tif (typeof dataFn === 'function') {\n\t\t\t\tconst rawResult = dataFn({\n\t\t\t\t\tpathname,\n\t\t\t\t\tparams: paramsValue,\n\t\t\t\t\tsearchParams,\n\t\t\t\t\t// Note: headers are not available in client-side navigation\n\t\t\t\t});\n\n\t\t\t\tconst data = processDataResult(rawResult, pathname, pathname);\n\t\t\t\treturn { key: pathname, data };\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Route data unavailable\n\t}\n\n\treturn null;\n}\n\nfunction determineReturnStructure(allData: Record<string, unknown>): unknown {\n\tconst dataKeys = Object.keys(allData);\n\n\tif (dataKeys.length > 1) {\n\t\treturn { __reroute_multiple__: true, ...allData };\n\t}\n\n\tif (dataKeys.length === 1) {\n\t\tconst singleKey = dataKeys[0];\n\t\t// Keep layout key structure for proper client-side handling\n\t\tif (singleKey.startsWith('__layout:')) {\n\t\t\treturn allData;\n\t\t}\n\t\treturn allData[singleKey];\n\t}\n\n\treturn null;\n}\n\nexport async function computeSSRDataForPath(\n\tparams: ComputeParams,\n): Promise<unknown> {\n\tconst { pathname, clientDir, cwd, isWatchMode, searchParams } = params;\n\n\t// Seed content modules for potential content pages\n\ttry {\n\t\tawait seedSSRModuleForPath(pathname, clientDir, cwd, isWatchMode);\n\t} catch {\n\t\t// Continue without seeding\n\t}\n\n\t// 1) Layout data\n\tconst allData = await loadLayoutData(\n\t\tpathname,\n\t\tclientDir,\n\t\tcwd,\n\t\tisWatchMode,\n\t\tsearchParams,\n\t);\n\n\t// 2) Content-level data\n\tconst contentResult = loadContentData(pathname, searchParams);\n\tif (contentResult) {\n\t\t// For content pages, return directly if it's the only data\n\t\tif (Object.keys(allData).length === 0) {\n\t\t\treturn contentResult.data;\n\t\t}\n\t\tallData[contentResult.key] = contentResult.data;\n\t}\n\n\t// 3) Route-level data\n\tconst routeResult = await loadMatchedRouteData(\n\t\tpathname,\n\t\tclientDir,\n\t\tcwd,\n\t\tisWatchMode,\n\t\tsearchParams,\n\t);\n\tif (routeResult) {\n\t\tallData[routeResult.key] = routeResult.data;\n\t}\n\n\treturn determineReturnStructure(allData);\n}\n\nfunction processLayoutDataResult(\n\trawResult: unknown,\n\tpathname: string,\n\tlayoutKey: string,\n): unknown {\n\tif (rawResult instanceof Promise) {\n\t\treturn createLazyMarker(pathname, layoutKey);\n\t}\n\n\tif (\n\t\ttypeof rawResult === 'object' &&\n\t\trawResult !== null &&\n\t\t!Array.isArray(rawResult)\n\t) {\n\t\tconst entries = Object.entries(rawResult as Record<string, unknown>);\n\t\tconst hasPromises = entries.some(([_, v]) => v instanceof Promise);\n\n\t\tif (hasPromises) {\n\t\t\tconst flattened: Record<string, unknown> = { __flatten__: true };\n\t\t\tfor (const [k, value] of entries) {\n\t\t\t\tif (value instanceof Promise) {\n\t\t\t\t\tflattened[k] = createLazyMarker(pathname, k);\n\t\t\t\t} else {\n\t\t\t\t\tflattened[k] = value;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn flattened;\n\t\t}\n\t}\n\n\treturn rawResult;\n}\n\nasync function processLayout(\n\tlayout: Doc,\n\tpathname: string,\n\tclientDir: string,\n\tisWatchMode: boolean,\n\tsearchParams?: URLSearchParams,\n): Promise<{ key: string; data: unknown } | null> {\n\tif (typeof layout?.path !== 'string') return null;\n\n\ttry {\n\t\t// Try to use bundled ssr export first (compiled binary)\n\t\tlet ssrExport = (layout as Doc)?.ssr as SSR | undefined;\n\n\t\t// Fallback to dynamic import if not bundled (dev mode)\n\t\tif (!ssrExport) {\n\t\t\tconst abs = join(clientDir, 'routes', String(layout.path));\n\t\t\tconst mod = await dynamicImport(abs, isWatchMode);\n\t\t\tssrExport = (mod as Doc)?.ssr as SSR | undefined;\n\t\t}\n\n\t\tconst dataFn = ssrExport?.data;\n\n\t\tif (typeof dataFn !== 'function') return null;\n\n\t\tconst pattern = String(layout.pattern || '/');\n\t\tconst layoutKey = `__layout:${pattern}`;\n\t\tconst rawResult = dataFn({\n\t\t\tpathname,\n\t\t\tparams: {},\n\t\t\tsearchParams,\n\t\t\t// Note: headers are not available in client-side navigation\n\t\t});\n\n\t\tconst processedData = processLayoutDataResult(\n\t\t\trawResult,\n\t\t\tpathname,\n\t\t\tlayoutKey,\n\t\t);\n\t\treturn { key: layoutKey, data: processedData };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nasync function loadLayoutData(\n\tpathname: string,\n\tclientDir: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n\tsearchParams?: URLSearchParams,\n): Promise<Record<string, unknown>> {\n\tconst allData: Record<string, unknown> = {};\n\n\ttry {\n\t\t// Check for bundled layouts first (compiled binary support)\n\t\tlet layouts = (globalThis as Record<string, unknown>).__REROUTE_LAYOUTS__ as\n\t\t\t| Doc[]\n\t\t\t| undefined;\n\n\t\t// Fallback to dynamic import (dev mode)\n\t\tif (!layouts) {\n\t\t\tconst routesPath = join(cwd, '.reroute', 'routes.ts');\n\t\t\tconst m = await dynamicImport(routesPath, isWatchMode);\n\t\t\tlayouts = m?.layouts as Doc[] | undefined;\n\t\t}\n\n\t\tconst matchingLayouts = getMatchingLayouts(pathname, layouts);\n\n\t\tfor (const layout of matchingLayouts) {\n\t\t\tconst result = await processLayout(\n\t\t\t\tlayout,\n\t\t\t\tpathname,\n\t\t\t\tclientDir,\n\t\t\t\tisWatchMode,\n\t\t\t\tsearchParams,\n\t\t\t);\n\n\t\t\tif (result) {\n\t\t\t\tallData[result.key] = result.data;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Routes unavailable\n\t}\n\n\treturn allData;\n}\n\nfunction processDataResult(\n\trawResult: unknown,\n\tpathname: string,\n\tkey: string,\n): unknown {\n\treturn processLayoutDataResult(rawResult, pathname, key);\n}\n\nfunction createLazyMarker(pathname: string, key: string): unknown {\n\treturn {\n\t\t__lazy__: true,\n\t\t__fetch__: `/__reroute_data?path=${encodeURIComponent(pathname)}&key=${encodeURIComponent(key)}`,\n\t};\n}\n",
|
|
29
|
+
"import type { OGImageOptions } from '../config';\nimport { findOGImageForPath } from './discovery';\n\n/**\n * Generate OG image meta tags for SSR\n */\nexport async function generateOGImageMetaTags(\n\tpathname: string,\n\tclientDir: string,\n\togConfig: OGImageOptions | undefined,\n): Promise<string> {\n\tif (!ogConfig?.enabled) {\n\t\treturn '';\n\t}\n\n\tconst width = ogConfig.width || 1200;\n\tconst height = ogConfig.height || 630;\n\n\t// Check if there's an OG image component for this route\n\tconst ogPath = await findOGImageForPath(pathname, clientDir);\n\tconst hasOGImage = ogPath !== null || ogConfig.defaultTemplate;\n\n\tif (!hasOGImage) {\n\t\treturn '';\n\t}\n\n\t// Generate OG image URL (relative path)\n\t// For root (/), use /index.png; for /blog, use /blog/index.png\n\tlet ogImagePath: string;\n\tif (pathname === '/' || pathname === '') {\n\t\togImagePath = '/__reroute_og/index.png';\n\t} else if (pathname.endsWith('/')) {\n\t\togImagePath = `/__reroute_og${pathname}index.png`;\n\t} else {\n\t\togImagePath = `/__reroute_og${pathname}.png`;\n\t}\n\n\t// Convert to absolute URL if baseUrl is provided\n\tconst baseUrl = ogConfig.baseUrl;\n\tif (baseUrl) {\n\t\t// Remove trailing slash from baseUrl if present\n\t\tconst cleanBaseUrl = baseUrl.replace(/\\/$/, '');\n\t\togImagePath = `${cleanBaseUrl}${ogImagePath}`;\n\t} else {\n\t\tconsole.warn(\n\t\t\t'[reroute] OG images require a baseUrl for proper social media sharing. Add baseUrl to ogImage config.',\n\t\t);\n\t}\n\n\t// Generate meta tags\n\t// Note: twitter:card is generated separately in global OG tags based on user config\n\treturn `<meta property=\"og:image\" content=\"${ogImagePath}\" />\n<meta property=\"og:image:width\" content=\"${width}\" />\n<meta property=\"og:image:height\" content=\"${height}\" />\n<meta name=\"twitter:image\" content=\"${ogImagePath}\" />`;\n}\n",
|
|
30
|
+
"import type { OGImageOptions, OGOptions } from '../../config';\nimport { buildHeadFromMeta } from '../../content/metadata';\nimport {\n\threflangTags,\n\tlocalizeContentHref,\n\ttoOgLocale,\n} from '../../i18n/pathname';\nimport { generateOGImageMetaTags } from '../../og/meta';\nimport { loadRoutesModule } from './compute';\nimport { resolveContentPathParts } from './content-path';\nimport { dynamicImport } from './imports';\nimport { getMatchingLayouts } from './layouts';\nimport { join } from './path';\nimport type { Doc, MetadataResult, SSR, SSRHeadContext } from './types';\n\n/**\n * Load meta and ssr exports from a route/layout module.\n * Uses bundled values first (for compiled binaries), falls back to dynamic import.\n */\nasync function loadModuleMetaAndSSR(\n\tmoduleObj: Doc | undefined,\n\tabsolutePath: string,\n\tisWatchMode: boolean,\n): Promise<{ meta?: Doc; ssr?: SSR }> {\n\t// Use bundled meta/ssr if available (compiled binary)\n\tconst bundledMeta = moduleObj?.meta as Doc | undefined;\n\tconst bundledSSR = moduleObj?.ssr as SSR | undefined;\n\n\tif (bundledMeta !== undefined || bundledSSR !== undefined) {\n\t\treturn { meta: bundledMeta, ssr: bundledSSR };\n\t}\n\n\t// Fallback to dynamic import (dev mode)\n\ttry {\n\t\tconst mod = await dynamicImport(absolutePath, isWatchMode);\n\t\treturn {\n\t\t\tmeta: (mod as Doc)?.meta as Doc | undefined,\n\t\t\tssr: (mod as Doc)?.ssr as SSR | undefined,\n\t\t};\n\t} catch {\n\t\treturn {};\n\t}\n}\n\n/**\n * Generate canonical URL meta tag\n */\nfunction generateCanonicalUrl(baseUrl: string, pathname: string): string {\n\t// Remove trailing slash from baseUrl if present\n\tconst cleanBaseUrl = baseUrl.replace(/\\/$/, '');\n\t// Remove query parameters from pathname for canonical URL\n\tconst cleanPathname = pathname.split('?')[0];\n\t// Canonical URLs should not have trailing slashes (except for root)\n\tconst normalizedPath =\n\t\tcleanPathname === '/' ? '/' : cleanPathname.replace(/\\/$/, '');\n\tconst canonicalUrl = `${cleanBaseUrl}${normalizedPath}`;\n\treturn `<link rel=\"canonical\" href=\"${canonicalUrl}\" />`;\n}\n\n/**\n * Generate og:url meta tag to match canonical URL\n */\nfunction generateOgUrl(baseUrl: string, pathname: string): string {\n\t// Remove trailing slash from baseUrl if present\n\tconst cleanBaseUrl = baseUrl.replace(/\\/$/, '');\n\t// Remove query parameters from pathname for og:url\n\tconst cleanPathname = pathname.split('?')[0];\n\t// og:url should not have trailing slashes (except for root)\n\tconst normalizedPath =\n\t\tcleanPathname === '/' ? '/' : cleanPathname.replace(/\\/$/, '');\n\tconst ogUrl = `${cleanBaseUrl}${normalizedPath}`;\n\treturn `<meta property=\"og:url\" content=\"${ogUrl}\" />`;\n}\n\n/**\n * Generate global OG tags from config (og:site_name, og:locale, twitter:site)\n */\nfunction generateGlobalOGTags(ogConfig: OGOptions): string {\n\tconst tags: string[] = [];\n\n\tif (ogConfig.siteName) {\n\t\ttags.push(\n\t\t\t`<meta property=\"og:site_name\" content=\"${escapeHtml(ogConfig.siteName)}\" />`,\n\t\t);\n\t}\n\n\tif (ogConfig.locale) {\n\t\ttags.push(\n\t\t\t`<meta property=\"og:locale\" content=\"${escapeHtml(ogConfig.locale)}\" />`,\n\t\t);\n\t}\n\n\tif (ogConfig.twitterSite) {\n\t\t// Normalize: ensure @ prefix for twitter:site\n\t\tconst handle = ogConfig.twitterSite.startsWith('@')\n\t\t\t? ogConfig.twitterSite\n\t\t\t: `@${ogConfig.twitterSite}`;\n\t\ttags.push(`<meta name=\"twitter:site\" content=\"${escapeHtml(handle)}\" />`);\n\t}\n\n\treturn tags.length ? tags.join('\\n') : '';\n}\n\nfunction escapeHtml(input: string): string {\n\treturn input\n\t\t.replace(/&/g, '&')\n\t\t.replace(/</g, '<')\n\t\t.replace(/>/g, '>')\n\t\t.replace(/\"/g, '"')\n\t\t.replace(/'/g, ''');\n}\n\n/**\n * Extract per-page head and metadata from layouts, routes, and content modules\n */\nexport async function extractPageMetadata(\n\tpathname: string,\n\tclientDir: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n\tcurrentStatusOverride?: number,\n\togImageConfig?: OGImageOptions,\n\togConfig?: OGOptions,\n\tbaseUrl?: string,\n\tautoCanonical?: boolean,\n\tssrData?: unknown,\n\ttitleSuffix?: string,\n): Promise<MetadataResult> {\n\tlet perPageHead = '';\n\tlet pageLang: string | undefined;\n\tlet statusOverride = currentStatusOverride;\n\n\t// Strip collection-scoped locale segment for route/layout matching\n\t// (`/blog/pt-BR/hello` → `/blog/hello`) while keeping the original path\n\t// for content metadata, canonicals, and OG.\n\tconst resolved = resolveContentPathParts(pathname);\n\tconst routingPathname = resolved?.routingPathname || pathname;\n\n\t// 1. Extract layout metadata hierarchically\n\tawait extractLayoutMetadata(\n\t\troutingPathname,\n\t\tclientDir,\n\t\tcwd,\n\t\tisWatchMode,\n\t\t(head) => {\n\t\t\tperPageHead += head;\n\t\t},\n\t\t(lang) => {\n\t\t\tif (!pageLang) pageLang = lang;\n\t\t},\n\t\togConfig,\n\t\ttitleSuffix,\n\t);\n\n\t// 2. Canonical URL and og:url (auto-generated, should be overridable by custom tags)\n\t// Default to true if baseUrl is set, unless explicitly disabled\n\tconst shouldGenerateCanonical =\n\t\tautoCanonical !== false && baseUrl !== undefined;\n\tif (shouldGenerateCanonical) {\n\t\tconst canonicalTag = generateCanonicalUrl(baseUrl, pathname);\n\t\tconst ogUrlTag = generateOgUrl(baseUrl, pathname);\n\t\tperPageHead += `\\n${canonicalTag}`;\n\t\tperPageHead += `\\n${ogUrlTag}`;\n\t}\n\n\t// 3. Global OG tags (og:site_name, og:locale, twitter:site)\n\tif (ogConfig) {\n\t\tconst ogGlobalTags = generateGlobalOGTags(ogConfig);\n\t\tif (ogGlobalTags) {\n\t\t\tperPageHead += `\\n${ogGlobalTags}`;\n\t\t}\n\t}\n\n\t// 4. OG Image meta tags (auto-generated, should be overridable by custom tags)\n\ttry {\n\t\tconst ogMetaTags = await generateOGImageMetaTags(\n\t\t\tpathname,\n\t\t\tclientDir,\n\t\t\togImageConfig,\n\t\t);\n\t\tif (ogMetaTags) {\n\t\t\tperPageHead += `\\n${ogMetaTags}`;\n\t\t}\n\t} catch (error) {\n\t\tconsole.warn('[reroute] Failed to generate OG image meta tags:', error);\n\t}\n\n\t// 5. Content page metadata (can override auto-generated tags)\n\textractContentMetadata(\n\t\tpathname,\n\t\t(head) => {\n\t\t\tperPageHead += head;\n\t\t},\n\t\t(lang) => {\n\t\t\tpageLang = lang;\n\t\t},\n\t\togConfig,\n\t\ttitleSuffix,\n\t\tbaseUrl,\n\t);\n\n\t// 6. Route-level metadata (most specific, can override everything)\n\tconst routeResult = await extractRouteMetadata(\n\t\tpathname,\n\t\tclientDir,\n\t\tcwd,\n\t\tisWatchMode,\n\t\tstatusOverride,\n\t\tssrData,\n\t\togConfig,\n\t\ttitleSuffix,\n\t\troutingPathname,\n\t);\n\tperPageHead += routeResult.head;\n\tif (routeResult.lang) pageLang = routeResult.lang;\n\tstatusOverride = routeResult.statusOverride;\n\n\treturn {\n\t\tperPageHead,\n\t\tpageLang,\n\t\tstatusOverride,\n\t\tssrExport: routeResult.ssrExport,\n\t};\n}\n\nasync function processLayoutForMetadata(\n\tlayout: Doc,\n\tclientDir: string,\n\tisWatchMode: boolean,\n\taddHead: (head: string) => void,\n\tsetLang: (lang: string) => void,\n\togConfig?: OGOptions,\n\ttitleSuffix?: string,\n): Promise<void> {\n\tif (typeof layout?.path !== 'string') return;\n\n\ttry {\n\t\tconst abs = join(clientDir, 'routes', String(layout.path));\n\t\tconst { meta, ssr } = await loadModuleMetaAndSSR(layout, abs, isWatchMode);\n\n\t\tif (meta) addHead(buildHeadFromMeta(meta, ogConfig, titleSuffix));\n\n\t\tif (ssr) {\n\t\t\t// Pass meta so head function can access layout metadata\n\t\t\tconst ssrHead = extractSSRHead(\n\t\t\t\tssr,\n\t\t\t\tundefined,\n\t\t\t\togConfig,\n\t\t\t\tundefined,\n\t\t\t\tmeta,\n\t\t\t\ttitleSuffix,\n\t\t\t);\n\t\t\tif (ssrHead) addHead(`\\n${ssrHead}`);\n\n\t\t\tconst lang = extractSSRLang(ssr);\n\t\t\tif (lang) setLang(lang);\n\t\t}\n\t} catch {\n\t\t// Continue to next layout\n\t}\n}\n\nasync function extractLayoutMetadata(\n\tpathname: string,\n\tclientDir: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n\taddHead: (head: string) => void,\n\tsetLang: (lang: string) => void,\n\togConfig?: OGOptions,\n\ttitleSuffix?: string,\n): Promise<void> {\n\ttry {\n\t\t// Use loadRoutesModule which handles bundled globals for compiled binaries\n\t\tconst m = await loadRoutesModule(cwd, isWatchMode);\n\t\tconst matchingLayouts = getMatchingLayouts(pathname, m?.layouts);\n\n\t\tfor (const layout of matchingLayouts) {\n\t\t\tawait processLayoutForMetadata(\n\t\t\t\tlayout,\n\t\t\t\tclientDir,\n\t\t\t\tisWatchMode,\n\t\t\t\taddHead,\n\t\t\t\tsetLang,\n\t\t\t\togConfig,\n\t\t\t\ttitleSuffix,\n\t\t\t);\n\t\t}\n\t} catch {\n\t\t// Routes module unavailable\n\t}\n}\n\nfunction resolveContentLocales(\n\tcollection: string,\n\tname: string,\n\tfallbackLocales: string[],\n): string[] {\n\tconst g = globalThis as Doc;\n\tconst byCollection = g.__REROUTE_COLLECTIONS__ as\n\t\t| Record<string, Array<{ name?: string; locale?: string }>>\n\t\t| undefined;\n\tconst items = byCollection?.[collection] || [];\n\tconst localesForEntry = [\n\t\t...new Set(\n\t\t\titems\n\t\t\t\t.filter((it) => it.name === name && it.locale)\n\t\t\t\t.map((it) => String(it.locale)),\n\t\t),\n\t];\n\treturn localesForEntry.length > 0 ? localesForEntry : fallbackLocales;\n}\n\nfunction addContentI18nHead(\n\taddHead: (head: string) => void,\n\tbaseUrl: string,\n\tcollection: string,\n\tname: string,\n\tlocales: string[],\n\tdefaultLocale: string,\n\tactiveLocale: string,\n): void {\n\tif (locales.length > 1) {\n\t\taddHead(\n\t\t\t`\\n${hreflangTags(baseUrl, collection, name, locales, defaultLocale)}`,\n\t\t);\n\t}\n\n\taddHead(\n\t\t`\\n<meta property=\"og:locale\" content=\"${toOgLocale(activeLocale)}\" />`,\n\t);\n\tfor (const loc of locales) {\n\t\tif (loc === activeLocale) continue;\n\t\taddHead(\n\t\t\t`\\n<meta property=\"og:locale:alternate\" content=\"${toOgLocale(loc)}\" />`,\n\t\t);\n\t}\n\n\tconst canonicalPath = localizeContentHref(\n\t\tcollection,\n\t\tname,\n\t\tactiveLocale,\n\t\tdefaultLocale,\n\t);\n\tconst base = baseUrl.replace(/\\/$/, '');\n\taddHead(`\\n<link rel=\"canonical\" href=\"${base}${canonicalPath}\" />`);\n\taddHead(`\\n<meta property=\"og:url\" content=\"${base}${canonicalPath}\" />`);\n}\n\nfunction extractContentMetadata(\n\tpathname: string,\n\taddHead: (head: string) => void,\n\tsetLang: (lang: string) => void,\n\togConfig?: OGOptions,\n\ttitleSuffix?: string,\n\tbaseUrl?: string,\n): void {\n\ttry {\n\t\tconst resolved = resolveContentPathParts(pathname);\n\t\tif (!resolved) return;\n\n\t\tconst g = globalThis as Doc;\n\t\tconst exp = g.__REROUTE_SSR_EXPORTS__?.[resolved.key] as\n\t\t\t| { meta?: unknown; ssr?: SSR }\n\t\t\t| undefined;\n\n\t\tconst meta = (exp as Doc)?.meta;\n\t\tconst ssr = exp?.ssr;\n\n\t\taddHead(buildHeadFromMeta(meta, ogConfig, titleSuffix));\n\n\t\t// Pass meta to extractSSRHead so head function can access content frontmatter\n\t\tconst ssrHead = extractSSRHead(\n\t\t\tssr,\n\t\t\tundefined,\n\t\t\togConfig,\n\t\t\tundefined,\n\t\t\tmeta,\n\t\t\ttitleSuffix,\n\t\t);\n\t\tif (ssrHead) addHead(`\\n${ssrHead}`);\n\n\t\tconst lang = extractSSRLang(ssr);\n\t\tif (lang) setLang(lang);\n\n\t\tconst i18n = g.__REROUTE_I18N__ as\n\t\t\t| {\n\t\t\t\t\tenabled?: boolean;\n\t\t\t\t\tlocales?: string[];\n\t\t\t\t\tdefaultLocale?: string;\n\t\t\t\t\tlocale?: string;\n\t\t\t }\n\t\t\t| undefined;\n\n\t\tif (i18n?.enabled && baseUrl && i18n.locales && i18n.locales.length > 1) {\n\t\t\tconst locales = resolveContentLocales(\n\t\t\t\tresolved.collection,\n\t\t\t\tresolved.name,\n\t\t\t\ti18n.locales,\n\t\t\t);\n\t\t\taddContentI18nHead(\n\t\t\t\taddHead,\n\t\t\t\tbaseUrl,\n\t\t\t\tresolved.collection,\n\t\t\t\tresolved.name,\n\t\t\t\tlocales,\n\t\t\t\ti18n.defaultLocale || 'en-US',\n\t\t\t\ti18n.locale || i18n.defaultLocale || 'en-US',\n\t\t\t);\n\t\t}\n\t} catch {\n\t\t// Content metadata unavailable\n\t}\n}\n\n/**\n * Get content metadata from __REROUTE_SSR_EXPORTS__ for a given pathname.\n * Returns the frontmatter metadata for content pages (e.g., blog posts).\n * Includes the slug derived from the pathname if not already present.\n */\nfunction getContentMetaFromExports(pathname: string): Doc | undefined {\n\ttry {\n\t\tconst resolved = resolveContentPathParts(pathname);\n\t\tif (!resolved) return undefined;\n\n\t\tconst slug = resolved.name;\n\t\tconst key = resolved.key;\n\t\tconst g = globalThis as Doc;\n\t\tconst exp = g.__REROUTE_SSR_EXPORTS__?.[key] as { meta?: Doc } | undefined;\n\n\t\tconst meta = exp?.meta;\n\t\tif (!meta) return undefined;\n\n\t\t// Include slug in metadata if not already present\n\t\tif (!meta.slug) {\n\t\t\treturn { ...meta, slug };\n\t\t}\n\n\t\treturn meta;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Route metadata extraction is complex\nasync function extractMatchedRouteMetadata(\n\tpathname: string,\n\tclientDir: string,\n\tm: Doc,\n\tisWatchMode: boolean,\n\tstatusOverride: number | undefined,\n\tssrData?: unknown,\n\togConfig?: OGOptions,\n\ttitleSuffix?: string,\n\troutingPathname?: string,\n): Promise<{\n\thead: string;\n\tlang?: string;\n\tstatusOverride?: number;\n\tssrExport?: SSR;\n}> {\n\tconst pathnameOnly = pathname.split('?')[0];\n\tconst matchPath = (routingPathname || pathname).split('?')[0];\n\tconst match =\n\t\ttypeof m.matchRoute === 'function' ? m.matchRoute(matchPath) : null;\n\tconst r = match?.route as Doc | undefined;\n\n\tif (!r) {\n\t\treturn { head: '', statusOverride: statusOverride || 404 };\n\t}\n\n\tif (r && typeof r.path === 'string') {\n\t\ttry {\n\t\t\tconst abs = join(clientDir, 'routes', String(r.path));\n\t\t\tconst { meta, ssr } = await loadModuleMetaAndSSR(r, abs, isWatchMode);\n\n\t\t\tlet head = '';\n\t\t\tif (meta) head += buildHeadFromMeta(meta, ogConfig, titleSuffix);\n\n\t\t\tif (ssr) {\n\t\t\t\t// Extract route-specific data from the full ssrData object\n\t\t\t\t// ssrData is keyed by pathname for routes\n\t\t\t\tlet routeData =\n\t\t\t\t\tssrData && typeof ssrData === 'object'\n\t\t\t\t\t\t? (ssrData as Record<string, unknown>)[pathname]\n\t\t\t\t\t\t: undefined;\n\n\t\t\t\t// If data is a Promise, await it for SSR head generation\n\t\t\t\tif (routeData && typeof routeData === 'object' && 'then' in routeData) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\trouteData = await (routeData as Promise<unknown>);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t'[extractMatchedRouteMetadata] Error awaiting route data for head:',\n\t\t\t\t\t\t\terr,\n\t\t\t\t\t\t);\n\t\t\t\t\t\trouteData = undefined;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// For content pages, use the content's frontmatter metadata instead of route's meta.\n\t\t\t\t// This allows ssr.head() in route files like [slug].tsx to receive blog post metadata.\n\t\t\t\tconst contentMeta = getContentMetaFromExports(pathnameOnly);\n\t\t\t\tconst headMeta = contentMeta || meta;\n\n\t\t\t\t// Pass existing head and meta to avoid duplicate OG tags when both meta and ssr.head() exist\n\t\t\t\tconst ssrHead = extractSSRHead(\n\t\t\t\t\tssr,\n\t\t\t\t\trouteData,\n\t\t\t\t\togConfig,\n\t\t\t\t\thead,\n\t\t\t\t\theadMeta,\n\t\t\t\t\ttitleSuffix,\n\t\t\t\t);\n\t\t\t\tif (ssrHead) head += `\\n${ssrHead}`;\n\t\t\t\t// Return SSR export for dynamic head generation at stream end\n\t\t\t\treturn {\n\t\t\t\t\thead,\n\t\t\t\t\tlang: extractSSRLang(ssr),\n\t\t\t\t\tstatusOverride,\n\t\t\t\t\tssrExport: ssr,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn { head, statusOverride };\n\t\t} catch {\n\t\t\t// Route module unavailable\n\t\t}\n\t}\n\n\treturn { head: '', statusOverride };\n}\n\nasync function extractRouteMetadata(\n\tpathname: string,\n\tclientDir: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n\tcurrentStatusOverride?: number,\n\tssrData?: unknown,\n\togConfig?: OGOptions,\n\ttitleSuffix?: string,\n\troutingPathname?: string,\n): Promise<{\n\thead: string;\n\tlang?: string;\n\tstatusOverride?: number;\n\tssrExport?: SSR;\n}> {\n\tlet head = '';\n\tlet lang: string | undefined;\n\tlet statusOverride = currentStatusOverride;\n\tlet ssrExport: SSR | undefined;\n\n\ttry {\n\t\tconst m = await loadRoutesModule(cwd, isWatchMode);\n\t\tif (!m) {\n\t\t\treturn { head, lang, statusOverride: statusOverride || 404 };\n\t\t}\n\n\t\tconst matchPath = (routingPathname || pathname).split('?')[0];\n\t\tconst match =\n\t\t\ttypeof m.matchRoute === 'function' ? m.matchRoute(matchPath) : null;\n\t\tconst r = match?.route as Doc | undefined;\n\n\t\tif (!r) {\n\t\t\tstatusOverride = statusOverride || 404;\n\t\t}\n\n\t\tif (r && typeof r.path === 'string') {\n\t\t\tconst result = await extractMatchedRouteMetadata(\n\t\t\t\tpathname,\n\t\t\t\tclientDir,\n\t\t\t\tm,\n\t\t\t\tisWatchMode,\n\t\t\t\tstatusOverride,\n\t\t\t\tssrData,\n\t\t\t\togConfig,\n\t\t\t\ttitleSuffix,\n\t\t\t\troutingPathname,\n\t\t\t);\n\n\t\t\thead = result.head;\n\t\t\tlang = result.lang;\n\t\t\tstatusOverride = result.statusOverride;\n\t\t\tssrExport = result.ssrExport;\n\t\t} else {\n\t\t\t// Try notFound route metadata only when no route matched\n\t\t\thead += await extractNotFoundMetadata(\n\t\t\t\tpathname,\n\t\t\t\tclientDir,\n\t\t\t\tm,\n\t\t\t\tisWatchMode,\n\t\t\t\t(l) => {\n\t\t\t\t\tlang = l;\n\t\t\t\t},\n\t\t\t\togConfig,\n\t\t\t\ttitleSuffix,\n\t\t\t);\n\t\t}\n\t} catch {\n\t\t// Routes module unavailable\n\t}\n\n\treturn { head, lang, statusOverride, ssrExport };\n}\n\nfunction findBestNotFoundRoute(list: Doc[], pathname: string): Doc | undefined {\n\tlet chosen: Doc | undefined;\n\tlet bestLen = -1;\n\n\tfor (const nf of list) {\n\t\tconst pat = String((nf as Doc)?.pattern || '/');\n\t\tif (!pathname.startsWith(pat)) continue;\n\n\t\tconst len = pat.split('/').filter(Boolean).length;\n\t\tif (len >= bestLen) {\n\t\t\tbestLen = len;\n\t\t\tchosen = nf;\n\t\t}\n\t}\n\n\treturn chosen;\n}\n\nasync function extractNotFoundMetadata(\n\tpathname: string,\n\tclientDir: string,\n\troutesModule: Doc,\n\tisWatchMode: boolean,\n\tsetLang: (lang: string) => void,\n\togConfig?: OGOptions,\n\ttitleSuffix?: string,\n): Promise<string> {\n\tlet head = '';\n\n\ttry {\n\t\tconst list = routesModule?.notFoundRoutes as Doc[] | undefined;\n\t\tif (!Array.isArray(list)) return head;\n\n\t\tconst chosen = findBestNotFoundRoute(list, pathname);\n\n\t\tif (chosen && typeof (chosen as Doc).path === 'string') {\n\t\t\tconst abs = join(clientDir, 'routes', String((chosen as Doc).path));\n\t\t\tconst { meta, ssr } = await loadModuleMetaAndSSR(\n\t\t\t\tchosen,\n\t\t\t\tabs,\n\t\t\t\tisWatchMode,\n\t\t\t);\n\n\t\t\tif (meta) head += buildHeadFromMeta(meta, ogConfig, titleSuffix);\n\n\t\t\t// Pass meta so head function can access 404 page metadata\n\t\t\tconst ssrHead = extractSSRHead(\n\t\t\t\tssr,\n\t\t\t\tundefined,\n\t\t\t\togConfig,\n\t\t\t\tundefined,\n\t\t\t\tmeta,\n\t\t\t\ttitleSuffix,\n\t\t\t);\n\t\t\tif (ssrHead) head += `\\n${ssrHead}`;\n\n\t\t\tconst lang = extractSSRLang(ssr);\n\t\t\tif (lang) setLang(lang);\n\t\t}\n\t} catch {\n\t\t// NotFound metadata unavailable\n\t}\n\n\treturn head;\n}\n\n/**\n * Parse title from head HTML string\n */\nfunction parseTitleFromHead(head: string): string | undefined {\n\tconst match = head.match(/<title[^>]*>(.*?)<\\/title>/is);\n\tif (match?.[1]) return unescapeHtml(match[1].trim());\n\treturn undefined;\n}\n\n/**\n * Parse description from head HTML string\n * Handles both attribute orders and content with escaped quotes\n */\nfunction parseDescriptionFromHead(head: string): string | undefined {\n\t// Try name-first format: <meta name=\"description\" content=\"...\">\n\tlet match = head.match(\n\t\t/<meta\\s+name=[\"']description[\"']\\s+content=[\"'](.+?)[\"']\\s*\\/?>/i,\n\t);\n\tif (match?.[1]) return unescapeHtml(match[1].trim());\n\n\t// Try content-first format: <meta content=\"...\" name=\"description\">\n\tmatch = head.match(\n\t\t/<meta\\s+content=[\"'](.+?)[\"']\\s+name=[\"']description[\"']\\s*\\/?>/i,\n\t);\n\tif (match?.[1]) return unescapeHtml(match[1].trim());\n\n\treturn undefined;\n}\n\n/**\n * Unescape basic HTML entities in extracted content\n */\nfunction unescapeHtml(input: string): string {\n\treturn input\n\t\t.replace(/"/g, '\"')\n\t\t.replace(/'/g, \"'\")\n\t\t.replace(/</g, '<')\n\t\t.replace(/>/g, '>')\n\t\t.replace(/&/g, '&');\n}\n\n/**\n * Check if a specific meta tag already exists in head HTML\n */\nfunction hasMetaTag(head: string, property: string): boolean {\n\t// Check both property=\"\" and name=\"\" syntax\n\tconst propRegex = new RegExp(\n\t\t`<meta\\\\s+(?:property|name)=[\"']${property}[\"']`,\n\t\t'i',\n\t);\n\tconst contentFirstRegex = new RegExp(\n\t\t`<meta\\\\s+content=[\"'][^\"']*[\"']\\\\s+(?:property|name)=[\"']${property}[\"']`,\n\t\t'i',\n\t);\n\treturn propRegex.test(head) || contentFirstRegex.test(head);\n}\n\n/**\n * Generate OG/Twitter tags from extracted title/description if not already present\n */\nfunction generateMissingOGTags(head: string, ogConfig?: OGOptions): string {\n\tconst title = parseTitleFromHead(head);\n\tconst description = parseDescriptionFromHead(head);\n\n\tconst additionalTags: string[] = [];\n\n\t// Generate og:title if title exists and og:title is missing\n\tif (title && !hasMetaTag(head, 'og:title')) {\n\t\tadditionalTags.push(\n\t\t\t`<meta property=\"og:title\" content=\"${escapeHtml(title)}\" />`,\n\t\t);\n\t}\n\n\t// Generate og:description if description exists and og:description is missing\n\tconst finalDescription = description || ogConfig?.siteDescription;\n\tif (finalDescription && !hasMetaTag(head, 'og:description')) {\n\t\tadditionalTags.push(\n\t\t\t`<meta property=\"og:description\" content=\"${escapeHtml(finalDescription)}\" />`,\n\t\t);\n\t}\n\n\t// Generate twitter:title if title exists and twitter:title is missing\n\tif (title && !hasMetaTag(head, 'twitter:title')) {\n\t\tadditionalTags.push(\n\t\t\t`<meta name=\"twitter:title\" content=\"${escapeHtml(title)}\" />`,\n\t\t);\n\t}\n\n\t// Generate twitter:description if description exists and twitter:description is missing\n\tif (finalDescription && !hasMetaTag(head, 'twitter:description')) {\n\t\tadditionalTags.push(\n\t\t\t`<meta name=\"twitter:description\" content=\"${escapeHtml(finalDescription)}\" />`,\n\t\t);\n\t}\n\n\t// Generate og:type if missing\n\tif (!hasMetaTag(head, 'og:type')) {\n\t\tconst ogType = ogConfig?.defaultType || 'website';\n\t\tadditionalTags.push(\n\t\t\t`<meta property=\"og:type\" content=\"${escapeHtml(ogType)}\" />`,\n\t\t);\n\t}\n\n\t// Generate twitter:card if missing\n\tif (!hasMetaTag(head, 'twitter:card')) {\n\t\tconst twitterCard = ogConfig?.twitterCard || 'summary_large_image';\n\t\tadditionalTags.push(\n\t\t\t`<meta name=\"twitter:card\" content=\"${twitterCard}\" />`,\n\t\t);\n\t}\n\n\treturn additionalTags.length > 0 ? `\\n${additionalTags.join('\\n')}` : '';\n}\n\nfunction extractSSRHead(\n\tssr: SSR | undefined,\n\tdata?: unknown,\n\togConfig?: OGOptions,\n\texistingHead?: string,\n\tmeta?: Doc,\n\ttitleSuffix?: string,\n): string {\n\tif (!ssr) return '';\n\n\tlet headContent = '';\n\n\t// Support function that receives data result and content meta\n\tif (typeof ssr.head === 'function') {\n\t\ttry {\n\t\t\tconst result = ssr.head({ data, meta } satisfies SSRHeadContext);\n\t\t\tif (typeof result === 'string') headContent = result;\n\t\t\telse if (Array.isArray(result))\n\t\t\t\theadContent = String((result as string[]).join('\\n'));\n\t\t} catch (err) {\n\t\t\tconsole.error('[extractSSRHead] Error calling head function:', err);\n\t\t\treturn '';\n\t\t}\n\t} else if (typeof ssr.head === 'string') {\n\t\theadContent = ssr.head;\n\t} else if (Array.isArray(ssr.head)) {\n\t\theadContent = String(ssr.head.join('\\n'));\n\t}\n\n\tif (!headContent) return '';\n\n\t// Apply titleSuffix to any title tags in the head content\n\tif (titleSuffix) {\n\t\theadContent = applyTitleSuffixToHead(headContent, titleSuffix);\n\t}\n\n\t// Auto-generate missing OG/Twitter tags from the head content\n\t// Check against both the ssr.head() output AND any existing head (from const meta)\n\tconst combinedHead = existingHead\n\t\t? `${existingHead}\\n${headContent}`\n\t\t: headContent;\n\tconst additionalOGTags = generateMissingOGTags(combinedHead, ogConfig);\n\n\treturn headContent + additionalOGTags;\n}\n\nfunction extractSSRLang(ssr: SSR | undefined): string | undefined {\n\tif (!ssr) return undefined;\n\n\tif (typeof ssr.lang === 'string' && ssr.lang.trim()) {\n\t\treturn ssr.lang.trim();\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Apply titleSuffix to any title tags in head HTML content.\n * Also updates og:title and twitter:title if they match the original title.\n */\nfunction applyTitleSuffixToHead(\n\theadContent: string,\n\ttitleSuffix: string,\n): string {\n\tlet result = headContent;\n\n\t// Find and update <title> tag\n\tconst titleMatch = result.match(/<title[^>]*>(.*?)<\\/title>/is);\n\tif (titleMatch?.[1]) {\n\t\tconst originalTitle = titleMatch[1].trim();\n\t\tif (originalTitle && !originalTitle.endsWith(titleSuffix)) {\n\t\t\tconst newTitle = `${originalTitle}${titleSuffix}`;\n\t\t\tresult = result.replace(\n\t\t\t\t/<title[^>]*>(.*?)<\\/title>/is,\n\t\t\t\t`<title>${escapeHtml(unescapeHtml(newTitle))}</title>`,\n\t\t\t);\n\n\t\t\t// Also update og:title if it matches the original title (without suffix)\n\t\t\tconst ogTitleRegex =\n\t\t\t\t/<meta\\s+(?:property\\s*=\\s*[\"']og:title[\"']\\s+content\\s*=\\s*[\"']([^\"']+)[\"']|content\\s*=\\s*[\"']([^\"']+)[\"']\\s+property\\s*=\\s*[\"']og:title[\"'])[^>]*>/gi;\n\t\t\tresult = result.replace(ogTitleRegex, (match, content1, content2) => {\n\t\t\t\tconst content = content1 || content2;\n\t\t\t\tif (content && !content.endsWith(titleSuffix)) {\n\t\t\t\t\tconst newContent = `${content}${titleSuffix}`;\n\t\t\t\t\treturn `<meta property=\"og:title\" content=\"${escapeHtml(unescapeHtml(newContent))}\" />`;\n\t\t\t\t}\n\t\t\t\treturn match;\n\t\t\t});\n\n\t\t\t// Also update twitter:title if it matches the original title (without suffix)\n\t\t\tconst twitterTitleRegex =\n\t\t\t\t/<meta\\s+(?:name\\s*=\\s*[\"']twitter:title[\"']\\s+content\\s*=\\s*[\"']([^\"']+)[\"']|content\\s*=\\s*[\"']([^\"']+)[\"']\\s+name\\s*=\\s*[\"']twitter:title[\"'])[^>]*>/gi;\n\t\t\tresult = result.replace(\n\t\t\t\ttwitterTitleRegex,\n\t\t\t\t(match, content1, content2) => {\n\t\t\t\t\tconst content = content1 || content2;\n\t\t\t\t\tif (content && !content.endsWith(titleSuffix)) {\n\t\t\t\t\t\tconst newContent = `${content}${titleSuffix}`;\n\t\t\t\t\t\treturn `<meta name=\"twitter:title\" content=\"${escapeHtml(unescapeHtml(newContent))}\" />`;\n\t\t\t\t\t}\n\t\t\t\t\treturn match;\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\t}\n\n\treturn result;\n}\n",
|
|
31
|
+
"import { readdir, stat } from 'node:fs/promises';\nimport { dynamicImport } from './imports';\nimport { join } from './path';\nimport type { Doc, PreloadResult } from './types';\n\n/**\n * Handle content module preloading for content pages\n */\nexport async function preloadContentModule(\n\tpathname: string,\n\tclientDir: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n): Promise<PreloadResult> {\n\tlet extraHead = '';\n\tlet hydrationScript = '';\n\tlet statusOverride: number | undefined;\n\n\ttry {\n\t\tconst { resolveContentPathParts } = await import('./content-path');\n\t\tconst resolved = resolveContentPathParts(pathname);\n\n\t\tif (!resolved) {\n\t\t\treturn { extraHead, hydrationScript };\n\t\t}\n\n\t\tconst { collection, name, key, locale } = resolved;\n\n\t\t// Check if this is a content collection\n\t\tif (!(await isContentCollection(clientDir, collection))) {\n\t\t\treturn { extraHead, hydrationScript };\n\t\t}\n\n\t\t// Try to find module path\n\t\tconst modulePath = await findContentModulePath(\n\t\t\tcollection,\n\t\t\tname,\n\t\t\tclientDir,\n\t\t\tcwd,\n\t\t\tisWatchMode,\n\t\t\tlocale,\n\t\t);\n\n\t\tif (!modulePath) {\n\t\t\tstatusOverride = 404;\n\t\t\treturn { extraHead, hydrationScript, statusOverride };\n\t\t}\n\t\textraHead += `\\n<link rel=\"modulepreload\" href=\"${modulePath}\" />`;\n\n\t\t// Store the module path and key for the inline loader\n\t\t// The actual loading happens via a blocking mechanism in the data script\n\t\t// Mark that this content module needs to be preloaded in the hydration script\n\t\thydrationScript += `<script>\n(globalThis.__REROUTE_CONTENT_PRELOAD__ ||= []).push({src:'${modulePath}',key:'${key}'});\n</script>`;\n\t} catch {\n\t\t// Return empty result on error\n\t}\n\n\treturn { extraHead, hydrationScript, statusOverride };\n}\n\nasync function isContentCollection(\n\tclientDir: string,\n\tcollection: string,\n): Promise<boolean> {\n\ttry {\n\t\t// In production (clientDir = .reroute), check for collection files instead of source dirs\n\t\tconst isProduction = clientDir.endsWith('.reroute');\n\n\t\tif (isProduction) {\n\t\t\t// Check if collection file exists in .reroute/collections/\n\t\t\tconst collectionFile = join(clientDir, 'collections', `${collection}.js`);\n\t\t\ttry {\n\t\t\t\tconst s = await stat(collectionFile);\n\t\t\t\treturn s?.isFile() ?? false;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// In development, check for content directory\n\t\tconst maybeDir = join(clientDir, 'routes', collection, 'content');\n\t\tconst s = await stat(maybeDir);\n\t\treturn typeof s?.isDirectory === 'function' ? s.isDirectory() : true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nasync function findLatestCandidate(\n\tchunkDir: string,\n\tcandidates: string[],\n): Promise<string> {\n\tlet latest = candidates[0];\n\tlet latestM = 0;\n\n\tfor (const candidateName of candidates) {\n\t\ttry {\n\t\t\tconst s = await stat(join(chunkDir, candidateName));\n\t\t\tif (s.mtimeMs >= latestM) {\n\t\t\t\tlatestM = s.mtimeMs;\n\t\t\t\tlatest = candidateName;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Skip inaccessible files\n\t\t}\n\t}\n\n\treturn latest;\n}\n\nasync function findContentModulePath(\n\tcollection: string,\n\tname: string,\n\tclientDir: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n\tlocale?: string,\n): Promise<string | null> {\n\t// 1. Try registry mapping\n\ttry {\n\t\tconst registryPath = join(cwd, '.reroute', 'content.ts');\n\t\tconst reg = await dynamicImport(registryPath, isWatchMode);\n\t\tconst get = reg?.getContentEntry as\n\t\t\t| ((c: string, n: string, l?: string) => { module?: string } | undefined)\n\t\t\t| undefined;\n\t\tconst entry =\n\t\t\ttypeof get === 'function' ? get(collection, name, locale) : undefined;\n\t\tconst moduleUrl = (entry as Doc)?.module as string | undefined;\n\n\t\tif (moduleUrl?.endsWith('.js')) {\n\t\t\treturn moduleUrl;\n\t\t}\n\t} catch {\n\t\t// Registry unavailable\n\t}\n\n\t// 2. Try newest chunk\n\ttry {\n\t\tconst chunkDir = join(cwd, '.reroute', 'chunks', collection);\n\t\tconst files = await readdir(chunkDir);\n\t\tconst candidates = files.filter(\n\t\t\t(n) => n.startsWith(`${name}.`) && n.endsWith('.js'),\n\t\t);\n\n\t\tif (candidates.length) {\n\t\t\tconst latest = await findLatestCandidate(chunkDir, candidates);\n\t\t\treturn `/${join('.reroute', 'chunks', collection, latest).replace(/\\\\+/g, '/')}`;\n\t\t}\n\t} catch {\n\t\t// Chunk directory unavailable\n\t}\n\n\t// 3. Try source files (locale subdir first when present)\n\ttry {\n\t\tconst contentRoot = join(clientDir, 'routes', collection, 'content');\n\t\tconst candidates = locale\n\t\t\t? [\n\t\t\t\t\tjoin(contentRoot, locale, `${name}.tsx`),\n\t\t\t\t\tjoin(contentRoot, locale, `${name}.ts`),\n\t\t\t\t\tjoin(contentRoot, `${name}.tsx`),\n\t\t\t\t\tjoin(contentRoot, `${name}.ts`),\n\t\t\t\t]\n\t\t\t: [join(contentRoot, `${name}.tsx`), join(contentRoot, `${name}.ts`)];\n\n\t\tfor (const abs of candidates) {\n\t\t\tif (await Bun.file(abs).exists()) {\n\t\t\t\tconst rel = abs.startsWith(clientDir)\n\t\t\t\t\t? abs.slice(clientDir.length).replace(/^[/\\\\]/, '')\n\t\t\t\t\t: join('routes', collection, 'content', `${name}.tsx`);\n\t\t\t\treturn `/${rel.replace(/\\\\+/g, '/')}`;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Source files unavailable\n\t}\n\n\treturn null;\n}\n\n/**\n * Reset SSR access tracking for a new request\n */\nexport function resetSSRAccessTracking(): void {\n\ttry {\n\t\t(globalThis as Doc).__REROUTE_SSR_ACCESSED__ = {};\n\t} catch {\n\t\t// Ignore errors in restricted environments\n\t}\n}\n\n/**\n * Create bundle preload link\n */\nexport function createBundlePreload(bundleUrl: string): string {\n\ttry {\n\t\tif (typeof bundleUrl === 'string' && bundleUrl.endsWith('.js')) {\n\t\t\treturn `\\n<link rel=\"modulepreload\" href=\"${bundleUrl}\" />`;\n\t\t}\n\t} catch {\n\t\t// Return empty on error\n\t}\n\treturn '';\n}\n",
|
|
32
|
+
"import { resolveContentPathParts } from '../content-path';\nimport { escapeJsonForScript } from './escape';\n\n/**\n * Duplicate SSR data for query param routes.\n * Stores at both /path?query and /path so useData() can find it.\n */\nexport function duplicateQueryParamData(\n\tssrData: Record<string, unknown>,\n): Record<string, unknown> {\n\tconst duplicated: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(ssrData)) {\n\t\tduplicated[key] = value;\n\t\tconst pathnameOnly = key.split('?')[0];\n\t\tif (pathnameOnly !== key) {\n\t\t\tduplicated[pathnameOnly] = value;\n\t\t}\n\t}\n\treturn duplicated;\n}\n\n/**\n * Get the content key from a pathname (e.g., /blog/my-post -> blog:my-post,\n * /blog/pt-BR/my-post -> blog:my-post:pt-BR)\n */\nfunction getContentKeyFromPathname(pathname: string): string | null {\n\treturn resolveContentPathParts(pathname)?.key ?? null;\n}\n\n/**\n * Create inline script for i18n hydration.\n */\nfunction createI18nScript(i18n?: Record<string, unknown>): string {\n\tif (!i18n?.enabled) return '';\n\ttry {\n\t\tconst json = escapeJsonForScript(JSON.stringify(i18n));\n\t\treturn `<script>(function(){try{var w=(typeof window!=='undefined'?window:globalThis);w.__REROUTE_I18N__=${json};}catch(e){}})();</script>`;\n\t} catch (error) {\n\t\tconsole.error('[reroute] Failed to serialize i18n hydration:', error);\n\t\treturn '';\n\t}\n}\n\n/**\n * Create inline script that captures DOM content HTML BEFORE React hydrates.\n * This eliminates the need to duplicate HTML in the response - we just read it from the DOM.\n * Must be placed BEFORE the React bundle script.\n *\n * Also sets __REROUTE_INITIAL_HYDRATION__ flag to true so components know this is\n * the first render and should not replace SSR'd content.\n */\nexport function createContentCaptureScript(): string {\n\t// This script:\n\t// 1. Sets initial hydration flag so components know not to replace content\n\t// 2. Finds all [data-reroute-key] elements and captures their innerHTML\n\t// into __REROUTE_CONTENT_HTML__ before React runs and potentially modifies them\n\t// 3. Finds the [data-reroute-route=\"ssr\"] element and captures its innerHTML\n\t// into __REROUTE_ROUTE_HTML__ for LazyRoute hydration\n\treturn `<script>(function(){try{var w=window;w.__REROUTE_INITIAL_HYDRATION__=true;var h={};document.querySelectorAll('[data-reroute-key]').forEach(function(el){var k=el.getAttribute('data-reroute-key');if(k)h[k]=el.innerHTML;});w.__REROUTE_CONTENT_HTML__=h;var r=document.querySelector('[data-reroute-route=\"ssr\"]');if(r){w.__REROUTE_ROUTE_HTML__=r.innerHTML;}else{var root=document.getElementById('root');if(root)w.__REROUTE_ROUTE_HTML__=root.innerHTML;}}catch(e){}})();</script>`;\n}\n\n/**\n * Create inline script for SSR exports hydration (includes toc, meta, etc.)\n * This ensures hooks like useToc have access to server-side data during hydration\n * Only includes data for the current pathname to avoid cross-request pollution\n */\nexport function createSSRExportsScript(pathname: string): string {\n\ttry {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: dynamic global access\n\t\tconst g = globalThis as any;\n\t\tconst ssrExports = g.__REROUTE_SSR_EXPORTS__;\n\n\t\tif (!ssrExports || typeof ssrExports !== 'object') {\n\t\t\treturn '';\n\t\t}\n\n\t\t// Only serialize the current page's exports, not all accumulated exports\n\t\tconst currentKey = getContentKeyFromPathname(pathname);\n\t\tif (!currentKey) {\n\t\t\treturn '';\n\t\t}\n\n\t\tconst currentExports = ssrExports[currentKey];\n\t\tif (!currentExports) {\n\t\t\treturn '';\n\t\t}\n\n\t\tconst filteredExports = { [currentKey]: currentExports };\n\t\tconst exportsJson = escapeJsonForScript(JSON.stringify(filteredExports));\n\n\t\treturn `<script>(function(){try{var w=(typeof window!=='undefined'?window:globalThis);w.__REROUTE_SSR_EXPORTS__=${exportsJson};}catch(e){}})();</script>`;\n\t} catch (error) {\n\t\tconsole.error('[reroute] Failed to create SSR exports script:', error);\n\t\treturn '';\n\t}\n}\n\n/**\n * Create inline script for browser telemetry config hydration\n * Injects telemetry.browser config so TelemetryProvider can use it\n */\nfunction createTelemetryConfigScript(\n\tbrowserTelemetryConfig?: Record<string, unknown>,\n): string {\n\tif (\n\t\t!browserTelemetryConfig ||\n\t\tObject.keys(browserTelemetryConfig).length === 0\n\t) {\n\t\treturn '';\n\t}\n\n\ttry {\n\t\tconst configJson = escapeJsonForScript(\n\t\t\tJSON.stringify(browserTelemetryConfig),\n\t\t);\n\t\treturn `<script>(function(){try{var w=(typeof window!=='undefined'?window:globalThis);w.__REROUTE_TELEMETRY_BROWSER__=${configJson};}catch(e){}})();</script>`;\n\t} catch (error) {\n\t\tconsole.error(\n\t\t\t'[reroute] Failed to serialize browser telemetry config:',\n\t\t\terror,\n\t\t);\n\t\treturn '';\n\t}\n}\n\n/**\n * Create inline script for trace context injection\n * Injects W3C Trace Context so browser spans can link to server spans\n */\nfunction createTraceContextScript(traceContext?: {\n\ttraceparent?: string;\n\ttracestate?: string;\n}): string {\n\tif (\n\t\t!traceContext?.traceparent ||\n\t\ttypeof traceContext.traceparent !== 'string'\n\t) {\n\t\treturn '';\n\t}\n\n\ttry {\n\t\tconst contextJson = escapeJsonForScript(JSON.stringify(traceContext));\n\t\treturn `<script>(function(){try{var w=(typeof window!=='undefined'?window:globalThis);w.__REROUTE_TRACE_CONTEXT__=${contextJson};}catch(e){}})();</script>`;\n\t} catch (error) {\n\t\tconsole.error('[reroute] Failed to serialize trace context:', error);\n\t\treturn '';\n\t}\n}\n\n/**\n * Create inline script for title suffix hydration\n * Injects titleSuffix so client-side navigation maintains the suffix\n */\nfunction createTitleSuffixScript(titleSuffix?: string): string {\n\tif (!titleSuffix) {\n\t\treturn '';\n\t}\n\n\ttry {\n\t\tconst suffixJson = escapeJsonForScript(JSON.stringify(titleSuffix));\n\t\treturn `<script>(function(){try{var w=(typeof window!=='undefined'?window:globalThis);w.__REROUTE_TITLE_SUFFIX__=${suffixJson};}catch(e){}})();</script>`;\n\t} catch (error) {\n\t\tconsole.error('[reroute] Failed to serialize title suffix:', error);\n\t\treturn '';\n\t}\n}\n\n/**\n * Create inline script for SSR pathname hydration\n * Tells the client which pathname was SSR'd so components can skip re-rendering\n * on the initial hydration pass.\n */\nfunction createSSRPathnameScript(pathname?: string): string {\n\tif (!pathname) {\n\t\treturn '';\n\t}\n\n\ttry {\n\t\tconst pathnameJson = escapeJsonForScript(JSON.stringify(pathname));\n\t\treturn `<script>(function(){try{var w=(typeof window!=='undefined'?window:globalThis);w.__REROUTE_SSR_PATHNAME__=${pathnameJson};}catch(e){}})();</script>`;\n\t} catch (error) {\n\t\tconsole.error('[reroute] Failed to serialize SSR pathname:', error);\n\t\treturn '';\n\t}\n}\n\n/**\n * Create inline script for SSR data hydration\n */\nexport function createDataScript(\n\tssrData: Record<string, unknown>,\n\terrorMsg?: string,\n\tpathname?: string,\n\tbrowserTelemetryConfig?: Record<string, unknown>,\n\ttraceContext?: { traceparent?: string; tracestate?: string },\n\ttitleSuffix?: string,\n\ti18n?: Record<string, unknown>,\n): string {\n\tlet script = '';\n\n\t// Capture DOM content HTML before React hydrates (reads from DOM, no duplication)\n\tscript += createContentCaptureScript();\n\n\t// Add SSR pathname script (tells client which route was SSR'd)\n\tscript += createSSRPathnameScript(pathname);\n\n\t// Add SSR exports script (for useToc, etc.) - scoped to current pathname\n\tif (pathname) {\n\t\tscript += createSSRExportsScript(pathname);\n\t}\n\n\t// Inject i18n hydration (locale + catalogs)\n\tscript += createI18nScript(i18n);\n\n\t// Inject browser telemetry config\n\tscript += createTelemetryConfigScript(browserTelemetryConfig);\n\n\t// Inject trace context for distributed tracing\n\tscript += createTraceContextScript(traceContext);\n\n\t// Inject title suffix for client-side navigation\n\tscript += createTitleSuffixScript(titleSuffix);\n\n\tif (ssrData && Object.keys(ssrData).length > 0) {\n\t\ttry {\n\t\t\tconst duplicatedData = duplicateQueryParamData(ssrData);\n\t\t\tconst dataJson = escapeJsonForScript(JSON.stringify(duplicatedData));\n\t\t\tconst keysJson = escapeJsonForScript(\n\t\t\t\tJSON.stringify(Object.keys(ssrData)),\n\t\t\t);\n\t\t\tscript += `<script>(function(){try{var w=(typeof window!=='undefined'?window:globalThis);w.__REROUTE_DATA__=Object.assign({},w.__REROUTE_DATA__||{},${dataJson});try{w.dispatchEvent(new CustomEvent('__reroute_data__',{detail:{keys:${keysJson}}}));}catch(e){}}catch(e){}})();</script>`;\n\t\t} catch (error) {\n\t\t\tconsole.error('[reroute] Failed to serialize SSR data:', error);\n\t\t}\n\t}\n\n\tif (errorMsg) {\n\t\tconst escapedMsg = JSON.stringify(errorMsg);\n\t\tscript += `<script>(function(){try{console.warn('%c[reroute] SSR Data Error','background: #ff5555; color: white; padding: 2px 6px; border-radius: 3px;',${escapedMsg});}catch(e){}})();</script>`;\n\t}\n\n\treturn script;\n}\n",
|
|
33
|
+
"export { createCollectionScript, processCollections } from './collections';\nexport { createDataScript, duplicateQueryParamData } from './data';\nexport { escapeJsonForScript } from './escape';\nexport { createFeedsScript, createLlmsScript } from './feeds';\n",
|
|
34
|
+
"import type { Span } from '@opentelemetry/api';\nimport { withParentSpan, withSpan } from 'reroute-js/telemetry/server';\nimport type { OGImageOptions, OGOptions } from '../../config';\nimport { cleanupPathScopedSSRData } from './cleanup';\nimport {\n\tloadCollections,\n\tresetSSRContentResults,\n\tsetGlobalCollections,\n} from './collections';\nimport { computeSSRData, setGlobalSSRData } from './compute';\nimport { extractPageMetadata } from './metadata';\nimport {\n\tcreateBundlePreload,\n\tpreloadContentModule,\n\tresetSSRAccessTracking,\n} from './preload';\nimport {\n\tcreateCollectionScript,\n\tcreateDataScript,\n\tcreateFeedsScript,\n\tcreateLlmsScript,\n\tprocessCollections,\n} from './scripts';\nimport { seedSSRModuleForPath } from './seed';\nimport { inlineThemeCSS } from './styles';\nimport type { ComputeResult, SSR } from './types';\n\n/**\n * Common SSR options shared between render and stream modes\n */\ntype SSRSetupOptions = {\n\tpathname: string;\n\tclientDir: string;\n\tcwd: string;\n\tisWatchMode: boolean;\n\tbundleUrl: string;\n\tcachedCollections?: Record<string, unknown[]>;\n\tcachedThemeCSS?: string;\n\tdebug?: boolean;\n\tmaxAge?: number;\n\tsearchParams?: URLSearchParams;\n\togConfig?: OGImageOptions;\n\togMetaConfig?: OGOptions;\n\tbaseUrl?: string;\n\tautoCanonical?: boolean;\n\tparentSpan?: Span | null;\n\t/** Request headers from the incoming HTTP request */\n\theaders?: Record<string, string>;\n\t/** Suffix to append to all page titles */\n\ttitleSuffix?: string;\n\t/** Maximum timeout in milliseconds for SSR setup */\n\ttimeout?: number;\n};\n\n/**\n * Result of SSR setup phase\n */\ntype SSRSetupResult = {\n\tssrData: Record<string, unknown>;\n\tssrError?: string;\n\tstatusContainer: { value: number | undefined };\n\tbyCollectionForSSR: Record<string, unknown[]>;\n\tbundlePreload: string;\n\tpreloadExtraHead: string;\n\tpreloadHydrationScript: string;\n\tpreloadStatusOverride?: number;\n\tinlineStyleTag: string;\n\tmetadataResult: {\n\t\tperPageHead: string;\n\t\tpageLang?: string;\n\t\tstatusOverride?: number;\n\t\t/** SSR export for dynamic head generation at stream end */\n\t\tssrExport?: SSR;\n\t};\n\tpending?: Promise<void>;\n};\n\ntype SSRSetupTimeoutState = {\n\ttimedOut: boolean;\n};\n\n/**\n * Perform common SSR setup steps shared between render and stream modes.\n * This includes seeding modules, computing data, loading collections, and preloading.\n */\nasync function performSSRSetup(\n\toptions: SSRSetupOptions,\n\tstreaming = false,\n): Promise<SSRSetupResult> {\n\tconst { pathname } = options;\n\tconst timeoutState: SSRSetupTimeoutState = { timedOut: false };\n\n\t// Add a global timeout for SSR setup to prevent indefinite hangs.\n\t// Consumer-provided timeout takes precedence.\n\tconst setupTimeout = options.timeout ?? (streaming ? 10000 : 30000);\n\tlet timeoutId: ReturnType<typeof setTimeout> | null = null;\n\tconst timeoutPromise = new Promise<never>((_, reject) => {\n\t\ttimeoutId = setTimeout(() => {\n\t\t\ttimeoutState.timedOut = true;\n\t\t\treject(\n\t\t\t\tnew Error(\n\t\t\t\t\t`[reroute] SSR setup timeout after ${setupTimeout}ms for ${pathname}`,\n\t\t\t\t),\n\t\t\t);\n\t\t}, setupTimeout);\n\t});\n\tconst setupPromise = withParentSpan(options.parentSpan, () =>\n\t\tperformSSRSetupInternal(options, streaming, timeoutState),\n\t);\n\tconst setupPromiseWithCleanup = setupPromise.finally(() => {\n\t\tif (timeoutState.timedOut) {\n\t\t\tcleanupPathScopedSSRData(pathname);\n\t\t}\n\t});\n\n\ttry {\n\t\treturn await Promise.race([setupPromiseWithCleanup, timeoutPromise]);\n\t} catch (error) {\n\t\tcleanupPathScopedSSRData(pathname);\n\t\tconsole.error('[reroute] SSR setup error:', error);\n\t\t// Return minimal setup result to allow page to render with fallback\n\t\treturn {\n\t\t\tssrData: {},\n\t\t\tssrError: error instanceof Error ? error.message : String(error),\n\t\t\tstatusContainer: { value: 500 },\n\t\t\tbyCollectionForSSR: {},\n\t\t\tbundlePreload: '',\n\t\t\tpreloadExtraHead: '',\n\t\t\tpreloadHydrationScript: '',\n\t\t\tinlineStyleTag: '',\n\t\t\tmetadataResult: { perPageHead: '' },\n\t\t};\n\t} finally {\n\t\tif (timeoutId) {\n\t\t\tclearTimeout(timeoutId);\n\t\t}\n\t}\n}\n\n/**\n * Internal SSR setup implementation (wrapped with timeout above)\n */\nasync function performSSRSetupInternal(\n\toptions: SSRSetupOptions,\n\tstreaming: boolean,\n\ttimeoutState: SSRSetupTimeoutState,\n): Promise<SSRSetupResult> {\n\tconst debug = options.debug ?? false;\n\tconst t0 = debug ? performance.now() : 0;\n\tconst {\n\t\tpathname,\n\t\tclientDir,\n\t\tcwd,\n\t\tisWatchMode,\n\t\tbundleUrl,\n\t\tmaxAge = 0,\n\t\tsearchParams,\n\t\togConfig,\n\t} = options;\n\tconst assertNotTimedOut = () => {\n\t\tif (timeoutState.timedOut) {\n\t\t\tthrow new Error(`[reroute] SSR setup cancelled for ${pathname}`);\n\t\t}\n\t};\n\n\t// Note: We don't reset SSR globals here because multiple requests may run concurrently.\n\t// Instead, we use pathname-scoped serialization to ensure each response only includes\n\t// its own data (see createContentHtmlScript and createSSRExportsScript).\n\n\t// 1. Seed content modules for SSR rendering\n\tawait withSpan('ssr.seed', async (span) => {\n\t\tspan.setAttribute('pathname', pathname);\n\t\tspan.setAttribute('streaming', streaming);\n\t\tawait seedSSRModuleForPath(pathname, clientDir, cwd, isWatchMode);\n\t\tif (debug)\n\t\t\tconsole.log(\n\t\t\t\t`[Setup] ${pathname} - seedModule: ${(performance.now() - t0).toFixed(2)}ms`,\n\t\t\t);\n\t});\n\tassertNotTimedOut();\n\n\t// 2. Reset SSR access tracking and create bundle preload\n\tresetSSRAccessTracking();\n\tresetSSRContentResults();\n\tconst bundlePreload = createBundlePreload(bundleUrl);\n\n\t// 3. Preload content module\n\tconst preloadResult = await withSpan('ssr.preload', async (span) => {\n\t\tspan.setAttribute('pathname', pathname);\n\t\tconst result = await preloadContentModule(\n\t\t\tpathname,\n\t\t\tclientDir,\n\t\t\tcwd,\n\t\t\tisWatchMode,\n\t\t);\n\t\tspan.setAttribute('has_module', !!result.modulePath);\n\t\tif (debug)\n\t\t\tconsole.log(\n\t\t\t\t`[Setup] ${pathname} - preloadContent: ${(performance.now() - t0).toFixed(2)}ms`,\n\t\t\t);\n\t\treturn result;\n\t});\n\tassertNotTimedOut();\n\n\t// 4. Compute SSR data\n\tconst computeResult: ComputeResult = await withSpan(\n\t\t'ssr.compute.data',\n\t\tasync (span) => {\n\t\t\tspan.setAttribute('pathname', pathname);\n\t\t\tspan.setAttribute('streaming', streaming);\n\t\t\tconst result = await computeSSRData(\n\t\t\t\tpathname,\n\t\t\t\tclientDir,\n\t\t\t\tcwd,\n\t\t\t\tisWatchMode,\n\t\t\t\t{ streaming, maxAge, headers: options.headers },\n\t\t\t\tsearchParams,\n\t\t\t);\n\t\t\tspan.setAttribute('has_error', !!result.error);\n\t\t\tspan.setAttribute('has_data', Object.keys(result.data).length > 0);\n\t\t\tif (debug)\n\t\t\t\tconsole.log(\n\t\t\t\t\t`[Setup] ${pathname} - computeSSRData: ${(performance.now() - t0).toFixed(2)}ms`,\n\t\t\t\t);\n\t\t\treturn result;\n\t\t},\n\t);\n\tassertNotTimedOut();\n\n\tsetGlobalSSRData(computeResult.data);\n\n\t// 5. Load content collections (use cached if available)\n\tconst usingCachedCollections = !!options.cachedCollections;\n\tconst byCollectionForSSR = await withSpan(\n\t\t'ssr.load.collections',\n\t\tasync (span) => {\n\t\t\tspan.setAttribute('pathname', pathname);\n\t\t\tspan.setAttribute('cached', usingCachedCollections);\n\t\t\tconst collections = options.cachedCollections\n\t\t\t\t? options.cachedCollections\n\t\t\t\t: await loadCollections(cwd, isWatchMode);\n\t\t\tspan.setAttribute('collection_count', Object.keys(collections).length);\n\t\t\tif (debug)\n\t\t\t\tconsole.log(\n\t\t\t\t\t`[Setup] ${pathname} - loadCollections (cached: ${usingCachedCollections}): ${(performance.now() - t0).toFixed(2)}ms`,\n\t\t\t\t);\n\t\t\treturn collections;\n\t\t},\n\t);\n\tassertNotTimedOut();\n\tsetGlobalCollections(byCollectionForSSR);\n\n\t// 6. Theme stylesheet tag (use cached if available)\n\tconst usingCachedCSS = !!options.cachedThemeCSS;\n\tconst inlineStyleTag = await withSpan('ssr.inline.css', async (span) => {\n\t\tconst css = options.cachedThemeCSS\n\t\t\t? options.cachedThemeCSS\n\t\t\t: await inlineThemeCSS(clientDir, isWatchMode);\n\t\tspan.setAttributes({\n\t\t\t'reroute.cached': usingCachedCSS,\n\t\t\t'reroute.css.size': css.length,\n\t\t});\n\t\tif (debug)\n\t\t\tconsole.log(\n\t\t\t\t`[Setup] ${pathname} - inlineThemeCSS (cached: ${usingCachedCSS}): ${(performance.now() - t0).toFixed(2)}ms`,\n\t\t\t);\n\t\treturn css;\n\t});\n\tassertNotTimedOut();\n\n\t// 7. Determine initial status override\n\tlet statusOverride = preloadResult.statusOverride;\n\tif (computeResult.statusContainer.value !== undefined) {\n\t\tstatusOverride = computeResult.statusContainer.value;\n\t}\n\n\t// 8. Extract page metadata (with SSR data for dynamic head tags)\n\tconst metadataResult = await withSpan(\n\t\t'ssr.extract.metadata',\n\t\tasync (span) => {\n\t\t\tconst result = await extractPageMetadata(\n\t\t\t\tpathname,\n\t\t\t\tclientDir,\n\t\t\t\tcwd,\n\t\t\t\tisWatchMode,\n\t\t\t\tstatusOverride,\n\t\t\t\togConfig,\n\t\t\t\toptions.ogMetaConfig,\n\t\t\t\toptions.baseUrl,\n\t\t\t\toptions.autoCanonical,\n\t\t\t\tcomputeResult.data,\n\t\t\t\toptions.titleSuffix,\n\t\t\t);\n\t\t\tspan.setAttributes({\n\t\t\t\t'reroute.pathname': pathname,\n\t\t\t\t'reroute.metadata.exists': !!result.perPageHead,\n\t\t\t});\n\t\t\tif (debug)\n\t\t\t\tconsole.log(\n\t\t\t\t\t`[Setup] ${pathname} - extractMetadata: ${(performance.now() - t0).toFixed(2)}ms`,\n\t\t\t\t);\n\t\t\treturn result;\n\t\t},\n\t);\n\tassertNotTimedOut();\n\n\treturn {\n\t\tssrData: computeResult.data,\n\t\tssrError: computeResult.error,\n\t\tstatusContainer: computeResult.statusContainer,\n\t\tbyCollectionForSSR,\n\t\tbundlePreload,\n\t\tpreloadExtraHead: preloadResult.extraHead,\n\t\tpreloadHydrationScript: preloadResult.hydrationScript,\n\t\tpreloadStatusOverride: preloadResult.statusOverride,\n\t\tinlineStyleTag,\n\t\tmetadataResult,\n\t\tpending: computeResult.pending,\n\t};\n}\n\n/**\n * Generate hydration scripts for collections that were accessed during SSR.\n */\nfunction generateCollectionScripts(\n\tbyCollectionForSSR: Record<string, unknown[]>,\n\tcapturedResults?: Record<string, unknown[]>,\n): string {\n\ttry {\n\t\tconst g = globalThis as unknown as { __REROUTE_SSR_ACCESSED__?: unknown };\n\t\tconst acc = g.__REROUTE_SSR_ACCESSED__;\n\n\t\tif (acc) {\n\t\t\tconst { subset, partial } = processCollections(\n\t\t\t\tacc,\n\t\t\t\tbyCollectionForSSR,\n\t\t\t\tcapturedResults,\n\t\t\t);\n\t\t\tif (Object.keys(subset).length > 0) {\n\t\t\t\treturn createCollectionScript(subset, partial);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\treturn '';\n}\n\n/**\n * Generate all hydration scripts for SSR\n */\nfunction generateHydrationScripts(\n\tssrData: Record<string, unknown>,\n\tisWatchMode: boolean,\n\tssrError?: string,\n\tcollectionScripts = '',\n\tpreloadScript = '',\n\tbundleUrl = '',\n\tpathname = '',\n\tbrowserTelemetryConfig?: Record<string, unknown>,\n\ttraceContext?: { traceparent?: string; tracestate?: string },\n\ttitleSuffix?: string,\n\ti18n?: Record<string, unknown>,\n): string {\n\tlet scripts = preloadScript;\n\n\t// Add collection scripts\n\tscripts += collectionScripts;\n\n\t// Add SSR data script (pathname is used to scope content HTML and exports to current request)\n\tscripts += createDataScript(\n\t\tssrData,\n\t\tisWatchMode ? ssrError : undefined,\n\t\tpathname,\n\t\tbrowserTelemetryConfig,\n\t\ttraceContext,\n\t\ttitleSuffix,\n\t\ti18n,\n\t);\n\n\t// Add feeds and llms scripts\n\tscripts += createFeedsScript();\n\tscripts += createLlmsScript();\n\n\t// Add bundle script\n\tif (bundleUrl) {\n\t\tscripts += `<script type=\"module\" src=\"${bundleUrl}\"></script>`;\n\t}\n\n\t// Add dev watcher in watch mode\n\tif (isWatchMode) {\n\t\tscripts += `<script type=\"module\" src=\"/__reroute_watch.js\"></script>`;\n\t}\n\n\treturn scripts;\n}\n\nexport type { SSRSetupOptions, SSRSetupResult };\nexport { performSSRSetup, generateCollectionScripts, generateHydrationScripts };\n",
|
|
35
|
+
"// Types\n\n// Core utilities\nexport * from './cache';\nexport * from './cleanup';\n// Content handling\nexport * from './collections';\n// HTTP utilities\nexport * from './compression';\n// SSR compute\nexport * from './compute';\nexport { computeSSRDataForPath } from './data';\n// HTML handling\nexport * from './html';\nexport * from './imports';\nexport * from './layouts';\nexport * from './metadata';\nexport * from './mime';\nexport * from './modules';\nexport * from './path';\nexport * from './preload';\nexport * from './route-match';\n// Script generation\nexport * from './scripts';\nexport * from './seed';\n// Shared setup\nexport * from './setup';\nexport * from './styles';\nexport * from './template';\nexport * from './types';\n",
|
|
36
|
+
"import type { Span } from '@opentelemetry/api';\nimport dedent from 'dedent';\nimport { cloneElement, type ReactElement } from 'react';\nimport { renderToString } from 'react-dom/server';\nimport {\n\tgetTraceContextForInjection,\n\twithSpan,\n\twithSpanSync,\n} from 'reroute-js/telemetry/server';\nimport { loadConfig, type OGImageOptions, type OGOptions } from '../config';\nimport { appendLlmsAlternateHead } from '../llms/discovery';\nimport {\n\ttype Doc,\n\tduplicateQueryParamData,\n\tgenerateCollectionScripts,\n\tgenerateHydrationScripts,\n\tperformSSRSetup,\n\tresetSSRContentResults,\n\tsetGlobalSSRData,\n\ttakeSSRContentResults,\n} from './lib';\nimport { applyIndexTemplate, loadIndexHtml } from './lib/html';\n\ntype SSRRenderOptions = {\n\tpathname: string;\n\trootComponent: ReactElement;\n\tclientDir: string;\n\tcwd: string;\n\tisWatchMode: boolean;\n\tbundleUrl: string;\n\tcachedCollections?: Record<string, unknown[]>;\n\tcachedThemeCSS?: string;\n\thead?: string;\n\tlang?: string;\n\tappId?: string;\n\tminify?: boolean;\n\tmaxAge?: number;\n\tsearchParams?: URLSearchParams;\n\togConfig?: OGImageOptions;\n\togMetaConfig?: OGOptions;\n\tbaseUrl?: string;\n\tautoCanonical?: boolean;\n\tparentSpan?: Span | null;\n\t/** Request headers from the incoming HTTP request */\n\theaders?: Record<string, string>;\n\t/** Suffix to append to all page titles */\n\ttitleSuffix?: string;\n\t/** Hydrated i18n state (opt-in) */\n\ti18n?: Record<string, unknown>;\n};\n\ntype SSRRenderResult = {\n\thtml: string;\n\tstatus: number;\n};\n\n/**\n * Render a React component to a complete HTML document for SSR.\n * Uses synchronous rendering (renderToString) for full HTML generation.\n */\nasync function renderSSRDocument(\n\toptions: SSRRenderOptions,\n): Promise<SSRRenderResult> {\n\tconst {\n\t\tpathname,\n\t\trootComponent,\n\t\tclientDir,\n\t\tcwd,\n\t\tisWatchMode,\n\t\tbundleUrl,\n\t\thead = '',\n\t\tlang = 'en',\n\t\tappId = 'root',\n\t\tmaxAge = 0,\n\t\tsearchParams,\n\t\togConfig,\n\t} = options;\n\n\t// Perform shared SSR setup\n\tconst setup = await performSSRSetup(\n\t\t{\n\t\t\tpathname,\n\t\t\tclientDir,\n\t\t\tcwd,\n\t\t\tisWatchMode,\n\t\t\tparentSpan: options.parentSpan,\n\t\t\tbundleUrl,\n\t\t\tmaxAge,\n\t\t\tsearchParams,\n\t\t\togConfig,\n\t\t\togMetaConfig: options.ogMetaConfig,\n\t\t\tbaseUrl: options.baseUrl,\n\t\t\tautoCanonical: options.autoCanonical,\n\t\t\tcachedCollections: options.cachedCollections,\n\t\t\tcachedThemeCSS: options.cachedThemeCSS,\n\t\t\theaders: options.headers,\n\t\t\ttitleSuffix: options.titleSuffix,\n\t\t},\n\t\tfalse, // not streaming\n\t);\n\n\t// Duplicate data for query param routes\n\tconst duplicatedData = duplicateQueryParamData(setup.ssrData);\n\tsetGlobalSSRData(duplicatedData);\n\n\t// Render React component to string.\n\t// The capture buffer is reset and read around the synchronous render so a\n\t// concurrent request cannot mix its items (and locale) into this response.\n\tlet capturedResults: Record<string, unknown[]> | undefined;\n\tconst appHtml = await withSpan('ssr.render.react', async (span) => {\n\t\tconst componentWithPathname = cloneElement<Doc>(rootComponent, {\n\t\t\tpathname,\n\t\t\tsearchParams,\n\t\t} as Doc);\n\t\tspan.setAttribute('reroute.pathname', pathname);\n\t\tresetSSRContentResults();\n\t\tconst html = renderToString(componentWithPathname);\n\t\tcapturedResults = takeSSRContentResults();\n\t\tspan.setAttribute('reroute.html.final.size', html.length);\n\t\treturn html;\n\t});\n\n\t// Load base template\n\tconst baseTemplate = await withSpan('ssr.load.template', async (span) => {\n\t\tconst template = await loadIndexHtml(clientDir);\n\t\tspan.setAttribute('reroute.template.size', template.length);\n\t\treturn template;\n\t});\n\n\t// Generate collection scripts from accessed collections\n\tconst collectionScripts = generateCollectionScripts(\n\t\tsetup.byCollectionForSSR,\n\t\tcapturedResults,\n\t);\n\n\t// Load config to get browser telemetry settings\n\tconst config = await loadConfig(cwd);\n\tconst browserTelemetryConfigRaw = config.telemetry?.browser as\n\t\t| Record<string, unknown>\n\t\t| undefined;\n\n\t// Serialize browser telemetry config (convert functions to strings for JSON)\n\tconst { serializeBrowserTelemetryConfig } = await import('./lib/serialize');\n\tconst browserTelemetryConfig = serializeBrowserTelemetryConfig(\n\t\tbrowserTelemetryConfigRaw,\n\t);\n\n\t// Extract trace context from parent span for distributed tracing\n\tconst traceContext = getTraceContextForInjection(options.parentSpan);\n\n\t// Generate all hydration scripts\n\tconst hydrationScript = await withSpan(\n\t\t'ssr.generate.scripts',\n\t\tasync (span) => {\n\t\t\tconst script = generateHydrationScripts(\n\t\t\t\tsetup.ssrData,\n\t\t\t\tisWatchMode,\n\t\t\t\tsetup.ssrError,\n\t\t\t\tcollectionScripts,\n\t\t\t\tsetup.preloadHydrationScript,\n\t\t\t\tbundleUrl,\n\t\t\t\tpathname,\n\t\t\t\tbrowserTelemetryConfig,\n\t\t\t\ttraceContext,\n\t\t\t\toptions.titleSuffix,\n\t\t\t\toptions.i18n,\n\t\t\t);\n\t\t\tspan.setAttribute('reroute.scripts.size', script.length);\n\t\t\treturn script;\n\t\t},\n\t);\n\n\t// Determine final status (only override if value is set)\n\tlet statusOverride = setup.preloadStatusOverride;\n\tif (setup.statusContainer.value !== undefined) {\n\t\tstatusOverride = setup.statusContainer.value;\n\t}\n\tif (setup.metadataResult.statusOverride !== undefined) {\n\t\tstatusOverride = setup.metadataResult.statusOverride;\n\t}\n\n\tconst extraHead = appendLlmsAlternateHead(\n\t\tsetup.bundlePreload + setup.preloadExtraHead,\n\t\tpathname,\n\t\tstatusOverride || 200,\n\t\tconfig.llms,\n\t);\n\tconst combinedHead = dedent(\n\t\t[\n\t\t\tdedent(head) || '',\n\t\t\tdedent(extraHead),\n\t\t\tdedent(setup.metadataResult.perPageHead),\n\t\t]\n\t\t\t.filter(Boolean)\n\t\t\t.join('\\n'),\n\t);\n\n\tconst i18nLocale = options.i18n?.enabled\n\t\t? String(options.i18n.locale || '')\n\t\t: '';\n\n\t// Apply template with SSR content\n\tconst html = withSpanSync('ssr.apply.template', (span) => {\n\t\tconst result = applyIndexTemplate(baseTemplate, appHtml, {\n\t\t\thead: [setup.inlineStyleTag, combinedHead].filter(Boolean).join('\\n'),\n\t\t\thydrationScript,\n\t\t\tlang: setup.metadataResult.pageLang || i18nLocale || lang,\n\t\t\tappId,\n\t\t});\n\t\tspan.setAttributes({\n\t\t\t'reroute.html.final.size': result.length,\n\t\t\t'reroute.html.original.size': appHtml.length,\n\t\t});\n\t\treturn result;\n\t});\n\n\treturn {\n\t\thtml,\n\t\tstatus: statusOverride || 200,\n\t};\n}\n\nexport { renderSSRDocument, type SSRRenderOptions, type SSRRenderResult };\n",
|
|
37
|
+
"import { pathToFileURL } from 'node:url';\nimport type { ReactElement } from 'react';\nimport { renderToStaticMarkup } from 'react-dom/server';\nimport type { LLMsOptions } from '../config';\nimport { join } from '../ssr/lib/path';\nimport type { Doc } from '../ssr/lib/types';\nimport { renderSSRDocument } from '../ssr/render';\nimport {\n\textractTitle,\n\tformatAsMarkdown,\n\tformatAsText,\n\thtmlToMarkdown,\n\tstripHtmlTags,\n} from './formatter';\n\n/**\n * Drop header / nav / aside chrome from LLM text. Keep <footer> so\n * legal and site links stay in .txt / .md mirrors.\n */\nexport function stripLayoutChrome(html: string): string {\n\treturn html\n\t\t.replace(/<nav[\\s\\S]*?<\\/nav>/gi, '')\n\t\t.replace(/<header[\\s\\S]*?<\\/header>/gi, '')\n\t\t.replace(/<aside[\\s\\S]*?<\\/aside>/gi, '');\n}\n\n/**\n * Check if a route should be excluded from LLM features\n * Supports strings (substring match), RegExp, and custom functions\n */\nexport function shouldExcludeRoute(\n\tpathname: string,\n\texcludeRoutes?: (string | RegExp | ((pathname: string) => boolean))[],\n): boolean {\n\tif (!excludeRoutes || excludeRoutes.length === 0) {\n\t\treturn false;\n\t}\n\n\treturn Boolean(\n\t\texcludeRoutes.find((pattern) => {\n\t\t\tif (typeof pattern === 'string') {\n\t\t\t\t// Substring match for simplicity\n\t\t\t\treturn pathname.includes(pattern);\n\t\t\t}\n\t\t\tif (pattern instanceof RegExp) {\n\t\t\t\treturn pattern.test(pathname);\n\t\t\t}\n\t\t\tif (typeof pattern === 'function') {\n\t\t\t\treturn pattern(pathname);\n\t\t\t}\n\t\t\treturn false;\n\t\t}),\n\t);\n}\n\nasync function loadCollectionItem(\n\tcwd: string,\n\tpathname: string,\n\tisWatchMode: boolean,\n): Promise<{ item: Doc; collection: string } | null> {\n\tconst parts = pathname.split('/').filter(Boolean);\n\tif (parts.length < 2) return null;\n\n\tconst collection = parts[0];\n\tconst collectionFile = join(\n\t\tcwd,\n\t\t'.reroute',\n\t\t'collections',\n\t\t`${collection}.js`,\n\t);\n\tconst exists = await Bun.file(collectionFile).exists();\n\tif (!exists) return null;\n\n\tconst url = `${pathToFileURL(collectionFile).href}${isWatchMode ? `?t=${Date.now()}` : ''}`;\n\tconst mod = await import(url);\n\tconst items = mod.items || [];\n\n\tconst item = items.find((i: Doc) => i.href === pathname);\n\tif (!item) return null;\n\n\treturn { item, collection };\n}\n\nasync function findComponentPath(\n\tclientDir: string,\n\tcollection: string,\n\tslug: string,\n): Promise<string | null> {\n\tconst extensions = ['.mdx', '.md', '.tsx', '.jsx'];\n\n\tfor (const ext of extensions) {\n\t\tconst testPath = join(\n\t\t\tclientDir,\n\t\t\t'routes',\n\t\t\tcollection,\n\t\t\t'content',\n\t\t\t`${slug}${ext}`,\n\t\t);\n\t\tconst file = Bun.file(testPath);\n\t\tif (await file.exists()) {\n\t\t\treturn testPath;\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction stripFrontmatterAndJsx(content: string, isMdx: boolean): string {\n\tlet rawContent = content.replace(/^---\\n[\\s\\S]*?\\n---\\n/, '');\n\n\tif (isMdx) {\n\t\trawContent = rawContent.replace(\n\t\t\t/^import\\s+.*?from\\s+['\"].*?['\"];?\\s*$/gm,\n\t\t\t'',\n\t\t);\n\t\trawContent = rawContent.replace(/<[A-Z][\\w]*[^>]*\\/>/g, '');\n\t\trawContent = rawContent.replace(\n\t\t\t/<[A-Z][\\w]*[^>]*>[\\s\\S]*?<\\/[A-Z][\\w]*>/g,\n\t\t\t'',\n\t\t);\n\t}\n\n\treturn rawContent.trim();\n}\n\nasync function extractMarkdownContent(\n\tcomponentPath: string,\n\tmeta: Record<string, unknown>,\n): Promise<{ title?: string; body: string; markdown: string }> {\n\tconst file = Bun.file(componentPath);\n\tlet rawContent = await file.text();\n\n\tconst isMdx = componentPath.endsWith('.mdx');\n\trawContent = stripFrontmatterAndJsx(rawContent, isMdx);\n\n\treturn {\n\t\ttitle: (meta.title as string) || extractTitle(rawContent),\n\t\tbody: stripHtmlTags(rawContent),\n\t\tmarkdown: rawContent,\n\t};\n}\n\nasync function extractComponentContent(\n\tcomponentPath: string,\n\tmeta: Record<string, unknown>,\n\tisWatchMode: boolean,\n): Promise<{ title?: string; body: string; markdown: string } | null> {\n\ttry {\n\t\tconst componentUrl = `${pathToFileURL(componentPath).href}${isWatchMode ? `?t=${Date.now()}` : ''}`;\n\t\tconst componentMod = await import(componentUrl);\n\n\t\tconst Component = componentMod.default;\n\t\tif (!Component || typeof Component !== 'function') return null;\n\n\t\tconst html = renderToStaticMarkup(Component({}));\n\n\t\treturn {\n\t\t\ttitle: (meta.title as string) || extractTitle(html),\n\t\t\tbody: stripHtmlTags(html),\n\t\t\tmarkdown: htmlToMarkdown(html),\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\n\t\t\t`[reroute/llms] Failed to import component ${componentPath}:`,\n\t\t\terror,\n\t\t);\n\t\treturn null;\n\t}\n}\n\n/**\n * Extract content from a collection item (markdown/mdx)\n */\nasync function extractCollectionContent(\n\tcwd: string,\n\tclientDir: string,\n\tpathname: string,\n\tisWatchMode: boolean,\n\t_format: 'txt' | 'md',\n): Promise<{ title?: string; body: string; markdown: string } | null> {\n\ttry {\n\t\tconst collectionData = await loadCollectionItem(cwd, pathname, isWatchMode);\n\t\tif (!collectionData) return null;\n\n\t\tconst { item, collection } = collectionData;\n\t\tconst slug = item.slug || item.name;\n\t\tif (!slug) return null;\n\n\t\tconst componentPath = await findComponentPath(clientDir, collection, slug);\n\t\tif (!componentPath) return null;\n\n\t\tconst meta = item.meta || {};\n\n\t\tif (componentPath.endsWith('.md') || componentPath.endsWith('.mdx')) {\n\t\t\treturn await extractMarkdownContent(componentPath, meta);\n\t\t}\n\n\t\treturn await extractComponentContent(componentPath, meta, isWatchMode);\n\t} catch (error) {\n\t\tconsole.error(\n\t\t\t`[reroute/llms] Failed to extract collection content for ${pathname}:`,\n\t\t\terror,\n\t\t);\n\t\treturn null;\n\t}\n}\n\n/**\n * Extract content from an SSR route using full app rendering\n */\nasync function extractSSRContent(\n\tpathname: string,\n\trootComponent: ReactElement,\n\tclientDir: string,\n\tcwd: string,\n\tisWatchMode: boolean,\n\tbundleUrl: string,\n\t_format: 'txt' | 'md',\n): Promise<{ title?: string; body: string; markdown: string } | null> {\n\ttry {\n\t\t// Use the full SSR rendering pipeline to get the complete HTML\n\t\t// This includes all providers, context, and hooks - just like a real request\n\t\tconst { html } = await renderSSRDocument({\n\t\t\tpathname,\n\t\t\trootComponent,\n\t\t\tclientDir,\n\t\t\tcwd,\n\t\t\tisWatchMode,\n\t\t\tbundleUrl,\n\t\t\tminify: false,\n\t\t\tmaxAge: 0,\n\t\t});\n\n\t\tlet contentHtml = html;\n\n\t\tconst appDivMatch = html.match(/<div id=\"[^\"]+\">(.+?)<\\/div>\\s*<script/s);\n\t\tif (appDivMatch) {\n\t\t\tcontentHtml = appDivMatch[1];\n\t\t}\n\n\t\tcontentHtml = stripLayoutChrome(contentHtml);\n\n\t\treturn {\n\t\t\ttitle: extractTitle(html),\n\t\t\tbody: stripHtmlTags(contentHtml),\n\t\t\tmarkdown: htmlToMarkdown(contentHtml),\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\n\t\t\t`[reroute/llms] Failed to extract SSR content for ${pathname}:`,\n\t\t\terror,\n\t\t);\n\t\treturn null;\n\t}\n}\n\n/**\n * Main content extraction function\n * Extracts clean, LLM-optimized content from any route type\n */\nexport async function extractContentForLLMs(options: {\n\tpathname: string;\n\trootComponent?: ReactElement;\n\tclientDir: string;\n\tcwd: string;\n\tisWatchMode: boolean;\n\tbundleUrl?: string;\n\tformat: 'txt' | 'md';\n\tllmsConfig?: LLMsOptions;\n}): Promise<string> {\n\tconst {\n\t\tpathname,\n\t\trootComponent,\n\t\tclientDir,\n\t\tcwd,\n\t\tisWatchMode,\n\t\tbundleUrl,\n\t\tformat,\n\t\tllmsConfig,\n\t} = options;\n\n\t// Check if route should be excluded\n\tif (shouldExcludeRoute(pathname, llmsConfig?.excludeRoutes)) {\n\t\tthrow new Error('Route excluded from LLM features');\n\t}\n\n\t// Try collection content first (most common case for content sites)\n\tlet content = await extractCollectionContent(\n\t\tcwd,\n\t\tclientDir,\n\t\tpathname,\n\t\tisWatchMode,\n\t\tformat,\n\t);\n\n\t// Fall back to SSR route (uses full app rendering)\n\tif (!content && rootComponent && bundleUrl) {\n\t\tcontent = await extractSSRContent(\n\t\t\tpathname,\n\t\t\trootComponent,\n\t\t\tclientDir,\n\t\t\tcwd,\n\t\t\tisWatchMode,\n\t\t\tbundleUrl,\n\t\t\tformat,\n\t\t);\n\t}\n\n\t// If we still don't have content, throw\n\tif (!content) {\n\t\tthrow new Error('Content not found');\n\t}\n\n\tif (format === 'md') {\n\t\treturn formatAsMarkdown({\n\t\t\ttitle: content.title,\n\t\t\tmarkdown: content.markdown,\n\t\t});\n\t}\n\n\treturn formatAsText({\n\t\ttitle: content.title,\n\t\tbody: content.body,\n\t});\n}\n",
|
|
38
|
+
"import type { ContentSignalPolicy, LLMsOptions } from '../config';\nimport { shouldExcludeRoute } from './extractor';\n\ntype LlmsDiscovery = {\n\tlinkValues: string[];\n\tcontentSignal?: string;\n\tvary?: string;\n};\n\nfunction yesNo(value: boolean | undefined): 'yes' | 'no' {\n\treturn value === false ? 'no' : 'yes';\n}\n\n/**\n * Format a Content-Signal header / robots.txt value.\n * `whenUndefined: 'default'` emits the allow-all policy used when llms is on.\n */\nexport function formatContentSignal(\n\tpolicy: ContentSignalPolicy | false | undefined,\n\twhenUndefined: 'default' | 'omit' = 'omit',\n): string | undefined {\n\tif (policy === false) return undefined;\n\tif (policy === undefined && whenUndefined === 'omit') return undefined;\n\n\tconst resolved = {\n\t\tsearch: true,\n\t\taiInput: true,\n\t\taiTrain: true,\n\t\t...policy,\n\t};\n\n\treturn `ai-train=${yesNo(resolved.aiTrain)}, search=${yesNo(resolved.search)}, ai-input=${yesNo(resolved.aiInput)}`;\n}\n\n/**\n * Markdown twin path for a page URL (`/` → `/index.md`).\n */\nexport function markdownTwinPath(pathname: string): string {\n\tconst path = pathname.split('?')[0];\n\tif (path === '/' || path === '') return '/index.md';\n\treturn `${path.replace(/\\/$/, '')}.md`;\n}\n\nexport function shouldAdvertiseMarkdownTwin(\n\tpathname: string,\n\tllmsConfig?: LLMsOptions,\n): boolean {\n\tif (!llmsConfig?.enabled) return false;\n\tif (llmsConfig.enableMarkdownExtension === false) return false;\n\tif (shouldExcludeRoute(pathname, llmsConfig.excludeRoutes)) return false;\n\treturn true;\n}\n\nexport function llmsAlternateLinkTag(\n\tpathname: string,\n\tllmsConfig?: LLMsOptions,\n): string {\n\tif (!shouldAdvertiseMarkdownTwin(pathname, llmsConfig)) return '';\n\treturn `<link rel=\"alternate\" type=\"text/markdown\" href=\"${markdownTwinPath(pathname)}\" />`;\n}\n\nexport function mergeVary(existing: string | undefined, token: string): string {\n\tconst parts = (existing ?? '')\n\t\t.split(',')\n\t\t.map((part) => part.trim())\n\t\t.filter(Boolean);\n\tif (!parts.some((part) => part.toLowerCase() === token.toLowerCase())) {\n\t\tparts.push(token);\n\t}\n\treturn parts.join(', ');\n}\n\nfunction appendLinkValues(\n\texisting: string | undefined,\n\tvalues: string[],\n): string {\n\tconst current = (existing ?? '')\n\t\t.split(',')\n\t\t.map((part) => part.trim())\n\t\t.filter(Boolean);\n\tfor (const value of values) {\n\t\tif (!current.includes(value)) current.push(value);\n\t}\n\treturn current.join(', ');\n}\n\nexport function buildLlmsDiscovery(\n\tpathname: string,\n\tllmsConfig?: LLMsOptions,\n): LlmsDiscovery | null {\n\tif (!llmsConfig?.enabled) return null;\n\n\tconst linkValues: string[] = [];\n\tif (shouldAdvertiseMarkdownTwin(pathname, llmsConfig)) {\n\t\tlinkValues.push(\n\t\t\t`<${markdownTwinPath(pathname)}>; rel=\"alternate\"; type=\"text/markdown\"`,\n\t\t);\n\t}\n\tif (llmsConfig.enableIndex !== false) {\n\t\tlinkValues.push('</llms.txt>; rel=\"llms-txt\"');\n\t}\n\n\tconst contentSignal = formatContentSignal(\n\t\tllmsConfig.contentSignal,\n\t\t'default',\n\t);\n\tconst vary =\n\t\tllmsConfig.enableContentNegotiation !== false ? 'Accept' : undefined;\n\n\tif (linkValues.length === 0 && !contentSignal && !vary) return null;\n\treturn { linkValues, contentSignal, vary };\n}\n\nfunction writeHeader(\n\theaders: Record<string, string>,\n\tcanonical: string,\n\tvalue: string,\n): void {\n\tconst lower = canonical.toLowerCase();\n\tif (headers[lower] !== undefined) headers[lower] = value;\n\tif (headers[canonical] !== undefined || headers[lower] === undefined) {\n\t\theaders[canonical] = value;\n\t}\n}\n\nfunction readHeader(\n\theaders: Record<string, string>,\n\tcanonical: string,\n): string | undefined {\n\treturn headers[canonical] ?? headers[canonical.toLowerCase()];\n}\n\n/**\n * Stamp discovery headers onto an HTML header bag. Idempotent.\n */\nexport function applyLlmsDiscoveryHeaders(\n\theaders: Record<string, string>,\n\tpathname: string,\n\tllmsConfig: LLMsOptions | undefined,\n\tstatus = 200,\n): void {\n\tif (status < 200 || status >= 300) return;\n\n\tconst discovery = buildLlmsDiscovery(pathname, llmsConfig);\n\tif (!discovery) return;\n\n\tif (discovery.linkValues.length > 0) {\n\t\twriteHeader(\n\t\t\theaders,\n\t\t\t'Link',\n\t\t\tappendLinkValues(readHeader(headers, 'Link'), discovery.linkValues),\n\t\t);\n\t}\n\tif (discovery.contentSignal) {\n\t\twriteHeader(headers, 'Content-Signal', discovery.contentSignal);\n\t}\n\tif (discovery.vary) {\n\t\twriteHeader(\n\t\t\theaders,\n\t\t\t'Vary',\n\t\t\tmergeVary(readHeader(headers, 'Vary'), discovery.vary),\n\t\t);\n\t}\n}\n\nexport function appendLlmsAlternateHead(\n\textraHead: string,\n\tpathname: string,\n\tstatus: number,\n\tllmsConfig?: LLMsOptions,\n): string {\n\tif (status !== 200) return extraHead;\n\tconst tag = llmsAlternateLinkTag(pathname, llmsConfig);\n\treturn tag ? `${extraHead}\\n${tag}` : extraHead;\n}\n\nexport type { LlmsDiscovery };\n",
|
|
39
|
+
"import type { ReactElement } from 'react';\nimport type { LLMsOptions } from '../config';\nimport { extractContentForLLMs, shouldExcludeRoute } from './extractor';\n\ntype RouteEntry = {\n\tpath: string;\n\tdate?: string;\n};\n\nfunction buildHeader(baseUrl: string, llmsConfig?: LLMsOptions): string[] {\n\tconst parts: string[] = [];\n\n\tif (llmsConfig?.siteName) {\n\t\tparts.push(`# ${llmsConfig.siteName}`);\n\t\tparts.push('');\n\t}\n\n\tif (llmsConfig?.siteDescription) {\n\t\tparts.push(llmsConfig.siteDescription);\n\t\tparts.push('');\n\t}\n\n\tif (parts.length > 0) {\n\t\tparts.push(`${baseUrl}/index.txt`);\n\t\tparts.push('');\n\t\tparts.push('---');\n\t\tparts.push('');\n\t}\n\n\treturn parts;\n}\n\nasync function discoverAllRoutes(\n\tcwd: string,\n\tcollections: string[],\n\tisWatchMode: boolean,\n\tllmsConfig?: LLMsOptions,\n): Promise<RouteEntry[]> {\n\tconst {\n\t\tdiscoverStaticRoutes,\n\t\tdiscoverCollectionRoutes,\n\t\tdiscoverSSRDataRoutes,\n\t} = await import('./index-generator');\n\n\tconst staticRoutes = await discoverStaticRoutes(cwd, isWatchMode);\n\tconst collectionsMap = await discoverCollectionRoutes(\n\t\tcwd,\n\t\tcollections,\n\t\tisWatchMode,\n\t\tllmsConfig,\n\t);\n\tconst ssrDataRoutes = await discoverSSRDataRoutes(\n\t\tcwd,\n\t\tstaticRoutes,\n\t\tisWatchMode,\n\t);\n\n\tconst allRoutes: RouteEntry[] = [];\n\n\tfor (const route of staticRoutes) {\n\t\tif (!shouldExcludeRoute(route.path, llmsConfig?.excludeRoutes)) {\n\t\t\tallRoutes.push({ path: route.path });\n\t\t}\n\t}\n\n\tfor (const [_collection, routes] of collectionsMap.entries()) {\n\t\tfor (const route of routes) {\n\t\t\tif (!shouldExcludeRoute(route.path, llmsConfig?.excludeRoutes)) {\n\t\t\t\tallRoutes.push({ path: route.path });\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const [_routePattern, routes] of ssrDataRoutes.entries()) {\n\t\tfor (const route of routes) {\n\t\t\tif (!shouldExcludeRoute(route.path, llmsConfig?.excludeRoutes)) {\n\t\t\t\tallRoutes.push({ path: route.path });\n\t\t\t}\n\t\t}\n\t}\n\n\treturn allRoutes;\n}\n\nfunction routeBelongsToCollection(path: string, collection: string): boolean {\n\treturn path === `/${collection}` || path.startsWith(`/${collection}/`);\n}\n\nfunction formatContentBlock(\n\tcontent: string,\n\troutePath: string,\n\tbaseUrl: string,\n): string[] {\n\tconst parts: string[] = [];\n\tconst lines = content.split('\\n');\n\tconst title = lines[0] || 'Untitled';\n\tconst body = lines.slice(lines[1] === '' ? 2 : 1).join('\\n');\n\n\tconst urlPath = routePath === '/' ? '/index' : routePath;\n\tconst url = `${baseUrl}${urlPath}.txt`;\n\n\tparts.push(title);\n\tparts.push('');\n\tparts.push(url);\n\tparts.push('');\n\tif (body.trim()) {\n\t\tparts.push(body);\n\t\tparts.push('');\n\t}\n\tparts.push('---');\n\tparts.push('');\n\n\treturn parts;\n}\n\nasync function extractRouteContents(\n\troutes: RouteEntry[],\n\toptions: {\n\t\trootComponent?: ReactElement;\n\t\tcwd: string;\n\t\tclientDir: string;\n\t\tbaseUrl: string;\n\t\tisWatchMode: boolean;\n\t\tbundleUrl?: string;\n\t\tllmsConfig?: LLMsOptions;\n\t},\n): Promise<{ parts: string[]; itemCount: number; currentSize: number }> {\n\tconst parts: string[] = [];\n\tconst maxSize = options.llmsConfig?.maxSize || 50 * 1024 * 1024;\n\tlet itemCount = 0;\n\tlet currentSize = 0;\n\n\tfor (const route of routes) {\n\t\ttry {\n\t\t\tconst content = await extractContentForLLMs({\n\t\t\t\tpathname: route.path,\n\t\t\t\trootComponent: options.rootComponent,\n\t\t\t\tclientDir: options.clientDir,\n\t\t\t\tcwd: options.cwd,\n\t\t\t\tisWatchMode: options.isWatchMode,\n\t\t\t\tbundleUrl: options.bundleUrl,\n\t\t\t\tformat: 'txt',\n\t\t\t\tllmsConfig: options.llmsConfig,\n\t\t\t});\n\n\t\t\tconst contentSize = Buffer.byteLength(content, 'utf8');\n\t\t\tif (currentSize + contentSize > maxSize) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[reroute/llms] Reached size limit (${maxSize} bytes), stopping at ${itemCount} items`,\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tconst blockParts = formatContentBlock(\n\t\t\t\tcontent,\n\t\t\t\troute.path,\n\t\t\t\toptions.baseUrl,\n\t\t\t);\n\t\t\tparts.push(...blockParts);\n\n\t\t\titemCount++;\n\t\t\tcurrentSize += contentSize;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\n\t\t\t\t`[reroute/llms] Failed to extract content for ${route.path}:`,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn { parts, itemCount, currentSize };\n}\n\nfunction buildFooter(parts: string[]): void {\n\tif (parts.length === 0) return;\n\n\t// Remove last separator\n\tif (parts[parts.length - 1] === '') parts.pop();\n\tif (parts[parts.length - 1] === '---') parts.pop();\n\tif (parts[parts.length - 1] === '') parts.pop();\n\n\tparts.push('---');\n\tparts.push('');\n\tparts.push(\n\t\t'Formats: .txt | .md | Accept: text/plain | Accept: text/markdown',\n\t);\n}\n\n/**\n * Generate /llms-full.txt - all site content in one file\n * Runtime-only generation with aggressive caching (7 days default)\n */\nexport async function generateLLMsFullContent(options: {\n\trootComponent?: ReactElement;\n\tcwd: string;\n\tclientDir: string;\n\tcollections: string[];\n\tbaseUrl: string;\n\tisWatchMode: boolean;\n\tbundleUrl?: string;\n\tllmsConfig?: LLMsOptions;\n\t/** Collection-scoped dump (`/help/llms-full.txt`). Omit for site-wide `/llms-full.txt`. */\n\ttargetCollection?: string;\n}): Promise<{\n\tcontent: string;\n\tstats: { itemCount: number; byteSize: number };\n}> {\n\tconst { cwd, collections, baseUrl, isWatchMode, llmsConfig } = options;\n\n\tconst parts = buildHeader(baseUrl, llmsConfig);\n\n\tconst discovered = await discoverAllRoutes(\n\t\tcwd,\n\t\tcollections,\n\t\tisWatchMode,\n\t\tllmsConfig,\n\t);\n\tconst targetCollection = options.targetCollection;\n\tconst allRoutes = targetCollection\n\t\t? discovered.filter((route) =>\n\t\t\t\trouteBelongsToCollection(route.path, targetCollection),\n\t\t\t)\n\t\t: discovered;\n\n\tconst maxItems = llmsConfig?.maxItems || 10000;\n\tconst routesToProcess = allRoutes.slice(0, maxItems);\n\n\tconst extracted = await extractRouteContents(routesToProcess, options);\n\tparts.push(...extracted.parts);\n\n\tbuildFooter(parts);\n\n\tconst fullContent = parts.join('\\n');\n\tconst byteSize = Buffer.byteLength(fullContent, 'utf8');\n\n\treturn {\n\t\tcontent: fullContent,\n\t\tstats: { itemCount: extracted.itemCount, byteSize },\n\t};\n}\n",
|
|
40
|
+
"import { pathToFileURL } from 'node:url';\nimport { resolveAppPaths } from '../app-paths';\nimport { type LLMsOptions, loadConfig } from '../config';\nimport { join } from '../ssr/lib/path';\nimport type { Doc, SSR } from '../ssr/lib/types';\nimport { shouldExcludeRoute } from './extractor';\n\ntype RouteEntry = {\n\tpath: string;\n\tsection?: string;\n\ttitle?: string;\n\tdescription?: string;\n};\n\nfunction oneLine(value: string): string {\n\treturn value.replace(/\\s+/g, ' ').trim();\n}\n\nfunction stringField(\n\trecord: Record<string, unknown> | undefined,\n\tkey: string,\n): string | undefined {\n\tconst value = record?.[key];\n\tif (typeof value !== 'string') return undefined;\n\tconst trimmed = oneLine(value);\n\treturn trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction metaFromUnknown(meta: unknown): {\n\ttitle?: string;\n\tdescription?: string;\n} {\n\tif (!meta || typeof meta !== 'object') return {};\n\tconst record = meta as Record<string, unknown>;\n\treturn {\n\t\ttitle: stringField(record, 'title') ?? stringField(record, 'name'),\n\t\tdescription:\n\t\t\tstringField(record, 'description') ?? stringField(record, 'excerpt'),\n\t};\n}\n\n/**\n * Discover static routes from routes.ts\n */\nexport async function discoverStaticRoutes(\n\tcwd: string,\n\tisWatchMode: boolean,\n): Promise<RouteEntry[]> {\n\tconst routes: RouteEntry[] = [];\n\n\ttry {\n\t\t// Check for bundled routes first (compiled binary support)\n\t\tlet routesList = (globalThis as Record<string, unknown>)\n\t\t\t.__REROUTE_ROUTES__ as Doc[] | undefined;\n\n\t\t// Fallback to dynamic import (dev mode)\n\t\tif (!routesList) {\n\t\t\tconst routesPath = join(cwd, '.reroute', 'routes.ts');\n\t\t\tconst url = pathToFileURL(routesPath).href;\n\t\t\tconst mod = await import(\n\t\t\t\t`${url}${isWatchMode ? `?t=${Date.now()}` : ''}`\n\t\t\t);\n\t\t\troutesList = (mod as Doc)?.routes as Doc[] | undefined;\n\t\t}\n\t\tif (Array.isArray(routesList)) {\n\t\t\tfor (const route of routesList) {\n\t\t\t\tconst pattern = String(route?.pattern || '');\n\n\t\t\t\t// Skip error pages, layouts, and dynamic routes\n\t\t\t\tif (\n\t\t\t\t\tpattern.includes('[404]') ||\n\t\t\t\t\tpattern.includes('[layout]') ||\n\t\t\t\t\tpattern.includes('[skeleton]') ||\n\t\t\t\t\tpattern.includes(':')\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst { title, description } = metaFromUnknown(route?.meta);\n\t\t\t\troutes.push({\n\t\t\t\t\tpath: pattern,\n\t\t\t\t\ttitle,\n\t\t\t\t\tdescription,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\tconsole.error('[reroute/llms] Failed to discover routes for index:', error);\n\t}\n\n\treturn routes;\n}\n\nasync function loadCollectionModule(\n\tcwd: string,\n\tcollection: string,\n\tisWatchMode: boolean,\n): Promise<Doc[] | null> {\n\tconst collectionFile = join(\n\t\tcwd,\n\t\t'.reroute',\n\t\t'collections',\n\t\t`${collection}.js`,\n\t);\n\tconst exists = await Bun.file(collectionFile).exists();\n\n\tif (!exists) return null;\n\n\tconst url = `${pathToFileURL(collectionFile).href}${isWatchMode ? `?t=${Date.now()}` : ''}`;\n\tconst mod = await import(url);\n\treturn mod.items || [];\n}\n\nfunction createRouteEntriesFromItems(\n\titems: Doc[],\n\tcollection: string,\n): RouteEntry[] {\n\tconst routes: RouteEntry[] = [];\n\n\tfor (const item of items) {\n\t\tconst href = String(item.href || '');\n\t\tif (href) {\n\t\t\tconst fromMeta = metaFromUnknown(item.meta);\n\t\t\tconst title =\n\t\t\t\tfromMeta.title ||\n\t\t\t\t(typeof item.name === 'string' ? item.name : undefined);\n\t\t\troutes.push({\n\t\t\t\tpath: href,\n\t\t\t\tsection: collection,\n\t\t\t\ttitle,\n\t\t\t\tdescription: fromMeta.description,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn routes;\n}\n\nfunction sortRoutesByDate(routes: RouteEntry[], items: Doc[]): void {\n\troutes.sort((a, b) => {\n\t\tconst aItem = items.find((i: Doc) => i.href === a.path);\n\t\tconst bItem = items.find((i: Doc) => i.href === b.path);\n\n\t\tconst aDate = aItem?.meta?.date;\n\t\tconst bDate = bItem?.meta?.date;\n\n\t\tif (aDate && bDate) {\n\t\t\treturn new Date(bDate).getTime() - new Date(aDate).getTime();\n\t\t}\n\n\t\treturn 0;\n\t});\n}\n\n/**\n * Discover collection content\n */\nexport async function discoverCollectionRoutes(\n\tcwd: string,\n\tcollections: string[],\n\tisWatchMode: boolean,\n\tllmsConfig?: LLMsOptions,\n): Promise<Map<string, RouteEntry[]>> {\n\tconst collectionsMap = new Map<string, RouteEntry[]>();\n\n\tfor (const collection of collections) {\n\t\tif (llmsConfig?.excludeCollections?.includes(collection)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst items = await loadCollectionModule(cwd, collection, isWatchMode);\n\t\t\tif (!items) continue;\n\n\t\t\tconst routes = createRouteEntriesFromItems(items, collection);\n\n\t\t\tif (routes.length > 0) {\n\t\t\t\tsortRoutesByDate(routes, items);\n\t\t\t\tcollectionsMap.set(collection, routes);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t`[reroute/llms] Failed to discover collection ${collection}:`,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn collectionsMap;\n}\n\nasync function loadRouteModule(\n\tcwd: string,\n\troutePath: string,\n\tisWatchMode: boolean,\n): Promise<Doc | null> {\n\ttry {\n\t\tconst abs = join(\n\t\t\tcwd,\n\t\t\tresolveAppPaths(await loadConfig(cwd)).routesDir,\n\t\t\troutePath,\n\t\t);\n\t\tconst modUrl = `${pathToFileURL(abs).href}${isWatchMode ? `?t=${Date.now()}` : ''}`;\n\t\treturn (await import(modUrl)) as Doc;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nasync function callDataFunction(\n\tdataFn: (ctx: Doc) => unknown | Promise<unknown>,\n\troutePath: string,\n): Promise<unknown> {\n\tconst mockSet = { status: 200 };\n\treturn await dataFn({\n\t\tpathname: routePath,\n\t\tparams: {},\n\t\tset: mockSet,\n\t});\n}\n\nfunction extractIdentifier(item: Record<string, unknown>): string | undefined {\n\treturn (item.version || item.slug || item.id || item.name || item.key) as\n\t\t| string\n\t\t| undefined;\n}\n\nfunction createEntriesFromArray(array: Doc[], routePath: string): RouteEntry[] {\n\tconst entries: RouteEntry[] = [];\n\n\tfor (const item of array) {\n\t\tif (typeof item !== 'object' || item === null) continue;\n\n\t\tconst identifier = extractIdentifier(item);\n\t\tif (identifier) {\n\t\t\tconst fromItem = metaFromUnknown(item);\n\t\t\tconst fromMeta = metaFromUnknown(item.meta);\n\t\t\tentries.push({\n\t\t\t\tpath: `${routePath}/${identifier}`,\n\t\t\t\tsection: routePath.split('/').filter(Boolean)[0],\n\t\t\t\ttitle: fromItem.title ?? fromMeta.title,\n\t\t\t\tdescription: fromItem.description ?? fromMeta.description,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn entries;\n}\n\n/**\n * Discover SSR data routes (routes with ssr.data that return arrays)\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: discovering ssr data routes is complex\nexport async function discoverSSRDataRoutes(\n\tcwd: string,\n\tstaticRoutes: RouteEntry[],\n\tisWatchMode: boolean,\n): Promise<Map<string, RouteEntry[]>> {\n\tconst ssrDataRoutes = new Map<string, RouteEntry[]>();\n\n\t// Check for bundled routes first (compiled binary support)\n\tconst bundledRoutes = (globalThis as Record<string, unknown>)\n\t\t.__REROUTE_ROUTES__ as Doc[] | undefined;\n\n\tif (bundledRoutes && Array.isArray(bundledRoutes)) {\n\t\t// In production: use pre-loaded SSR data from bundled routes\n\t\t// Match static routes against bundled routes to get their ssr.data\n\t\tfor (const route of staticRoutes) {\n\t\t\t// Find the matching bundled route by path\n\t\t\tconst bundledRoute = bundledRoutes.find((r) => r?.path === route.path);\n\n\t\t\tif (!bundledRoute) continue;\n\n\t\t\tconst ssr = (bundledRoute as Doc)?.ssr as Doc | undefined;\n\t\t\tconst dataFn = (ssr as Doc)?.data as\n\t\t\t\t| ((ctx: Doc) => unknown | Promise<unknown>)\n\t\t\t\t| undefined;\n\n\t\t\tif (typeof dataFn !== 'function') continue;\n\n\t\t\tconst result = await callDataFunction(dataFn, route.path);\n\t\t\tconst arrays = findArraysInData(result);\n\n\t\t\tfor (const array of arrays) {\n\t\t\t\tif (array.length === 0) continue;\n\n\t\t\t\tconst entries = createEntriesFromArray(array, route.path);\n\t\t\t\tif (entries.length > 0) {\n\t\t\t\t\tssrDataRoutes.set(route.path, entries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Dev mode: dynamically import route modules\n\t\tfor (const route of staticRoutes) {\n\t\t\tconst mod = await loadRouteModule(cwd, route.path, isWatchMode);\n\t\t\tif (!mod) continue;\n\n\t\t\tconst ssr = (mod as Doc)?.ssr as SSR | undefined;\n\t\t\tconst dataFn = ssr?.data;\n\n\t\t\tif (typeof dataFn !== 'function') continue;\n\n\t\t\tconst result = await callDataFunction(dataFn, route.path);\n\t\t\tconst arrays = findArraysInData(result);\n\n\t\t\tfor (const array of arrays) {\n\t\t\t\tif (array.length === 0) continue;\n\n\t\t\t\tconst entries = createEntriesFromArray(array, route.path);\n\t\t\t\tif (entries.length > 0) {\n\t\t\t\t\tssrDataRoutes.set(route.path, entries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ssrDataRoutes;\n}\n\n/**\n * Find arrays in data object (recursive)\n */\nfunction findArraysInData(obj: unknown): Doc[] {\n\tconst arrays: Doc[] = [];\n\n\tif (!obj || typeof obj !== 'object') {\n\t\treturn arrays;\n\t}\n\n\tfor (const value of Object.values(obj)) {\n\t\tif (Array.isArray(value)) {\n\t\t\tarrays.push(value);\n\t\t} else if (value && typeof value === 'object') {\n\t\t\tarrays.push(...findArraysInData(value));\n\t\t}\n\t}\n\n\treturn arrays;\n}\n\nfunction addIndexHeader(parts: string[], llmsConfig?: LLMsOptions): void {\n\tif (llmsConfig?.siteName) {\n\t\tparts.push(`# ${llmsConfig.siteName}`);\n\t\tparts.push('');\n\t}\n\n\tif (llmsConfig?.siteDescription) {\n\t\tparts.push(llmsConfig.siteDescription);\n\t\tparts.push('');\n\t}\n}\n\nfunction groupStaticRoutes(\n\tstaticRoutes: RouteEntry[],\n\tllmsConfig?: LLMsOptions,\n): { sections: Map<string, RouteEntry[]>; rootRoutes: RouteEntry[] } {\n\tconst sections = new Map<string, RouteEntry[]>();\n\tconst rootRoutes: RouteEntry[] = [];\n\n\tfor (const route of staticRoutes) {\n\t\tif (shouldExcludeRoute(route.path, llmsConfig?.excludeRoutes)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (route.path === '/') {\n\t\t\trootRoutes.push(route);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst firstSegment = route.path.split('/').filter(Boolean)[0];\n\t\tif (!firstSegment) {\n\t\t\trootRoutes.push(route);\n\t\t} else {\n\t\t\tif (!sections.has(firstSegment)) {\n\t\t\t\tsections.set(firstSegment, []);\n\t\t\t}\n\t\t\tsections.get(firstSegment)?.push(route);\n\t\t}\n\t}\n\n\treturn { sections, rootRoutes };\n}\n\nfunction formatRouteUrl(routePath: string, baseUrl: string): string {\n\tconst urlPath = routePath === '/' ? '/index' : routePath;\n\treturn `${baseUrl}${urlPath}.txt`;\n}\n\nfunction getRouteTitle(route: RouteEntry): string {\n\treturn (\n\t\troute.title ||\n\t\t(route.path === '/' ? 'Home' : route.path.split('/').pop() || 'Page')\n\t);\n}\n\nfunction formatIndexLine(\n\troute: RouteEntry,\n\turl: string,\n\tsummaries: boolean,\n): string {\n\tconst link = `- [${getRouteTitle(route)}](${url})`;\n\tif (!(summaries && route.description)) return link;\n\treturn `${link}: ${route.description}`;\n}\n\nfunction summariesEnabled(llmsConfig?: LLMsOptions): boolean {\n\treturn llmsConfig?.indexSummaries !== false;\n}\n\nfunction addRootRoutes(\n\tparts: string[],\n\trootRoutes: RouteEntry[],\n\tbaseUrl: string,\n\tisScoped: boolean,\n\tllmsConfig?: LLMsOptions,\n): void {\n\tif (isScoped || rootRoutes.length === 0) return;\n\n\tconst summaries = summariesEnabled(llmsConfig);\n\tfor (const route of rootRoutes) {\n\t\tparts.push(\n\t\t\tformatIndexLine(route, formatRouteUrl(route.path, baseUrl), summaries),\n\t\t);\n\t}\n\tparts.push('');\n}\n\nfunction addCollectionSection(\n\tparts: string[],\n\tcollection: string,\n\troutes: RouteEntry[],\n\tbaseUrl: string,\n\tllmsConfig?: LLMsOptions,\n): void {\n\tconst filteredRoutes = routes.filter(\n\t\t(r) => !shouldExcludeRoute(r.path, llmsConfig?.excludeRoutes),\n\t);\n\n\tif (filteredRoutes.length === 0) return;\n\n\tconst sectionName = collection.charAt(0).toUpperCase() + collection.slice(1);\n\tparts.push(`## ${sectionName}`);\n\tparts.push('');\n\n\tconst summaries = summariesEnabled(llmsConfig);\n\tfor (const route of filteredRoutes) {\n\t\tparts.push(\n\t\t\tformatIndexLine(route, `${baseUrl}${route.path}.txt`, summaries),\n\t\t);\n\t}\n\tparts.push('');\n}\n\nfunction shouldIncludeSection(\n\tsection: string,\n\tisScoped: boolean,\n\ttargetCollection: string | null,\n): boolean {\n\treturn !isScoped || section === targetCollection;\n}\n\nfunction addSSRDataSection(\n\tparts: string[],\n\troutePattern: string,\n\troutes: RouteEntry[],\n\tbaseUrl: string,\n\tisScoped: boolean,\n\ttargetCollection: string | null,\n\tllmsConfig?: LLMsOptions,\n): void {\n\tconst routeSection = routePattern.split('/').filter(Boolean)[0];\n\n\tif (!shouldIncludeSection(routeSection, isScoped, targetCollection)) {\n\t\treturn;\n\t}\n\n\tconst filteredRoutes = routes.filter(\n\t\t(r) => !shouldExcludeRoute(r.path, llmsConfig?.excludeRoutes),\n\t);\n\n\tif (filteredRoutes.length === 0) return;\n\n\tconst sectionName =\n\t\trouteSection?.charAt(0).toUpperCase() + routeSection?.slice(1) || 'Content';\n\tparts.push(`## ${sectionName}`);\n\tparts.push('');\n\n\tconst summaries = summariesEnabled(llmsConfig);\n\tfor (const route of filteredRoutes) {\n\t\tparts.push(\n\t\t\tformatIndexLine(route, `${baseUrl}${route.path}.txt`, summaries),\n\t\t);\n\t}\n\tparts.push('');\n}\n\nfunction addStaticSection(\n\tparts: string[],\n\tsection: string,\n\troutes: RouteEntry[],\n\tbaseUrl: string,\n\tisScoped: boolean,\n\ttargetCollection: string | null,\n\tcollectionsMap: Map<string, RouteEntry[]>,\n\tllmsConfig?: LLMsOptions,\n): void {\n\tif (!shouldIncludeSection(section, isScoped, targetCollection)) {\n\t\treturn;\n\t}\n\n\tconst filteredRoutes = routes.filter(\n\t\t(r) => !shouldExcludeRoute(r.path, llmsConfig?.excludeRoutes),\n\t);\n\n\tif (filteredRoutes.length === 0 || collectionsMap.has(section)) return;\n\n\tconst sectionName = section.charAt(0).toUpperCase() + section.slice(1);\n\tparts.push(`## ${sectionName}`);\n\tparts.push('');\n\n\tconst summaries = summariesEnabled(llmsConfig);\n\tfor (const route of filteredRoutes) {\n\t\tparts.push(\n\t\t\tformatIndexLine(route, `${baseUrl}${route.path}.txt`, summaries),\n\t\t);\n\t}\n\tparts.push('');\n}\n\n/**\n * Generate /llms.txt index\n * Token-efficient index: title, description, and link per page, grouped by section.\n */\nexport async function generateLLMsIndex(options: {\n\tcwd: string;\n\tcollections: string[];\n\tbaseUrl: string;\n\tisWatchMode: boolean;\n\tllmsConfig?: LLMsOptions;\n\t/** Collection-scoped index (`/help/llms.txt`). Omit for site-wide `/llms.txt`. */\n\ttargetCollection?: string;\n}): Promise<string> {\n\tconst { cwd, collections, baseUrl, isWatchMode, llmsConfig } = options;\n\tconst targetCollection = options.targetCollection ?? null;\n\tconst isScoped = targetCollection !== null;\n\n\tconst parts: string[] = [];\n\taddIndexHeader(parts, llmsConfig);\n\n\tconst staticRoutes = await discoverStaticRoutes(cwd, isWatchMode);\n\tconst collectionsMap = await discoverCollectionRoutes(\n\t\tcwd,\n\t\tcollections,\n\t\tisWatchMode,\n\t\tllmsConfig,\n\t);\n\tconst ssrDataRoutes = await discoverSSRDataRoutes(\n\t\tcwd,\n\t\tstaticRoutes,\n\t\tisWatchMode,\n\t);\n\n\tconst { sections, rootRoutes } = groupStaticRoutes(staticRoutes, llmsConfig);\n\n\taddRootRoutes(parts, rootRoutes, baseUrl, isScoped, llmsConfig);\n\n\tfor (const [collection, routes] of collectionsMap.entries()) {\n\t\tif (routes.length > 0) {\n\t\t\taddCollectionSection(parts, collection, routes, baseUrl, llmsConfig);\n\t\t}\n\t}\n\n\tfor (const [routePattern, routes] of ssrDataRoutes.entries()) {\n\t\tif (routes.length > 0) {\n\t\t\taddSSRDataSection(\n\t\t\t\tparts,\n\t\t\t\troutePattern,\n\t\t\t\troutes,\n\t\t\t\tbaseUrl,\n\t\t\t\tisScoped,\n\t\t\t\ttargetCollection,\n\t\t\t\tllmsConfig,\n\t\t\t);\n\t\t}\n\t}\n\n\tfor (const [section, routes] of sections.entries()) {\n\t\tif (routes.length > 0) {\n\t\t\taddStaticSection(\n\t\t\t\tparts,\n\t\t\t\tsection,\n\t\t\t\troutes,\n\t\t\t\tbaseUrl,\n\t\t\t\tisScoped,\n\t\t\t\ttargetCollection,\n\t\t\t\tcollectionsMap,\n\t\t\t\tllmsConfig,\n\t\t\t);\n\t\t}\n\t}\n\n\tparts.push('---');\n\tparts.push('');\n\tparts.push(\n\t\t'Formats: .txt | .md | Accept: text/plain | Accept: text/markdown',\n\t);\n\n\treturn parts.join('\\n');\n}\n",
|
|
41
|
+
"/**\n * LLMs Feature - Native LLM-friendly content formats\n *\n * Provides:\n * - .txt and .md extensions for any route\n * - Accept header content negotiation (text/plain, text/markdown)\n * - /llms.txt index of all content\n * - /llms-full.txt bundle of all content\n * - HTML discovery (Link / rel=alternate, Content-Signal)\n *\n * All formats are token-efficient: minimal metadata, maximum useful content\n */\n\nexport type { LlmsDiscovery } from './discovery';\nexport {\n\tappendLlmsAlternateHead,\n\tapplyLlmsDiscoveryHeaders,\n\tbuildLlmsDiscovery,\n\tformatContentSignal,\n\tllmsAlternateLinkTag,\n\tmarkdownTwinPath,\n\tmergeVary,\n\tshouldAdvertiseMarkdownTwin,\n} from './discovery';\nexport {\n\textractContentForLLMs,\n\tshouldExcludeRoute,\n\tstripLayoutChrome,\n} from './extractor';\nexport {\n\textractTitle,\n\tformatAsMarkdown,\n\tformatAsText,\n\tgenerateLLMsFooter,\n\thtmlToMarkdown,\n\tstripHtmlTags,\n} from './formatter';\nexport { generateLLMsFullContent } from './full-generator';\nexport { generateLLMsIndex } from './index-generator';\n",
|
|
42
|
+
"import type { ImageResponseOptions } from '@vercel/og';\nimport { createElement } from 'react';\nimport type { OGImageFont } from '../config';\nimport type { OGImageProps } from './types';\n\n// biome-ignore lint/suspicious/noExplicitAny: what we can do here?\ntype Any = any;\n/**\n * Render an OG image component to PNG using Satori and resvg\n */\n\nexport async function renderOGImageToPNG(\n\tComponent: Any,\n\tprops: OGImageProps,\n\toptions: {\n\t\twidth?: number;\n\t\theight?: number;\n\t\tfonts?: OGImageFont[];\n\t} = {},\n): Promise<Buffer> {\n\tconst width = options.width || 1200;\n\tconst height = options.height || 630;\n\n\ttry {\n\t\t// Dynamically import @vercel/og to avoid bundling it if not used\n\t\tconst { ImageResponse } = await import('@vercel/og');\n\n\t\t// Create React element from component\n\t\tconst element = createElement(Component, props);\n\n\t\t// Use ImageResponse to render the component\n\t\tconst imageResponse = new ImageResponse(element, {\n\t\t\twidth,\n\t\t\theight,\n\t\t\tfonts:\n\t\t\t\toptions.fonts?.length && options.fonts.length > 0\n\t\t\t\t\t? (options.fonts as ImageResponseOptions['fonts'])\n\t\t\t\t\t: undefined,\n\t\t});\n\n\t\t// Convert Response to Buffer\n\t\tconst arrayBuffer = await imageResponse.arrayBuffer();\n\t\treturn Buffer.from(arrayBuffer);\n\t} catch (error) {\n\t\tconsole.error('[reroute] Failed to render OG image:', error);\n\t\tthrow new Error(\n\t\t\t`Failed to render OG image: ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t}\n}\n\n/**\n * Default OG image template\n * Renders a simple card with title and description\n */\nexport function DefaultOGTemplate({\n\tmeta,\n\tavatar,\n\tsiteName: siteNameProp,\n}: OGImageProps) {\n\tconst title = meta?.title || 'Untitled';\n\tconst description = meta?.description || '';\n\tconst siteName = siteNameProp || meta?.site_name || meta?.siteName || '';\n\n\treturn createElement(\n\t\t'div',\n\t\t{\n\t\t\tstyle: {\n\t\t\t\tdisplay: 'flex',\n\t\t\t\tflexDirection: 'column',\n\t\t\t\twidth: '100%',\n\t\t\t\theight: '100%',\n\t\t\t\tbackgroundColor: '#ffffff',\n\t\t\t\tpadding: '80px',\n\t\t\t\tfontFamily: 'system-ui, sans-serif',\n\t\t\t},\n\t\t},\n\t\tcreateElement(\n\t\t\t'div',\n\t\t\t{\n\t\t\t\tstyle: {\n\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\tflexDirection: 'column',\n\t\t\t\t\twidth: '100%',\n\t\t\t\t\theight: '100%',\n\t\t\t\t\tborder: '4px solid #000000',\n\t\t\t\t\tborderRadius: '24px',\n\t\t\t\t\tpadding: '64px',\n\t\t\t\t\tjustifyContent: 'space-between',\n\t\t\t\t},\n\t\t\t},\n\t\t\tcreateElement(\n\t\t\t\t'div',\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\tflexDirection: 'column',\n\t\t\t\t\t\tgap: '24px',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tcreateElement(\n\t\t\t\t\t'h1',\n\t\t\t\t\t{\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tfontSize: '72px',\n\t\t\t\t\t\t\tfontWeight: 'bold',\n\t\t\t\t\t\t\tlineHeight: 1.2,\n\t\t\t\t\t\t\tmargin: 0,\n\t\t\t\t\t\t\tcolor: '#000000',\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\ttitle,\n\t\t\t\t),\n\t\t\t\tdescription &&\n\t\t\t\t\tcreateElement(\n\t\t\t\t\t\t'p',\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\tfontSize: '32px',\n\t\t\t\t\t\t\t\tlineHeight: 1.5,\n\t\t\t\t\t\t\t\tmargin: 0,\n\t\t\t\t\t\t\t\tcolor: '#666666',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t),\n\t\t\t),\n\t\t\tcreateElement(\n\t\t\t\t'div',\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\talignItems: 'center',\n\t\t\t\t\t\tjustifyContent: 'space-between',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsiteName &&\n\t\t\t\t\tcreateElement(\n\t\t\t\t\t\t'div',\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\t\t\tfontSize: '28px',\n\t\t\t\t\t\t\t\tcolor: '#999999',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tsiteName,\n\t\t\t\t\t),\n\t\t\t\tavatar &&\n\t\t\t\t\tcreateElement('img', {\n\t\t\t\t\t\tsrc: avatar,\n\t\t\t\t\t\twidth: 80,\n\t\t\t\t\t\theight: 80,\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tborderRadius: '50%',\n\t\t\t\t\t\t\tobjectFit: 'cover',\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t!avatar &&\n\t\t\t\t\tcreateElement('div', {\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\twidth: '80px',\n\t\t\t\t\t\t\theight: '80px',\n\t\t\t\t\t\t\tborderRadius: '50%',\n\t\t\t\t\t\t\tbackgroundColor: '#6366f1',\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t),\n\t\t),\n\t);\n}\n",
|
|
43
|
+
"export {\n\textractParamsFromPath,\n\tfindOGImageForPath,\n\tloadOGImageComponent,\n} from './discovery';\nexport { generateOGImageMetaTags } from './meta';\nexport { DefaultOGTemplate, renderOGImageToPNG } from './render';\nexport type { OGImageProps } from './types';\n",
|
|
44
|
+
"import type { RobotsOptions, RobotsPolicy } from '../config';\nimport { formatContentSignal } from '../llms/discovery';\nimport type { RobotsDiscoveryResult } from './discovery';\n\nfunction getEffectivePolicies(policies: RobotsPolicy[]): RobotsPolicy[] {\n\tif (policies.length > 0) return policies;\n\n\treturn [\n\t\t{\n\t\t\tuserAgent: '*',\n\t\t\tallow: ['/'],\n\t\t},\n\t];\n}\n\n/** `Disallow: /` hides the whole site. Anchor the apex so `/bud` stays crawlable. */\nfunction robotsDisallowPath(path: string): string {\n\treturn path === '/' ? '/$' : path;\n}\n\nfunction getAutoDisallows(discovery: RobotsDiscoveryResult): Set<string> {\n\treturn new Set<string>([\n\t\t...discovery.sitemapExcludedRoutes.map(robotsDisallowPath),\n\t\t...discovery.redirectSourcePaths.map(robotsDisallowPath),\n\t]);\n}\n\nfunction addAllowDirectives(lines: string[], policy: RobotsPolicy): void {\n\tif (!policy.allow || policy.allow.length === 0) return;\n\n\tfor (const path of policy.allow) {\n\t\tlines.push(`Allow: ${path}`);\n\t}\n}\n\nfunction getDisallows(\n\tpolicy: RobotsPolicy,\n\tautoDisallows: Set<string>,\n): Set<string> {\n\tconst disallows = new Set(policy.disallow || []);\n\n\tif (policy.userAgent === '*') {\n\t\tfor (const path of autoDisallows) {\n\t\t\tdisallows.add(path);\n\t\t}\n\t}\n\n\treturn disallows;\n}\n\nfunction addDisallowDirectives(\n\tlines: string[],\n\tpolicy: RobotsPolicy,\n\tautoDisallows: Set<string>,\n): void {\n\tconst disallows = getDisallows(policy, autoDisallows);\n\n\tif (disallows.size > 0) {\n\t\tfor (const path of Array.from(disallows).sort()) {\n\t\t\tlines.push(`Disallow: ${path}`);\n\t\t}\n\t}\n}\n\nfunction addContentSignalLine(\n\tlines: string[],\n\tpolicy: RobotsPolicy,\n\trobotsConfig: RobotsOptions,\n): void {\n\tif (policy.userAgent !== '*') return;\n\n\tconst value = formatContentSignal(robotsConfig.contentSignal);\n\tif (value) lines.push(`Content-Signal: ${value}`);\n}\n\nfunction addPolicyBlock(\n\tlines: string[],\n\tpolicy: RobotsPolicy,\n\tautoDisallows: Set<string>,\n\trobotsConfig: RobotsOptions,\n): void {\n\tlines.push(`User-agent: ${policy.userAgent}`);\n\taddContentSignalLine(lines, policy, robotsConfig);\n\n\taddAllowDirectives(lines, policy);\n\taddDisallowDirectives(lines, policy, autoDisallows);\n\n\tif (policy.crawlDelay !== undefined) {\n\t\tlines.push(`Crawl-delay: ${policy.crawlDelay}`);\n\t}\n\n\tlines.push('');\n}\n\nfunction addGlobalCrawlDelay(\n\tlines: string[],\n\tcrawlDelay: number | undefined,\n\teffectivePolicies: RobotsPolicy[],\n): void {\n\tconst hasWildcardCrawlDelay = effectivePolicies.some(\n\t\t(p) => p.userAgent === '*' && p.crawlDelay !== undefined,\n\t);\n\n\tif (crawlDelay !== undefined && !hasWildcardCrawlDelay) {\n\t\tlines.push(`Crawl-delay: ${crawlDelay}`);\n\t\tlines.push('');\n\t}\n}\n\nfunction addSitemapReference(\n\tlines: string[],\n\trobotsConfig: RobotsOptions,\n\tsitemapEnabled: boolean,\n): void {\n\tif (\n\t\trobotsConfig.referenceSitemap !== false &&\n\t\tsitemapEnabled &&\n\t\trobotsConfig.baseUrl\n\t) {\n\t\tlines.push(`Sitemap: ${robotsConfig.baseUrl}/sitemap.xml`);\n\t}\n}\n\n/**\n * Generate robots.txt content from discovery result and configuration\n */\nexport function generateRobots(\n\tdiscovery: RobotsDiscoveryResult,\n\trobotsConfig: RobotsOptions,\n\tsitemapEnabled: boolean,\n): string {\n\tconst lines: string[] = [];\n\tconst { policies = [], customRules, crawlDelay } = robotsConfig;\n\n\tconst effectivePolicies = getEffectivePolicies(policies);\n\tconst autoDisallows = getAutoDisallows(discovery);\n\n\tfor (const policy of effectivePolicies) {\n\t\taddPolicyBlock(lines, policy, autoDisallows, robotsConfig);\n\t}\n\n\taddGlobalCrawlDelay(lines, crawlDelay, effectivePolicies);\n\n\tif (customRules) {\n\t\tlines.push(customRules.trim());\n\t\tlines.push('');\n\t}\n\n\taddSitemapReference(lines, robotsConfig, sitemapEnabled);\n\n\treturn `${lines.join('\\n').trim()}\\n`;\n}\n",
|
|
45
|
+
"export * from './discovery';\nexport * from './generator';\nexport * from './policies';\n",
|
|
46
|
+
"import { pathToFileURL } from 'node:url';\nimport { resolveAppPaths } from '../app-paths';\nimport { loadConfig, type RSSOptions } from '../config';\nimport { join } from '../ssr/lib/path';\nimport type { Doc, SSR } from '../ssr/lib/types';\n\ntype RSSItem = {\n\ttitle: string;\n\tlink: string;\n\tdescription?: string;\n\tpubDate?: string;\n\tauthor?: string;\n\tguid?: string;\n\tcontent?: string;\n\tlocale?: string;\n};\n\ntype RSSFeed = {\n\ttitle: string;\n\tlink: string;\n\tdescription: string;\n\titems: RSSItem[];\n\tlanguage?: string;\n};\n\ntype RSSDiscoveryResult = {\n\tcollections: Map<string, RSSItem[]>;\n\tssrDataFeeds: Map<string, RSSItem[]>;\n};\n\n/**\n * Discover RSS feed items from collections\n */\nfunction extractDescription(\n\tmeta: Record<string, unknown>,\n\trssConfig?: RSSOptions,\n): string {\n\tif (rssConfig?.extractDescription) {\n\t\ttry {\n\t\t\treturn rssConfig.extractDescription(meta) || '';\n\t\t} catch (error) {\n\t\t\tconsole.warn('[reroute/rss] extractDescription failed:', error);\n\t\t}\n\t}\n\treturn (meta.description || meta.excerpt || '') as string;\n}\n\nfunction extractPubDate(\n\tmeta: Record<string, unknown>,\n\trssConfig?: RSSOptions,\n): string | undefined {\n\tif (rssConfig?.extractPubDate) {\n\t\ttry {\n\t\t\tconst pubDate = rssConfig.extractPubDate(meta);\n\t\t\tif (pubDate) {\n\t\t\t\treturn new Date(pubDate).toUTCString();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.warn('[reroute/rss] extractPubDate failed:', error);\n\t\t}\n\t}\n\n\tif (meta.date) {\n\t\treturn new Date(meta.date as string).toUTCString();\n\t}\n\tif (meta.publishedAt) {\n\t\treturn new Date(meta.publishedAt as string).toUTCString();\n\t}\n\treturn undefined;\n}\n\nfunction extractAuthor(\n\tmeta: Record<string, unknown>,\n\trssConfig?: RSSOptions,\n): string | undefined {\n\tif (rssConfig?.extractAuthor) {\n\t\ttry {\n\t\t\tconst author = rssConfig.extractAuthor(meta);\n\t\t\tif (author) {\n\t\t\t\treturn author;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.warn('[reroute/rss] extractAuthor failed:', error);\n\t\t}\n\t}\n\n\tif (meta.author) {\n\t\treturn String(meta.author);\n\t}\n\treturn undefined;\n}\n\nfunction extractContent(\n\tmeta: Record<string, unknown>,\n\trssConfig?: RSSOptions,\n): string | undefined {\n\tif (rssConfig?.extractContent) {\n\t\ttry {\n\t\t\tconst content = rssConfig.extractContent(meta);\n\t\t\tif (content) {\n\t\t\t\treturn content;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.warn('[reroute/rss] extractContent failed:', error);\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction createRSSItemFromMetaAndHref(\n\thref: string,\n\tmeta: Record<string, unknown>,\n\tbaseUrl: string,\n\titem: Record<string, unknown>,\n\trssConfig?: RSSOptions,\n): RSSItem {\n\tconst rssItem: RSSItem = {\n\t\ttitle: (meta.title || item.name || 'Untitled') as string,\n\t\tlink: `${baseUrl}${href}`,\n\t\tguid: `${baseUrl}${href}`,\n\t\tdescription: extractDescription(meta, rssConfig),\n\t};\n\n\tconst pubDate = extractPubDate(meta, rssConfig);\n\tif (pubDate) {\n\t\trssItem.pubDate = pubDate;\n\t}\n\n\tconst author = extractAuthor(meta, rssConfig);\n\tif (author) {\n\t\trssItem.author = author;\n\t}\n\n\tconst content = extractContent(meta, rssConfig);\n\tif (content) {\n\t\trssItem.content = content;\n\t}\n\n\treturn rssItem;\n}\n\nasync function discoverCollectionFeedItems(\n\tcwd: string,\n\tcollection: string,\n\tisWatchMode: boolean,\n\tbaseUrl: string,\n\trssConfig?: RSSOptions,\n): Promise<RSSItem[]> {\n\tconst items: RSSItem[] = [];\n\n\ttry {\n\t\t// Load pre-generated collection module\n\t\tconst collectionFile = join(\n\t\t\tcwd,\n\t\t\t'.reroute',\n\t\t\t'collections',\n\t\t\t`${collection}.js`,\n\t\t);\n\t\tconst exists = await Bun.file(collectionFile).exists();\n\n\t\tif (!exists) {\n\t\t\treturn items;\n\t\t}\n\n\t\tconst url = `${Bun.pathToFileURL(collectionFile).href}${isWatchMode ? `?t=${Date.now()}` : ''}`;\n\t\tconst mod = await import(url);\n\t\tconst contentItems = mod.items || [];\n\t\tconst defaultLocale = (mod.defaultLocale as string | undefined) || 'en-US';\n\t\tconst { localizeContentHref } = await import('../i18n/pathname');\n\n\t\tfor (const item of contentItems) {\n\t\t\tconst name = String(item.name || '');\n\t\t\tconst locale = (item.locale as string) || defaultLocale;\n\t\t\tconst href = name\n\t\t\t\t? localizeContentHref(collection, name, locale, defaultLocale)\n\t\t\t\t: String(item.href || '');\n\t\t\tconst meta = item.meta || {};\n\n\t\t\tif (href) {\n\t\t\t\tconst rssItem = createRSSItemFromMetaAndHref(\n\t\t\t\t\thref,\n\t\t\t\t\tmeta,\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\titem,\n\t\t\t\t\trssConfig,\n\t\t\t\t);\n\t\t\t\trssItem.locale = locale;\n\t\t\t\titems.push(rssItem);\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\tconsole.error(\n\t\t\t`[reroute/rss] Failed to discover collection ${collection}:`,\n\t\t\terror,\n\t\t);\n\t}\n\n\t// Sort by pubDate (newest first)\n\tsortByPubDate(items);\n\n\treturn items;\n}\n\n/**\n * Discover RSS feed items from SSR data routes\n */\nasync function discoverSSRDataFeedItems(\n\tcwd: string,\n\troutePattern: string,\n\troutePath: string,\n\tisWatchMode: boolean,\n\tbaseUrl: string,\n\trssConfig?: RSSOptions,\n): Promise<RSSItem[]> {\n\tconst items: RSSItem[] = [];\n\n\ttry {\n\t\tconst abs = join(\n\t\t\tcwd,\n\t\t\tresolveAppPaths(await loadConfig(cwd)).routesDir,\n\t\t\troutePath,\n\t\t);\n\t\tconst mod = (await import(\n\t\t\tpathToFileURL(abs).href + (isWatchMode ? `?t=${Date.now()}` : '')\n\t\t)) as Doc;\n\n\t\tconst ssr = (mod as Doc)?.ssr as SSR | undefined;\n\t\tconst dataFn = ssr?.data;\n\n\t\tif (typeof dataFn === 'function') {\n\t\t\t// Call the data function with mock context\n\t\t\tconst mockSet = { status: 200 };\n\t\t\tconst result = await dataFn({\n\t\t\t\tpathname: routePattern,\n\t\t\t\tparams: {},\n\t\t\t\tset: mockSet,\n\t\t\t});\n\n\t\t\t// Extract RSS urls from data\n\t\t\tconst extractedItems = extractRSSItemsFromData(\n\t\t\t\tresult,\n\t\t\t\troutePattern,\n\t\t\t\tbaseUrl,\n\t\t\t\trssConfig,\n\t\t\t);\n\t\t\titems.push(...extractedItems);\n\t\t}\n\t} catch {\n\t\t// Route doesn't have ssr.data or it failed - skip silently\n\t}\n\n\treturn items;\n}\n\n/**\n * Extract RSS urls from ssr.data result\n * Supports custom extractUrls function or automatic array discovery\n */\nfunction sortByPubDate(items: RSSItem[]): void {\n\titems.sort((a, b) => {\n\t\tif (!a.pubDate) return 1;\n\t\tif (!b.pubDate) return -1;\n\t\treturn new Date(b.pubDate).getTime() - new Date(a.pubDate).getTime();\n\t});\n}\n\nfunction processExtractedItems(\n\textractedItems: unknown[],\n\troutePattern: string,\n\tbaseUrl: string,\n\trssConfig?: RSSOptions,\n): RSSItem[] {\n\tconst items: RSSItem[] = [];\n\n\tfor (const item of extractedItems) {\n\t\tif (typeof item === 'object' && item !== null) {\n\t\t\tconst rssItem = createRSSItemFromData(\n\t\t\t\titem as Record<string, unknown>,\n\t\t\t\troutePattern,\n\t\t\t\tbaseUrl,\n\t\t\t\trssConfig,\n\t\t\t);\n\t\t\tif (rssItem) {\n\t\t\t\titems.push(rssItem);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn items;\n}\n\nfunction findArrays(obj: Doc): Doc[] {\n\tconst arrays: Doc[] = [];\n\n\tfor (const value of Object.values(obj)) {\n\t\tif (Array.isArray(value)) {\n\t\t\tarrays.push(value);\n\t\t} else if (value && typeof value === 'object') {\n\t\t\tarrays.push(...findArrays(value));\n\t\t}\n\t}\n\n\treturn arrays;\n}\n\nfunction extractRSSItemsFromData(\n\tdata: unknown,\n\troutePattern: string,\n\tbaseUrl: string,\n\trssConfig?: RSSOptions,\n): RSSItem[] {\n\tif (!data || typeof data !== 'object') {\n\t\treturn [];\n\t}\n\n\t// Use custom extractUrls if provided\n\tif (rssConfig?.extractUrls) {\n\t\ttry {\n\t\t\tconst extractedItems = rssConfig.extractUrls(data, routePattern);\n\t\t\tif (Array.isArray(extractedItems)) {\n\t\t\t\tconst items = processExtractedItems(\n\t\t\t\t\textractedItems,\n\t\t\t\t\troutePattern,\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\trssConfig,\n\t\t\t\t);\n\t\t\t\tsortByPubDate(items);\n\t\t\t\treturn items;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.warn('[reroute/rss] extractUrls failed:', error);\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Otherwise, recursively discover arrays in the data\n\tconst arrays = findArrays(data);\n\tconst items: RSSItem[] = [];\n\n\tfor (const array of arrays) {\n\t\tif (array.length > 0) {\n\t\t\titems.push(\n\t\t\t\t...processExtractedItems(array, routePattern, baseUrl, rssConfig),\n\t\t\t);\n\t\t}\n\t}\n\n\tsortByPubDate(items);\n\treturn items;\n}\n\n/**\n * Create RSS item from data object\n */\nfunction extractIdentifier(\n\titem: Record<string, unknown>,\n\troutePattern: string,\n\trssConfig?: RSSOptions,\n): string | null {\n\tif (rssConfig?.extractUrl) {\n\t\ttry {\n\t\t\tconst identifier = rssConfig.extractUrl(item, routePattern);\n\t\t\t// Return false explicitly skips this item (no fallback to default extraction)\n\t\t\tif (identifier === false) return null;\n\t\t\tif (identifier) return String(identifier);\n\t\t} catch (error) {\n\t\t\tconsole.warn('[reroute/rss] extractUrl failed:', error);\n\t\t}\n\t}\n\n\treturn String(\n\t\titem.version || item.slug || item.id || item.name || item.key || '',\n\t);\n}\n\nfunction extractPubDateFromItem(\n\titem: Record<string, unknown>,\n\trssConfig?: RSSOptions,\n): string | undefined {\n\tif (rssConfig?.extractPubDate) {\n\t\ttry {\n\t\t\tconst pubDate = rssConfig.extractPubDate(item);\n\t\t\tif (pubDate) {\n\t\t\t\treturn new Date(pubDate).toUTCString();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.warn('[reroute/rss] extractPubDate failed:', error);\n\t\t}\n\t}\n\n\tif (item.publishedAt && typeof item.publishedAt === 'string') {\n\t\treturn new Date(item.publishedAt).toUTCString();\n\t}\n\tif (item.date && typeof item.date === 'string') {\n\t\treturn new Date(item.date).toUTCString();\n\t}\n\tif (item.createdAt && typeof item.createdAt === 'string') {\n\t\treturn new Date(item.createdAt).toUTCString();\n\t}\n\treturn undefined;\n}\n\nfunction createRSSItemFromData(\n\titem: Record<string, unknown>,\n\troutePattern: string,\n\tbaseUrl: string,\n\trssConfig?: RSSOptions,\n): RSSItem | null {\n\tconst identifier = extractIdentifier(item, routePattern, rssConfig);\n\n\tif (!identifier) {\n\t\treturn null;\n\t}\n\n\tconst link = `${baseUrl}${routePattern}/${identifier}`;\n\n\tconst rssItem: RSSItem = {\n\t\ttitle: String(item.title || item.name || identifier),\n\t\tlink,\n\t\tguid: link,\n\t\tdescription: extractDescription(item, rssConfig),\n\t};\n\n\tconst pubDate = extractPubDateFromItem(item, rssConfig);\n\tif (pubDate) {\n\t\trssItem.pubDate = pubDate;\n\t}\n\n\tconst author = extractAuthor(item, rssConfig);\n\tif (author) {\n\t\trssItem.author = author;\n\t}\n\n\tconst content = extractContent(item, rssConfig);\n\tif (content) {\n\t\trssItem.content = content;\n\t}\n\n\treturn rssItem;\n}\n\n/**\n * Main RSS discovery function\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: discovering RSS data is complex\nasync function discoverRSSData(\n\tcwd: string,\n\tcollections: string[],\n\tbaseUrl: string,\n\tisWatchMode: boolean,\n\trssConfig?: RSSOptions,\n): Promise<RSSDiscoveryResult> {\n\t// Discover collection content in parallel\n\tconst collectionsMap = new Map<string, RSSItem[]>();\n\tconst collectionPromises = collections.map(async (collection) => {\n\t\tconst items = await discoverCollectionFeedItems(\n\t\t\tcwd,\n\t\t\tcollection,\n\t\t\tisWatchMode,\n\t\t\tbaseUrl,\n\t\t\trssConfig,\n\t\t);\n\t\treturn items.length > 0 ? { collection, items } : null;\n\t});\n\n\tconst collectionResults = await Promise.all(collectionPromises);\n\tfor (const result of collectionResults) {\n\t\tif (result) {\n\t\t\tcollectionsMap.set(result.collection, result.items);\n\t\t}\n\t}\n\n\t// Discover SSR data routes\n\tconst ssrDataFeeds = new Map<string, RSSItem[]>();\n\n\t// Check for bundled routes first (compiled binary support)\n\tconst bundledRoutes = (globalThis as Record<string, unknown>)\n\t\t.__REROUTE_ROUTES__ as Doc[] | undefined;\n\n\tconst { discoverRoutes } = await import('../sitemap/discovery');\n\tconst { staticRoutes } = await discoverRoutes(cwd, isWatchMode);\n\n\tif (bundledRoutes && Array.isArray(bundledRoutes)) {\n\t\t// In production: use pre-loaded SSR data from bundled routes\n\t\t// Process all routes in parallel\n\t\t// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: discovering ssr data routes is complex\n\t\tconst routePromises = staticRoutes.map(async (route) => {\n\t\t\t// Skip routes excluded from SSR data discovery (prefix match)\n\t\t\tif (\n\t\t\t\trssConfig?.excludeDiscovery?.some((excluded) =>\n\t\t\t\t\troute.pattern.startsWith(excluded),\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Find the matching bundled route\n\t\t\tconst bundledRoute = bundledRoutes.find(\n\t\t\t\t(r) => r?.pattern === route.pattern,\n\t\t\t);\n\n\t\t\tif (!bundledRoute) return null;\n\n\t\t\tconst ssr = (bundledRoute as Doc)?.ssr as Doc | undefined;\n\t\t\tconst dataFn = (ssr as Doc)?.data as\n\t\t\t\t| ((ctx: Doc) => unknown | Promise<unknown>)\n\t\t\t\t| undefined;\n\n\t\t\tif (typeof dataFn === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tconst mockSet = { status: 200 };\n\t\t\t\t\tconst result = await dataFn({\n\t\t\t\t\t\tpathname: route.pattern,\n\t\t\t\t\t\tparams: {},\n\t\t\t\t\t\tset: mockSet,\n\t\t\t\t\t});\n\n\t\t\t\t\tconst items = extractRSSItemsFromData(\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\troute.pattern,\n\t\t\t\t\t\tbaseUrl,\n\t\t\t\t\t\trssConfig,\n\t\t\t\t\t);\n\n\t\t\t\t\tif (items.length > 0) {\n\t\t\t\t\t\treturn { pattern: route.pattern, items };\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Skip routes that fail\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn null;\n\t\t});\n\n\t\tconst results = await Promise.all(routePromises);\n\t\tfor (const result of results) {\n\t\t\tif (result) {\n\t\t\t\tssrDataFeeds.set(result.pattern, result.items);\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Dev mode: dynamically import route modules in parallel\n\t\tconst routePromises = staticRoutes.map(async (route) => {\n\t\t\t// Skip routes excluded from SSR data discovery (prefix match)\n\t\t\tif (\n\t\t\t\trssConfig?.excludeDiscovery?.some((excluded) =>\n\t\t\t\t\troute.pattern.startsWith(excluded),\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst items = await discoverSSRDataFeedItems(\n\t\t\t\tcwd,\n\t\t\t\troute.pattern,\n\t\t\t\troute.path,\n\t\t\t\tisWatchMode,\n\t\t\t\tbaseUrl,\n\t\t\t\trssConfig,\n\t\t\t);\n\n\t\t\tif (items.length > 0) {\n\t\t\t\treturn { pattern: route.pattern, items };\n\t\t\t}\n\n\t\t\treturn null;\n\t\t});\n\n\t\tconst results = await Promise.all(routePromises);\n\t\tfor (const result of results) {\n\t\t\tif (result) {\n\t\t\t\tssrDataFeeds.set(result.pattern, result.items);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tcollections: collectionsMap,\n\t\tssrDataFeeds,\n\t};\n}\n\nexport {\n\tdiscoverRSSData,\n\tdiscoverSSRDataFeedItems,\n\ttype RSSDiscoveryResult,\n\ttype RSSFeed,\n\ttype RSSItem,\n};\n",
|
|
47
|
+
"export * from './discovery';\nexport * from './generator';\n",
|
|
48
|
+
"export * from './discovery';\nexport * from './generator';\n",
|
|
49
|
+
"import type { Span } from '@opentelemetry/api';\nimport { cloneElement, type ReactElement } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport {\n\tgetTraceContextForInjection,\n\twithParentSpan,\n\twithSpan,\n} from 'reroute-js/telemetry/server';\nimport { loadConfig, type OGImageOptions, type OGOptions } from '../config';\nimport { appendLlmsAlternateHead } from '../llms/discovery';\nimport {\n\tcleanupPathScopedSSRData,\n\tcreateDataScript,\n\tcreateFeedsScript,\n\tcreateLlmsScript,\n\ttype Doc,\n\tgenerateCollectionScripts,\n\tperformSSRSetup,\n\ttype SSR,\n} from './lib';\nimport {\n\tdeduplicateMetaTags,\n\tloadIndexHtml,\n\tremoveOverriddenMetaTags,\n} from './lib/html';\nimport { isThenable } from './lib/imports';\nimport { splitTemplate } from './lib/template';\n\ntype StreamingSSROptions = {\n\tpathname: string;\n\trootComponent: ReactElement;\n\tclientDir: string;\n\tcwd: string;\n\tisWatchMode: boolean;\n\tbundleUrl: string;\n\tcachedCollections?: Record<string, unknown[]>;\n\tcachedThemeCSS?: string;\n\tcachedIndexHtml?: string;\n\tdebug?: boolean;\n\thead?: string;\n\tlang?: string;\n\tappId?: string;\n\tmaxAge?: number;\n\tsearchParams?: URLSearchParams;\n\togConfig?: OGImageOptions;\n\togMetaConfig?: OGOptions;\n\tbaseUrl?: string;\n\tautoCanonical?: boolean;\n\tparentSpan?: Span | null;\n\t/** Request headers from the incoming HTTP request */\n\theaders?: Record<string, string>;\n\t/** Maximum streaming timeout in milliseconds (default: 120000 / 120s) */\n\ttimeout?: number;\n\t/** Suffix to append to all page titles */\n\ttitleSuffix?: string;\n\t/** Hydrated i18n state (opt-in) */\n\ti18n?: Record<string, unknown>;\n};\n\ntype StreamingSSRResult = {\n\tstream: ReadableStream<Uint8Array>;\n\tstatus: number;\n};\n\n/**\n * Render a React component to a streaming HTML response.\n * Uses React 19's streaming with progressive Suspense resolution.\n */\nasync function renderSSRDocumentStream(\n\toptions: StreamingSSROptions,\n): Promise<StreamingSSRResult> {\n\tconst debug = options.debug ?? false;\n\tconst t0 = debug ? performance.now() : 0;\n\tconst {\n\t\tpathname,\n\t\trootComponent,\n\t\tclientDir,\n\t\tcwd,\n\t\tisWatchMode,\n\t\tbundleUrl,\n\t\thead = '',\n\t\tlang = 'en',\n\t\tappId = 'root',\n\t\tmaxAge = 0,\n\t\tsearchParams,\n\t\togConfig,\n\t} = options;\n\n\tif (debug) console.log(`[renderStream] ${pathname} - start: 0ms`);\n\n\t// Perform shared SSR setup\n\tconst setup = await performSSRSetup(\n\t\t{\n\t\t\tpathname,\n\t\t\tclientDir,\n\t\t\tcwd,\n\t\t\tisWatchMode,\n\t\t\tparentSpan: options.parentSpan,\n\t\t\tbundleUrl,\n\t\t\tmaxAge,\n\t\t\tsearchParams,\n\t\t\togConfig,\n\t\t\togMetaConfig: options.ogMetaConfig,\n\t\t\tbaseUrl: options.baseUrl,\n\t\t\tautoCanonical: options.autoCanonical,\n\t\t\tcachedCollections: options.cachedCollections,\n\t\t\tcachedThemeCSS: options.cachedThemeCSS,\n\t\t\tdebug,\n\t\t\theaders: options.headers,\n\t\t\ttitleSuffix: options.titleSuffix,\n\t\t\ttimeout: options.timeout,\n\t\t},\n\t\ttrue, // streaming mode\n\t);\n\tif (debug)\n\t\tconsole.log(\n\t\t\t`[renderStream] ${pathname} - performSSRSetup: ${(performance.now() - t0).toFixed(2)}ms`,\n\t\t);\n\n\t// Determine status (only override if value is set)\n\tlet statusOverride = setup.preloadStatusOverride;\n\tif (setup.statusContainer.value !== undefined) {\n\t\tstatusOverride = setup.statusContainer.value;\n\t}\n\tif (setup.metadataResult.statusOverride !== undefined) {\n\t\tstatusOverride = setup.metadataResult.statusOverride;\n\t}\n\n\t// Set up streaming\n\tconst encoder = new TextEncoder();\n\tconst { readable, writable } = new TransformStream<Uint8Array>();\n\tconst writer = writable.getWriter();\n\n\t// Load and split template (use cached if available)\n\tconst usingCache = !!options.cachedIndexHtml;\n\tconst { htmlHead, rootStart, rootEnd, htmlTail } = await withSpan(\n\t\t'ssr.load.template',\n\t\tasync (span) => {\n\t\t\tconst template = options.cachedIndexHtml\n\t\t\t\t? options.cachedIndexHtml\n\t\t\t\t: await loadIndexHtml(clientDir);\n\n\t\t\tspan.setAttribute('reroute.cached', usingCache);\n\t\t\tspan.setAttribute('reroute.template.size', template.length);\n\n\t\t\tif (debug)\n\t\t\t\tconsole.log(\n\t\t\t\t\t`[renderStream] ${pathname} - loadIndexHtml (cached: ${usingCache}): ${(performance.now() - t0).toFixed(2)}ms`,\n\t\t\t\t);\n\n\t\t\tconst parts = splitTemplate(template, appId);\n\n\t\t\tif (debug)\n\t\t\t\tconsole.log(\n\t\t\t\t\t`[renderStream] ${pathname} - splitTemplate: ${(performance.now() - t0).toFixed(2)}ms`,\n\t\t\t\t);\n\n\t\t\treturn {\n\t\t\t\t...parts,\n\t\t\t};\n\t\t},\n\t);\n\n\t// Build head with lang attribute (replace existing lang or add new one)\n\tconst i18nLocale = options.i18n?.enabled\n\t\t? String(options.i18n.locale || '')\n\t\t: '';\n\tconst pageLang = setup.metadataResult.pageLang || i18nLocale || lang;\n\tlet headWithLang = htmlHead.replace(/<html([^>]*)>/i, (_m, attrs: string) => {\n\t\tconst hasLang = /(^|\\s)lang\\s*=/.test(attrs);\n\t\tconst newAttrs = hasLang\n\t\t\t? attrs.replace(\n\t\t\t\t\t/lang\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)/i,\n\t\t\t\t\t`lang=\"${pageLang}\"`,\n\t\t\t\t)\n\t\t\t: `${attrs} lang=\"${pageLang}\"`;\n\t\treturn `<html${newAttrs}>`;\n\t});\n\n\t// Build extra head content\n\tconst extraHeadBase =\n\t\tsetup.bundlePreload +\n\t\tsetup.preloadExtraHead +\n\t\tsetup.metadataResult.perPageHead;\n\n\t// Remove default title/description from template if we have page-specific ones\n\tif (/<title[\\s\\S]*?<\\/title>/i.test(extraHeadBase)) {\n\t\theadWithLang = headWithLang.replace(/<title[\\s\\S]*?<\\/title>/i, '');\n\t}\n\tif (/<meta\\s+name\\s*=\\s*['\"]description['\"][^>]*>/i.test(extraHeadBase)) {\n\t\theadWithLang = headWithLang.replace(\n\t\t\t/<meta\\s+name\\s*=\\s*['\"]description['\"][^>]*>/i,\n\t\t\t'',\n\t\t);\n\t}\n\n\t// Remove OG/Twitter meta tags from template that will be overridden\n\theadWithLang = removeOverriddenMetaTags(headWithLang, extraHeadBase);\n\n\t// Load config to get browser telemetry settings\n\tconst config = await loadConfig(cwd);\n\tconst extraHead = appendLlmsAlternateHead(\n\t\textraHeadBase,\n\t\tpathname,\n\t\tstatusOverride || 200,\n\t\tconfig.llms,\n\t);\n\tconst browserTelemetryConfigRaw = config.telemetry?.browser as\n\t\t| Record<string, unknown>\n\t\t| undefined;\n\n\t// Serialize browser telemetry config (convert functions to strings for JSON)\n\tconst { serializeBrowserTelemetryConfig } = await import('./lib/serialize');\n\tconst browserTelemetryConfig = serializeBrowserTelemetryConfig(\n\t\tbrowserTelemetryConfigRaw,\n\t);\n\n\t// Extract trace context from parent span for distributed tracing\n\tconst traceContext = getTraceContextForInjection(options.parentSpan);\n\n\t// Start streaming in background\n\tstreamSSRContent(writer, encoder, {\n\t\trootComponent,\n\t\tpathname,\n\t\tsearchParams,\n\t\theadWithLang,\n\t\thead,\n\t\textraHead,\n\t\tinlineStyleTag: setup.inlineStyleTag,\n\t\tpageLang,\n\t\trootStart,\n\t\trootEnd,\n\t\thtmlTail,\n\t\tbundleUrl,\n\t\tisWatchMode,\n\t\tdebug,\n\t\tssrData: setup.ssrData,\n\t\tssrError: setup.ssrError,\n\t\tbyCollectionForSSR: setup.byCollectionForSSR,\n\t\tpreloadHydrationScript: setup.preloadHydrationScript,\n\t\tbrowserTelemetryConfig,\n\t\ttraceContext,\n\t\tparentSpan: options.parentSpan,\n\t\ttimeout: options.timeout,\n\t\tssrExport: setup.metadataResult.ssrExport,\n\t\ttitleSuffix: options.titleSuffix,\n\t\ti18n: options.i18n,\n\t});\n\n\tif (debug)\n\t\tconsole.log(\n\t\t\t`[renderStream] ${pathname} - returning stream: ${(performance.now() - t0).toFixed(2)}ms`,\n\t\t);\n\treturn { stream: readable, status: statusOverride || 200 };\n}\n\ntype StreamContext = {\n\trootComponent: ReactElement;\n\tpathname: string;\n\tsearchParams?: URLSearchParams;\n\theadWithLang: string;\n\thead: string;\n\textraHead: string;\n\tinlineStyleTag: string;\n\tpageLang: string;\n\trootStart: string;\n\trootEnd: string;\n\thtmlTail: string;\n\tbundleUrl: string;\n\tisWatchMode: boolean;\n\tdebug: boolean;\n\tssrData: Record<string, unknown>;\n\tssrError?: string;\n\tbyCollectionForSSR: Record<string, unknown[]>;\n\tpreloadHydrationScript: string;\n\tbrowserTelemetryConfig?: Record<string, unknown>;\n\ttraceContext?: { traceparent?: string; tracestate?: string };\n\tparentSpan?: Span | null;\n\ttimeout?: number;\n\t/** SSR export for dynamic head generation at stream end */\n\tssrExport?: SSR;\n\t/** Suffix to append to page titles */\n\ttitleSuffix?: string;\n\t/** Hydrated i18n state (opt-in) */\n\ti18n?: Record<string, unknown>;\n};\n\n/**\n * Stream SSR content progressively\n * NOTE: This runs during the HTTP request lifecycle - Elysia waits for the stream\n * to complete before calling onAfterResponse\n */\nasync function streamSSRContent(\n\twriter: WritableStreamDefaultWriter<Uint8Array>,\n\tencoder: TextEncoder,\n\tctx: StreamContext,\n): Promise<void> {\n\t// Execute within parent span context to properly nest spans\n\treturn withParentSpan(ctx.parentSpan, async () => {\n\t\tconst t0 = ctx.debug ? performance.now() : 0;\n\t\t// Set a maximum streaming timeout to prevent server hangs\n\t\t// This ensures the stream always completes even if React is waiting for promises\n\t\t// Default to 120 seconds, but allow configuration for long operations like AI analysis\n\t\tconst STREAM_TIMEOUT_MS = ctx.timeout ?? 120000;\n\t\tlet streamTimedOut = false;\n\t\tlet timeoutId: ReturnType<typeof setTimeout> | null = null;\n\n\t\tconst cleanup = () => {\n\t\t\tif (timeoutId) {\n\t\t\t\tclearTimeout(timeoutId);\n\t\t\t\ttimeoutId = null;\n\t\t\t}\n\t\t};\n\n\t\ttry {\n\t\t\t// 1. Send HTML head immediately (shell)\n\t\t\tconst combinedHead = deduplicateMetaTags(\n\t\t\t\t[ctx.inlineStyleTag, ctx.head, ctx.extraHead]\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join('\\n'),\n\t\t\t);\n\t\t\t// Inject combined head BEFORE </head> (lang is already handled in headWithLang)\n\t\t\tconst headWithLangAndContent = ctx.headWithLang.replace(\n\t\t\t\t/<\\/head>/i,\n\t\t\t\t`${combinedHead ? `${combinedHead}\\n` : ''}</head>`,\n\t\t\t);\n\t\t\tconst headContent = `${headWithLangAndContent}`;\n\t\t\tawait writer.write(encoder.encode(headContent));\n\t\t\tif (ctx.debug)\n\t\t\t\tconsole.log(\n\t\t\t\t\t`[Stream] ${ctx.pathname} - first byte written: ${(performance.now() - t0).toFixed(2)}ms`,\n\t\t\t\t);\n\n\t\t\t// 2. Send root div start\n\t\t\tawait writer.write(encoder.encode(ctx.rootStart));\n\n\t\t\t// 3. Stream React content (both create and pipe)\n\t\t\t// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Stream creation and piping requires timeout handling\n\t\t\tawait withSpan('ssr.document.stream', async (span) => {\n\t\t\t\tconst componentWithPathname = cloneElement(ctx.rootComponent, {\n\t\t\t\t\tpathname: ctx.pathname,\n\t\t\t\t\tsearchParams: ctx.searchParams,\n\t\t\t\t} as Doc);\n\n\t\t\t\tspan.setAttribute('reroute.pathname', ctx.pathname);\n\n\t\t\t\tlet streamError: Error | null = null;\n\t\t\t\tconst streamPromise = renderToReadableStream(componentWithPathname, {\n\t\t\t\t\tonError(error) {\n\t\t\t\t\t\tconsole.error('[reroute] SSR stream error:', error);\n\t\t\t\t\t\tstreamError =\n\t\t\t\t\t\t\terror instanceof Error ? error : new Error(String(error));\n\t\t\t\t\t},\n\t\t\t\t}).catch((err) => {\n\t\t\t\t\tstreamError = err;\n\t\t\t\t\tthrow err;\n\t\t\t\t});\n\n\t\t\t\tconst reactStream = await streamPromise;\n\t\t\t\tif (streamError) throw streamError;\n\n\t\t\t\t// Pipe React stream to writer\n\t\t\t\t// Set up streaming timeout - if React takes too long, force close\n\t\t\t\tconst streamTimeoutPromise = new Promise<{\n\t\t\t\t\tdone: true;\n\t\t\t\t\tvalue: undefined;\n\t\t\t\t}>((resolve) => {\n\t\t\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[reroute] Stream timeout after ${STREAM_TIMEOUT_MS}ms for ${ctx.pathname}, forcing close`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tstreamTimedOut = true;\n\t\t\t\t\t\tresolve({ done: true, value: undefined });\n\t\t\t\t\t}, STREAM_TIMEOUT_MS);\n\t\t\t\t});\n\n\t\t\t\tconst reader = reactStream.getReader();\n\t\t\t\tlet chunkCount = 0;\n\t\t\t\twhile (true) {\n\t\t\t\t\t// Race between getting next chunk and timeout\n\t\t\t\t\tconst result = await Promise.race([\n\t\t\t\t\t\treader.read(),\n\t\t\t\t\t\tstreamTimeoutPromise,\n\t\t\t\t\t]);\n\n\t\t\t\t\tif (result.done || streamTimedOut) {\n\t\t\t\t\t\tif (streamTimedOut) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait reader.cancel(\n\t\t\t\t\t\t\t\t\t`[reroute] stream timeout for ${ctx.pathname}`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t} catch {}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Release the reader lock before breaking\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treader.releaseLock();\n\t\t\t\t\t\t} catch {}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif (result.value) {\n\t\t\t\t\t\t// Decode, obfuscate emails, and re-encode if needed\n\t\t\t\t\t\tawait writer.write(result.value);\n\t\t\t\t\t\tchunkCount++;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcleanup();\n\n\t\t\t\tspan.setAttributes({\n\t\t\t\t\t'reroute.stream.chunks': chunkCount,\n\t\t\t\t\t'reroute.stream.timedOut': streamTimedOut,\n\t\t\t\t});\n\t\t\t});\n\n\t\t\tif (streamTimedOut) {\n\t\t\t\tcleanupPathScopedSSRData(ctx.pathname);\n\t\t\t}\n\n\t\t\t// 4. Close root div\n\t\t\tawait writer.write(encoder.encode(ctx.rootEnd));\n\n\t\t\t// 5. Generate scripts\n\t\t\tconst allScripts = await withSpan(\n\t\t\t\t'ssr.generate.scripts',\n\t\t\t\tasync (span) => {\n\t\t\t\t\tconst scripts = buildStreamingScripts(ctx);\n\t\t\t\t\tspan.setAttribute('reroute.script_size', scripts.length);\n\t\t\t\t\treturn scripts;\n\t\t\t\t},\n\t\t\t);\n\t\t\tawait writer.write(encoder.encode(allScripts + ctx.htmlTail));\n\t\t\tawait writer.close();\n\t\t} catch (error) {\n\t\t\tcleanup();\n\t\t\tcleanupPathScopedSSRData(ctx.pathname);\n\t\t\tconsole.error('[reroute] Stream error:', error);\n\t\t\ttry {\n\t\t\t\tawait writer.abort(error);\n\t\t\t} catch {\n\t\t\t\t// Writer might already be closed\n\t\t\t}\n\t\t}\n\t});\n}\n\n/**\n * Build all scripts for streaming SSR\n */\nfunction buildStreamingScripts(ctx: StreamContext): string {\n\t// Generate collection scripts\n\tconst collectionScripts = generateCollectionScripts(ctx.byCollectionForSSR);\n\n\t// Create snapshot of resolved SSR data\n\tconst ssrDataSnapshot: Record<string, unknown> = {};\n\tfor (const key of Object.keys(ctx.ssrData)) {\n\t\tconst value = ctx.ssrData[key];\n\t\tif (!isThenable(value)) {\n\t\t\tssrDataSnapshot[key] = value;\n\t\t}\n\t}\n\n\t// Build scripts\n\tconst dataScript = createDataScript(\n\t\tssrDataSnapshot,\n\t\tctx.isWatchMode ? ctx.ssrError : undefined,\n\t\tctx.pathname,\n\t\tctx.browserTelemetryConfig,\n\t\tctx.traceContext,\n\t\tctx.titleSuffix,\n\t\tctx.i18n,\n\t);\n\tconst feedsScript = createFeedsScript();\n\tconst llmsScript = createLlmsScript();\n\n\tlet allScripts =\n\t\tdataScript +\n\t\tfeedsScript +\n\t\tllmsScript +\n\t\tcollectionScripts +\n\t\tctx.preloadHydrationScript;\n\n\t// Generate meta update script if route has dynamic head function\n\tconst metaUpdateScript = generateMetaUpdateScript(ctx, ssrDataSnapshot);\n\tif (metaUpdateScript) {\n\t\tallScripts += metaUpdateScript;\n\t}\n\n\t// Add bundle script\n\tallScripts += `<script type=\"module\" src=\"${ctx.bundleUrl}\"></script>`;\n\n\t// Add dev watcher\n\tif (ctx.isWatchMode) {\n\t\tallScripts += `<script type=\"module\" src=\"/__reroute_watch.js\"></script>`;\n\t}\n\n\treturn allScripts;\n}\n\n/**\n * Apply titleSuffix to title-related fields in meta updates\n */\nfunction applyTitleSuffixToMetaUpdates(\n\tmetaUpdates: Record<string, string>,\n\ttitleSuffix: string,\n): void {\n\tif (metaUpdates.title && !metaUpdates.title.endsWith(titleSuffix)) {\n\t\tmetaUpdates.title = metaUpdates.title + titleSuffix;\n\t}\n\tif (\n\t\tmetaUpdates['og:title'] &&\n\t\t!metaUpdates['og:title'].endsWith(titleSuffix)\n\t) {\n\t\tmetaUpdates['og:title'] = metaUpdates['og:title'] + titleSuffix;\n\t}\n\tif (\n\t\tmetaUpdates['twitter:title'] &&\n\t\t!metaUpdates['twitter:title'].endsWith(titleSuffix)\n\t) {\n\t\tmetaUpdates['twitter:title'] = metaUpdates['twitter:title'] + titleSuffix;\n\t}\n}\n\n/**\n * Generate a script to update head meta tags at stream end.\n * This is used when streaming SSR needs to update dynamic meta tags\n * after data has resolved.\n */\nfunction generateMetaUpdateScript(\n\tctx: StreamContext,\n\tssrDataSnapshot: Record<string, unknown>,\n): string {\n\t// Only generate if route has dynamic head function\n\tif (typeof ctx.ssrExport?.head !== 'function') {\n\t\treturn '';\n\t}\n\n\t// Get the resolved route data\n\tconst routeData = ssrDataSnapshot[ctx.pathname];\n\tif (!routeData) {\n\t\treturn '';\n\t}\n\n\t// Call the head function with resolved data\n\tlet headResult: string | string[] | undefined;\n\ttry {\n\t\theadResult = ctx.ssrExport.head({ data: routeData });\n\t} catch (err) {\n\t\tconsole.error('[reroute] Error calling ssr.head() at stream end:', err);\n\t\treturn '';\n\t}\n\n\tif (!headResult) {\n\t\treturn '';\n\t}\n\n\t// Convert to string\n\tconst headHtml = Array.isArray(headResult)\n\t\t? headResult.join('\\n')\n\t\t: headResult;\n\tif (!headHtml) {\n\t\treturn '';\n\t}\n\n\t// Parse the head HTML to extract meta tag values\n\tconst metaUpdates = parseHeadForMetaUpdates(headHtml);\n\tif (Object.keys(metaUpdates).length === 0) {\n\t\treturn '';\n\t}\n\n\t// Apply titleSuffix to title and related meta tags\n\tif (ctx.titleSuffix) {\n\t\tapplyTitleSuffixToMetaUpdates(metaUpdates, ctx.titleSuffix);\n\t}\n\n\t// Generate inline script to update the head\n\tconst updatesJson = JSON.stringify(metaUpdates);\n\treturn `<script>(function(){var u=${updatesJson};if(u.title){document.title=u.title}Object.keys(u).forEach(function(k){if(k==='title')return;var s=k.split(':');var isOg=s[0]==='og'||s[0]==='twitter';var el=isOg?document.querySelector('meta[property=\"'+k+'\"]')||document.querySelector('meta[name=\"'+k+'\"]'):document.querySelector('meta[name=\"'+k+'\"]');if(el){el.setAttribute('content',u[k])}else{var m=document.createElement('meta');m.setAttribute(isOg?'property':'name',k);m.setAttribute('content',u[k]);document.head.appendChild(m)}})})()</script>`;\n}\n\n/**\n * Parse head HTML to extract values for meta tag updates\n */\nfunction parseHeadForMetaUpdates(headHtml: string): Record<string, string> {\n\tconst updates: Record<string, string> = {};\n\n\t// Extract title\n\tconst titleMatch = headHtml.match(/<title[^>]*>(.*?)<\\/title>/is);\n\tif (titleMatch?.[1]) {\n\t\tupdates.title = unescapeHtmlEntities(titleMatch[1].trim());\n\t}\n\n\t// Extract meta description\n\tlet descMatch = headHtml.match(\n\t\t/<meta\\s+name=[\"']description[\"']\\s+content=[\"'](.+?)[\"']/i,\n\t);\n\tif (!descMatch) {\n\t\tdescMatch = headHtml.match(\n\t\t\t/<meta\\s+content=[\"'](.+?)[\"']\\s+name=[\"']description[\"']/i,\n\t\t);\n\t}\n\tif (descMatch?.[1]) {\n\t\tupdates.description = unescapeHtmlEntities(descMatch[1].trim());\n\t}\n\n\t// Extract og:title\n\tlet ogTitleMatch = headHtml.match(\n\t\t/<meta\\s+property=[\"']og:title[\"']\\s+content=[\"'](.+?)[\"']/i,\n\t);\n\tif (!ogTitleMatch) {\n\t\togTitleMatch = headHtml.match(\n\t\t\t/<meta\\s+content=[\"'](.+?)[\"']\\s+property=[\"']og:title[\"']/i,\n\t\t);\n\t}\n\tif (ogTitleMatch?.[1]) {\n\t\tupdates['og:title'] = unescapeHtmlEntities(ogTitleMatch[1].trim());\n\t} else if (updates.title) {\n\t\t// Fallback: use title for og:title\n\t\tupdates['og:title'] = updates.title;\n\t}\n\n\t// Extract og:description\n\tlet ogDescMatch = headHtml.match(\n\t\t/<meta\\s+property=[\"']og:description[\"']\\s+content=[\"'](.+?)[\"']/i,\n\t);\n\tif (!ogDescMatch) {\n\t\togDescMatch = headHtml.match(\n\t\t\t/<meta\\s+content=[\"'](.+?)[\"']\\s+property=[\"']og:description[\"']/i,\n\t\t);\n\t}\n\tif (ogDescMatch?.[1]) {\n\t\tupdates['og:description'] = unescapeHtmlEntities(ogDescMatch[1].trim());\n\t} else if (updates.description) {\n\t\t// Fallback: use description for og:description\n\t\tupdates['og:description'] = updates.description;\n\t}\n\n\t// Extract twitter:title\n\tlet twitterTitleMatch = headHtml.match(\n\t\t/<meta\\s+name=[\"']twitter:title[\"']\\s+content=[\"'](.+?)[\"']/i,\n\t);\n\tif (!twitterTitleMatch) {\n\t\ttwitterTitleMatch = headHtml.match(\n\t\t\t/<meta\\s+content=[\"'](.+?)[\"']\\s+name=[\"']twitter:title[\"']/i,\n\t\t);\n\t}\n\tif (twitterTitleMatch?.[1]) {\n\t\tupdates['twitter:title'] = unescapeHtmlEntities(\n\t\t\ttwitterTitleMatch[1].trim(),\n\t\t);\n\t} else if (updates.title) {\n\t\t// Fallback: use title for twitter:title\n\t\tupdates['twitter:title'] = updates.title;\n\t}\n\n\t// Extract twitter:description\n\tlet twitterDescMatch = headHtml.match(\n\t\t/<meta\\s+name=[\"']twitter:description[\"']\\s+content=[\"'](.+?)[\"']/i,\n\t);\n\tif (!twitterDescMatch) {\n\t\ttwitterDescMatch = headHtml.match(\n\t\t\t/<meta\\s+content=[\"'](.+?)[\"']\\s+name=[\"']twitter:description[\"']/i,\n\t\t);\n\t}\n\tif (twitterDescMatch?.[1]) {\n\t\tupdates['twitter:description'] = unescapeHtmlEntities(\n\t\t\ttwitterDescMatch[1].trim(),\n\t\t);\n\t} else if (updates.description) {\n\t\t// Fallback: use description for twitter:description\n\t\tupdates['twitter:description'] = updates.description;\n\t}\n\n\treturn updates;\n}\n\n/**\n * Unescape HTML entities\n */\nfunction unescapeHtmlEntities(str: string): string {\n\treturn str\n\t\t.replace(/"/g, '\"')\n\t\t.replace(/'/g, \"'\")\n\t\t.replace(/</g, '<')\n\t\t.replace(/>/g, '>')\n\t\t.replace(/&/g, '&');\n}\n\nexport { renderSSRDocumentStream };\nexport type { StreamingSSROptions, StreamingSSRResult };\n",
|
|
50
|
+
"export * from './lib';\nexport * from './render';\nexport * from './stream';\n",
|
|
51
|
+
"export * from './app-paths';\nexport * from './bundler';\nexport * from './config';\nexport * from './content';\nexport * from './i18n';\nexport * from './llms';\nexport * from './og';\nexport * from './robots';\nexport * from './rss';\nexport * from './sitemap';\nexport * from './ssr';\n",
|
|
52
|
+
"export * from './src';\n",
|
|
5
53
|
"import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/**\n * Get the reroute command to execute\n * Handles both development mode (running from source) and installed mode\n */\nexport function getRerouteCommand(): string {\n\t// Check if we're running from source (development mode)\n\t// Look for bin.ts in the expected location relative to this file\n\tconst binPath = join(import.meta.dir, '..', '..', 'bin.ts');\n\tconst isBunDev = existsSync(binPath);\n\n\tif (isBunDev) {\n\t\t// Running via bun during development from source\n\t\treturn `bun ${binPath}`;\n\t}\n\n\t// Check for reroute binary in node_modules/.bin, checking up to 3 levels up\n\tconst cwd = process.cwd();\n\tconst nodeModulesPaths = [\n\t\tjoin(cwd, 'node_modules', '.bin', 'reroute'),\n\t\tjoin(cwd, '..', 'node_modules', '.bin', 'reroute'),\n\t\tjoin(cwd, '..', '..', 'node_modules', '.bin', 'reroute'),\n\t\tjoin(cwd, '..', '..', '..', 'node_modules', '.bin', 'reroute'),\n\t];\n\n\tfor (const binPath of nodeModulesPaths) {\n\t\tif (existsSync(binPath)) {\n\t\t\treturn binPath;\n\t\t}\n\t}\n\n\t// Fallback: assume reroute is installed globally and available in PATH\n\treturn 'reroute';\n}\n",
|
|
6
54
|
"/**\n * Log colorization utilities\n *\n * Provides colorization for log prefixes in both blessed (TUI) and ANSI (console) formats.\n * Consumers can use [api] and [app] prefixes to differentiate their application logs.\n */\n\n/**\n * Map of prefix patterns to blessed color tags\n * Each prefix has a unique color for easy visual distinction\n */\nconst PREFIX_COLORS: Record<string, string> = {\n\t'[reroute]': '{cyan-fg}', // Base reroute logs - cyan\n\t'[reroute/gen]': '{bright-blue-fg}', // Generator - bright blue\n\t'[reroute/content]': '{magenta-fg}', // Content - magenta\n\t'[reroute/markdown]': '{yellow-fg}', // Markdown - yellow\n\t'[reroute/theme]': '{red-fg}', // Theme CSS - red\n\t'[reroute/mdx]': '{bright-red-fg}', // MDX - bright red\n\t'[reroute/dev]': '{green-fg}', // Dev command - green\n\t'[reroute/start]': '{bright-cyan-fg}', // Start command - bright cyan\n\t'[reroute/build]': '{bright-magenta-fg}', // Build command - bright magenta\n\t'[reroute/analyze]': '{bright-green-fg}', // Analyze command - bright green\n\t'[api]': '{blue-fg}', // API logs - blue\n\t'[app]': '{bright-green-fg}', // App logs - bright green\n};\n\n/**\n * Colorize log prefixes using blessed color tags (for TUI output)\n *\n * @param text - The log text to colorize\n * @returns Text with colorized prefixes using blessed tags\n */\nexport function colorizeLogPrefix(text: string): string {\n\tlet colored = text;\n\t// Sort prefixes by length (longest first) to match more specific prefixes before general ones\n\t// e.g., [reroute/markdown] should match before [reroute]\n\tconst sortedPrefixes = Object.entries(PREFIX_COLORS).sort(\n\t\t([a], [b]) => b.length - a.length,\n\t);\n\n\tfor (const [prefix, colorTag] of sortedPrefixes) {\n\t\t// Replace prefix with colored version\n\t\tcolored = colored.replace(\n\t\t\tnew RegExp(`(${prefix.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')})`, 'g'),\n\t\t\t`${colorTag}$1{/}`,\n\t\t);\n\t}\n\n\treturn colored;\n}\n\n/**\n * Convert blessed color tags to ANSI escape codes (for console output)\n *\n * @param text - Text with blessed color tags\n * @returns Text with ANSI escape codes\n */\nexport function blessedToAnsi(text: string): string {\n\treturn (\n\t\ttext\n\t\t\t// Standard colors\n\t\t\t.replace(/\\{cyan-fg\\}/g, '\\x1b[36m')\n\t\t\t.replace(/\\{blue-fg\\}/g, '\\x1b[34m')\n\t\t\t.replace(/\\{magenta-fg\\}/g, '\\x1b[35m')\n\t\t\t.replace(/\\{yellow-fg\\}/g, '\\x1b[33m')\n\t\t\t.replace(/\\{green-fg\\}/g, '\\x1b[32m')\n\t\t\t.replace(/\\{red-fg\\}/g, '\\x1b[31m')\n\t\t\t// Bright colors\n\t\t\t.replace(/\\{bright-cyan-fg\\}/g, '\\x1b[96m')\n\t\t\t.replace(/\\{bright-blue-fg\\}/g, '\\x1b[94m')\n\t\t\t.replace(/\\{bright-magenta-fg\\}/g, '\\x1b[95m')\n\t\t\t.replace(/\\{bright-yellow-fg\\}/g, '\\x1b[93m')\n\t\t\t.replace(/\\{bright-green-fg\\}/g, '\\x1b[92m')\n\t\t\t.replace(/\\{bright-red-fg\\}/g, '\\x1b[91m')\n\t\t\t.replace(/\\{\\/\\}/g, '\\x1b[0m')\n\t);\n}\n\n/**\n * Colorize log prefixes and convert to ANSI codes (for console output)\n *\n * @param text - The log text to colorize\n * @returns Text with colorized prefixes using ANSI escape codes\n */\nexport function colorizeLogPrefixAnsi(text: string): string {\n\treturn blessedToAnsi(colorizeLogPrefix(text));\n}\n\n/**\n * Create a handler function for server output that colorizes logs\n *\n * @param buffer - Reference to a buffer string for incomplete lines\n * @returns Handler function for Buffer data\n */\nexport function createServerOutputHandler(buffer: { current: string }) {\n\treturn (data: Buffer) => {\n\t\tconst text = buffer.current + data.toString();\n\t\tconst lines = text.split('\\n');\n\t\tbuffer.current = lines.pop() || ''; // Keep last incomplete line\n\n\t\tfor (const line of lines) {\n\t\t\t// Skip empty lines\n\t\t\tconst trimmedLine = line.trim();\n\t\t\tif (!trimmedLine) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Colorize the line (preserves existing prefixes or leaves unprefixed lines as-is)\n\t\t\tconst ansiColored = colorizeLogPrefixAnsi(line);\n\t\t\tconsole.log(ansiColored);\n\t\t}\n\t};\n}\n",
|
|
7
55
|
"/**\n * Markdown integration utility\n *\n * Manages markdown and syntax highlighting packages for the Reroute framework\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\n/**\n * Check if a package is available in package.json, searching up to 5 parent directories\n */\nfunction isPackageAvailable(cwd: string, packageName: string): boolean {\n\tlet currentDir = cwd;\n\n\t// Check up to 5 levels up\n\tfor (let i = 0; i < 5; i++) {\n\t\tconst packageJsonPath = join(currentDir, 'package.json');\n\n\t\tif (existsSync(packageJsonPath)) {\n\t\t\ttry {\n\t\t\t\tconst packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));\n\t\t\t\tconst deps = {\n\t\t\t\t\t...packageJson.dependencies,\n\t\t\t\t\t...packageJson.devDependencies,\n\t\t\t\t\t...packageJson.peerDependencies,\n\t\t\t\t};\n\n\t\t\t\tif (packageName in deps) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Continue to parent directory\n\t\t\t}\n\t\t}\n\n\t\tconst parentDir = dirname(currentDir);\n\t\t// Stop if we've reached the root\n\t\tif (parentDir === currentDir) {\n\t\t\tbreak;\n\t\t}\n\t\tcurrentDir = parentDir;\n\t}\n\n\treturn false;\n}\n\n/**\n * Check if streamdown is available in the project\n */\nfunction isStreamdownAvailable(cwd: string): boolean {\n\treturn isPackageAvailable(cwd, 'streamdown');\n}\n\n/**\n * Check if Shiki is available in the project (deprecated: now built into streamdown)\n */\nfunction isShikiAvailable(cwd: string): boolean {\n\treturn isPackageAvailable(cwd, '@shikijs/rehype');\n}\n\n/**\n * @deprecated Streamdown includes GFM support built-in\n * Check if remark-gfm (GitHub Flavored Markdown) is available\n */\nfunction isRemarkGfmAvailable(cwd: string): boolean {\n\treturn isPackageAvailable(cwd, 'remark-gfm');\n}\n\n/**\n * Check if @mdx-js/mdx is available in the project\n */\nfunction isMdxAvailable(cwd: string): boolean {\n\treturn isPackageAvailable(cwd, '@mdx-js/mdx');\n}\n\n/**\n * Get markdown configuration status for the project\n */\nexport function getMarkdownConfig(cwd: string) {\n\tconst hasStreamdown = isStreamdownAvailable(cwd);\n\tconst hasShiki = isShikiAvailable(cwd);\n\tconst hasRemarkGfm = isRemarkGfmAvailable(cwd);\n\tconst hasMdx = isMdxAvailable(cwd);\n\n\treturn {\n\t\thasStreamdown,\n\t\thasShiki,\n\t\thasRemarkGfm,\n\t\thasMdx,\n\t\tisConfigured: hasStreamdown,\n\t};\n}\n\n/**\n * Log markdown configuration status\n */\nexport function logMarkdownStatus(cwd: string): void {\n\tconst config = getMarkdownConfig(cwd);\n\n\tif (config.isConfigured) {\n\t\tconsole.log(\n\t\t\t'[reroute/markdown] Streamdown available (GFM, syntax highlighting, math, mermaid built-in)',\n\t\t);\n\t\tif (config.hasMdx) {\n\t\t\tconsole.log('[reroute/markdown] MDX compiler enabled (JSX in markdown)');\n\t\t}\n\t}\n}\n\n/**\n * Check if markdown packages are configured and log status\n */\nexport function checkMarkdownIfConfigured(cwd: string): boolean {\n\ttry {\n\t\tconst config = getMarkdownConfig(cwd);\n\t\tif (config.isConfigured) {\n\t\t\tlogMarkdownStatus(cwd);\n\t\t}\n\t\treturn config.isConfigured;\n\t} catch (e) {\n\t\tconsole.warn('[reroute/markdown] check failed:', e);\n\t\treturn false;\n\t}\n}\n",
|
|
8
56
|
"/**\n * Markdown/MDX processor for route files\n *\n * Processes .md and .mdx files with frontmatter support for use in the routing system\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { basename, dirname, extname, join, relative, resolve } from 'node:path';\nimport matter from 'gray-matter';\n\n// biome-ignore lint/suspicious/noExplicitAny: yes\ntype Any = any;\n\n/**\n * HTML attribute to JSX prop mapping\n */\nconst htmlToJsxAttributes: Record<string, string> = {\n\tclass: 'className',\n\tfor: 'htmlFor',\n\t'accept-charset': 'acceptCharset',\n\t'http-equiv': 'httpEquiv',\n\taccesskey: 'accessKey',\n\tcontenteditable: 'contentEditable',\n\tcrossorigin: 'crossOrigin',\n\ttabindex: 'tabIndex',\n\t// SVG attributes\n\t'stroke-width': 'strokeWidth',\n\t'stroke-linecap': 'strokeLinecap',\n\t'stroke-linejoin': 'strokeLinejoin',\n\t'stroke-miterlimit': 'strokeMiterlimit',\n\t'stroke-dasharray': 'strokeDasharray',\n\t'stroke-dashoffset': 'strokeDashoffset',\n\t'stroke-opacity': 'strokeOpacity',\n\t'fill-opacity': 'fillOpacity',\n\t'fill-rule': 'fillRule',\n\t'clip-path': 'clipPath',\n\t'clip-rule': 'clipRule',\n\t'font-family': 'fontFamily',\n\t'font-size': 'fontSize',\n\t'font-weight': 'fontWeight',\n\t'text-anchor': 'textAnchor',\n\t'text-decoration': 'textDecoration',\n\t'vector-effect': 'vectorEffect',\n};\n\nfunction convertPropertiesToJsx(\n\tproperties: Record<string, Any>,\n): Record<string, Any> {\n\tconst newProperties: Record<string, Any> = {};\n\tfor (const [key, value] of Object.entries(properties)) {\n\t\tconst jsxKey = htmlToJsxAttributes[key] || key;\n\t\tnewProperties[jsxKey] = value;\n\t}\n\treturn newProperties;\n}\n\nfunction visitNode(node: Any): void {\n\tif (node.type === 'element' && node.properties) {\n\t\tnode.properties = convertPropertiesToJsx(node.properties);\n\t}\n\tif (node.children) {\n\t\tfor (const child of node.children) {\n\t\t\tvisitNode(child);\n\t\t}\n\t}\n}\n\n/**\n * Rehype plugin to convert HTML attributes to React JSX props\n */\nfunction rehypeHtmlToJsx() {\n\treturn (tree: Any) => {\n\t\tvisitNode(tree);\n\t\treturn tree;\n\t};\n}\n\nexport interface MarkdownFrontmatter {\n\ttitle?: string;\n\tdescription?: string;\n\tdate?: string;\n\tslug?: string;\n\texcerpt?: string;\n\tdraft?: boolean;\n\t[key: string]: Any;\n}\n\nexport interface ProcessedMarkdown {\n\tfrontmatter: MarkdownFrontmatter;\n\tcontent: string;\n\trawContent: string;\n\thasJSX: boolean;\n}\n\nexport interface HeadingNode {\n\tlevel: number;\n\ttext: string;\n\tslug: string;\n}\n\n/**\n * Frontmatter parser using gray-matter\n * Extracts metadata from markdown files between --- delimiters\n * Supports YAML data types: strings, numbers, booleans, arrays, objects, null\n */\nfunction parseFrontmatter(content: string): {\n\tdata: MarkdownFrontmatter;\n\tcontent: string;\n} {\n\ttry {\n\t\tconst { data, content: markdownContent } = matter(content);\n\t\treturn { data, content: markdownContent };\n\t} catch (error) {\n\t\tconsole.warn('[reroute/frontmatter] Failed to parse frontmatter:', error);\n\t\treturn { data: {}, content };\n\t}\n}\n\n/**\n * Check if markdown content contains JSX syntax\n */\nfunction hasJSXSyntax(content: string): boolean {\n\t// Simple heuristic: look for JSX-like patterns\n\tconst jsxPatterns = [\n\t\t/<[A-Z][a-zA-Z0-9]*/, // Component tags like <Component>\n\t\t/import\\s+{[^}]+}\\s+from/, // ES6 imports\n\t\t/import\\s+\\w+\\s+from/, // Default imports\n\t\t/export\\s+(default|const|function)/, // Exports\n\t\t/<\\w+\\s+\\w+={/, // Props with curly braces\n\t];\n\n\treturn jsxPatterns.some((pattern) => pattern.test(content));\n}\n\n/**\n * Process a markdown/MDX file and return structured data\n */\nexport function processMarkdownFile(filePath: string): ProcessedMarkdown {\n\tconst rawContent = readFileSync(filePath, 'utf-8');\n\tconst { data, content } = parseFrontmatter(rawContent);\n\tconst hasJSX = hasJSXSyntax(content);\n\n\treturn {\n\t\tfrontmatter: data,\n\t\tcontent,\n\t\trawContent,\n\t\thasJSX,\n\t};\n}\n\n/**\n * Check if @mdx-js/mdx is available in the project\n */\nfunction isMdxAvailable(cwd: string): boolean {\n\tlet currentDir = cwd;\n\n\t// Check up to 5 levels up\n\tfor (let i = 0; i < 5; i++) {\n\t\tconst packageJsonPath = join(currentDir, 'package.json');\n\n\t\tif (existsSync(packageJsonPath)) {\n\t\t\ttry {\n\t\t\t\tconst packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));\n\t\t\t\tconst deps = {\n\t\t\t\t\t...packageJson.dependencies,\n\t\t\t\t\t...packageJson.devDependencies,\n\t\t\t\t\t...packageJson.peerDependencies,\n\t\t\t\t};\n\n\t\t\t\tif ('@mdx-js/mdx' in deps) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Continue to parent directory\n\t\t\t}\n\t\t}\n\n\t\tconst parentDir = dirname(currentDir);\n\t\t// Stop if we've reached the root\n\t\tif (parentDir === currentDir) {\n\t\t\tbreak;\n\t\t}\n\t\tcurrentDir = parentDir;\n\t}\n\n\treturn false;\n}\n\nfunction tryLoadModule(cwd: string, moduleName: string): Any {\n\tconst pathsToTry = [\n\t\tjoin(cwd, `node_modules/${moduleName}`),\n\t\tjoin(cwd, `../node_modules/${moduleName}`),\n\t\tjoin(cwd, `../../node_modules/${moduleName}`),\n\t\tjoin(cwd, `../../../node_modules/${moduleName}`),\n\t];\n\n\tfor (const modulePath of pathsToTry) {\n\t\ttry {\n\t\t\treturn require(modulePath);\n\t\t} catch {\n\t\t\t// Try next path\n\t\t}\n\t}\n\treturn null;\n}\n\nfunction loadMdxCompiler(cwd: string): Any {\n\tconst mdxModule = tryLoadModule(cwd, '@mdx-js/mdx');\n\tif (!mdxModule) {\n\t\tconsole.error('[reroute/mdx] Could not load @mdx-js/mdx from any path');\n\t\treturn null;\n\t}\n\treturn mdxModule.compile;\n}\n\nfunction setupRemarkPlugins(cwd: string, enableGfm: boolean): Any[] {\n\tconst remarkPlugins = [] as Any[];\n\n\tif (!enableGfm) return remarkPlugins;\n\n\tconst remarkGfm = tryLoadModule(cwd, 'remark-gfm');\n\tif (remarkGfm) {\n\t\tremarkPlugins.push(remarkGfm.default || remarkGfm);\n\t} else {\n\t\tconsole.warn(\n\t\t\t'[reroute/mdx] remark-gfm not available for MDX compilation (Streamdown includes GFM for .md files)',\n\t\t);\n\t}\n\n\treturn remarkPlugins;\n}\n\nfunction setupRehypePlugins(cwd: string, theme: string): Any[] {\n\tconst rehypePlugins = [] as Any[];\n\n\tconst rehypeRaw = tryLoadModule(cwd, 'rehype-raw');\n\tif (rehypeRaw) {\n\t\trehypePlugins.push([\n\t\t\trehypeRaw.default || rehypeRaw,\n\t\t\t{\n\t\t\t\tpassThrough: [\n\t\t\t\t\t'mdxFlowExpression',\n\t\t\t\t\t'mdxJsxFlowElement',\n\t\t\t\t\t'mdxJsxTextElement',\n\t\t\t\t\t'mdxTextExpression',\n\t\t\t\t\t'mdxjsEsm',\n\t\t\t\t],\n\t\t\t},\n\t\t]);\n\t\trehypePlugins.push(rehypeHtmlToJsx);\n\t}\n\n\tconst rehypeShiki = tryLoadModule(cwd, '@shikijs/rehype');\n\tif (rehypeShiki) {\n\t\trehypePlugins.push([rehypeShiki.default || rehypeShiki, { theme }]);\n\t} else {\n\t\tconsole.warn(\n\t\t\t'[reroute/mdx] @shikijs/rehype not available, skipping highlighting',\n\t\t);\n\t}\n\n\treturn rehypePlugins;\n}\n\nfunction formatMdxError(error: unknown, options: { filename?: string }): void {\n\tconst errorMessage = error instanceof Error ? error.message : String(error);\n\tconst errorLocation =\n\t\terror &&\n\t\ttypeof error === 'object' &&\n\t\t'place' in error &&\n\t\terror.place &&\n\t\ttypeof error.place === 'object'\n\t\t\t? `at line ${(error.place as Any).line || '?'}, column ${(error.place as Any).column || '?'}`\n\t\t\t: '';\n\tconst fileInfo = options.filename ? ` in ${options.filename}` : '';\n\n\tconsole.warn(\n\t\t`[reroute/mdx] Compilation failed${fileInfo}${errorLocation ? ` ${errorLocation}` : ''}: ${errorMessage}`,\n\t);\n\tconsole.warn(\n\t\t'[reroute/mdx] MDX compilation failed (Streamdown will handle .md files)',\n\t);\n}\n\n/**\n * Compile MDX content to JSX using @mdx-js/mdx\n */\nasync function compileMdx(\n\tcontent: string,\n\tcwd: string,\n\toptions: {\n\t\tenableGfm?: boolean;\n\t\ttheme?: string;\n\t\tfilename?: string;\n\t} = {},\n): Promise<string | null> {\n\tif (!isMdxAvailable(cwd)) {\n\t\tconsole.warn(\n\t\t\t'[reroute/mdx] @mdx-js/mdx not installed. Install with: bun add @mdx-js/mdx',\n\t\t);\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst compile = loadMdxCompiler(cwd);\n\t\tif (!compile) return null;\n\n\t\tconst { enableGfm = true, theme = 'github-dark' } = options;\n\n\t\tconst remarkPlugins = setupRemarkPlugins(cwd, enableGfm);\n\t\tconst rehypePlugins = setupRehypePlugins(cwd, theme);\n\n\t\tconst result = await compile(content, {\n\t\t\tremarkPlugins,\n\t\t\trehypePlugins,\n\t\t\toutputFormat: 'program',\n\t\t\tdevelopment: false,\n\t\t});\n\n\t\treturn String(result);\n\t} catch (error) {\n\t\tformatMdxError(error, options);\n\t\treturn null;\n\t}\n}\n\n/**\n * Rewrite relative imports in compiled MDX to be correct from the target location\n */\nfunction rewriteImportPaths(\n\tcode: string,\n\tsourcePath: string,\n\ttargetPath: string,\n): string {\n\t// Find all import statements with relative paths\n\tconst importRegex =\n\t\t/import\\s+(?:{[^}]+}|[\\w\\s,]+)\\s+from\\s+['\"](\\.[^'\"]+)['\"]/g;\n\n\treturn code.replace(importRegex, (match, importPath) => {\n\t\t// Resolve the import path relative to the source file\n\t\tconst sourceDir = dirname(sourcePath);\n\t\tconst absoluteImportPath = resolve(sourceDir, importPath);\n\n\t\t// Calculate the new relative path from the target file\n\t\tconst targetDir = dirname(targetPath);\n\t\tlet newRelativePath = relative(targetDir, absoluteImportPath);\n\n\t\t// Ensure the path starts with ./ or ../\n\t\tif (!newRelativePath.startsWith('.')) {\n\t\t\tnewRelativePath = `./${newRelativePath}`;\n\t\t}\n\n\t\t// Replace backslashes with forward slashes for consistency\n\t\tnewRelativePath = newRelativePath.replace(/\\\\/g, '/');\n\n\t\t// Replace the import path in the original match\n\t\treturn match.replace(importPath, newRelativePath);\n\t});\n}\n\n/**\n * Generate a React component wrapper for markdown content\n * This creates a TypeScript module that can be imported as a route\n */\nexport async function generateMarkdownModule(\n\tprocessed: ProcessedMarkdown,\n\tcwd: string,\n\toptions: {\n\t\tenableGfm?: boolean;\n\t\ttheme?: string;\n\t\tisMdx?: boolean;\n\t\tfilename?: string;\n\t\tsourcePath?: string;\n\t\ttargetPath?: string;\n\t\tcomponentsPath?: string;\n\t} = {},\n): Promise<string> {\n\tconst { frontmatter, content } = processed;\n\tconst {\n\t\tenableGfm = true,\n\t\ttheme = 'github-dark',\n\t\tisMdx = false,\n\t\tfilename,\n\t\tsourcePath,\n\t\ttargetPath,\n\t\tcomponentsPath,\n\t} = options;\n\n\t// Prefer MDX engine for both .mdx and .md when available to ensure\n\t// identical SSR/CSR output. MD is a subset of MDX, so compile it too.\n\tlet compiledMdx = await compileMdx(content, cwd, {\n\t\tenableGfm,\n\t\ttheme,\n\t\tfilename,\n\t});\n\tif (compiledMdx) {\n\t\t// Rewrite import paths if source and target paths are provided\n\t\tif (sourcePath && targetPath) {\n\t\t\tcompiledMdx = rewriteImportPaths(compiledMdx, sourcePath, targetPath);\n\t\t}\n\t\t// Add TypeScript type definitions for MDX props\n\t\t// Using proper JSX types to avoid ElementType errors\n\t\tconst typeDefinitions = `// TypeScript types for MDX components\ntype MDXProps = {\n components?: Record<string, React.ComponentType<any>>;\n [key: string]: any;\n};\n\n// Suppress JSX element type checking for MDX-generated code\n// @ts-nocheck enables this for the entire generated file\n`;\n\n\t\t// Fix type annotations in the compiled MDX by adding types to function parameters\n\t\t// Also remove 'export default' from MDXContent if we're adding custom components wrapper\n\t\tconst typedMdx = compiledMdx\n\t\t\t.replace(\n\t\t\t\t/function\\s+_createMdxContent\\s*\\(\\s*props\\s*\\)/g,\n\t\t\t\t'function _createMdxContent(props: MDXProps)',\n\t\t\t)\n\t\t\t.replace(\n\t\t\t\t/function\\s+MDXContent\\s*\\(\\s*props\\s*\\)/g,\n\t\t\t\t'function MDXContent(props: MDXProps)',\n\t\t\t);\n\n\t\t// MDX already supports passing components as props at runtime\n\t\t// No need to import them at build time - this avoids bundling issues\n\n\t\t// Already a complete program that exports a default component\n\t\treturn `// @ts-nocheck\n// Generated from ${isMdx ? 'MDX' : 'Markdown'} file with @mdx-js/mdx\n// TypeScript checking is disabled for MDX-generated code\n\n${typeDefinitions}\n\n// MDX compiled content with imports and component\n${typedMdx}\n\n// Frontmatter exports\nexport const meta = ${JSON.stringify(frontmatter, null, 2)};\n\n// Table of Contents\nexport const toc = ${JSON.stringify(extractHeadings(content), null, 2)};\n\nexport const ssr = {\n head: [\n '<meta property=\"og:type\" content=\"article\" />',\n '<meta name=\"twitter:card\" content=\"summary_large_image\" />',\n ],\n};\n`;\n\t}\n\n\t// Regular markdown - wrap with Markdown component\n\tconst escapedContent = content\n\t\t.replace(/\\\\/g, '\\\\\\\\')\n\t\t.replace(/`/g, '\\\\`')\n\t\t.replace(/\\$/g, '\\\\$');\n\n\t// Build plugin imports\n\tconst pluginImports: string[] = [];\n\tconst remarkPluginsList: string[] = [];\n\tconst rehypePluginsList: string[] = [];\n\n\t// Add custom components import if provided\n\tif (componentsPath) {\n\t\tpluginImports.push(`import markdownComponents from '${componentsPath}';`);\n\t}\n\n\t// Note: GFM is built into Streamdown, no need to import remark-gfm for .md files\n\t// This is only used for MDX files\n\tif (enableGfm) {\n\t\tpluginImports.push(\"import remarkGfm from 'remark-gfm';\");\n\t\tremarkPluginsList.push('remarkGfm');\n\t}\n\n\t// Note: Shiki is async and breaks SSR - skip it\n\n\tconst pluginImportsStr =\n\t\tpluginImports.length > 0 ? `${pluginImports.join('\\n')}\\n` : '';\n\tconst remarkPluginsStr =\n\t\tremarkPluginsList.length > 0\n\t\t\t? `remarkPlugins={[${remarkPluginsList.join(', ')}]}`\n\t\t\t: '';\n\tconst rehypePluginsStr =\n\t\trehypePluginsList.length > 0\n\t\t\t? `rehypePlugins={[${rehypePluginsList.join(', ')}]}`\n\t\t\t: '';\n\tconst componentsStr = componentsPath ? 'components={markdownComponents}' : '';\n\tconst pluginsPropsStr = [remarkPluginsStr, rehypePluginsStr, componentsStr]\n\t\t.filter(Boolean)\n\t\t.join('\\n ');\n\n\treturn `// Generated from markdown file\nimport { Markdown } from 'reroute-js/react';\n${pluginImportsStr}\nconst meta = ${JSON.stringify(frontmatter, null, 2)};\n\nconst ssr = {\n head: [\n '<meta property=\"og:type\" content=\"article\" />',\n '<meta name=\"twitter:card\" content=\"summary_large_image\" />',\n ],\n};\n\nconst markdownContent = \\`${escapedContent}\\`;\n\nfunction MarkdownRoute() {\n return (\n <div>\n <Markdown\n ${pluginsPropsStr}\n >\n {markdownContent}\n </Markdown>\n </div>\n );\n}\n\n// Table of Contents\nexport const toc = ${JSON.stringify(extractHeadings(content), null, 2)};\n\nexport { meta, ssr, toc };\nexport default MarkdownRoute;\n`;\n}\n\n/**\n * Generate a plain fallback TSX module when markdown tooling\n * is not configured. Renders raw markdown content in a <pre> block\n * so routes still work without extra deps.\n */\nexport function generatePlainMarkdownModule(\n\tprocessed: ProcessedMarkdown,\n): string {\n\tconst { frontmatter, content } = processed;\n\tconst escapedContent = content\n\t\t.replace(/\\\\/g, '\\\\\\\\')\n\t\t.split(String.fromCharCode(96))\n\t\t.join(`\\\\${String.fromCharCode(96)}`)\n\t\t.split('$')\n\t\t.join('\\\\$');\n\n\treturn `// Generated from markdown file (plain fallback)\n// No markdown renderer detected; showing raw content.\nexport const meta = ${JSON.stringify(frontmatter, null, 2)};\n\n// Table of Contents (empty for fallback)\nexport const toc = [];\n\nexport const ssr = {\n head: [\n '<meta property=\"og:type\" content=\"article\" />',\n '<meta name=\"twitter:card\" content=\"summary_large_image\" />',\n ],\n};\n\nconst markdownContent = \\`${escapedContent}\\`;\n\nexport default function MarkdownFallback() {\n return (\n <div style={{ padding: '1rem' }}>\n {meta?.title && (\n <h1 style={{ fontSize: '1.5rem', fontWeight: 600 }}>{meta.title}</h1>\n )}\n <div style={{\n margin: '0.75rem 0',\n padding: '0.75rem',\n borderLeft: '4px solid #eab308',\n background: '#fefce8',\n color: '#713f12'\n }}>\n <strong>Markdown rendering not configured.</strong>\n <div style={{ marginTop: '0.25rem' }}>\n Install: streamdown (required - includes GFM, syntax highlighting, math, and mermaid), @mdx-js/mdx (optional for JSX in markdown).\n </div>\n </div>\n <pre style={{ whiteSpace: 'pre-wrap', lineHeight: 1.6 }}>{markdownContent}</pre>\n </div>\n );\n}\n`;\n}\n\n/**\n * Check if a file is a markdown file\n */\nexport function isMarkdownFile(filePath: string): boolean {\n\tconst ext = extname(filePath).toLowerCase();\n\treturn ext === '.md' || ext === '.mdx';\n}\n\n/**\n * Get the route name from a markdown filename\n * Converts kebab-case to the route path\n */\nexport function getMarkdownRouteName(filePath: string): string {\n\tconst name = basename(filePath, extname(filePath));\n\treturn name;\n}\n\n/**\n * Extract headings from markdown content\n * Parses markdown headers (# ## ### etc) and returns structured data\n */\nexport function extractHeadings(\n\tcontent: string,\n\toptions: { minLevel?: number; maxLevel?: number } = {},\n): HeadingNode[] {\n\tconst { minLevel = 1, maxLevel = 6 } = options;\n\tconst headings: HeadingNode[] = [];\n\n\t// Match markdown headings: # Title, ## Title, etc.\n\tconst headingRegex = /^(#{1,6})\\s+(.+)$/gm;\n\tlet match: RegExpExecArray | null = headingRegex.exec(content);\n\n\twhile (match !== null) {\n\t\tconst level = match[1].length;\n\n\t\t// Only include headings within the specified level range\n\t\tif (level >= minLevel && level <= maxLevel) {\n\t\t\tconst text = match[2].trim();\n\t\t\t// Generate slug from heading text (lowercase, replace spaces with hyphens)\n\t\t\tconst slug = text\n\t\t\t\t.toLowerCase()\n\t\t\t\t.replace(/[^\\w\\s-]/g, '')\n\t\t\t\t.replace(/\\s+/g, '-')\n\t\t\t\t.replace(/-+/g, '-')\n\t\t\t\t.trim();\n\n\t\t\theadings.push({ level, text, slug });\n\t\t}\n\n\t\tmatch = headingRegex.exec(content);\n\t}\n\n\treturn headings;\n}\n\n/**\n * Strip markdown syntax and return plain text\n * Useful for creating search-friendly content\n */\nexport function stripMarkdown(content: string): string {\n\tlet text = content;\n\n\t// Keep code block content but remove fences (code is often searchable!)\n\ttext = text.replace(/```(?:\\w+)?\\n?([\\s\\S]*?)```/g, '$1');\n\t// Keep inline code content but remove backticks\n\ttext = text.replace(/`([^`]+)`/g, '$1');\n\n\t// Remove HTML tags\n\ttext = text.replace(/<[^>]+>/g, '');\n\n\t// Remove images\n\ttext = text.replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, '$1');\n\n\t// Remove links but keep text\n\ttext = text.replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1');\n\n\t// Remove headings markers\n\ttext = text.replace(/^#{1,6}\\s+/gm, '');\n\n\t// Remove bold/italic\n\ttext = text.replace(/[*_]{1,3}([^*_]+)[*_]{1,3}/g, '$1');\n\n\t// Remove blockquotes\n\ttext = text.replace(/^>\\s+/gm, '');\n\n\t// Remove horizontal rules\n\ttext = text.replace(/^[-*_]{3,}$/gm, '');\n\n\t// Remove list markers\n\ttext = text.replace(/^[-*+]\\s+/gm, '');\n\ttext = text.replace(/^\\d+\\.\\s+/gm, '');\n\n\t// Normalize whitespace\n\ttext = text.replace(/\\n{3,}/g, '\\n\\n');\n\ttext = text.trim();\n\n\treturn text;\n}\n",
|
|
9
57
|
"/**\n * Production mode utility\n *\n * Provides utilities to check if the CLI is running in production mode.\n * Production mode can be enabled either via NODE_ENV=production or --prod flag.\n */\n\n/**\n * Check if production mode is enabled\n * @param args Command line arguments to check for --prod flag\n * @returns true if --prod flag is present\n */\nexport function isProductionMode(args: string[] = []): boolean {\n\treturn args.includes('--prod');\n}\n\n/**\n * Set NODE_ENV to production if --prod flag is present\n * This ensures child processes inherit the production setting\n * @param args Command line arguments to check for --prod flag\n */\nexport function ensureProductionEnv(args: string[] = []): void {\n\tif (args.includes('--prod')) {\n\t\tprocess.env.NODE_ENV = 'production';\n\t\tBun.env.NODE_ENV = 'production';\n\t}\n}\n\n/**\n * Filter out --prod flag from arguments since it's handled globally\n * @param args Command line arguments\n * @returns Filtered arguments without --prod\n */\nexport function filterProdFlag(args: string[]): string[] {\n\treturn args.filter((arg) => arg !== '--prod');\n}\n",
|
|
10
|
-
"/**\n * Server utilities for waiting and polling\n */\n\n/**\n * Wait for server to be ready by checking if it responds to HTTP requests\n * Uses exponential backoff for efficient polling without artificial delays\n * This ensures the server is fully initialized before declaring it ready\n */\nexport async function waitForServerReady(\n\tport = 3000,\n\ttimeout = 30000, // 30 seconds, more reasonable for large projects\n): Promise<void> {\n\tconst startTime = Date.now();\n\tlet checkInterval = 50; // Start with 50ms\n\tconst maxInterval = 1000; // Max 1 second between checks\n\n\twhile (Date.now() - startTime < timeout) {\n\t\ttry {\n\t\t\tconst response = await fetch(`http://localhost:${port}/`, {\n\t\t\t\tmethod: 'GET',\n\t\t\t\tsignal: AbortSignal.timeout(2000), // 2 second timeout per request\n\t\t\t});\n\t\t\t// Accept both success and 404 (means server is responding)\n\t\t\tif (response.status < 500) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Server not ready yet, continue with exponential backoff\n\t\t}\n\n\t\tawait new Promise((resolve) => setTimeout(resolve, checkInterval));\n\n\t\t// Exponential backoff: increase interval but cap at maxInterval\n\t\tcheckInterval = Math.min(checkInterval * 1.5, maxInterval);\n\t}\n\n\tthrow new Error(\n\t\t`Timeout waiting for server to respond after ${timeout}ms. ` +\n\t\t\t`Make sure your server is starting correctly on port ${port}.`,\n\t);\n}\n",
|
|
11
|
-
"/**\n * Client CSS sync utility\n *\n * Bundles
|
|
58
|
+
"/**\n * Server utilities for waiting and polling\n */\n\nexport async function isServerRunning(\n\tport: number,\n\ttimeout = 1000,\n): Promise<boolean> {\n\ttry {\n\t\tconst response = await fetch(`http://localhost:${port}/`, {\n\t\t\tmethod: 'GET',\n\t\t\tsignal: AbortSignal.timeout(timeout),\n\t\t});\n\t\treturn response.status < 500;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Wait for server to be ready by checking if it responds to HTTP requests\n * Uses exponential backoff for efficient polling without artificial delays\n * This ensures the server is fully initialized before declaring it ready\n */\nexport async function waitForServerReady(\n\tport = 3000,\n\ttimeout = 30000, // 30 seconds, more reasonable for large projects\n): Promise<void> {\n\tconst startTime = Date.now();\n\tlet checkInterval = 50; // Start with 50ms\n\tconst maxInterval = 1000; // Max 1 second between checks\n\n\twhile (Date.now() - startTime < timeout) {\n\t\ttry {\n\t\t\tconst response = await fetch(`http://localhost:${port}/`, {\n\t\t\t\tmethod: 'GET',\n\t\t\t\tsignal: AbortSignal.timeout(2000), // 2 second timeout per request\n\t\t\t});\n\t\t\t// Accept both success and 404 (means server is responding)\n\t\t\tif (response.status < 500) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Server not ready yet, continue with exponential backoff\n\t\t}\n\n\t\tawait new Promise((resolve) => setTimeout(resolve, checkInterval));\n\n\t\t// Exponential backoff: increase interval but cap at maxInterval\n\t\tcheckInterval = Math.min(checkInterval * 1.5, maxInterval);\n\t}\n\n\tthrow new Error(\n\t\t`Timeout waiting for server to respond after ${timeout}ms. ` +\n\t\t\t`Make sure your server is starting correctly on port ${port}.`,\n\t);\n}\n",
|
|
59
|
+
"/**\n * Client CSS sync utility\n *\n * Bundles the client CSS entry (resolving @import) into\n * .reroute/bundles/index.css and .reroute/bundles/index.<hash>.css\n * for serving at /bundles/… alongside the JS.\n *\n * Default entry is src/client/index.css (parent of app.src).\n *\n * DX pattern — keep the design system modular and import from the entry:\n *\n * src/client/index.css → @import './theme/index.css';\n * src/client/theme/index.css → tokens, fonts, base, components…\n *\n * Absolute public URLs (e.g. url('/assets/fonts/…')) and remote @imports\n * are left untouched. Relative url() assets are not rewritten.\n */\n\nimport { createHash } from 'node:crypto';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { mkdir, readdir, unlink, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, resolve } from 'node:path';\nimport { loadConfig, resolveAppPaths } from 'reroute-js/core';\n\nconst HASHED_THEME_RE = /^index\\.[a-z0-9]{8,}\\.css$/i;\n\nasync function getCssPaths(cwd: string) {\n\tconst paths = resolveAppPaths(await loadConfig(cwd));\n\tconst input = join(cwd, paths.cssEntry);\n\tconst bundlesDir = join(cwd, '.reroute/bundles');\n\n\treturn { input, bundlesDir };\n}\n\nfunction themeCssFileName(css: string): string {\n\tconst hash = createHash('sha1').update(css).digest('hex').slice(0, 8);\n\treturn `index.${hash}.css`;\n}\n\n/**\n * Match @import with optional url(), quoted/unquoted path, and trailing condition.\n * Examples:\n * @import url('./theme/index.css');\n * @import './theme/index.css';\n * @import url('./print.css') print;\n */\nconst IMPORT_RE =\n\t/@import\\s+(?:url\\(\\s*)?(?:'([^']+)'|\"([^\"]+)\"|([^'\")\\s]+))\\s*\\)?\\s*([^;]*);/g;\n\nfunction isFileImport(specifier: string): boolean {\n\tif (\n\t\tspecifier.startsWith('http://') ||\n\t\tspecifier.startsWith('https://') ||\n\t\tspecifier.startsWith('data:') ||\n\t\tspecifier.startsWith('/')\n\t) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\n/**\n * Resolve relative @import into a single CSS string. Non-file imports\n * (absolute `/…`, http(s), data, or @import with media conditions) stay as-is.\n */\nexport function bundleCssEntrypoint(entryPath: string): string {\n\tconst absEntry = resolve(entryPath);\n\tconst stack = new Set<string>();\n\tconst completed = new Set<string>();\n\n\tfunction bundleFile(filePath: string): string {\n\t\tconst absPath = resolve(filePath);\n\n\t\t// Already inlined earlier in this graph — skip duplicates.\n\t\tif (completed.has(absPath)) {\n\t\t\treturn '';\n\t\t}\n\t\tif (stack.has(absPath)) {\n\t\t\tthrow new Error(`[reroute/theme] Circular @import detected: ${absPath}`);\n\t\t}\n\t\tif (!existsSync(absPath)) {\n\t\t\tthrow new Error(`[reroute/theme] Missing CSS import: ${absPath}`);\n\t\t}\n\n\t\tstack.add(absPath);\n\t\tconst source = readFileSync(absPath, 'utf-8');\n\t\tconst baseDir = dirname(absPath);\n\n\t\tconst bundled = source.replace(\n\t\t\tIMPORT_RE,\n\t\t\t(full, single: string, double: string, bare: string, rest: string) => {\n\t\t\t\tconst specifier = single || double || bare;\n\t\t\t\tconst condition = (rest ?? '').trim();\n\n\t\t\t\t// Keep conditional / non-file imports for the browser to resolve.\n\t\t\t\tif (condition || !isFileImport(specifier)) {\n\t\t\t\t\treturn full;\n\t\t\t\t}\n\n\t\t\t\tconst importedPath = isAbsolute(specifier)\n\t\t\t\t\t? specifier\n\t\t\t\t\t: resolve(baseDir, specifier);\n\n\t\t\t\treturn `/* ${specifier} */\\n${bundleFile(importedPath)}\\n`;\n\t\t\t},\n\t\t);\n\n\t\tstack.delete(absPath);\n\t\tcompleted.add(absPath);\n\t\treturn bundled;\n\t}\n\n\treturn bundleFile(absEntry);\n}\n\nasync function pruneStaleThemeCss(\n\tbundlesDir: string,\n\tkeep: string,\n): Promise<void> {\n\tconst files = await readdir(bundlesDir);\n\tfor (const file of files) {\n\t\tif (HASHED_THEME_RE.test(file) && file !== keep) {\n\t\t\tawait unlink(join(bundlesDir, file));\n\t\t}\n\t}\n}\n\n/**\n * Bundle index.css (with @import resolution) into .reroute/bundles.\n * Returns true if a sync was performed, false if skipped.\n */\nexport async function syncThemeCss(cwd: string): Promise<boolean> {\n\tconst { input, bundlesDir } = await getCssPaths(cwd);\n\n\tif (!existsSync(input)) {\n\t\treturn false;\n\t}\n\n\tawait mkdir(bundlesDir, { recursive: true });\n\n\ttry {\n\t\tconst css = bundleCssEntrypoint(input);\n\t\tconst hashedName = themeCssFileName(css);\n\t\tawait writeFile(join(bundlesDir, 'index.css'), css, 'utf-8');\n\t\tawait writeFile(join(bundlesDir, hashedName), css, 'utf-8');\n\t\tawait pruneStaleThemeCss(bundlesDir, hashedName);\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tif (message.startsWith('[reroute/theme]')) {\n\t\t\tthrow err;\n\t\t}\n\t\tthrow new Error(`[reroute/theme] Failed to bundle index.css:\\n${message}`);\n\t}\n\n\tconsole.log('[reroute/theme] synced /bundles/index.css');\n\treturn true;\n}\n",
|
|
12
60
|
"import path from 'node:path';\n\n// Version and commit injected at build time\ndeclare const __REROUTE_VERSION__: string;\ndeclare const __REROUTE_COMMIT__: string;\n\nexport async function getVersionString(): Promise<string> {\n\tconst version = await getVersion();\n\tconst commit = await getCommit();\n\treturn `Reroute v${version} ${commit}`;\n}\n\nasync function getVersion(): Promise<string> {\n\t// Use version injected at build time\n\tif (typeof __REROUTE_VERSION__ !== 'undefined') {\n\t\treturn __REROUTE_VERSION__;\n\t}\n\n\t// Fallback for development: try to find package.json\n\tconst possiblePaths = [\n\t\tpath.join(import.meta.dir, '../../../../package.json'),\n\t\tpath.join(import.meta.dir, '../../../package.json'),\n\t\tpath.join(import.meta.dir, '../../package.json'),\n\t\tpath.join(import.meta.dir, '../package.json'),\n\t];\n\n\tfor (const pkgPath of possiblePaths) {\n\t\ttry {\n\t\t\tconst pkg = await Bun.file(pkgPath).json();\n\t\t\treturn pkg.version;\n\t\t} catch {}\n\t}\n\n\treturn 'unknown';\n}\n\nasync function getCommit(): Promise<string> {\n\t// Use commit injected at build time\n\tif (typeof __REROUTE_COMMIT__ !== 'undefined') {\n\t\treturn __REROUTE_COMMIT__;\n\t}\n\n\t// Fallback for development\n\treturn 'dev';\n}\n"
|
|
13
61
|
],
|
|
14
|
-
"mappings": "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",
|
|
15
|
-
"debugId": "
|
|
62
|
+
"mappings": "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",
|
|
63
|
+
"debugId": "A3AF9FC7095ACC7D64756E2164756E21",
|
|
16
64
|
"names": []
|
|
17
65
|
}
|