@pyreon/zero 0.12.10 → 0.12.12

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/lib/ai.js CHANGED
@@ -10,9 +10,20 @@ const ROUTE_EXTENSIONS = [
10
10
  *
11
11
  * @param files Array of file paths like ["index.tsx", "users/[id].tsx"]
12
12
  * @param defaultMode Default rendering mode from config
13
+ * @param exportsMap Optional map of filePath → detected exports. When
14
+ * provided, the resulting FileRoute objects carry export info that the
15
+ * code generator uses to optimize imports (skip metadata namespace
16
+ * imports for routes that only export `default`).
13
17
  */
14
- function parseFileRoutes(files, defaultMode = "ssr") {
15
- return files.filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext))).map((filePath) => parseFilePath(filePath, defaultMode)).sort(sortRoutes);
18
+ function parseFileRoutes(files, defaultMode = "ssr", exportsMap) {
19
+ return files.filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext))).map((filePath) => {
20
+ const route = parseFilePath(filePath, defaultMode);
21
+ const exp = exportsMap?.get(filePath);
22
+ return exp ? {
23
+ ...route,
24
+ exports: exp
25
+ } : route;
26
+ }).sort(sortRoutes);
16
27
  }
17
28
  function parseFilePath(filePath, defaultMode) {
18
29
  let route = filePath;
package/lib/ai.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ai.js","names":[],"sources":["../src/fs-router.ts","../src/ai.ts"],"sourcesContent":["import type { FileRoute, RenderMode } from './types'\n\n// ─── File-system route conventions ──────────────────────────────────────────\n//\n// src/routes/\n// _layout.tsx → layout for all routes\n// index.tsx → /\n// about.tsx → /about\n// users/\n// _layout.tsx → layout for /users/*\n// _loading.tsx → loading fallback for /users/*\n// _error.tsx → error boundary for /users/*\n// index.tsx → /users\n// [id].tsx → /users/:id\n// [id]/\n// settings.tsx → /users/:id/settings\n// blog/\n// [...slug].tsx → /blog/* (catch-all)\n//\n// Conventions:\n// [param] → dynamic segment → :param\n// [...param] → catch-all → :param*\n// _layout → layout wrapper — must use <RouterView /> to render child routes\n// (props.children is NOT passed — the router handles nesting)\n// _error → error component\n// _loading → loading component\n// _404 → not-found component (renders on 404)\n// _not-found → alias for _404\n// (group) → route group (directory ignored in URL)\n\nconst ROUTE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js']\n\n/**\n * Parse a set of file paths (relative to routes dir) into FileRoute objects.\n *\n * @param files Array of file paths like [\"index.tsx\", \"users/[id].tsx\"]\n * @param defaultMode Default rendering mode from config\n */\nexport function parseFileRoutes(files: string[], defaultMode: RenderMode = 'ssr'): FileRoute[] {\n return files\n .filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext)))\n .map((filePath) => parseFilePath(filePath, defaultMode))\n .sort(sortRoutes)\n}\n\nfunction parseFilePath(filePath: string, defaultMode: RenderMode): FileRoute {\n // Remove extension\n let route = filePath\n for (const ext of ROUTE_EXTENSIONS) {\n if (route.endsWith(ext)) {\n route = route.slice(0, -ext.length)\n break\n }\n }\n\n const fileName = getFileName(route)\n const isLayout = fileName === '_layout'\n const isError = fileName === '_error'\n const isLoading = fileName === '_loading'\n const isNotFound = fileName === '_404' || fileName === '_not-found'\n const isCatchAll = route.includes('[...')\n\n // Get directory path (strip groups for consistent grouping)\n const parts = route.split('/')\n parts.pop() // remove filename\n const dirPath = parts.filter((s) => !(s.startsWith('(') && s.endsWith(')'))).join('/')\n\n // Convert file path to URL pattern\n const urlPath = filePathToUrlPath(route)\n const depth = urlPath === '/' ? 0 : urlPath.split('/').filter(Boolean).length\n\n return {\n filePath,\n urlPath,\n dirPath,\n depth,\n isLayout,\n isError,\n isLoading,\n isNotFound,\n isCatchAll,\n renderMode: defaultMode,\n }\n}\n\n/**\n * Convert a file path (without extension) to a URL path pattern.\n *\n * Examples:\n * \"index\" → \"/\"\n * \"about\" → \"/about\"\n * \"users/index\" → \"/users\"\n * \"users/[id]\" → \"/users/:id\"\n * \"blog/[...slug]\" → \"/blog/:slug*\"\n * \"(auth)/login\" → \"/login\" (group stripped)\n * \"_layout\" → \"/\" (layout marker)\n */\nexport function filePathToUrlPath(filePath: string): string {\n const segments = filePath.split('/')\n const urlSegments: string[] = []\n\n for (const seg of segments) {\n // Skip route groups \"(name)\"\n if (seg.startsWith('(') && seg.endsWith(')')) continue\n\n // Skip special files\n if (seg === '_layout' || seg === '_error' || seg === '_loading' || seg === '_404' || seg === '_not-found') continue\n\n // \"index\" maps to the parent path\n if (seg === 'index') continue\n\n // Catch-all: [...param] → :param*\n const catchAll = seg.match(/^\\[\\.\\.\\.(\\w+)\\]$/)\n if (catchAll) {\n urlSegments.push(`:${catchAll[1]}*`)\n continue\n }\n\n // Dynamic: [param] → :param\n const dynamic = seg.match(/^\\[(\\w+)\\]$/)\n if (dynamic) {\n urlSegments.push(`:${dynamic[1]}`)\n continue\n }\n\n urlSegments.push(seg)\n }\n\n const path = `/${urlSegments.join('/')}`\n return path || '/'\n}\n\n/** Sort routes: static before dynamic, catch-all last. */\nfunction sortRoutes(a: FileRoute, b: FileRoute): number {\n // Catch-all routes go last\n if (a.isCatchAll !== b.isCatchAll) return a.isCatchAll ? 1 : -1\n // Layouts go first within same depth\n if (a.isLayout !== b.isLayout) return a.isLayout ? -1 : 1\n // Static segments before dynamic\n const aDynamic = a.urlPath.includes(':')\n const bDynamic = b.urlPath.includes(':')\n if (aDynamic !== bDynamic) return aDynamic ? 1 : -1\n // Alphabetical\n return a.urlPath.localeCompare(b.urlPath)\n}\n\nfunction getFileName(filePath: string): string {\n const parts = filePath.split('/')\n return parts[parts.length - 1] ?? ''\n}\n\n// ─── Route generation (for Vite plugin) ─────────────────────────────────────\n\n/** Internal tree node for building nested route structures. */\ninterface RouteNode {\n /** Page routes at this directory level. */\n pages: FileRoute[]\n /** Layout file for this directory (if any). */\n layout?: FileRoute\n /** Error boundary file (if any). */\n error?: FileRoute\n /** Loading fallback file (if any). */\n loading?: FileRoute\n /** Not-found (404) file (if any). */\n notFound?: FileRoute\n /** Child directories. */\n children: Map<string, RouteNode>\n}\n\n/**\n * Group flat file routes into a directory tree.\n */\nfunction getOrCreateChild(node: RouteNode, segment: string): RouteNode {\n let child = node.children.get(segment)\n if (!child) {\n child = { pages: [], children: new Map() }\n node.children.set(segment, child)\n }\n return child\n}\n\nfunction resolveNode(root: RouteNode, dirPath: string): RouteNode {\n let node = root\n if (dirPath) {\n for (const segment of dirPath.split('/')) {\n node = getOrCreateChild(node, segment)\n }\n }\n return node\n}\n\nfunction placeRoute(node: RouteNode, route: FileRoute) {\n if (route.isLayout) node.layout = route\n else if (route.isError) node.error = route\n else if (route.isLoading) node.loading = route\n else if (route.isNotFound) node.notFound = route\n else node.pages.push(route)\n}\n\nfunction buildRouteTree(routes: FileRoute[]): RouteNode {\n const root: RouteNode = { pages: [], children: new Map() }\n for (const route of routes) {\n placeRoute(resolveNode(root, route.dirPath), route)\n }\n return root\n}\n\n/**\n * Generate a virtual module that exports a nested route tree.\n * Wires up layouts as parent routes with children, loaders, guards,\n * error/loading components, middleware, and meta from route module exports.\n */\nexport interface GenerateRouteModuleOptions {\n /**\n * When true, skip lazy() for route components and use static imports.\n * Use for SSG/prerender mode where all routes are rendered at build time\n * and code splitting provides no benefit. Avoids Rolldown warnings about\n * static + dynamic imports of the same module.\n */\n staticImports?: boolean\n}\n\nexport function generateRouteModule(\n files: string[],\n routesDir: string,\n options?: GenerateRouteModuleOptions,\n): string {\n const routes = parseFileRoutes(files)\n const tree = buildRouteTree(routes)\n const imports: string[] = []\n let importCounter = 0\n const useStaticImports = options?.staticImports ?? false\n\n function nextImport(filePath: string, exportName = 'default'): string {\n const name = `_${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n if (exportName === 'default') {\n imports.push(`import ${name} from \"${fullPath}\"`)\n } else {\n imports.push(`import { ${exportName} as ${name} } from \"${fullPath}\"`)\n }\n return name\n }\n\n function nextLazy(filePath: string, loadingName?: string, errorName?: string): string {\n const name = `_${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n\n if (useStaticImports) {\n // SSG mode: static import avoids Rolldown warnings about\n // static + dynamic imports of the same module\n imports.push(`import ${name} from \"${fullPath}\"`)\n } else {\n const opts: string[] = []\n if (loadingName) opts.push(`loading: ${loadingName}`)\n if (errorName) opts.push(`error: ${errorName}`)\n const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : ''\n imports.push(`const ${name} = lazy(() => import(\"${fullPath}\")${optsStr})`)\n }\n return name\n }\n\n function nextModuleImport(filePath: string): string {\n const name = `_m${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n imports.push(`import * as ${name} from \"${fullPath}\"`)\n return name\n }\n\n function generatePageRoute(\n page: FileRoute,\n indent: string,\n loadingName: string | undefined,\n errorName: string | undefined,\n notFoundName: string | undefined,\n ): string {\n const mod = nextModuleImport(page.filePath)\n const comp = nextLazy(page.filePath, loadingName, errorName)\n\n const props: string[] = [\n `${indent} path: ${JSON.stringify(page.urlPath)}`,\n `${indent} component: ${comp}`,\n `${indent} loader: ${mod}.loader`,\n `${indent} beforeEnter: ${mod}.guard`,\n `${indent} meta: { ...${mod}.meta, renderMode: ${mod}.renderMode }`,\n ]\n\n // Only emit errorComponent when there's an actual _error file in scope\n // or the route module exports an error component. Avoids referencing\n // undefined .error exports that produce noisy bundler warnings.\n if (errorName) {\n props.push(`${indent} errorComponent: ${mod}.error || ${errorName}`)\n }\n\n if (notFoundName) {\n props.push(`${indent} notFoundComponent: ${notFoundName}`)\n }\n\n return `${indent}{\\n${props.join(',\\n')}\\n${indent}}`\n }\n\n function wrapWithLayout(\n node: RouteNode,\n children: string[],\n indent: string,\n errorName: string | undefined,\n notFoundName: string | undefined,\n ): string {\n const layout = node.layout as FileRoute\n const layoutMod = nextModuleImport(layout.filePath)\n const layoutComp = nextImport(layout.filePath, 'layout')\n\n const props: string[] = [\n `${indent}path: ${JSON.stringify(layout.urlPath)}`,\n `${indent}component: ${layoutComp}`,\n `${indent}loader: ${layoutMod}.loader`,\n `${indent}beforeEnter: ${layoutMod}.guard`,\n `${indent}meta: { ...${layoutMod}.meta, renderMode: ${layoutMod}.renderMode }`,\n ]\n if (errorName) {\n props.push(`${indent}errorComponent: ${errorName}`)\n }\n if (notFoundName) {\n props.push(`${indent}notFoundComponent: ${notFoundName}`)\n }\n if (children.length > 0) {\n props.push(`${indent}children: [\\n${children.join(',\\n')}\\n${indent}]`)\n }\n\n return `${indent}{\\n${props.map((p) => ` ${p}`).join(',\\n')}\\n${indent}}`\n }\n\n /**\n * Generate route definitions for a tree node.\n */\n function generateNode(node: RouteNode, depth: number): string[] {\n const indent = ' '.repeat(depth + 1)\n\n const errorName = node.error ? nextImport(node.error.filePath) : undefined\n const loadingName = node.loading ? nextImport(node.loading.filePath) : undefined\n const notFoundName = node.notFound ? nextImport(node.notFound.filePath) : undefined\n\n const childRouteDefs: string[] = []\n for (const [, childNode] of node.children) {\n childRouteDefs.push(...generateNode(childNode, depth + 1))\n }\n\n const pageRouteDefs = node.pages.map((page) =>\n generatePageRoute(page, indent, loadingName, errorName, notFoundName),\n )\n\n const allChildren = [...pageRouteDefs, ...childRouteDefs]\n\n if (node.layout) {\n return [wrapWithLayout(node, allChildren, indent, errorName, notFoundName)]\n }\n return allChildren\n }\n\n const routeDefs = generateNode(tree, 0)\n\n return [\n `import { lazy } from \"@pyreon/router\"`,\n '',\n ...imports,\n '',\n // Filter out undefined properties at runtime\n `function clean(routes) {`,\n ` return routes.map(r => {`,\n ` const c = {}`,\n ` for (const k in r) if (r[k] !== undefined) c[k] = r[k]`,\n ` if (c.children) c.children = clean(c.children)`,\n ` return c`,\n ` })`,\n `}`,\n '',\n `export const routes = clean([`,\n routeDefs.join(',\\n'),\n `])`,\n ].join('\\n')\n}\n\n/**\n * Generate a virtual module that maps URL patterns to their middleware exports.\n * Used by the server entry to dispatch per-route middleware.\n */\nexport function generateMiddlewareModule(files: string[], routesDir: string): string {\n const routes = parseFileRoutes(files)\n const imports: string[] = []\n const entries: string[] = []\n let counter = 0\n\n for (const route of routes) {\n if (route.isLayout || route.isError || route.isLoading || route.isNotFound) continue\n const name = `_mw${counter++}`\n const fullPath = `${routesDir}/${route.filePath}`\n imports.push(`import { middleware as ${name} } from \"${fullPath}\"`)\n entries.push(` { pattern: ${JSON.stringify(route.urlPath)}, middleware: ${name} }`)\n }\n\n return [\n ...imports,\n '',\n `export const routeMiddleware = [`,\n entries.join(',\\n'),\n `].filter(e => e.middleware)`,\n ].join('\\n')\n}\n\n/**\n * Scan a directory for route files.\n * Returns paths relative to the routes directory.\n */\nexport async function scanRouteFiles(routesDir: string): Promise<string[]> {\n const { readdir } = await import('node:fs/promises')\n const { join, relative } = await import('node:path')\n\n const files: string[] = []\n\n async function walk(dir: string) {\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n if (entry.isDirectory()) {\n await walk(fullPath)\n } else if (ROUTE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {\n files.push(relative(routesDir, fullPath))\n }\n }\n }\n\n await walk(routesDir)\n return files\n}\n","/**\n * AI integration utilities for Zero.\n *\n * - llms.txt auto-generation from routes and API routes\n * - JSON-LD auto-inference from route meta + Meta props\n * - AI plugin manifest (/.well-known/ai-plugin.json) from API routes\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { aiPlugin } from \"@pyreon/zero/ai\"\n *\n * export default {\n * plugins: [\n * aiPlugin({\n * name: \"My App\",\n * origin: \"https://example.com\",\n * description: \"A modern web application\",\n * }),\n * ],\n * }\n * ```\n */\nimport type { Plugin } from 'vite'\nimport { parseFileRoutes } from './fs-router'\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface AiPluginConfig {\n /** App/API name. */\n name: string\n /** App description for AI agents. */\n description: string\n /** Base URL. e.g. \"https://example.com\" */\n origin: string\n /** Contact email (required by OpenAI plugin spec). */\n contactEmail?: string\n /** Legal info URL. */\n legalUrl?: string\n /** Logo URL for the plugin. */\n logoUrl?: string\n /** Routes directory relative to project root. Default: \"src/routes\" */\n routesDir?: string\n /** API routes directory relative to project root. Default: \"src/api\" */\n apiDir?: string\n /**\n * API route descriptions — map of pattern to description.\n * Used for llms.txt and ai-plugin.json.\n *\n * @example\n * ```ts\n * apiDescriptions: {\n * \"GET /api/posts\": \"List all blog posts, supports ?page=N&limit=N\",\n * \"GET /api/posts/:id\": \"Get a single post by ID\",\n * \"POST /api/posts\": \"Create a new post (requires auth)\",\n * }\n * ```\n */\n apiDescriptions?: Record<string, string>\n /**\n * Page descriptions — map of URL path to description.\n * Used for llms.txt. Falls back to route meta.title/description.\n */\n pageDescriptions?: Record<string, string>\n /**\n * Additional content to append to llms.txt.\n * Useful for authentication instructions, rate limits, etc.\n */\n llmsExtra?: string\n}\n\n// ─── llms.txt generation ────────────────────────────────────────────────────\n\n/**\n * Generate llms.txt content from route files and config.\n *\n * Format follows the llms.txt proposal:\n * ```\n * # {name}\n * > {description}\n *\n * ## Pages\n * - [/about](/about): About page\n *\n * ## API\n * - GET /api/posts: List posts\n * ```\n *\n * @internal Exported for testing.\n */\nexport function generateLlmsTxt(\n routeFiles: string[],\n apiFiles: string[],\n config: AiPluginConfig,\n): string {\n const lines: string[] = []\n\n // Header\n lines.push(`# ${config.name}`)\n lines.push(`> ${config.description}`)\n lines.push('')\n\n // Pages section\n const routes = parseFileRoutes(routeFiles)\n const pages = routes.filter(\n (r) => !r.isLayout && !r.isError && !r.isLoading && !r.isNotFound\n && !r.isCatchAll && !r.urlPath.includes(':'),\n )\n\n if (pages.length > 0) {\n lines.push('## Pages')\n lines.push('')\n for (const page of pages) {\n const desc = config.pageDescriptions?.[page.urlPath]\n const url = `${config.origin}${page.urlPath === '/' ? '' : page.urlPath}`\n if (desc) {\n lines.push(`- [${page.urlPath}](${url}): ${desc}`)\n } else {\n lines.push(`- [${page.urlPath}](${url})`)\n }\n }\n lines.push('')\n }\n\n // Dynamic routes (documented separately — AI needs to know about params)\n const dynamicRoutes = routes.filter(\n (r) => !r.isLayout && !r.isError && !r.isLoading && !r.isNotFound\n && (r.urlPath.includes(':') || r.isCatchAll),\n )\n if (dynamicRoutes.length > 0) {\n lines.push('## Dynamic Pages')\n lines.push('')\n for (const route of dynamicRoutes) {\n const desc = config.pageDescriptions?.[route.urlPath]\n if (desc) {\n lines.push(`- ${route.urlPath}: ${desc}`)\n } else {\n lines.push(`- ${route.urlPath}`)\n }\n }\n lines.push('')\n }\n\n // API section\n const apiPatterns = parseApiFiles(apiFiles)\n if (apiPatterns.length > 0 || config.apiDescriptions) {\n lines.push('## API Endpoints')\n lines.push('')\n\n // From apiDescriptions (most detailed — user-provided)\n if (config.apiDescriptions) {\n for (const [endpoint, desc] of Object.entries(config.apiDescriptions)) {\n lines.push(`- ${endpoint}: ${desc}`)\n }\n }\n\n // From auto-discovered API files (only those not already described)\n const describedPatterns = new Set(\n Object.keys(config.apiDescriptions ?? {}).map((k) => k.replace(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\s+/, '')),\n )\n for (const pattern of apiPatterns) {\n if (!describedPatterns.has(pattern)) {\n lines.push(`- ${pattern}`)\n }\n }\n lines.push('')\n }\n\n // Extra content\n if (config.llmsExtra) {\n lines.push(config.llmsExtra)\n lines.push('')\n }\n\n return lines.join('\\n')\n}\n\n/**\n * Generate llms-full.txt — expanded version with more detail.\n * Includes all route metadata and API descriptions.\n *\n * @internal Exported for testing.\n */\nexport function generateLlmsFullTxt(\n routeFiles: string[],\n apiFiles: string[],\n config: AiPluginConfig,\n): string {\n const lines: string[] = []\n\n lines.push(`# ${config.name} — Full Reference`)\n lines.push(`> ${config.description}`)\n lines.push('')\n lines.push(`Base URL: ${config.origin}`)\n lines.push('')\n\n // All pages with details\n const routes = parseFileRoutes(routeFiles)\n const pages = routes.filter(\n (r) => !r.isLayout && !r.isError && !r.isLoading && !r.isNotFound,\n )\n\n if (pages.length > 0) {\n lines.push('## All Routes')\n lines.push('')\n for (const page of pages) {\n const desc = config.pageDescriptions?.[page.urlPath] ?? ''\n const dynamic = page.urlPath.includes(':') ? ' (dynamic)' : ''\n const catchAll = page.isCatchAll ? ' (catch-all)' : ''\n lines.push(`### ${page.urlPath}${dynamic}${catchAll}`)\n if (desc) lines.push(desc)\n lines.push(`- File: ${page.filePath}`)\n lines.push(`- Render mode: ${page.renderMode}`)\n lines.push('')\n }\n }\n\n // API endpoints with full detail\n if (config.apiDescriptions) {\n lines.push('## API Reference')\n lines.push('')\n for (const [endpoint, desc] of Object.entries(config.apiDescriptions)) {\n lines.push(`### ${endpoint}`)\n lines.push(desc)\n lines.push('')\n }\n }\n\n if (config.llmsExtra) {\n lines.push('## Additional Information')\n lines.push('')\n lines.push(config.llmsExtra)\n lines.push('')\n }\n\n return lines.join('\\n')\n}\n\n// ─── JSON-LD auto-inference ─────────────────────────────────────────────────\n\nexport interface InferJsonLdOptions {\n /** Page URL. */\n url: string\n /** Page title. */\n title?: string\n /** Page description. */\n description?: string\n /** Page image. */\n image?: string\n /** Site name. */\n siteName?: string\n /** Page type hint. */\n type?: 'website' | 'article' | 'product' | 'profile'\n /** Article metadata. */\n publishedTime?: string\n /** Article author. */\n author?: string\n /** Article tags. */\n tags?: string[]\n /** Breadcrumb path segments. */\n breadcrumbs?: Array<{ name: string; url: string }>\n}\n\n/**\n * Auto-infer JSON-LD structured data from page metadata.\n *\n * Returns an array of JSON-LD objects (multiple schemas can apply to one page).\n * For example, an article page gets both `Article` and `BreadcrumbList`.\n *\n * @example\n * ```tsx\n * const schemas = inferJsonLd({\n * url: \"https://example.com/blog/my-post\",\n * title: \"My Post\",\n * description: \"A great article\",\n * type: \"article\",\n * author: \"Vit Bokisch\",\n * publishedTime: \"2026-03-31\",\n * })\n * // → [Article schema, BreadcrumbList schema]\n * ```\n */\nexport function inferJsonLd(options: InferJsonLdOptions): Record<string, unknown>[] {\n const schemas: Record<string, unknown>[] = []\n\n // Base: WebPage or Article\n if (options.type === 'article') {\n const article: Record<string, unknown> = {\n '@context': 'https://schema.org',\n '@type': 'Article',\n headline: options.title,\n url: options.url,\n }\n if (options.description) article.description = options.description\n if (options.image) article.image = options.image\n if (options.publishedTime) article.datePublished = options.publishedTime\n if (options.author) {\n article.author = { '@type': 'Person', name: options.author }\n }\n if (options.tags && options.tags.length > 0) {\n article.keywords = options.tags.join(', ')\n }\n if (options.siteName) {\n article.publisher = { '@type': 'Organization', name: options.siteName }\n }\n schemas.push(article)\n } else if (options.type === 'product') {\n const product: Record<string, unknown> = {\n '@context': 'https://schema.org',\n '@type': 'Product',\n name: options.title,\n url: options.url,\n }\n if (options.description) product.description = options.description\n if (options.image) product.image = options.image\n schemas.push(product)\n } else {\n const webpage: Record<string, unknown> = {\n '@context': 'https://schema.org',\n '@type': 'WebPage',\n name: options.title,\n url: options.url,\n }\n if (options.description) webpage.description = options.description\n if (options.image) webpage.thumbnailUrl = options.image\n schemas.push(webpage)\n }\n\n // BreadcrumbList from URL path or explicit breadcrumbs\n if (options.breadcrumbs && options.breadcrumbs.length > 0) {\n schemas.push({\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n itemListElement: options.breadcrumbs.map((bc, i) => ({\n '@type': 'ListItem',\n position: i + 1,\n name: bc.name,\n item: bc.url,\n })),\n })\n } else {\n // Auto-generate breadcrumbs from URL path\n const urlObj = safeParseUrl(options.url)\n if (urlObj) {\n const segments = urlObj.pathname.split('/').filter(Boolean)\n if (segments.length > 0) {\n const items = [\n { '@type': 'ListItem', position: 1, name: 'Home', item: urlObj.origin },\n ]\n let path = ''\n for (let i = 0; i < segments.length; i++) {\n path += `/${segments[i]}`\n items.push({\n '@type': 'ListItem',\n position: i + 2,\n name: capitalize(segments[i]!.replace(/-/g, ' ')),\n item: `${urlObj.origin}${path}`,\n })\n }\n schemas.push({\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n itemListElement: items,\n })\n }\n }\n }\n\n return schemas\n}\n\n// ─── AI plugin manifest ─────────────────────────────────────────────────────\n\n/**\n * Generate an OpenAI-compatible AI plugin manifest.\n *\n * Follows the /.well-known/ai-plugin.json spec.\n *\n * @internal Exported for testing.\n */\nexport function generateAiPluginManifest(config: AiPluginConfig): Record<string, unknown> {\n return {\n schema_version: 'v1',\n name_for_human: config.name,\n name_for_model: config.name.toLowerCase().replace(/\\s+/g, '_').replace(/[^a-z0-9_]/g, ''),\n description_for_human: config.description,\n description_for_model: config.description,\n auth: { type: 'none' },\n api: {\n type: 'openapi',\n url: `${config.origin}/.well-known/openapi.yaml`,\n },\n logo_url: config.logoUrl ?? `${config.origin}/favicon.svg`,\n contact_email: config.contactEmail ?? '',\n legal_info_url: config.legalUrl ?? `${config.origin}/legal`,\n }\n}\n\n/**\n * Generate a minimal OpenAPI 3.0 spec from API route descriptions.\n *\n * @internal Exported for testing.\n */\nexport function generateOpenApiSpec(\n apiFiles: string[],\n config: AiPluginConfig,\n): Record<string, unknown> {\n const paths: Record<string, Record<string, unknown>> = {}\n\n // From user-provided descriptions\n if (config.apiDescriptions) {\n for (const [endpoint, desc] of Object.entries(config.apiDescriptions)) {\n const match = endpoint.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\s+(.+)$/)\n if (match) {\n const method = match[1]!.toLowerCase()\n const path = match[2]!\n // Convert :param to {param} for OpenAPI\n const openApiPath = path.replace(/:(\\w+)/g, '{$1}')\n if (!paths[openApiPath]) paths[openApiPath] = {}\n paths[openApiPath][method] = {\n summary: desc,\n responses: { '200': { description: 'Success' } },\n }\n }\n }\n }\n\n // Auto-discovered API files (fill in gaps)\n for (const pattern of parseApiFiles(apiFiles)) {\n const openApiPath = pattern.replace(/:(\\w+)/g, '{$1}')\n if (!paths[openApiPath]) {\n paths[openApiPath] = {\n get: {\n summary: `${openApiPath} endpoint`,\n responses: { '200': { description: 'Success' } },\n },\n }\n }\n }\n\n return {\n openapi: '3.0.0',\n info: {\n title: config.name,\n description: config.description,\n version: '1.0.0',\n },\n servers: [{ url: config.origin }],\n paths,\n }\n}\n\n// ─── Vite plugin ────────────────────────────────────────────────────────────\n\n/**\n * AI integration Vite plugin.\n *\n * Generates at build time:\n * - `/llms.txt` — concise site summary for AI agents\n * - `/llms-full.txt` — detailed reference for AI agents\n * - `/.well-known/ai-plugin.json` — OpenAI plugin manifest\n * - `/.well-known/openapi.yaml` — minimal OpenAPI spec from API routes\n *\n * In dev, serves these files via middleware.\n *\n * @example\n * ```ts\n * import { aiPlugin } from \"@pyreon/zero/ai\"\n *\n * export default {\n * plugins: [\n * aiPlugin({\n * name: \"My App\",\n * origin: \"https://example.com\",\n * description: \"A modern web application\",\n * apiDescriptions: {\n * \"GET /api/posts\": \"List blog posts\",\n * \"GET /api/posts/:id\": \"Get post by ID\",\n * },\n * }),\n * ],\n * }\n * ```\n */\nexport function aiPlugin(config: AiPluginConfig): Plugin {\n let root = ''\n let isBuild = false\n let routeFiles: string[] = []\n let apiFiles: string[] = []\n\n return {\n name: 'pyreon-zero-ai',\n enforce: 'post',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n isBuild = resolvedConfig.command === 'build'\n },\n\n async buildStart() {\n // Scan for route and API files\n try {\n const { join } = await import('node:path')\n\n const routesDir = join(root, config.routesDir ?? 'src/routes')\n const apiDir = join(root, config.apiDir ?? 'src/api')\n\n routeFiles = await scanDir(routesDir, routesDir)\n apiFiles = await scanDir(apiDir, apiDir)\n } catch {\n // Directories may not exist\n }\n },\n\n configureServer(server) {\n server.middlewares.use(async (req, res, next) => {\n const url = req.url ?? ''\n\n if (url === '/llms.txt') {\n res.setHeader('Content-Type', 'text/plain; charset=utf-8')\n res.end(generateLlmsTxt(routeFiles, apiFiles, config))\n return\n }\n\n if (url === '/llms-full.txt') {\n res.setHeader('Content-Type', 'text/plain; charset=utf-8')\n res.end(generateLlmsFullTxt(routeFiles, apiFiles, config))\n return\n }\n\n if (url === '/.well-known/ai-plugin.json') {\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify(generateAiPluginManifest(config), null, 2))\n return\n }\n\n if (url === '/.well-known/openapi.yaml' || url === '/.well-known/openapi.json') {\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify(generateOpenApiSpec(apiFiles, config), null, 2))\n return\n }\n\n next()\n })\n },\n\n async generateBundle() {\n if (!isBuild) return\n\n this.emitFile({\n type: 'asset',\n fileName: 'llms.txt',\n source: generateLlmsTxt(routeFiles, apiFiles, config),\n })\n\n this.emitFile({\n type: 'asset',\n fileName: 'llms-full.txt',\n source: generateLlmsFullTxt(routeFiles, apiFiles, config),\n })\n\n this.emitFile({\n type: 'asset',\n fileName: '.well-known/ai-plugin.json',\n source: JSON.stringify(generateAiPluginManifest(config), null, 2),\n })\n\n this.emitFile({\n type: 'asset',\n fileName: '.well-known/openapi.json',\n source: JSON.stringify(generateOpenApiSpec(apiFiles, config), null, 2),\n })\n },\n }\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction parseApiFiles(files: string[]): string[] {\n return files\n .filter((f) => f.endsWith('.ts') || f.endsWith('.js'))\n .map((f) => {\n let path = f.replace(/\\.\\w+$/, '').replace(/\\/index$/, '')\n if (!path.startsWith('/')) path = `/${path}`\n // Convert [param] to :param\n path = path.replace(/\\[\\.\\.\\.(\\w+)\\]/g, ':$1*').replace(/\\[(\\w+)\\]/g, ':$1')\n return `/api${path === '/' ? '' : path}`\n })\n}\n\nasync function scanDir(dir: string, base: string): Promise<string[]> {\n const { readdir, stat } = await import('node:fs/promises')\n const { join, relative } = await import('node:path')\n\n try {\n const entries = await readdir(dir)\n const files: string[] = []\n for (const entry of entries) {\n const full = join(dir, entry)\n const s = await stat(full)\n if (s.isDirectory()) {\n files.push(...(await scanDir(full, base)))\n } else {\n files.push(relative(base, full))\n }\n }\n return files\n } catch {\n return []\n }\n}\n\nfunction safeParseUrl(url: string): URL | null {\n try {\n return new URL(url)\n } catch {\n return null\n }\n}\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ai.js","names":[],"sources":["../src/fs-router.ts","../src/ai.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { FileRoute, RenderMode, RouteFileExports } from './types'\n\n// ─── File-system route conventions ──────────────────────────────────────────\n//\n// src/routes/\n// _layout.tsx → layout for all routes\n// index.tsx → /\n// about.tsx → /about\n// users/\n// _layout.tsx → layout for /users/*\n// _loading.tsx → loading fallback for /users/*\n// _error.tsx → error boundary for /users/*\n// index.tsx → /users\n// [id].tsx → /users/:id\n// [id]/\n// settings.tsx → /users/:id/settings\n// blog/\n// [...slug].tsx → /blog/* (catch-all)\n//\n// Conventions:\n// [param] → dynamic segment → :param\n// [...param] → catch-all → :param*\n// _layout → layout wrapper — must use <RouterView /> to render child routes\n// (props.children is NOT passed — the router handles nesting)\n// _error → error component\n// _loading → loading component\n// _404 → not-found component (renders on 404)\n// _not-found → alias for _404\n// (group) → route group (directory ignored in URL)\n\nconst ROUTE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js']\n\n/** Names whose top-level export presence we care about. */\nconst ROUTE_EXPORT_NAMES = [\n 'loader',\n 'guard',\n 'meta',\n 'renderMode',\n 'error',\n 'middleware',\n] as const\n\ntype RouteExportName = (typeof ROUTE_EXPORT_NAMES)[number]\n\n/**\n * Detect which optional metadata exports a route file source declares.\n *\n * Walks the source character-by-character, tracking string-literal and\n * comment state, then collects top-level `export …` statements. This is\n * more accurate than regex (no false matches inside string literals,\n * template literals, or comments) and lighter than a full AST parse\n * (no oxc/babel dependency, ~1µs per file).\n *\n * Recognizes:\n * • `export const NAME = …`\n * • `export let NAME = …`\n * • `export var NAME = …`\n * • `export function NAME(…)`\n * • `export async function NAME(…)`\n * • `export { NAME }` and `export { localName as NAME }`\n * • `export { NAME } from '…'` (re-export)\n *\n * Names checked: loader, guard, meta, renderMode, error, middleware.\n */\nexport function detectRouteExports(source: string): RouteFileExports {\n const found = new Set<RouteExportName>()\n const tokens = scanTopLevelExportTokens(source)\n\n for (const tok of tokens) {\n if (tok.kind === 'declaration') {\n // `export const NAME` / `export function NAME`\n if ((ROUTE_EXPORT_NAMES as readonly string[]).includes(tok.name)) {\n found.add(tok.name as RouteExportName)\n }\n } else {\n // `export { localName as exportedName, ... }`\n for (const name of tok.names) {\n if ((ROUTE_EXPORT_NAMES as readonly string[]).includes(name)) {\n found.add(name as RouteExportName)\n }\n }\n }\n }\n\n // Capture literal `meta` and `renderMode` initializers when present\n // so the route generator can inline them and avoid forcing a static\n // import of the entire route module just to read the metadata.\n // Strip any trailing `as const` / `satisfies T` type assertions —\n // the generated routes module is plain JS, not TS.\n //\n // We then run `isPureLiteral()` to make sure the captured expression\n // doesn't reference any free identifiers (e.g. `meta = { title: foo }`\n // where `foo` is a const declared elsewhere in the file). Inlining\n // such an expression into the routes module would produce a runtime\n // ReferenceError, so we drop the literal and let the generator fall\n // back to a static module import in those cases.\n const rawMeta = found.has('meta') ? extractLiteralExport(source, 'meta') : undefined\n const rawRenderMode = found.has('renderMode')\n ? extractLiteralExport(source, 'renderMode')\n : undefined\n const cleanMeta = rawMeta !== undefined ? stripTypeAssertions(rawMeta) : undefined\n const cleanRenderMode =\n rawRenderMode !== undefined ? stripTypeAssertions(rawRenderMode) : undefined\n const metaLiteral = cleanMeta !== undefined && isPureLiteral(cleanMeta) ? cleanMeta : undefined\n const renderModeLiteral =\n cleanRenderMode !== undefined && isPureLiteral(cleanRenderMode) ? cleanRenderMode : undefined\n\n return {\n hasLoader: found.has('loader'),\n hasGuard: found.has('guard'),\n hasMeta: found.has('meta'),\n hasRenderMode: found.has('renderMode'),\n hasError: found.has('error'),\n hasMiddleware: found.has('middleware'),\n ...(metaLiteral !== undefined ? { metaLiteral } : {}),\n ...(renderModeLiteral !== undefined ? { renderModeLiteral } : {}),\n }\n}\n\n/**\n * Extract the literal initializer of an `export const NAME = …` statement\n * as a raw text slice — used by the route generator to inline `meta` and\n * `renderMode` values into the generated routes module.\n *\n * Walks the source character-by-character respecting strings, template\n * literals, comments, and brace/bracket/paren nesting. The slice runs\n * from the first non-whitespace character after `=` to the matching\n * end-of-expression terminator (`;`, newline at depth 0, or top-level\n * `export`). Whatever the slice contains is handed to V8 verbatim by\n * embedding it inside `{ … }` in the generated module — which means\n * the original source must already be valid JavaScript (which it is,\n * since the route file compiles).\n *\n * Returns `undefined` when extraction fails for any reason — the\n * generator falls back to a static module import in that case.\n */\nfunction extractLiteralExport(source: string, name: string): string | undefined {\n // Find `export const NAME = ` at top level. Reuse the same\n // string/comment/depth tracking as the token scanner so we don't\n // false-match inside literals.\n const len = source.length\n let i = 0\n let depth = 0\n\n const isIdCont = (c: string) => /[A-Za-z0-9_$]/.test(c)\n const skipWs = (p: number): number => {\n while (p < len && /\\s/.test(source[p] as string)) p++\n return p\n }\n\n while (i < len) {\n const ch = source[i] as string\n const next = source[i + 1] ?? ''\n\n // Skip comments\n if (ch === '/' && next === '/') {\n while (i < len && source[i] !== '\\n') i++\n continue\n }\n if (ch === '/' && next === '*') {\n i += 2\n while (i < len - 1 && !(source[i] === '*' && source[i + 1] === '/')) i++\n i += 2\n continue\n }\n\n // Skip string literals\n if (ch === '\"' || ch === \"'\") {\n const quote = ch\n i++\n while (i < len && source[i] !== quote) {\n if (source[i] === '\\\\') i += 2\n else i++\n }\n i++\n continue\n }\n if (ch === '`') {\n i++\n while (i < len && source[i] !== '`') {\n if (source[i] === '\\\\') {\n i += 2\n continue\n }\n if (source[i] === '$' && source[i + 1] === '{') {\n i += 2\n let exprDepth = 1\n while (i < len && exprDepth > 0) {\n const c = source[i] as string\n if (c === '{') exprDepth++\n else if (c === '}') exprDepth--\n if (exprDepth === 0) {\n i++\n break\n }\n i++\n }\n continue\n }\n i++\n }\n i++\n continue\n }\n\n // Brace depth tracking\n if (ch === '{') {\n depth++\n i++\n continue\n }\n if (ch === '}') {\n depth--\n i++\n continue\n }\n\n // Look for `export const NAME = …` at depth 0\n if (depth === 0 && ch === 'e') {\n const afterExport = source.slice(i, i + 6) === 'export' && !isIdCont(source[i + 6] ?? '')\n if (afterExport) {\n let p = skipWs(i + 6)\n if (source.slice(p, p + 5) === 'const' && !isIdCont(source[p + 5] ?? '')) {\n p = skipWs(p + 5)\n // Check that the identifier matches our target name\n if (\n source.slice(p, p + name.length) === name &&\n !isIdCont(source[p + name.length] ?? '')\n ) {\n p = skipWs(p + name.length)\n if (source[p] === '=') {\n p = skipWs(p + 1)\n return readExpressionUntilEnd(source, p)\n }\n }\n }\n i = i + 6\n continue\n }\n }\n\n i++\n }\n\n return undefined\n}\n\n/**\n * Read a JavaScript expression starting at `start` and return the raw\n * text up to (but not including) its end. The end is whichever comes\n * first of:\n * • a `;` at depth 0\n * • a newline at depth 0 that is not inside a string/template\n * • the next top-level `export` / `const` / `function` keyword\n * • end of file\n *\n * Tracks `()`, `[]`, and `{}` nesting plus string/template/comment\n * state so depth-0 boundaries are detected correctly even for nested\n * objects, arrays, and tagged templates.\n */\nfunction readExpressionUntilEnd(source: string, start: number): string | undefined {\n const len = source.length\n let i = start\n let depth = 0 // combined paren/bracket/brace depth\n\n while (i < len) {\n const ch = source[i] as string\n const next = source[i + 1] ?? ''\n\n // End conditions at depth 0\n if (depth === 0) {\n if (ch === ';') return source.slice(start, i).trim() || undefined\n if (ch === '\\n') {\n // Allow trailing whitespace/comma but stop at the newline.\n // Some authors close objects on the same line, others span\n // them across lines — the depth check above handles the\n // multi-line case so a depth-0 newline really is the end.\n const trimmed = source.slice(start, i).trim()\n if (trimmed.length === 0) {\n i++\n continue\n }\n return trimmed\n }\n }\n\n // Skip comments\n if (ch === '/' && next === '/') {\n while (i < len && source[i] !== '\\n') i++\n continue\n }\n if (ch === '/' && next === '*') {\n i += 2\n while (i < len - 1 && !(source[i] === '*' && source[i + 1] === '/')) i++\n i += 2\n continue\n }\n\n // Skip strings\n if (ch === '\"' || ch === \"'\") {\n const quote = ch\n i++\n while (i < len && source[i] !== quote) {\n if (source[i] === '\\\\') i += 2\n else i++\n }\n i++\n continue\n }\n if (ch === '`') {\n i++\n while (i < len && source[i] !== '`') {\n if (source[i] === '\\\\') {\n i += 2\n continue\n }\n if (source[i] === '$' && source[i + 1] === '{') {\n i += 2\n let exprDepth = 1\n while (i < len && exprDepth > 0) {\n const c = source[i] as string\n if (c === '{') exprDepth++\n else if (c === '}') exprDepth--\n if (exprDepth === 0) {\n i++\n break\n }\n i++\n }\n continue\n }\n i++\n }\n i++\n continue\n }\n\n // Track depth across all bracket families\n if (ch === '{' || ch === '[' || ch === '(') {\n depth++\n i++\n continue\n }\n if (ch === '}' || ch === ']' || ch === ')') {\n depth--\n if (depth < 0) {\n // We ran past our scope without seeing a terminator. The\n // expression must have ended right before this closer.\n return source.slice(start, i).trim() || undefined\n }\n i++\n continue\n }\n\n i++\n }\n\n // Hit EOF without an explicit terminator — return whatever we have\n // if it looks plausible, otherwise undefined.\n const trimmed = source.slice(start).trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n/**\n * True if `text` is a pure JS literal — only string/number/boolean/null\n * literals plus the structural punctuation needed to compose them into\n * objects, arrays, and tuples. Identifiers, operators, function calls,\n * template-literal expression slots, and references to other names all\n * disqualify the expression.\n *\n * Walks the source character-by-character, tracking string/template/\n * comment state. Inside a string or template head (no `${}` slot) every\n * character is fine; outside strings, only the structural symbols\n * `{}[](),:` plus whitespace, digits, the literal keywords `true`,\n * `false`, `null`, and `-` (for negative numbers) are allowed.\n *\n * The check is conservative on purpose — anything fancier than a flat\n * literal falls back to the static-import path, which still works,\n * just at the cost of one un-split chunk.\n */\nfunction isPureLiteral(text: string): boolean {\n const len = text.length\n let i = 0\n\n while (i < len) {\n const ch = text[i] as string\n\n // Strings — anything inside is literal data\n if (ch === '\"' || ch === \"'\") {\n const quote = ch\n i++\n while (i < len && text[i] !== quote) {\n if (text[i] === '\\\\') i += 2\n else i++\n }\n i++\n continue\n }\n\n // Template literals — only allowed if they contain no ${} slots\n if (ch === '`') {\n i++\n while (i < len && text[i] !== '`') {\n if (text[i] === '\\\\') {\n i += 2\n continue\n }\n if (text[i] === '$' && text[i + 1] === '{') {\n // Template with an expression slot — not a pure literal\n return false\n }\n i++\n }\n i++\n continue\n }\n\n // Whitespace + structural punctuation are fine\n if (/\\s/.test(ch)) {\n i++\n continue\n }\n if (ch === '{' || ch === '}' || ch === '[' || ch === ']' || ch === ',' || ch === ':') {\n i++\n continue\n }\n\n // Number literals (including leading - and 0x/0b/0o)\n if (/[0-9]/.test(ch) || (ch === '-' && /[0-9]/.test(text[i + 1] ?? ''))) {\n while (i < len && /[0-9a-fA-Fxob.eE+\\-_]/.test(text[i] as string)) i++\n continue\n }\n\n // Allowed bare identifiers — only the literal keywords\n if (text.slice(i, i + 4) === 'true' && !isIdContChar(text[i + 4] ?? '')) {\n i += 4\n continue\n }\n if (text.slice(i, i + 5) === 'false' && !isIdContChar(text[i + 5] ?? '')) {\n i += 5\n continue\n }\n if (text.slice(i, i + 4) === 'null' && !isIdContChar(text[i + 4] ?? '')) {\n i += 4\n continue\n }\n if (text.slice(i, i + 9) === 'undefined' && !isIdContChar(text[i + 9] ?? '')) {\n i += 9\n continue\n }\n\n // Property keys can be unquoted identifiers — they're followed by `:`.\n // Walk over the identifier; if the next non-whitespace char is `:`,\n // accept it as a key. Otherwise the identifier is a free reference\n // and the expression isn't pure.\n if (/[A-Za-z_$]/.test(ch)) {\n let end = i + 1\n while (end < len && isIdContChar(text[end] as string)) end++\n let after = end\n while (after < len && /\\s/.test(text[after] as string)) after++\n if (text[after] === ':') {\n // unquoted property key — fine\n i = end\n continue\n }\n return false\n }\n\n // Anything else (operators, parens for function calls, etc.) → not pure\n return false\n }\n\n return true\n}\n\nfunction isIdContChar(c: string): boolean {\n return /[A-Za-z0-9_$]/.test(c)\n}\n\n/**\n * Strip TypeScript type-only suffixes (`as const`, `as SomeType`,\n * `satisfies SomeType`) from a literal expression so the generated\n * JS module is syntactically valid.\n *\n * The route file is TypeScript so authors freely write\n * `export const renderMode = 'ssg' as const` — but the generated\n * `virtual:zero/routes` module is JavaScript and can't keep the cast.\n * Strip from the rightmost top-level `as` or `satisfies` keyword.\n */\nexport function stripTypeAssertions(literal: string): string {\n let result = literal.trim()\n\n // Walk from the right at depth 0, find the LAST occurrence of\n // ` as ` or ` satisfies ` and cut everything to the right of it.\n // We use a depth-aware right-to-left scan because the literal can\n // contain `as`/`satisfies` inside nested objects (e.g. a string\n // value `'satisfies the schema'` should be left untouched).\n let depth = 0\n for (let i = result.length - 1; i > 0; i--) {\n const ch = result[i] as string\n if (ch === ')' || ch === ']' || ch === '}') depth++\n else if (ch === '(' || ch === '[' || ch === '{') depth--\n\n if (depth !== 0) continue\n\n // Check for ` as ` boundary\n if (\n i >= 4 &&\n result[i - 3] === ' ' &&\n result[i - 2] === 'a' &&\n result[i - 1] === 's' &&\n result[i] === ' '\n ) {\n result = result.slice(0, i - 3).trim()\n i = result.length\n depth = 0\n continue\n }\n // Check for ` satisfies ` boundary\n if (\n i >= 11 &&\n result.slice(i - 10, i + 1) === ' satisfies '\n ) {\n result = result.slice(0, i - 10).trim()\n i = result.length\n depth = 0\n continue\n }\n }\n\n return result\n}\n\n/**\n * Lightweight tokenizer for the export forms detectRouteExports cares about.\n * Returns an array of either:\n * • `{ kind: 'declaration', name }` — `export const NAME = …`\n * • `{ kind: 'list', names }` — `export { NAME, other as NAME2 }`\n *\n * Only top-level statements (brace depth 0) are considered. String literals,\n * template literals, and comments are skipped so their contents can't trigger\n * false matches.\n */\ntype ExportToken =\n | { kind: 'declaration'; name: string }\n | { kind: 'list'; names: string[] }\n\nfunction scanTopLevelExportTokens(source: string): ExportToken[] {\n const tokens: ExportToken[] = []\n const len = source.length\n let i = 0\n let depth = 0 // brace depth — we only care about top-level (depth 0)\n\n // Identifier characters used to skip past names and to validate that\n // a match isn't a substring of a longer identifier.\n const isIdStart = (c: string) => /[A-Za-z_$]/.test(c)\n const isIdCont = (c: string) => /[A-Za-z0-9_$]/.test(c)\n\n // Read an identifier starting at position p; returns [name, nextPos] or null.\n const readIdentifier = (p: number): [string, number] | null => {\n if (p >= len || !isIdStart(source[p] as string)) return null\n let end = p + 1\n while (end < len && isIdCont(source[end] as string)) end++\n return [source.slice(p, end), end]\n }\n\n // Skip whitespace including newlines.\n const skipWs = (p: number): number => {\n while (p < len && /\\s/.test(source[p] as string)) p++\n return p\n }\n\n // Match the literal `keyword` at position p, requiring an identifier\n // boundary on both sides. Returns nextPos or -1.\n const matchKeyword = (p: number, keyword: string): number => {\n if (source.slice(p, p + keyword.length) !== keyword) return -1\n const after = p + keyword.length\n if (after < len && isIdCont(source[after] as string)) return -1\n if (p > 0 && isIdCont(source[p - 1] as string)) return -1\n return after\n }\n\n while (i < len) {\n const ch = source[i] as string\n const next = source[i + 1] ?? ''\n\n // ── Comments ──────────────────────────────────────────────────────\n if (ch === '/' && next === '/') {\n // Line comment — skip to newline\n while (i < len && source[i] !== '\\n') i++\n continue\n }\n if (ch === '/' && next === '*') {\n // Block comment — skip to closing */\n i += 2\n while (i < len - 1 && !(source[i] === '*' && source[i + 1] === '/')) i++\n i += 2\n continue\n }\n\n // ── String / template literals ────────────────────────────────────\n if (ch === '\"' || ch === \"'\") {\n const quote = ch\n i++\n while (i < len && source[i] !== quote) {\n if (source[i] === '\\\\') i += 2\n else i++\n }\n i++\n continue\n }\n if (ch === '`') {\n // Template literal — skip to closing backtick, handling ${...} blocks\n i++\n while (i < len && source[i] !== '`') {\n if (source[i] === '\\\\') {\n i += 2\n continue\n }\n if (source[i] === '$' && source[i + 1] === '{') {\n // Skip a balanced ${ ... } expression\n i += 2\n let exprDepth = 1\n while (i < len && exprDepth > 0) {\n const c = source[i] as string\n if (c === '{') exprDepth++\n else if (c === '}') exprDepth--\n if (exprDepth === 0) {\n i++\n break\n }\n i++\n }\n continue\n }\n i++\n }\n i++\n continue\n }\n\n // ── Brace depth tracking ──────────────────────────────────────────\n if (ch === '{') {\n depth++\n i++\n continue\n }\n if (ch === '}') {\n depth--\n i++\n continue\n }\n\n // ── `export …` at top level ──────────────────────────────────────\n if (depth === 0 && ch === 'e') {\n const afterExport = matchKeyword(i, 'export')\n if (afterExport > 0) {\n // Found `export` token at top level. Look at what follows.\n let p = skipWs(afterExport)\n\n // `export default …` — not a named export we care about\n const afterDefault = matchKeyword(p, 'default')\n if (afterDefault > 0) {\n i = afterDefault\n continue\n }\n\n // `export { … }` (export list, possibly followed by `from '…'`)\n if (source[p] === '{') {\n p++\n const names: string[] = []\n while (p < len && source[p] !== '}') {\n p = skipWs(p)\n if (source[p] === '}') break\n const id = readIdentifier(p)\n if (!id) {\n p++\n continue\n }\n const [first, afterFirst] = id\n // `localName as exportedName` — the EXPORTED name is what counts\n let exportedName = first\n const afterFirstWs = skipWs(afterFirst)\n const afterAs = matchKeyword(afterFirstWs, 'as')\n if (afterAs > 0) {\n const aliasStart = skipWs(afterAs)\n const alias = readIdentifier(aliasStart)\n if (alias) {\n exportedName = alias[0]\n p = alias[1]\n } else {\n p = afterFirst\n }\n } else {\n p = afterFirst\n }\n names.push(exportedName)\n p = skipWs(p)\n if (source[p] === ',') p++\n }\n tokens.push({ kind: 'list', names })\n i = p + 1 // past closing brace\n continue\n }\n\n // `export async function NAME …`\n const afterAsync = matchKeyword(p, 'async')\n if (afterAsync > 0) p = skipWs(afterAsync)\n\n // `export const | let | var | function NAME …`\n let foundDecl = false\n for (const kw of ['const', 'let', 'var', 'function'] as const) {\n const afterKw = matchKeyword(p, kw)\n if (afterKw > 0) {\n const nameStart = skipWs(afterKw)\n const id = readIdentifier(nameStart)\n if (id) {\n tokens.push({ kind: 'declaration', name: id[0] })\n i = id[1] // advance past the identifier we just consumed\n foundDecl = true\n break\n }\n }\n }\n // If we couldn't recognize a declaration form, advance past `export`\n // so the outer loop doesn't re-match the same token forever.\n if (!foundDecl) i = afterExport\n continue\n }\n }\n\n i++\n }\n\n return tokens\n}\n\n/** All-false exports record. Used when source detection fails. */\nconst EMPTY_EXPORTS: RouteFileExports = {\n hasLoader: false,\n hasGuard: false,\n hasMeta: false,\n hasRenderMode: false,\n hasError: false,\n hasMiddleware: false,\n}\n\n/**\n * True if a route file declares ANY metadata export.\n * Used by the code generator to decide whether to emit a static\n * `import * as mod` (for metadata access) instead of lazy().\n */\nexport function hasAnyMetaExport(exports: RouteFileExports): boolean {\n return (\n exports.hasLoader ||\n exports.hasGuard ||\n exports.hasMeta ||\n exports.hasRenderMode ||\n exports.hasError ||\n exports.hasMiddleware\n )\n}\n\n/**\n * Parse a set of file paths (relative to routes dir) into FileRoute objects.\n *\n * @param files Array of file paths like [\"index.tsx\", \"users/[id].tsx\"]\n * @param defaultMode Default rendering mode from config\n * @param exportsMap Optional map of filePath → detected exports. When\n * provided, the resulting FileRoute objects carry export info that the\n * code generator uses to optimize imports (skip metadata namespace\n * imports for routes that only export `default`).\n */\nexport function parseFileRoutes(\n files: string[],\n defaultMode: RenderMode = 'ssr',\n exportsMap?: Map<string, RouteFileExports>,\n): FileRoute[] {\n return files\n .filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext)))\n .map((filePath) => {\n const route = parseFilePath(filePath, defaultMode)\n const exp = exportsMap?.get(filePath)\n return exp ? { ...route, exports: exp } : route\n })\n .sort(sortRoutes)\n}\n\nfunction parseFilePath(filePath: string, defaultMode: RenderMode): FileRoute {\n // Remove extension\n let route = filePath\n for (const ext of ROUTE_EXTENSIONS) {\n if (route.endsWith(ext)) {\n route = route.slice(0, -ext.length)\n break\n }\n }\n\n const fileName = getFileName(route)\n const isLayout = fileName === '_layout'\n const isError = fileName === '_error'\n const isLoading = fileName === '_loading'\n const isNotFound = fileName === '_404' || fileName === '_not-found'\n const isCatchAll = route.includes('[...')\n\n // Get directory path (strip groups for consistent grouping)\n const parts = route.split('/')\n parts.pop() // remove filename\n const dirPath = parts.filter((s) => !(s.startsWith('(') && s.endsWith(')'))).join('/')\n\n // Convert file path to URL pattern\n const urlPath = filePathToUrlPath(route)\n const depth = urlPath === '/' ? 0 : urlPath.split('/').filter(Boolean).length\n\n return {\n filePath,\n urlPath,\n dirPath,\n depth,\n isLayout,\n isError,\n isLoading,\n isNotFound,\n isCatchAll,\n renderMode: defaultMode,\n }\n}\n\n/**\n * Convert a file path (without extension) to a URL path pattern.\n *\n * Examples:\n * \"index\" → \"/\"\n * \"about\" → \"/about\"\n * \"users/index\" → \"/users\"\n * \"users/[id]\" → \"/users/:id\"\n * \"blog/[...slug]\" → \"/blog/:slug*\"\n * \"(auth)/login\" → \"/login\" (group stripped)\n * \"_layout\" → \"/\" (layout marker)\n */\nexport function filePathToUrlPath(filePath: string): string {\n const segments = filePath.split('/')\n const urlSegments: string[] = []\n\n for (const seg of segments) {\n // Skip route groups \"(name)\"\n if (seg.startsWith('(') && seg.endsWith(')')) continue\n\n // Skip special files\n if (seg === '_layout' || seg === '_error' || seg === '_loading' || seg === '_404' || seg === '_not-found') continue\n\n // \"index\" maps to the parent path\n if (seg === 'index') continue\n\n // Catch-all: [...param] → :param*\n const catchAll = seg.match(/^\\[\\.\\.\\.(\\w+)\\]$/)\n if (catchAll) {\n urlSegments.push(`:${catchAll[1]}*`)\n continue\n }\n\n // Dynamic: [param] → :param\n const dynamic = seg.match(/^\\[(\\w+)\\]$/)\n if (dynamic) {\n urlSegments.push(`:${dynamic[1]}`)\n continue\n }\n\n urlSegments.push(seg)\n }\n\n const path = `/${urlSegments.join('/')}`\n return path || '/'\n}\n\n/** Sort routes: static before dynamic, catch-all last. */\nfunction sortRoutes(a: FileRoute, b: FileRoute): number {\n // Catch-all routes go last\n if (a.isCatchAll !== b.isCatchAll) return a.isCatchAll ? 1 : -1\n // Layouts go first within same depth\n if (a.isLayout !== b.isLayout) return a.isLayout ? -1 : 1\n // Static segments before dynamic\n const aDynamic = a.urlPath.includes(':')\n const bDynamic = b.urlPath.includes(':')\n if (aDynamic !== bDynamic) return aDynamic ? 1 : -1\n // Alphabetical\n return a.urlPath.localeCompare(b.urlPath)\n}\n\nfunction getFileName(filePath: string): string {\n const parts = filePath.split('/')\n return parts[parts.length - 1] ?? ''\n}\n\n// ─── Route generation (for Vite plugin) ─────────────────────────────────────\n\n/** Internal tree node for building nested route structures. */\ninterface RouteNode {\n /** Page routes at this directory level. */\n pages: FileRoute[]\n /** Layout file for this directory (if any). */\n layout?: FileRoute\n /** Error boundary file (if any). */\n error?: FileRoute\n /** Loading fallback file (if any). */\n loading?: FileRoute\n /** Not-found (404) file (if any). */\n notFound?: FileRoute\n /** Child directories. */\n children: Map<string, RouteNode>\n}\n\n/**\n * Group flat file routes into a directory tree.\n */\nfunction getOrCreateChild(node: RouteNode, segment: string): RouteNode {\n let child = node.children.get(segment)\n if (!child) {\n child = { pages: [], children: new Map() }\n node.children.set(segment, child)\n }\n return child\n}\n\nfunction resolveNode(root: RouteNode, dirPath: string): RouteNode {\n let node = root\n if (dirPath) {\n for (const segment of dirPath.split('/')) {\n node = getOrCreateChild(node, segment)\n }\n }\n return node\n}\n\nfunction placeRoute(node: RouteNode, route: FileRoute) {\n if (route.isLayout) node.layout = route\n else if (route.isError) node.error = route\n else if (route.isLoading) node.loading = route\n else if (route.isNotFound) node.notFound = route\n else node.pages.push(route)\n}\n\nfunction buildRouteTree(routes: FileRoute[]): RouteNode {\n const root: RouteNode = { pages: [], children: new Map() }\n for (const route of routes) {\n placeRoute(resolveNode(root, route.dirPath), route)\n }\n return root\n}\n\n/**\n * Generate a virtual module that exports a nested route tree.\n * Wires up layouts as parent routes with children, loaders, guards,\n * error/loading components, middleware, and meta from route module exports.\n */\nexport interface GenerateRouteModuleOptions {\n /**\n * When true, skip lazy() for route components and use static imports.\n * Use for SSG/prerender mode where all routes are rendered at build time\n * and code splitting provides no benefit at request time.\n */\n staticImports?: boolean\n}\n\nexport function generateRouteModule(\n files: string[],\n routesDir: string,\n options?: GenerateRouteModuleOptions,\n): string {\n // Synchronously read each route file's source and detect its optional\n // metadata exports. This produces the optimal shape every time:\n // • `lazy(() => import(...))` for routes with no metadata\n // • Direct `mod.loader`/`.guard`/`.meta` for routes with metadata\n // • Zero `IMPORT_IS_UNDEFINED` and zero `INEFFECTIVE_DYNAMIC_IMPORT` warnings\n //\n // If a file can't be read (e.g. caller passing synthetic paths), the\n // FileRoute gets EMPTY_EXPORTS — the generator emits the same lazy()\n // shape used for routes that genuinely have no metadata. Callers that\n // need metadata wiring with synthetic paths should use\n // `generateRouteModuleFromRoutes()` directly with explicit exports.\n const exportsMap = new Map<string, RouteFileExports>()\n for (const filePath of files) {\n if (!ROUTE_EXTENSIONS.some((ext) => filePath.endsWith(ext))) continue\n try {\n const source = readFileSync(join(routesDir, filePath), 'utf-8')\n exportsMap.set(filePath, detectRouteExports(source))\n } catch {\n exportsMap.set(filePath, EMPTY_EXPORTS)\n }\n }\n return generateRouteModuleFromRoutes(\n parseFileRoutes(files, undefined, exportsMap),\n routesDir,\n options,\n )\n}\n\n/**\n * Lower-level entry point that accepts pre-parsed FileRoute[] (so callers\n * can attach `.exports` info from source detection). Use this when you've\n * already read the files and want optimal output.\n */\nexport function generateRouteModuleFromRoutes(\n routes: FileRoute[],\n routesDir: string,\n options?: GenerateRouteModuleOptions,\n): string {\n const tree = buildRouteTree(routes)\n const imports: string[] = []\n let importCounter = 0\n const useStaticOnly = options?.staticImports ?? false\n\n // Track whether we need lazy() at all (omitted in static-only mode and\n // when there are no routes that use it).\n let needsLazyImport = false\n\n function nextImport(filePath: string, exportName = 'default'): string {\n const name = `_${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n if (exportName === 'default') {\n imports.push(`import ${name} from \"${fullPath}\"`)\n } else {\n imports.push(`import { ${exportName} as ${name} } from \"${fullPath}\"`)\n }\n return name\n }\n\n function nextModuleImport(filePath: string): string {\n const name = `_m${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n imports.push(`import * as ${name} from \"${fullPath}\"`)\n return name\n }\n\n function nextLazy(filePath: string, loadingName?: string, errorName?: string): string {\n const name = `_${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n needsLazyImport = true\n const opts: string[] = []\n if (loadingName) opts.push(`loading: ${loadingName}`)\n if (errorName) opts.push(`error: ${errorName}`)\n const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : ''\n imports.push(`const ${name} = lazy(() => import(\"${fullPath}\")${optsStr})`)\n return name\n }\n\n /**\n * Emit a `meta: { ... }` prop using the literal initializers captured\n * from the route file source. Either or both of `metaLiteral` and\n * `renderModeLiteral` may be present; the result is always a single\n * inline object literal.\n */\n function emitInlineMeta(exp: RouteFileExports, props: string[], indent: string): void {\n if (!exp.hasMeta && !exp.hasRenderMode) return\n const parts: string[] = []\n if (exp.hasMeta && exp.metaLiteral !== undefined) {\n parts.push(`...(${exp.metaLiteral})`)\n }\n if (exp.hasRenderMode && exp.renderModeLiteral !== undefined) {\n parts.push(`renderMode: ${exp.renderModeLiteral}`)\n }\n if (parts.length > 0) {\n props.push(`${indent} meta: { ${parts.join(', ')} }`)\n }\n }\n\n function generatePageRoute(\n page: FileRoute,\n indent: string,\n loadingName: string | undefined,\n errorName: string | undefined,\n notFoundName: string | undefined,\n ): string {\n const exp = page.exports ?? EMPTY_EXPORTS\n const props: string[] = [`${indent} path: ${JSON.stringify(page.urlPath)}`]\n const hasMeta = hasAnyMetaExport(exp)\n\n if (useStaticOnly) {\n // SSG / static mode: bundle everything synchronously, no lazy().\n if (hasMeta) {\n // Single namespace import covers component AND metadata.\n const mod = nextModuleImport(page.filePath)\n props.push(`${indent} component: ${mod}.default`)\n if (exp.hasLoader) props.push(`${indent} loader: ${mod}.loader`)\n if (exp.hasGuard) props.push(`${indent} beforeEnter: ${mod}.guard`)\n if (exp.hasMeta || exp.hasRenderMode) {\n const metaParts: string[] = []\n if (exp.hasMeta) metaParts.push(`...${mod}.meta`)\n if (exp.hasRenderMode) metaParts.push(`renderMode: ${mod}.renderMode`)\n props.push(`${indent} meta: { ${metaParts.join(', ')} }`)\n }\n if (errorName) {\n const errorRef = exp.hasError ? `${mod}.error || ${errorName}` : errorName\n props.push(`${indent} errorComponent: ${errorRef}`)\n }\n } else {\n // No metadata — single static default import.\n const comp = nextImport(page.filePath, 'default')\n props.push(`${indent} component: ${comp}`)\n if (errorName) props.push(`${indent} errorComponent: ${errorName}`)\n }\n } else {\n // SSR/SPA mode: prefer lazy() for code splitting wherever possible.\n //\n // Three cases, in order of preference:\n // 1. metaLiteral / renderModeLiteral are extracted AND there's\n // no loader/guard/error/middleware → fully lazy. Component\n // is `lazy()`'d, metadata is inlined as a literal in the\n // generated module. The route file's entire dependency\n // graph chunks separately.\n // 2. metaLiteral / renderModeLiteral are extracted but a\n // function-shaped export (loader/guard/error/middleware)\n // is also present → mixed: component still lazy, metadata\n // inlined, function exports come from a static `import * as`.\n // The static import shares the chunk with the lazy chunk\n // via Rolldown's deduplication.\n // 3. No literal extraction succeeded → fall back to the previous\n // pessimistic shape: single namespace import covering both\n // component and metadata.\n const inlineableMeta =\n (!exp.hasMeta || exp.metaLiteral !== undefined) &&\n (!exp.hasRenderMode || exp.renderModeLiteral !== undefined)\n const needsFunctionExports = exp.hasLoader || exp.hasGuard || exp.hasError\n\n if (hasMeta && inlineableMeta && !needsFunctionExports) {\n // Optimal path — component lazy, metadata inlined.\n const comp = nextLazy(page.filePath, loadingName, errorName)\n props.push(`${indent} component: ${comp}`)\n emitInlineMeta(exp, props, indent)\n if (errorName) props.push(`${indent} errorComponent: ${errorName}`)\n } else if (hasMeta && inlineableMeta) {\n // Mixed — metadata is inlinable but the route also exports\n // function-shaped values (loader/guard/error). Wrap them as\n // lazy thunks so the route file's full dependency tree stays\n // out of the main bundle: each thunk calls the same dynamic\n // import as the lazy() component, and Rolldown deduplicates\n // them into one chunk. Inlining the literal metadata is what\n // makes this safe — without it, the meta access would force\n // a static import that would collide with the dynamic one.\n const comp = nextLazy(page.filePath, loadingName, errorName)\n const fullPath = `${routesDir}/${page.filePath}`\n props.push(`${indent} component: ${comp}`)\n if (exp.hasLoader) {\n props.push(\n `${indent} loader: (ctx) => import(\"${fullPath}\").then((m) => m.loader(ctx))`,\n )\n }\n if (exp.hasGuard) {\n props.push(\n `${indent} beforeEnter: (to, from) => import(\"${fullPath}\").then((m) => m.guard(to, from))`,\n )\n }\n emitInlineMeta(exp, props, indent)\n if (errorName) {\n // For error components we can't easily await — pass the lazy\n // thunk through `lazy()` so the router resolves it like any\n // other lazy component when an error fires.\n const errorRef = exp.hasError\n ? `lazy(() => import(\"${fullPath}\").then((m) => ({ default: m.error })))`\n : errorName\n if (exp.hasError) needsLazyImport = true\n props.push(`${indent} errorComponent: ${errorRef}`)\n }\n } else if (hasMeta) {\n // Fallback — metadata couldn't be extracted as a literal (e.g.\n // computed values, references to other declarations). Fall\n // back to the pessimistic single-namespace-import shape.\n const mod = nextModuleImport(page.filePath)\n props.push(`${indent} component: ${mod}.default`)\n if (exp.hasLoader) props.push(`${indent} loader: ${mod}.loader`)\n if (exp.hasGuard) props.push(`${indent} beforeEnter: ${mod}.guard`)\n if (exp.hasMeta || exp.hasRenderMode) {\n const metaParts: string[] = []\n if (exp.hasMeta) metaParts.push(`...${mod}.meta`)\n if (exp.hasRenderMode) metaParts.push(`renderMode: ${mod}.renderMode`)\n props.push(`${indent} meta: { ${metaParts.join(', ')} }`)\n }\n if (errorName) {\n const errorRef = exp.hasError ? `${mod}.error || ${errorName}` : errorName\n props.push(`${indent} errorComponent: ${errorRef}`)\n }\n } else {\n // No metadata at all — pure lazy() for code splitting.\n const comp = nextLazy(page.filePath, loadingName, errorName)\n props.push(`${indent} component: ${comp}`)\n if (errorName) props.push(`${indent} errorComponent: ${errorName}`)\n }\n }\n\n if (notFoundName) {\n props.push(`${indent} notFoundComponent: ${notFoundName}`)\n }\n\n return `${indent}{\\n${props.join(',\\n')}\\n${indent}}`\n }\n\n function wrapWithLayout(\n node: RouteNode,\n children: string[],\n indent: string,\n errorName: string | undefined,\n notFoundName: string | undefined,\n ): string {\n const layout = node.layout as FileRoute\n const exp = layout.exports ?? EMPTY_EXPORTS\n const hasMeta = hasAnyMetaExport(exp)\n\n // Decide between two import shapes:\n // • Layout HAS metadata exports → single `import * as mod` for both\n // the layout component (mod.layout) AND metadata. One import.\n // • Layout has NO metadata → just `import { layout as _N }`. One import.\n let layoutComp: string\n let layoutMod: string | undefined\n\n if (hasMeta) {\n // Single namespace import covers both component and metadata.\n layoutMod = nextModuleImport(layout.filePath)\n layoutComp = `${layoutMod}.layout`\n } else {\n // No metadata — named `layout` import is enough.\n layoutComp = nextImport(layout.filePath, 'layout')\n }\n\n const props: string[] = [\n `${indent}path: ${JSON.stringify(layout.urlPath)}`,\n `${indent}component: ${layoutComp}`,\n ]\n\n if (layoutMod !== undefined) {\n if (exp.hasLoader) props.push(`${indent}loader: ${layoutMod}.loader`)\n if (exp.hasGuard) props.push(`${indent}beforeEnter: ${layoutMod}.guard`)\n if (exp.hasMeta || exp.hasRenderMode) {\n const metaParts: string[] = []\n if (exp.hasMeta) metaParts.push(`...${layoutMod}.meta`)\n if (exp.hasRenderMode) metaParts.push(`renderMode: ${layoutMod}.renderMode`)\n props.push(`${indent}meta: { ${metaParts.join(', ')} }`)\n }\n }\n\n if (errorName) {\n props.push(`${indent}errorComponent: ${errorName}`)\n }\n if (notFoundName) {\n props.push(`${indent}notFoundComponent: ${notFoundName}`)\n }\n if (children.length > 0) {\n props.push(`${indent}children: [\\n${children.join(',\\n')}\\n${indent}]`)\n }\n\n return `${indent}{\\n${props.map((p) => ` ${p}`).join(',\\n')}\\n${indent}}`\n }\n\n /**\n * Generate route definitions for a tree node.\n */\n function generateNode(node: RouteNode, depth: number): string[] {\n const indent = ' '.repeat(depth + 1)\n\n const errorName = node.error ? nextImport(node.error.filePath) : undefined\n const loadingName = node.loading ? nextImport(node.loading.filePath) : undefined\n const notFoundName = node.notFound ? nextImport(node.notFound.filePath) : undefined\n\n const childRouteDefs: string[] = []\n for (const [, childNode] of node.children) {\n childRouteDefs.push(...generateNode(childNode, depth + 1))\n }\n\n const pageRouteDefs = node.pages.map((page) =>\n generatePageRoute(page, indent, loadingName, errorName, notFoundName),\n )\n\n const allChildren = [...pageRouteDefs, ...childRouteDefs]\n\n if (node.layout) {\n return [wrapWithLayout(node, allChildren, indent, errorName, notFoundName)]\n }\n return allChildren\n }\n\n const routeDefs = generateNode(tree, 0)\n\n const lines: string[] = []\n if (needsLazyImport) lines.push(`import { lazy } from \"@pyreon/router\"`, '')\n lines.push(...imports, '')\n\n lines.push(\n // Filter out undefined properties at runtime\n `function clean(routes) {`,\n ` return routes.map(r => {`,\n ` const c = {}`,\n ` for (const k in r) if (r[k] !== undefined) c[k] = r[k]`,\n ` if (c.children) c.children = clean(c.children)`,\n ` return c`,\n ` })`,\n `}`,\n '',\n `export const routes = clean([`,\n routeDefs.join(',\\n'),\n `])`,\n )\n\n return lines.join('\\n')\n}\n\n/**\n * Generate a virtual module that maps URL patterns to their middleware exports.\n * Used by the server entry to dispatch per-route middleware.\n */\nexport function generateMiddlewareModule(files: string[], routesDir: string): string {\n const routes = parseFileRoutes(files)\n const imports: string[] = []\n const entries: string[] = []\n let counter = 0\n\n for (const route of routes) {\n if (route.isLayout || route.isError || route.isLoading || route.isNotFound) continue\n const name = `_mw${counter++}`\n const fullPath = `${routesDir}/${route.filePath}`\n imports.push(`import { middleware as ${name} } from \"${fullPath}\"`)\n entries.push(` { pattern: ${JSON.stringify(route.urlPath)}, middleware: ${name} }`)\n }\n\n return [\n ...imports,\n '',\n `export const routeMiddleware = [`,\n entries.join(',\\n'),\n `].filter(e => e.middleware)`,\n ].join('\\n')\n}\n\n/**\n * Scan a directory for route files.\n * Returns paths relative to the routes directory.\n */\nexport async function scanRouteFiles(routesDir: string): Promise<string[]> {\n const { readdir } = await import('node:fs/promises')\n const { relative } = await import('node:path')\n\n const files: string[] = []\n\n async function walk(dir: string) {\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n if (entry.isDirectory()) {\n await walk(fullPath)\n } else if (ROUTE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {\n files.push(relative(routesDir, fullPath))\n }\n }\n }\n\n await walk(routesDir)\n return files\n}\n\n/**\n * Scan route files AND read each one to detect optional metadata exports\n * (loader, guard, meta, renderMode, error, middleware).\n *\n * Returns FileRoute[] with `.exports` populated, ready to feed into\n * `generateRouteModuleFromRoutes()` for optimal output:\n * • lazy() for components without metadata (best code splitting)\n * • Direct property access for components with metadata (no _pick)\n * • No spurious IMPORT_IS_UNDEFINED warnings\n */\nexport async function scanRouteFilesWithExports(\n routesDir: string,\n defaultMode: RenderMode = 'ssr',\n): Promise<FileRoute[]> {\n const { readFile } = await import('node:fs/promises')\n\n const files = await scanRouteFiles(routesDir)\n const exportsMap = new Map<string, RouteFileExports>()\n\n await Promise.all(\n files.map(async (filePath) => {\n try {\n const source = await readFile(join(routesDir, filePath), 'utf-8')\n exportsMap.set(filePath, detectRouteExports(source))\n } catch {\n // File can't be read — generator treats this as no metadata\n // and emits the optimal lazy() shape.\n exportsMap.set(filePath, EMPTY_EXPORTS)\n }\n }),\n )\n\n return parseFileRoutes(files, defaultMode, exportsMap)\n}\n","/**\n * AI integration utilities for Zero.\n *\n * - llms.txt auto-generation from routes and API routes\n * - JSON-LD auto-inference from route meta + Meta props\n * - AI plugin manifest (/.well-known/ai-plugin.json) from API routes\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { aiPlugin } from \"@pyreon/zero/ai\"\n *\n * export default {\n * plugins: [\n * aiPlugin({\n * name: \"My App\",\n * origin: \"https://example.com\",\n * description: \"A modern web application\",\n * }),\n * ],\n * }\n * ```\n */\nimport type { Plugin } from 'vite'\nimport { parseFileRoutes } from './fs-router'\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface AiPluginConfig {\n /** App/API name. */\n name: string\n /** App description for AI agents. */\n description: string\n /** Base URL. e.g. \"https://example.com\" */\n origin: string\n /** Contact email (required by OpenAI plugin spec). */\n contactEmail?: string\n /** Legal info URL. */\n legalUrl?: string\n /** Logo URL for the plugin. */\n logoUrl?: string\n /** Routes directory relative to project root. Default: \"src/routes\" */\n routesDir?: string\n /** API routes directory relative to project root. Default: \"src/api\" */\n apiDir?: string\n /**\n * API route descriptions — map of pattern to description.\n * Used for llms.txt and ai-plugin.json.\n *\n * @example\n * ```ts\n * apiDescriptions: {\n * \"GET /api/posts\": \"List all blog posts, supports ?page=N&limit=N\",\n * \"GET /api/posts/:id\": \"Get a single post by ID\",\n * \"POST /api/posts\": \"Create a new post (requires auth)\",\n * }\n * ```\n */\n apiDescriptions?: Record<string, string>\n /**\n * Page descriptions — map of URL path to description.\n * Used for llms.txt. Falls back to route meta.title/description.\n */\n pageDescriptions?: Record<string, string>\n /**\n * Additional content to append to llms.txt.\n * Useful for authentication instructions, rate limits, etc.\n */\n llmsExtra?: string\n}\n\n// ─── llms.txt generation ────────────────────────────────────────────────────\n\n/**\n * Generate llms.txt content from route files and config.\n *\n * Format follows the llms.txt proposal:\n * ```\n * # {name}\n * > {description}\n *\n * ## Pages\n * - [/about](/about): About page\n *\n * ## API\n * - GET /api/posts: List posts\n * ```\n *\n * @internal Exported for testing.\n */\nexport function generateLlmsTxt(\n routeFiles: string[],\n apiFiles: string[],\n config: AiPluginConfig,\n): string {\n const lines: string[] = []\n\n // Header\n lines.push(`# ${config.name}`)\n lines.push(`> ${config.description}`)\n lines.push('')\n\n // Pages section\n const routes = parseFileRoutes(routeFiles)\n const pages = routes.filter(\n (r) => !r.isLayout && !r.isError && !r.isLoading && !r.isNotFound\n && !r.isCatchAll && !r.urlPath.includes(':'),\n )\n\n if (pages.length > 0) {\n lines.push('## Pages')\n lines.push('')\n for (const page of pages) {\n const desc = config.pageDescriptions?.[page.urlPath]\n const url = `${config.origin}${page.urlPath === '/' ? '' : page.urlPath}`\n if (desc) {\n lines.push(`- [${page.urlPath}](${url}): ${desc}`)\n } else {\n lines.push(`- [${page.urlPath}](${url})`)\n }\n }\n lines.push('')\n }\n\n // Dynamic routes (documented separately — AI needs to know about params)\n const dynamicRoutes = routes.filter(\n (r) => !r.isLayout && !r.isError && !r.isLoading && !r.isNotFound\n && (r.urlPath.includes(':') || r.isCatchAll),\n )\n if (dynamicRoutes.length > 0) {\n lines.push('## Dynamic Pages')\n lines.push('')\n for (const route of dynamicRoutes) {\n const desc = config.pageDescriptions?.[route.urlPath]\n if (desc) {\n lines.push(`- ${route.urlPath}: ${desc}`)\n } else {\n lines.push(`- ${route.urlPath}`)\n }\n }\n lines.push('')\n }\n\n // API section\n const apiPatterns = parseApiFiles(apiFiles)\n if (apiPatterns.length > 0 || config.apiDescriptions) {\n lines.push('## API Endpoints')\n lines.push('')\n\n // From apiDescriptions (most detailed — user-provided)\n if (config.apiDescriptions) {\n for (const [endpoint, desc] of Object.entries(config.apiDescriptions)) {\n lines.push(`- ${endpoint}: ${desc}`)\n }\n }\n\n // From auto-discovered API files (only those not already described)\n const describedPatterns = new Set(\n Object.keys(config.apiDescriptions ?? {}).map((k) => k.replace(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\s+/, '')),\n )\n for (const pattern of apiPatterns) {\n if (!describedPatterns.has(pattern)) {\n lines.push(`- ${pattern}`)\n }\n }\n lines.push('')\n }\n\n // Extra content\n if (config.llmsExtra) {\n lines.push(config.llmsExtra)\n lines.push('')\n }\n\n return lines.join('\\n')\n}\n\n/**\n * Generate llms-full.txt — expanded version with more detail.\n * Includes all route metadata and API descriptions.\n *\n * @internal Exported for testing.\n */\nexport function generateLlmsFullTxt(\n routeFiles: string[],\n apiFiles: string[],\n config: AiPluginConfig,\n): string {\n const lines: string[] = []\n\n lines.push(`# ${config.name} — Full Reference`)\n lines.push(`> ${config.description}`)\n lines.push('')\n lines.push(`Base URL: ${config.origin}`)\n lines.push('')\n\n // All pages with details\n const routes = parseFileRoutes(routeFiles)\n const pages = routes.filter(\n (r) => !r.isLayout && !r.isError && !r.isLoading && !r.isNotFound,\n )\n\n if (pages.length > 0) {\n lines.push('## All Routes')\n lines.push('')\n for (const page of pages) {\n const desc = config.pageDescriptions?.[page.urlPath] ?? ''\n const dynamic = page.urlPath.includes(':') ? ' (dynamic)' : ''\n const catchAll = page.isCatchAll ? ' (catch-all)' : ''\n lines.push(`### ${page.urlPath}${dynamic}${catchAll}`)\n if (desc) lines.push(desc)\n lines.push(`- File: ${page.filePath}`)\n lines.push(`- Render mode: ${page.renderMode}`)\n lines.push('')\n }\n }\n\n // API endpoints with full detail\n if (config.apiDescriptions) {\n lines.push('## API Reference')\n lines.push('')\n for (const [endpoint, desc] of Object.entries(config.apiDescriptions)) {\n lines.push(`### ${endpoint}`)\n lines.push(desc)\n lines.push('')\n }\n }\n\n if (config.llmsExtra) {\n lines.push('## Additional Information')\n lines.push('')\n lines.push(config.llmsExtra)\n lines.push('')\n }\n\n return lines.join('\\n')\n}\n\n// ─── JSON-LD auto-inference ─────────────────────────────────────────────────\n\nexport interface InferJsonLdOptions {\n /** Page URL. */\n url: string\n /** Page title. */\n title?: string\n /** Page description. */\n description?: string\n /** Page image. */\n image?: string\n /** Site name. */\n siteName?: string\n /** Page type hint. */\n type?: 'website' | 'article' | 'product' | 'profile'\n /** Article metadata. */\n publishedTime?: string\n /** Article author. */\n author?: string\n /** Article tags. */\n tags?: string[]\n /** Breadcrumb path segments. */\n breadcrumbs?: Array<{ name: string; url: string }>\n}\n\n/**\n * Auto-infer JSON-LD structured data from page metadata.\n *\n * Returns an array of JSON-LD objects (multiple schemas can apply to one page).\n * For example, an article page gets both `Article` and `BreadcrumbList`.\n *\n * @example\n * ```tsx\n * const schemas = inferJsonLd({\n * url: \"https://example.com/blog/my-post\",\n * title: \"My Post\",\n * description: \"A great article\",\n * type: \"article\",\n * author: \"Vit Bokisch\",\n * publishedTime: \"2026-03-31\",\n * })\n * // → [Article schema, BreadcrumbList schema]\n * ```\n */\nexport function inferJsonLd(options: InferJsonLdOptions): Record<string, unknown>[] {\n const schemas: Record<string, unknown>[] = []\n\n // Base: WebPage or Article\n if (options.type === 'article') {\n const article: Record<string, unknown> = {\n '@context': 'https://schema.org',\n '@type': 'Article',\n headline: options.title,\n url: options.url,\n }\n if (options.description) article.description = options.description\n if (options.image) article.image = options.image\n if (options.publishedTime) article.datePublished = options.publishedTime\n if (options.author) {\n article.author = { '@type': 'Person', name: options.author }\n }\n if (options.tags && options.tags.length > 0) {\n article.keywords = options.tags.join(', ')\n }\n if (options.siteName) {\n article.publisher = { '@type': 'Organization', name: options.siteName }\n }\n schemas.push(article)\n } else if (options.type === 'product') {\n const product: Record<string, unknown> = {\n '@context': 'https://schema.org',\n '@type': 'Product',\n name: options.title,\n url: options.url,\n }\n if (options.description) product.description = options.description\n if (options.image) product.image = options.image\n schemas.push(product)\n } else {\n const webpage: Record<string, unknown> = {\n '@context': 'https://schema.org',\n '@type': 'WebPage',\n name: options.title,\n url: options.url,\n }\n if (options.description) webpage.description = options.description\n if (options.image) webpage.thumbnailUrl = options.image\n schemas.push(webpage)\n }\n\n // BreadcrumbList from URL path or explicit breadcrumbs\n if (options.breadcrumbs && options.breadcrumbs.length > 0) {\n schemas.push({\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n itemListElement: options.breadcrumbs.map((bc, i) => ({\n '@type': 'ListItem',\n position: i + 1,\n name: bc.name,\n item: bc.url,\n })),\n })\n } else {\n // Auto-generate breadcrumbs from URL path\n const urlObj = safeParseUrl(options.url)\n if (urlObj) {\n const segments = urlObj.pathname.split('/').filter(Boolean)\n if (segments.length > 0) {\n const items = [\n { '@type': 'ListItem', position: 1, name: 'Home', item: urlObj.origin },\n ]\n let path = ''\n for (let i = 0; i < segments.length; i++) {\n path += `/${segments[i]}`\n items.push({\n '@type': 'ListItem',\n position: i + 2,\n name: capitalize(segments[i]!.replace(/-/g, ' ')),\n item: `${urlObj.origin}${path}`,\n })\n }\n schemas.push({\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n itemListElement: items,\n })\n }\n }\n }\n\n return schemas\n}\n\n// ─── AI plugin manifest ─────────────────────────────────────────────────────\n\n/**\n * Generate an OpenAI-compatible AI plugin manifest.\n *\n * Follows the /.well-known/ai-plugin.json spec.\n *\n * @internal Exported for testing.\n */\nexport function generateAiPluginManifest(config: AiPluginConfig): Record<string, unknown> {\n return {\n schema_version: 'v1',\n name_for_human: config.name,\n name_for_model: config.name.toLowerCase().replace(/\\s+/g, '_').replace(/[^a-z0-9_]/g, ''),\n description_for_human: config.description,\n description_for_model: config.description,\n auth: { type: 'none' },\n api: {\n type: 'openapi',\n url: `${config.origin}/.well-known/openapi.yaml`,\n },\n logo_url: config.logoUrl ?? `${config.origin}/favicon.svg`,\n contact_email: config.contactEmail ?? '',\n legal_info_url: config.legalUrl ?? `${config.origin}/legal`,\n }\n}\n\n/**\n * Generate a minimal OpenAPI 3.0 spec from API route descriptions.\n *\n * @internal Exported for testing.\n */\nexport function generateOpenApiSpec(\n apiFiles: string[],\n config: AiPluginConfig,\n): Record<string, unknown> {\n const paths: Record<string, Record<string, unknown>> = {}\n\n // From user-provided descriptions\n if (config.apiDescriptions) {\n for (const [endpoint, desc] of Object.entries(config.apiDescriptions)) {\n const match = endpoint.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\s+(.+)$/)\n if (match) {\n const method = match[1]!.toLowerCase()\n const path = match[2]!\n // Convert :param to {param} for OpenAPI\n const openApiPath = path.replace(/:(\\w+)/g, '{$1}')\n if (!paths[openApiPath]) paths[openApiPath] = {}\n paths[openApiPath][method] = {\n summary: desc,\n responses: { '200': { description: 'Success' } },\n }\n }\n }\n }\n\n // Auto-discovered API files (fill in gaps)\n for (const pattern of parseApiFiles(apiFiles)) {\n const openApiPath = pattern.replace(/:(\\w+)/g, '{$1}')\n if (!paths[openApiPath]) {\n paths[openApiPath] = {\n get: {\n summary: `${openApiPath} endpoint`,\n responses: { '200': { description: 'Success' } },\n },\n }\n }\n }\n\n return {\n openapi: '3.0.0',\n info: {\n title: config.name,\n description: config.description,\n version: '1.0.0',\n },\n servers: [{ url: config.origin }],\n paths,\n }\n}\n\n// ─── Vite plugin ────────────────────────────────────────────────────────────\n\n/**\n * AI integration Vite plugin.\n *\n * Generates at build time:\n * - `/llms.txt` — concise site summary for AI agents\n * - `/llms-full.txt` — detailed reference for AI agents\n * - `/.well-known/ai-plugin.json` — OpenAI plugin manifest\n * - `/.well-known/openapi.yaml` — minimal OpenAPI spec from API routes\n *\n * In dev, serves these files via middleware.\n *\n * @example\n * ```ts\n * import { aiPlugin } from \"@pyreon/zero/ai\"\n *\n * export default {\n * plugins: [\n * aiPlugin({\n * name: \"My App\",\n * origin: \"https://example.com\",\n * description: \"A modern web application\",\n * apiDescriptions: {\n * \"GET /api/posts\": \"List blog posts\",\n * \"GET /api/posts/:id\": \"Get post by ID\",\n * },\n * }),\n * ],\n * }\n * ```\n */\nexport function aiPlugin(config: AiPluginConfig): Plugin {\n let root = ''\n let isBuild = false\n let routeFiles: string[] = []\n let apiFiles: string[] = []\n\n return {\n name: 'pyreon-zero-ai',\n enforce: 'post',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n isBuild = resolvedConfig.command === 'build'\n },\n\n async buildStart() {\n // Scan for route and API files\n try {\n const { join } = await import('node:path')\n\n const routesDir = join(root, config.routesDir ?? 'src/routes')\n const apiDir = join(root, config.apiDir ?? 'src/api')\n\n routeFiles = await scanDir(routesDir, routesDir)\n apiFiles = await scanDir(apiDir, apiDir)\n } catch {\n // Directories may not exist\n }\n },\n\n configureServer(server) {\n server.middlewares.use(async (req, res, next) => {\n const url = req.url ?? ''\n\n if (url === '/llms.txt') {\n res.setHeader('Content-Type', 'text/plain; charset=utf-8')\n res.end(generateLlmsTxt(routeFiles, apiFiles, config))\n return\n }\n\n if (url === '/llms-full.txt') {\n res.setHeader('Content-Type', 'text/plain; charset=utf-8')\n res.end(generateLlmsFullTxt(routeFiles, apiFiles, config))\n return\n }\n\n if (url === '/.well-known/ai-plugin.json') {\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify(generateAiPluginManifest(config), null, 2))\n return\n }\n\n if (url === '/.well-known/openapi.yaml' || url === '/.well-known/openapi.json') {\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify(generateOpenApiSpec(apiFiles, config), null, 2))\n return\n }\n\n next()\n })\n },\n\n async generateBundle() {\n if (!isBuild) return\n\n this.emitFile({\n type: 'asset',\n fileName: 'llms.txt',\n source: generateLlmsTxt(routeFiles, apiFiles, config),\n })\n\n this.emitFile({\n type: 'asset',\n fileName: 'llms-full.txt',\n source: generateLlmsFullTxt(routeFiles, apiFiles, config),\n })\n\n this.emitFile({\n type: 'asset',\n fileName: '.well-known/ai-plugin.json',\n source: JSON.stringify(generateAiPluginManifest(config), null, 2),\n })\n\n this.emitFile({\n type: 'asset',\n fileName: '.well-known/openapi.json',\n source: JSON.stringify(generateOpenApiSpec(apiFiles, config), null, 2),\n })\n },\n }\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction parseApiFiles(files: string[]): string[] {\n return files\n .filter((f) => f.endsWith('.ts') || f.endsWith('.js'))\n .map((f) => {\n let path = f.replace(/\\.\\w+$/, '').replace(/\\/index$/, '')\n if (!path.startsWith('/')) path = `/${path}`\n // Convert [param] to :param\n path = path.replace(/\\[\\.\\.\\.(\\w+)\\]/g, ':$1*').replace(/\\[(\\w+)\\]/g, ':$1')\n return `/api${path === '/' ? '' : path}`\n })\n}\n\nasync function scanDir(dir: string, base: string): Promise<string[]> {\n const { readdir, stat } = await import('node:fs/promises')\n const { join, relative } = await import('node:path')\n\n try {\n const entries = await readdir(dir)\n const files: string[] = []\n for (const entry of entries) {\n const full = join(dir, entry)\n const s = await stat(full)\n if (s.isDirectory()) {\n files.push(...(await scanDir(full, base)))\n } else {\n files.push(relative(base, full))\n }\n }\n return files\n } catch {\n return []\n }\n}\n\nfunction safeParseUrl(url: string): URL | null {\n try {\n return new URL(url)\n } catch {\n return null\n }\n}\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1)\n}\n"],"mappings":"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"}
@@ -1,3 +1,5 @@
1
+ import { join } from "node:path";
2
+
1
3
  //#region src/fs-router.ts
2
4
  const ROUTE_EXTENSIONS = [
3
5
  ".tsx",
@@ -11,7 +13,7 @@ const ROUTE_EXTENSIONS = [
11
13
  */
12
14
  async function scanRouteFiles(routesDir) {
13
15
  const { readdir } = await import("node:fs/promises");
14
- const { join, relative } = await import("node:path");
16
+ const { relative } = await import("node:path");
15
17
  const files = [];
16
18
  async function walk(dir) {
17
19
  const entries = await readdir(dir, { withFileTypes: true });
@@ -27,4 +29,4 @@ async function scanRouteFiles(routesDir) {
27
29
 
28
30
  //#endregion
29
31
  export { scanRouteFiles };
30
- //# sourceMappingURL=fs-router-BkbIWqek.js.map
32
+ //# sourceMappingURL=fs-router-3xzp-4Wj.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-router-3xzp-4Wj.js","names":[],"sources":["../src/fs-router.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { FileRoute, RenderMode, RouteFileExports } from './types'\n\n// ─── File-system route conventions ──────────────────────────────────────────\n//\n// src/routes/\n// _layout.tsx → layout for all routes\n// index.tsx → /\n// about.tsx → /about\n// users/\n// _layout.tsx → layout for /users/*\n// _loading.tsx → loading fallback for /users/*\n// _error.tsx → error boundary for /users/*\n// index.tsx → /users\n// [id].tsx → /users/:id\n// [id]/\n// settings.tsx → /users/:id/settings\n// blog/\n// [...slug].tsx → /blog/* (catch-all)\n//\n// Conventions:\n// [param] → dynamic segment → :param\n// [...param] → catch-all → :param*\n// _layout → layout wrapper — must use <RouterView /> to render child routes\n// (props.children is NOT passed — the router handles nesting)\n// _error → error component\n// _loading → loading component\n// _404 → not-found component (renders on 404)\n// _not-found → alias for _404\n// (group) → route group (directory ignored in URL)\n\nconst ROUTE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js']\n\n/** Names whose top-level export presence we care about. */\nconst ROUTE_EXPORT_NAMES = [\n 'loader',\n 'guard',\n 'meta',\n 'renderMode',\n 'error',\n 'middleware',\n] as const\n\ntype RouteExportName = (typeof ROUTE_EXPORT_NAMES)[number]\n\n/**\n * Detect which optional metadata exports a route file source declares.\n *\n * Walks the source character-by-character, tracking string-literal and\n * comment state, then collects top-level `export …` statements. This is\n * more accurate than regex (no false matches inside string literals,\n * template literals, or comments) and lighter than a full AST parse\n * (no oxc/babel dependency, ~1µs per file).\n *\n * Recognizes:\n * • `export const NAME = …`\n * • `export let NAME = …`\n * • `export var NAME = …`\n * • `export function NAME(…)`\n * • `export async function NAME(…)`\n * • `export { NAME }` and `export { localName as NAME }`\n * • `export { NAME } from '…'` (re-export)\n *\n * Names checked: loader, guard, meta, renderMode, error, middleware.\n */\nexport function detectRouteExports(source: string): RouteFileExports {\n const found = new Set<RouteExportName>()\n const tokens = scanTopLevelExportTokens(source)\n\n for (const tok of tokens) {\n if (tok.kind === 'declaration') {\n // `export const NAME` / `export function NAME`\n if ((ROUTE_EXPORT_NAMES as readonly string[]).includes(tok.name)) {\n found.add(tok.name as RouteExportName)\n }\n } else {\n // `export { localName as exportedName, ... }`\n for (const name of tok.names) {\n if ((ROUTE_EXPORT_NAMES as readonly string[]).includes(name)) {\n found.add(name as RouteExportName)\n }\n }\n }\n }\n\n // Capture literal `meta` and `renderMode` initializers when present\n // so the route generator can inline them and avoid forcing a static\n // import of the entire route module just to read the metadata.\n // Strip any trailing `as const` / `satisfies T` type assertions —\n // the generated routes module is plain JS, not TS.\n //\n // We then run `isPureLiteral()` to make sure the captured expression\n // doesn't reference any free identifiers (e.g. `meta = { title: foo }`\n // where `foo` is a const declared elsewhere in the file). Inlining\n // such an expression into the routes module would produce a runtime\n // ReferenceError, so we drop the literal and let the generator fall\n // back to a static module import in those cases.\n const rawMeta = found.has('meta') ? extractLiteralExport(source, 'meta') : undefined\n const rawRenderMode = found.has('renderMode')\n ? extractLiteralExport(source, 'renderMode')\n : undefined\n const cleanMeta = rawMeta !== undefined ? stripTypeAssertions(rawMeta) : undefined\n const cleanRenderMode =\n rawRenderMode !== undefined ? stripTypeAssertions(rawRenderMode) : undefined\n const metaLiteral = cleanMeta !== undefined && isPureLiteral(cleanMeta) ? cleanMeta : undefined\n const renderModeLiteral =\n cleanRenderMode !== undefined && isPureLiteral(cleanRenderMode) ? cleanRenderMode : undefined\n\n return {\n hasLoader: found.has('loader'),\n hasGuard: found.has('guard'),\n hasMeta: found.has('meta'),\n hasRenderMode: found.has('renderMode'),\n hasError: found.has('error'),\n hasMiddleware: found.has('middleware'),\n ...(metaLiteral !== undefined ? { metaLiteral } : {}),\n ...(renderModeLiteral !== undefined ? { renderModeLiteral } : {}),\n }\n}\n\n/**\n * Extract the literal initializer of an `export const NAME = …` statement\n * as a raw text slice — used by the route generator to inline `meta` and\n * `renderMode` values into the generated routes module.\n *\n * Walks the source character-by-character respecting strings, template\n * literals, comments, and brace/bracket/paren nesting. The slice runs\n * from the first non-whitespace character after `=` to the matching\n * end-of-expression terminator (`;`, newline at depth 0, or top-level\n * `export`). Whatever the slice contains is handed to V8 verbatim by\n * embedding it inside `{ … }` in the generated module — which means\n * the original source must already be valid JavaScript (which it is,\n * since the route file compiles).\n *\n * Returns `undefined` when extraction fails for any reason — the\n * generator falls back to a static module import in that case.\n */\nfunction extractLiteralExport(source: string, name: string): string | undefined {\n // Find `export const NAME = ` at top level. Reuse the same\n // string/comment/depth tracking as the token scanner so we don't\n // false-match inside literals.\n const len = source.length\n let i = 0\n let depth = 0\n\n const isIdCont = (c: string) => /[A-Za-z0-9_$]/.test(c)\n const skipWs = (p: number): number => {\n while (p < len && /\\s/.test(source[p] as string)) p++\n return p\n }\n\n while (i < len) {\n const ch = source[i] as string\n const next = source[i + 1] ?? ''\n\n // Skip comments\n if (ch === '/' && next === '/') {\n while (i < len && source[i] !== '\\n') i++\n continue\n }\n if (ch === '/' && next === '*') {\n i += 2\n while (i < len - 1 && !(source[i] === '*' && source[i + 1] === '/')) i++\n i += 2\n continue\n }\n\n // Skip string literals\n if (ch === '\"' || ch === \"'\") {\n const quote = ch\n i++\n while (i < len && source[i] !== quote) {\n if (source[i] === '\\\\') i += 2\n else i++\n }\n i++\n continue\n }\n if (ch === '`') {\n i++\n while (i < len && source[i] !== '`') {\n if (source[i] === '\\\\') {\n i += 2\n continue\n }\n if (source[i] === '$' && source[i + 1] === '{') {\n i += 2\n let exprDepth = 1\n while (i < len && exprDepth > 0) {\n const c = source[i] as string\n if (c === '{') exprDepth++\n else if (c === '}') exprDepth--\n if (exprDepth === 0) {\n i++\n break\n }\n i++\n }\n continue\n }\n i++\n }\n i++\n continue\n }\n\n // Brace depth tracking\n if (ch === '{') {\n depth++\n i++\n continue\n }\n if (ch === '}') {\n depth--\n i++\n continue\n }\n\n // Look for `export const NAME = …` at depth 0\n if (depth === 0 && ch === 'e') {\n const afterExport = source.slice(i, i + 6) === 'export' && !isIdCont(source[i + 6] ?? '')\n if (afterExport) {\n let p = skipWs(i + 6)\n if (source.slice(p, p + 5) === 'const' && !isIdCont(source[p + 5] ?? '')) {\n p = skipWs(p + 5)\n // Check that the identifier matches our target name\n if (\n source.slice(p, p + name.length) === name &&\n !isIdCont(source[p + name.length] ?? '')\n ) {\n p = skipWs(p + name.length)\n if (source[p] === '=') {\n p = skipWs(p + 1)\n return readExpressionUntilEnd(source, p)\n }\n }\n }\n i = i + 6\n continue\n }\n }\n\n i++\n }\n\n return undefined\n}\n\n/**\n * Read a JavaScript expression starting at `start` and return the raw\n * text up to (but not including) its end. The end is whichever comes\n * first of:\n * • a `;` at depth 0\n * • a newline at depth 0 that is not inside a string/template\n * • the next top-level `export` / `const` / `function` keyword\n * • end of file\n *\n * Tracks `()`, `[]`, and `{}` nesting plus string/template/comment\n * state so depth-0 boundaries are detected correctly even for nested\n * objects, arrays, and tagged templates.\n */\nfunction readExpressionUntilEnd(source: string, start: number): string | undefined {\n const len = source.length\n let i = start\n let depth = 0 // combined paren/bracket/brace depth\n\n while (i < len) {\n const ch = source[i] as string\n const next = source[i + 1] ?? ''\n\n // End conditions at depth 0\n if (depth === 0) {\n if (ch === ';') return source.slice(start, i).trim() || undefined\n if (ch === '\\n') {\n // Allow trailing whitespace/comma but stop at the newline.\n // Some authors close objects on the same line, others span\n // them across lines — the depth check above handles the\n // multi-line case so a depth-0 newline really is the end.\n const trimmed = source.slice(start, i).trim()\n if (trimmed.length === 0) {\n i++\n continue\n }\n return trimmed\n }\n }\n\n // Skip comments\n if (ch === '/' && next === '/') {\n while (i < len && source[i] !== '\\n') i++\n continue\n }\n if (ch === '/' && next === '*') {\n i += 2\n while (i < len - 1 && !(source[i] === '*' && source[i + 1] === '/')) i++\n i += 2\n continue\n }\n\n // Skip strings\n if (ch === '\"' || ch === \"'\") {\n const quote = ch\n i++\n while (i < len && source[i] !== quote) {\n if (source[i] === '\\\\') i += 2\n else i++\n }\n i++\n continue\n }\n if (ch === '`') {\n i++\n while (i < len && source[i] !== '`') {\n if (source[i] === '\\\\') {\n i += 2\n continue\n }\n if (source[i] === '$' && source[i + 1] === '{') {\n i += 2\n let exprDepth = 1\n while (i < len && exprDepth > 0) {\n const c = source[i] as string\n if (c === '{') exprDepth++\n else if (c === '}') exprDepth--\n if (exprDepth === 0) {\n i++\n break\n }\n i++\n }\n continue\n }\n i++\n }\n i++\n continue\n }\n\n // Track depth across all bracket families\n if (ch === '{' || ch === '[' || ch === '(') {\n depth++\n i++\n continue\n }\n if (ch === '}' || ch === ']' || ch === ')') {\n depth--\n if (depth < 0) {\n // We ran past our scope without seeing a terminator. The\n // expression must have ended right before this closer.\n return source.slice(start, i).trim() || undefined\n }\n i++\n continue\n }\n\n i++\n }\n\n // Hit EOF without an explicit terminator — return whatever we have\n // if it looks plausible, otherwise undefined.\n const trimmed = source.slice(start).trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n/**\n * True if `text` is a pure JS literal — only string/number/boolean/null\n * literals plus the structural punctuation needed to compose them into\n * objects, arrays, and tuples. Identifiers, operators, function calls,\n * template-literal expression slots, and references to other names all\n * disqualify the expression.\n *\n * Walks the source character-by-character, tracking string/template/\n * comment state. Inside a string or template head (no `${}` slot) every\n * character is fine; outside strings, only the structural symbols\n * `{}[](),:` plus whitespace, digits, the literal keywords `true`,\n * `false`, `null`, and `-` (for negative numbers) are allowed.\n *\n * The check is conservative on purpose — anything fancier than a flat\n * literal falls back to the static-import path, which still works,\n * just at the cost of one un-split chunk.\n */\nfunction isPureLiteral(text: string): boolean {\n const len = text.length\n let i = 0\n\n while (i < len) {\n const ch = text[i] as string\n\n // Strings — anything inside is literal data\n if (ch === '\"' || ch === \"'\") {\n const quote = ch\n i++\n while (i < len && text[i] !== quote) {\n if (text[i] === '\\\\') i += 2\n else i++\n }\n i++\n continue\n }\n\n // Template literals — only allowed if they contain no ${} slots\n if (ch === '`') {\n i++\n while (i < len && text[i] !== '`') {\n if (text[i] === '\\\\') {\n i += 2\n continue\n }\n if (text[i] === '$' && text[i + 1] === '{') {\n // Template with an expression slot — not a pure literal\n return false\n }\n i++\n }\n i++\n continue\n }\n\n // Whitespace + structural punctuation are fine\n if (/\\s/.test(ch)) {\n i++\n continue\n }\n if (ch === '{' || ch === '}' || ch === '[' || ch === ']' || ch === ',' || ch === ':') {\n i++\n continue\n }\n\n // Number literals (including leading - and 0x/0b/0o)\n if (/[0-9]/.test(ch) || (ch === '-' && /[0-9]/.test(text[i + 1] ?? ''))) {\n while (i < len && /[0-9a-fA-Fxob.eE+\\-_]/.test(text[i] as string)) i++\n continue\n }\n\n // Allowed bare identifiers — only the literal keywords\n if (text.slice(i, i + 4) === 'true' && !isIdContChar(text[i + 4] ?? '')) {\n i += 4\n continue\n }\n if (text.slice(i, i + 5) === 'false' && !isIdContChar(text[i + 5] ?? '')) {\n i += 5\n continue\n }\n if (text.slice(i, i + 4) === 'null' && !isIdContChar(text[i + 4] ?? '')) {\n i += 4\n continue\n }\n if (text.slice(i, i + 9) === 'undefined' && !isIdContChar(text[i + 9] ?? '')) {\n i += 9\n continue\n }\n\n // Property keys can be unquoted identifiers — they're followed by `:`.\n // Walk over the identifier; if the next non-whitespace char is `:`,\n // accept it as a key. Otherwise the identifier is a free reference\n // and the expression isn't pure.\n if (/[A-Za-z_$]/.test(ch)) {\n let end = i + 1\n while (end < len && isIdContChar(text[end] as string)) end++\n let after = end\n while (after < len && /\\s/.test(text[after] as string)) after++\n if (text[after] === ':') {\n // unquoted property key — fine\n i = end\n continue\n }\n return false\n }\n\n // Anything else (operators, parens for function calls, etc.) → not pure\n return false\n }\n\n return true\n}\n\nfunction isIdContChar(c: string): boolean {\n return /[A-Za-z0-9_$]/.test(c)\n}\n\n/**\n * Strip TypeScript type-only suffixes (`as const`, `as SomeType`,\n * `satisfies SomeType`) from a literal expression so the generated\n * JS module is syntactically valid.\n *\n * The route file is TypeScript so authors freely write\n * `export const renderMode = 'ssg' as const` — but the generated\n * `virtual:zero/routes` module is JavaScript and can't keep the cast.\n * Strip from the rightmost top-level `as` or `satisfies` keyword.\n */\nexport function stripTypeAssertions(literal: string): string {\n let result = literal.trim()\n\n // Walk from the right at depth 0, find the LAST occurrence of\n // ` as ` or ` satisfies ` and cut everything to the right of it.\n // We use a depth-aware right-to-left scan because the literal can\n // contain `as`/`satisfies` inside nested objects (e.g. a string\n // value `'satisfies the schema'` should be left untouched).\n let depth = 0\n for (let i = result.length - 1; i > 0; i--) {\n const ch = result[i] as string\n if (ch === ')' || ch === ']' || ch === '}') depth++\n else if (ch === '(' || ch === '[' || ch === '{') depth--\n\n if (depth !== 0) continue\n\n // Check for ` as ` boundary\n if (\n i >= 4 &&\n result[i - 3] === ' ' &&\n result[i - 2] === 'a' &&\n result[i - 1] === 's' &&\n result[i] === ' '\n ) {\n result = result.slice(0, i - 3).trim()\n i = result.length\n depth = 0\n continue\n }\n // Check for ` satisfies ` boundary\n if (\n i >= 11 &&\n result.slice(i - 10, i + 1) === ' satisfies '\n ) {\n result = result.slice(0, i - 10).trim()\n i = result.length\n depth = 0\n continue\n }\n }\n\n return result\n}\n\n/**\n * Lightweight tokenizer for the export forms detectRouteExports cares about.\n * Returns an array of either:\n * • `{ kind: 'declaration', name }` — `export const NAME = …`\n * • `{ kind: 'list', names }` — `export { NAME, other as NAME2 }`\n *\n * Only top-level statements (brace depth 0) are considered. String literals,\n * template literals, and comments are skipped so their contents can't trigger\n * false matches.\n */\ntype ExportToken =\n | { kind: 'declaration'; name: string }\n | { kind: 'list'; names: string[] }\n\nfunction scanTopLevelExportTokens(source: string): ExportToken[] {\n const tokens: ExportToken[] = []\n const len = source.length\n let i = 0\n let depth = 0 // brace depth — we only care about top-level (depth 0)\n\n // Identifier characters used to skip past names and to validate that\n // a match isn't a substring of a longer identifier.\n const isIdStart = (c: string) => /[A-Za-z_$]/.test(c)\n const isIdCont = (c: string) => /[A-Za-z0-9_$]/.test(c)\n\n // Read an identifier starting at position p; returns [name, nextPos] or null.\n const readIdentifier = (p: number): [string, number] | null => {\n if (p >= len || !isIdStart(source[p] as string)) return null\n let end = p + 1\n while (end < len && isIdCont(source[end] as string)) end++\n return [source.slice(p, end), end]\n }\n\n // Skip whitespace including newlines.\n const skipWs = (p: number): number => {\n while (p < len && /\\s/.test(source[p] as string)) p++\n return p\n }\n\n // Match the literal `keyword` at position p, requiring an identifier\n // boundary on both sides. Returns nextPos or -1.\n const matchKeyword = (p: number, keyword: string): number => {\n if (source.slice(p, p + keyword.length) !== keyword) return -1\n const after = p + keyword.length\n if (after < len && isIdCont(source[after] as string)) return -1\n if (p > 0 && isIdCont(source[p - 1] as string)) return -1\n return after\n }\n\n while (i < len) {\n const ch = source[i] as string\n const next = source[i + 1] ?? ''\n\n // ── Comments ──────────────────────────────────────────────────────\n if (ch === '/' && next === '/') {\n // Line comment — skip to newline\n while (i < len && source[i] !== '\\n') i++\n continue\n }\n if (ch === '/' && next === '*') {\n // Block comment — skip to closing */\n i += 2\n while (i < len - 1 && !(source[i] === '*' && source[i + 1] === '/')) i++\n i += 2\n continue\n }\n\n // ── String / template literals ────────────────────────────────────\n if (ch === '\"' || ch === \"'\") {\n const quote = ch\n i++\n while (i < len && source[i] !== quote) {\n if (source[i] === '\\\\') i += 2\n else i++\n }\n i++\n continue\n }\n if (ch === '`') {\n // Template literal — skip to closing backtick, handling ${...} blocks\n i++\n while (i < len && source[i] !== '`') {\n if (source[i] === '\\\\') {\n i += 2\n continue\n }\n if (source[i] === '$' && source[i + 1] === '{') {\n // Skip a balanced ${ ... } expression\n i += 2\n let exprDepth = 1\n while (i < len && exprDepth > 0) {\n const c = source[i] as string\n if (c === '{') exprDepth++\n else if (c === '}') exprDepth--\n if (exprDepth === 0) {\n i++\n break\n }\n i++\n }\n continue\n }\n i++\n }\n i++\n continue\n }\n\n // ── Brace depth tracking ──────────────────────────────────────────\n if (ch === '{') {\n depth++\n i++\n continue\n }\n if (ch === '}') {\n depth--\n i++\n continue\n }\n\n // ── `export …` at top level ──────────────────────────────────────\n if (depth === 0 && ch === 'e') {\n const afterExport = matchKeyword(i, 'export')\n if (afterExport > 0) {\n // Found `export` token at top level. Look at what follows.\n let p = skipWs(afterExport)\n\n // `export default …` — not a named export we care about\n const afterDefault = matchKeyword(p, 'default')\n if (afterDefault > 0) {\n i = afterDefault\n continue\n }\n\n // `export { … }` (export list, possibly followed by `from '…'`)\n if (source[p] === '{') {\n p++\n const names: string[] = []\n while (p < len && source[p] !== '}') {\n p = skipWs(p)\n if (source[p] === '}') break\n const id = readIdentifier(p)\n if (!id) {\n p++\n continue\n }\n const [first, afterFirst] = id\n // `localName as exportedName` — the EXPORTED name is what counts\n let exportedName = first\n const afterFirstWs = skipWs(afterFirst)\n const afterAs = matchKeyword(afterFirstWs, 'as')\n if (afterAs > 0) {\n const aliasStart = skipWs(afterAs)\n const alias = readIdentifier(aliasStart)\n if (alias) {\n exportedName = alias[0]\n p = alias[1]\n } else {\n p = afterFirst\n }\n } else {\n p = afterFirst\n }\n names.push(exportedName)\n p = skipWs(p)\n if (source[p] === ',') p++\n }\n tokens.push({ kind: 'list', names })\n i = p + 1 // past closing brace\n continue\n }\n\n // `export async function NAME …`\n const afterAsync = matchKeyword(p, 'async')\n if (afterAsync > 0) p = skipWs(afterAsync)\n\n // `export const | let | var | function NAME …`\n let foundDecl = false\n for (const kw of ['const', 'let', 'var', 'function'] as const) {\n const afterKw = matchKeyword(p, kw)\n if (afterKw > 0) {\n const nameStart = skipWs(afterKw)\n const id = readIdentifier(nameStart)\n if (id) {\n tokens.push({ kind: 'declaration', name: id[0] })\n i = id[1] // advance past the identifier we just consumed\n foundDecl = true\n break\n }\n }\n }\n // If we couldn't recognize a declaration form, advance past `export`\n // so the outer loop doesn't re-match the same token forever.\n if (!foundDecl) i = afterExport\n continue\n }\n }\n\n i++\n }\n\n return tokens\n}\n\n/** All-false exports record. Used when source detection fails. */\nconst EMPTY_EXPORTS: RouteFileExports = {\n hasLoader: false,\n hasGuard: false,\n hasMeta: false,\n hasRenderMode: false,\n hasError: false,\n hasMiddleware: false,\n}\n\n/**\n * True if a route file declares ANY metadata export.\n * Used by the code generator to decide whether to emit a static\n * `import * as mod` (for metadata access) instead of lazy().\n */\nexport function hasAnyMetaExport(exports: RouteFileExports): boolean {\n return (\n exports.hasLoader ||\n exports.hasGuard ||\n exports.hasMeta ||\n exports.hasRenderMode ||\n exports.hasError ||\n exports.hasMiddleware\n )\n}\n\n/**\n * Parse a set of file paths (relative to routes dir) into FileRoute objects.\n *\n * @param files Array of file paths like [\"index.tsx\", \"users/[id].tsx\"]\n * @param defaultMode Default rendering mode from config\n * @param exportsMap Optional map of filePath → detected exports. When\n * provided, the resulting FileRoute objects carry export info that the\n * code generator uses to optimize imports (skip metadata namespace\n * imports for routes that only export `default`).\n */\nexport function parseFileRoutes(\n files: string[],\n defaultMode: RenderMode = 'ssr',\n exportsMap?: Map<string, RouteFileExports>,\n): FileRoute[] {\n return files\n .filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext)))\n .map((filePath) => {\n const route = parseFilePath(filePath, defaultMode)\n const exp = exportsMap?.get(filePath)\n return exp ? { ...route, exports: exp } : route\n })\n .sort(sortRoutes)\n}\n\nfunction parseFilePath(filePath: string, defaultMode: RenderMode): FileRoute {\n // Remove extension\n let route = filePath\n for (const ext of ROUTE_EXTENSIONS) {\n if (route.endsWith(ext)) {\n route = route.slice(0, -ext.length)\n break\n }\n }\n\n const fileName = getFileName(route)\n const isLayout = fileName === '_layout'\n const isError = fileName === '_error'\n const isLoading = fileName === '_loading'\n const isNotFound = fileName === '_404' || fileName === '_not-found'\n const isCatchAll = route.includes('[...')\n\n // Get directory path (strip groups for consistent grouping)\n const parts = route.split('/')\n parts.pop() // remove filename\n const dirPath = parts.filter((s) => !(s.startsWith('(') && s.endsWith(')'))).join('/')\n\n // Convert file path to URL pattern\n const urlPath = filePathToUrlPath(route)\n const depth = urlPath === '/' ? 0 : urlPath.split('/').filter(Boolean).length\n\n return {\n filePath,\n urlPath,\n dirPath,\n depth,\n isLayout,\n isError,\n isLoading,\n isNotFound,\n isCatchAll,\n renderMode: defaultMode,\n }\n}\n\n/**\n * Convert a file path (without extension) to a URL path pattern.\n *\n * Examples:\n * \"index\" → \"/\"\n * \"about\" → \"/about\"\n * \"users/index\" → \"/users\"\n * \"users/[id]\" → \"/users/:id\"\n * \"blog/[...slug]\" → \"/blog/:slug*\"\n * \"(auth)/login\" → \"/login\" (group stripped)\n * \"_layout\" → \"/\" (layout marker)\n */\nexport function filePathToUrlPath(filePath: string): string {\n const segments = filePath.split('/')\n const urlSegments: string[] = []\n\n for (const seg of segments) {\n // Skip route groups \"(name)\"\n if (seg.startsWith('(') && seg.endsWith(')')) continue\n\n // Skip special files\n if (seg === '_layout' || seg === '_error' || seg === '_loading' || seg === '_404' || seg === '_not-found') continue\n\n // \"index\" maps to the parent path\n if (seg === 'index') continue\n\n // Catch-all: [...param] → :param*\n const catchAll = seg.match(/^\\[\\.\\.\\.(\\w+)\\]$/)\n if (catchAll) {\n urlSegments.push(`:${catchAll[1]}*`)\n continue\n }\n\n // Dynamic: [param] → :param\n const dynamic = seg.match(/^\\[(\\w+)\\]$/)\n if (dynamic) {\n urlSegments.push(`:${dynamic[1]}`)\n continue\n }\n\n urlSegments.push(seg)\n }\n\n const path = `/${urlSegments.join('/')}`\n return path || '/'\n}\n\n/** Sort routes: static before dynamic, catch-all last. */\nfunction sortRoutes(a: FileRoute, b: FileRoute): number {\n // Catch-all routes go last\n if (a.isCatchAll !== b.isCatchAll) return a.isCatchAll ? 1 : -1\n // Layouts go first within same depth\n if (a.isLayout !== b.isLayout) return a.isLayout ? -1 : 1\n // Static segments before dynamic\n const aDynamic = a.urlPath.includes(':')\n const bDynamic = b.urlPath.includes(':')\n if (aDynamic !== bDynamic) return aDynamic ? 1 : -1\n // Alphabetical\n return a.urlPath.localeCompare(b.urlPath)\n}\n\nfunction getFileName(filePath: string): string {\n const parts = filePath.split('/')\n return parts[parts.length - 1] ?? ''\n}\n\n// ─── Route generation (for Vite plugin) ─────────────────────────────────────\n\n/** Internal tree node for building nested route structures. */\ninterface RouteNode {\n /** Page routes at this directory level. */\n pages: FileRoute[]\n /** Layout file for this directory (if any). */\n layout?: FileRoute\n /** Error boundary file (if any). */\n error?: FileRoute\n /** Loading fallback file (if any). */\n loading?: FileRoute\n /** Not-found (404) file (if any). */\n notFound?: FileRoute\n /** Child directories. */\n children: Map<string, RouteNode>\n}\n\n/**\n * Group flat file routes into a directory tree.\n */\nfunction getOrCreateChild(node: RouteNode, segment: string): RouteNode {\n let child = node.children.get(segment)\n if (!child) {\n child = { pages: [], children: new Map() }\n node.children.set(segment, child)\n }\n return child\n}\n\nfunction resolveNode(root: RouteNode, dirPath: string): RouteNode {\n let node = root\n if (dirPath) {\n for (const segment of dirPath.split('/')) {\n node = getOrCreateChild(node, segment)\n }\n }\n return node\n}\n\nfunction placeRoute(node: RouteNode, route: FileRoute) {\n if (route.isLayout) node.layout = route\n else if (route.isError) node.error = route\n else if (route.isLoading) node.loading = route\n else if (route.isNotFound) node.notFound = route\n else node.pages.push(route)\n}\n\nfunction buildRouteTree(routes: FileRoute[]): RouteNode {\n const root: RouteNode = { pages: [], children: new Map() }\n for (const route of routes) {\n placeRoute(resolveNode(root, route.dirPath), route)\n }\n return root\n}\n\n/**\n * Generate a virtual module that exports a nested route tree.\n * Wires up layouts as parent routes with children, loaders, guards,\n * error/loading components, middleware, and meta from route module exports.\n */\nexport interface GenerateRouteModuleOptions {\n /**\n * When true, skip lazy() for route components and use static imports.\n * Use for SSG/prerender mode where all routes are rendered at build time\n * and code splitting provides no benefit at request time.\n */\n staticImports?: boolean\n}\n\nexport function generateRouteModule(\n files: string[],\n routesDir: string,\n options?: GenerateRouteModuleOptions,\n): string {\n // Synchronously read each route file's source and detect its optional\n // metadata exports. This produces the optimal shape every time:\n // • `lazy(() => import(...))` for routes with no metadata\n // • Direct `mod.loader`/`.guard`/`.meta` for routes with metadata\n // • Zero `IMPORT_IS_UNDEFINED` and zero `INEFFECTIVE_DYNAMIC_IMPORT` warnings\n //\n // If a file can't be read (e.g. caller passing synthetic paths), the\n // FileRoute gets EMPTY_EXPORTS — the generator emits the same lazy()\n // shape used for routes that genuinely have no metadata. Callers that\n // need metadata wiring with synthetic paths should use\n // `generateRouteModuleFromRoutes()` directly with explicit exports.\n const exportsMap = new Map<string, RouteFileExports>()\n for (const filePath of files) {\n if (!ROUTE_EXTENSIONS.some((ext) => filePath.endsWith(ext))) continue\n try {\n const source = readFileSync(join(routesDir, filePath), 'utf-8')\n exportsMap.set(filePath, detectRouteExports(source))\n } catch {\n exportsMap.set(filePath, EMPTY_EXPORTS)\n }\n }\n return generateRouteModuleFromRoutes(\n parseFileRoutes(files, undefined, exportsMap),\n routesDir,\n options,\n )\n}\n\n/**\n * Lower-level entry point that accepts pre-parsed FileRoute[] (so callers\n * can attach `.exports` info from source detection). Use this when you've\n * already read the files and want optimal output.\n */\nexport function generateRouteModuleFromRoutes(\n routes: FileRoute[],\n routesDir: string,\n options?: GenerateRouteModuleOptions,\n): string {\n const tree = buildRouteTree(routes)\n const imports: string[] = []\n let importCounter = 0\n const useStaticOnly = options?.staticImports ?? false\n\n // Track whether we need lazy() at all (omitted in static-only mode and\n // when there are no routes that use it).\n let needsLazyImport = false\n\n function nextImport(filePath: string, exportName = 'default'): string {\n const name = `_${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n if (exportName === 'default') {\n imports.push(`import ${name} from \"${fullPath}\"`)\n } else {\n imports.push(`import { ${exportName} as ${name} } from \"${fullPath}\"`)\n }\n return name\n }\n\n function nextModuleImport(filePath: string): string {\n const name = `_m${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n imports.push(`import * as ${name} from \"${fullPath}\"`)\n return name\n }\n\n function nextLazy(filePath: string, loadingName?: string, errorName?: string): string {\n const name = `_${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n needsLazyImport = true\n const opts: string[] = []\n if (loadingName) opts.push(`loading: ${loadingName}`)\n if (errorName) opts.push(`error: ${errorName}`)\n const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : ''\n imports.push(`const ${name} = lazy(() => import(\"${fullPath}\")${optsStr})`)\n return name\n }\n\n /**\n * Emit a `meta: { ... }` prop using the literal initializers captured\n * from the route file source. Either or both of `metaLiteral` and\n * `renderModeLiteral` may be present; the result is always a single\n * inline object literal.\n */\n function emitInlineMeta(exp: RouteFileExports, props: string[], indent: string): void {\n if (!exp.hasMeta && !exp.hasRenderMode) return\n const parts: string[] = []\n if (exp.hasMeta && exp.metaLiteral !== undefined) {\n parts.push(`...(${exp.metaLiteral})`)\n }\n if (exp.hasRenderMode && exp.renderModeLiteral !== undefined) {\n parts.push(`renderMode: ${exp.renderModeLiteral}`)\n }\n if (parts.length > 0) {\n props.push(`${indent} meta: { ${parts.join(', ')} }`)\n }\n }\n\n function generatePageRoute(\n page: FileRoute,\n indent: string,\n loadingName: string | undefined,\n errorName: string | undefined,\n notFoundName: string | undefined,\n ): string {\n const exp = page.exports ?? EMPTY_EXPORTS\n const props: string[] = [`${indent} path: ${JSON.stringify(page.urlPath)}`]\n const hasMeta = hasAnyMetaExport(exp)\n\n if (useStaticOnly) {\n // SSG / static mode: bundle everything synchronously, no lazy().\n if (hasMeta) {\n // Single namespace import covers component AND metadata.\n const mod = nextModuleImport(page.filePath)\n props.push(`${indent} component: ${mod}.default`)\n if (exp.hasLoader) props.push(`${indent} loader: ${mod}.loader`)\n if (exp.hasGuard) props.push(`${indent} beforeEnter: ${mod}.guard`)\n if (exp.hasMeta || exp.hasRenderMode) {\n const metaParts: string[] = []\n if (exp.hasMeta) metaParts.push(`...${mod}.meta`)\n if (exp.hasRenderMode) metaParts.push(`renderMode: ${mod}.renderMode`)\n props.push(`${indent} meta: { ${metaParts.join(', ')} }`)\n }\n if (errorName) {\n const errorRef = exp.hasError ? `${mod}.error || ${errorName}` : errorName\n props.push(`${indent} errorComponent: ${errorRef}`)\n }\n } else {\n // No metadata — single static default import.\n const comp = nextImport(page.filePath, 'default')\n props.push(`${indent} component: ${comp}`)\n if (errorName) props.push(`${indent} errorComponent: ${errorName}`)\n }\n } else {\n // SSR/SPA mode: prefer lazy() for code splitting wherever possible.\n //\n // Three cases, in order of preference:\n // 1. metaLiteral / renderModeLiteral are extracted AND there's\n // no loader/guard/error/middleware → fully lazy. Component\n // is `lazy()`'d, metadata is inlined as a literal in the\n // generated module. The route file's entire dependency\n // graph chunks separately.\n // 2. metaLiteral / renderModeLiteral are extracted but a\n // function-shaped export (loader/guard/error/middleware)\n // is also present → mixed: component still lazy, metadata\n // inlined, function exports come from a static `import * as`.\n // The static import shares the chunk with the lazy chunk\n // via Rolldown's deduplication.\n // 3. No literal extraction succeeded → fall back to the previous\n // pessimistic shape: single namespace import covering both\n // component and metadata.\n const inlineableMeta =\n (!exp.hasMeta || exp.metaLiteral !== undefined) &&\n (!exp.hasRenderMode || exp.renderModeLiteral !== undefined)\n const needsFunctionExports = exp.hasLoader || exp.hasGuard || exp.hasError\n\n if (hasMeta && inlineableMeta && !needsFunctionExports) {\n // Optimal path — component lazy, metadata inlined.\n const comp = nextLazy(page.filePath, loadingName, errorName)\n props.push(`${indent} component: ${comp}`)\n emitInlineMeta(exp, props, indent)\n if (errorName) props.push(`${indent} errorComponent: ${errorName}`)\n } else if (hasMeta && inlineableMeta) {\n // Mixed — metadata is inlinable but the route also exports\n // function-shaped values (loader/guard/error). Wrap them as\n // lazy thunks so the route file's full dependency tree stays\n // out of the main bundle: each thunk calls the same dynamic\n // import as the lazy() component, and Rolldown deduplicates\n // them into one chunk. Inlining the literal metadata is what\n // makes this safe — without it, the meta access would force\n // a static import that would collide with the dynamic one.\n const comp = nextLazy(page.filePath, loadingName, errorName)\n const fullPath = `${routesDir}/${page.filePath}`\n props.push(`${indent} component: ${comp}`)\n if (exp.hasLoader) {\n props.push(\n `${indent} loader: (ctx) => import(\"${fullPath}\").then((m) => m.loader(ctx))`,\n )\n }\n if (exp.hasGuard) {\n props.push(\n `${indent} beforeEnter: (to, from) => import(\"${fullPath}\").then((m) => m.guard(to, from))`,\n )\n }\n emitInlineMeta(exp, props, indent)\n if (errorName) {\n // For error components we can't easily await — pass the lazy\n // thunk through `lazy()` so the router resolves it like any\n // other lazy component when an error fires.\n const errorRef = exp.hasError\n ? `lazy(() => import(\"${fullPath}\").then((m) => ({ default: m.error })))`\n : errorName\n if (exp.hasError) needsLazyImport = true\n props.push(`${indent} errorComponent: ${errorRef}`)\n }\n } else if (hasMeta) {\n // Fallback — metadata couldn't be extracted as a literal (e.g.\n // computed values, references to other declarations). Fall\n // back to the pessimistic single-namespace-import shape.\n const mod = nextModuleImport(page.filePath)\n props.push(`${indent} component: ${mod}.default`)\n if (exp.hasLoader) props.push(`${indent} loader: ${mod}.loader`)\n if (exp.hasGuard) props.push(`${indent} beforeEnter: ${mod}.guard`)\n if (exp.hasMeta || exp.hasRenderMode) {\n const metaParts: string[] = []\n if (exp.hasMeta) metaParts.push(`...${mod}.meta`)\n if (exp.hasRenderMode) metaParts.push(`renderMode: ${mod}.renderMode`)\n props.push(`${indent} meta: { ${metaParts.join(', ')} }`)\n }\n if (errorName) {\n const errorRef = exp.hasError ? `${mod}.error || ${errorName}` : errorName\n props.push(`${indent} errorComponent: ${errorRef}`)\n }\n } else {\n // No metadata at all — pure lazy() for code splitting.\n const comp = nextLazy(page.filePath, loadingName, errorName)\n props.push(`${indent} component: ${comp}`)\n if (errorName) props.push(`${indent} errorComponent: ${errorName}`)\n }\n }\n\n if (notFoundName) {\n props.push(`${indent} notFoundComponent: ${notFoundName}`)\n }\n\n return `${indent}{\\n${props.join(',\\n')}\\n${indent}}`\n }\n\n function wrapWithLayout(\n node: RouteNode,\n children: string[],\n indent: string,\n errorName: string | undefined,\n notFoundName: string | undefined,\n ): string {\n const layout = node.layout as FileRoute\n const exp = layout.exports ?? EMPTY_EXPORTS\n const hasMeta = hasAnyMetaExport(exp)\n\n // Decide between two import shapes:\n // • Layout HAS metadata exports → single `import * as mod` for both\n // the layout component (mod.layout) AND metadata. One import.\n // • Layout has NO metadata → just `import { layout as _N }`. One import.\n let layoutComp: string\n let layoutMod: string | undefined\n\n if (hasMeta) {\n // Single namespace import covers both component and metadata.\n layoutMod = nextModuleImport(layout.filePath)\n layoutComp = `${layoutMod}.layout`\n } else {\n // No metadata — named `layout` import is enough.\n layoutComp = nextImport(layout.filePath, 'layout')\n }\n\n const props: string[] = [\n `${indent}path: ${JSON.stringify(layout.urlPath)}`,\n `${indent}component: ${layoutComp}`,\n ]\n\n if (layoutMod !== undefined) {\n if (exp.hasLoader) props.push(`${indent}loader: ${layoutMod}.loader`)\n if (exp.hasGuard) props.push(`${indent}beforeEnter: ${layoutMod}.guard`)\n if (exp.hasMeta || exp.hasRenderMode) {\n const metaParts: string[] = []\n if (exp.hasMeta) metaParts.push(`...${layoutMod}.meta`)\n if (exp.hasRenderMode) metaParts.push(`renderMode: ${layoutMod}.renderMode`)\n props.push(`${indent}meta: { ${metaParts.join(', ')} }`)\n }\n }\n\n if (errorName) {\n props.push(`${indent}errorComponent: ${errorName}`)\n }\n if (notFoundName) {\n props.push(`${indent}notFoundComponent: ${notFoundName}`)\n }\n if (children.length > 0) {\n props.push(`${indent}children: [\\n${children.join(',\\n')}\\n${indent}]`)\n }\n\n return `${indent}{\\n${props.map((p) => ` ${p}`).join(',\\n')}\\n${indent}}`\n }\n\n /**\n * Generate route definitions for a tree node.\n */\n function generateNode(node: RouteNode, depth: number): string[] {\n const indent = ' '.repeat(depth + 1)\n\n const errorName = node.error ? nextImport(node.error.filePath) : undefined\n const loadingName = node.loading ? nextImport(node.loading.filePath) : undefined\n const notFoundName = node.notFound ? nextImport(node.notFound.filePath) : undefined\n\n const childRouteDefs: string[] = []\n for (const [, childNode] of node.children) {\n childRouteDefs.push(...generateNode(childNode, depth + 1))\n }\n\n const pageRouteDefs = node.pages.map((page) =>\n generatePageRoute(page, indent, loadingName, errorName, notFoundName),\n )\n\n const allChildren = [...pageRouteDefs, ...childRouteDefs]\n\n if (node.layout) {\n return [wrapWithLayout(node, allChildren, indent, errorName, notFoundName)]\n }\n return allChildren\n }\n\n const routeDefs = generateNode(tree, 0)\n\n const lines: string[] = []\n if (needsLazyImport) lines.push(`import { lazy } from \"@pyreon/router\"`, '')\n lines.push(...imports, '')\n\n lines.push(\n // Filter out undefined properties at runtime\n `function clean(routes) {`,\n ` return routes.map(r => {`,\n ` const c = {}`,\n ` for (const k in r) if (r[k] !== undefined) c[k] = r[k]`,\n ` if (c.children) c.children = clean(c.children)`,\n ` return c`,\n ` })`,\n `}`,\n '',\n `export const routes = clean([`,\n routeDefs.join(',\\n'),\n `])`,\n )\n\n return lines.join('\\n')\n}\n\n/**\n * Generate a virtual module that maps URL patterns to their middleware exports.\n * Used by the server entry to dispatch per-route middleware.\n */\nexport function generateMiddlewareModule(files: string[], routesDir: string): string {\n const routes = parseFileRoutes(files)\n const imports: string[] = []\n const entries: string[] = []\n let counter = 0\n\n for (const route of routes) {\n if (route.isLayout || route.isError || route.isLoading || route.isNotFound) continue\n const name = `_mw${counter++}`\n const fullPath = `${routesDir}/${route.filePath}`\n imports.push(`import { middleware as ${name} } from \"${fullPath}\"`)\n entries.push(` { pattern: ${JSON.stringify(route.urlPath)}, middleware: ${name} }`)\n }\n\n return [\n ...imports,\n '',\n `export const routeMiddleware = [`,\n entries.join(',\\n'),\n `].filter(e => e.middleware)`,\n ].join('\\n')\n}\n\n/**\n * Scan a directory for route files.\n * Returns paths relative to the routes directory.\n */\nexport async function scanRouteFiles(routesDir: string): Promise<string[]> {\n const { readdir } = await import('node:fs/promises')\n const { relative } = await import('node:path')\n\n const files: string[] = []\n\n async function walk(dir: string) {\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n if (entry.isDirectory()) {\n await walk(fullPath)\n } else if (ROUTE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {\n files.push(relative(routesDir, fullPath))\n }\n }\n }\n\n await walk(routesDir)\n return files\n}\n\n/**\n * Scan route files AND read each one to detect optional metadata exports\n * (loader, guard, meta, renderMode, error, middleware).\n *\n * Returns FileRoute[] with `.exports` populated, ready to feed into\n * `generateRouteModuleFromRoutes()` for optimal output:\n * • lazy() for components without metadata (best code splitting)\n * • Direct property access for components with metadata (no _pick)\n * • No spurious IMPORT_IS_UNDEFINED warnings\n */\nexport async function scanRouteFilesWithExports(\n routesDir: string,\n defaultMode: RenderMode = 'ssr',\n): Promise<FileRoute[]> {\n const { readFile } = await import('node:fs/promises')\n\n const files = await scanRouteFiles(routesDir)\n const exportsMap = new Map<string, RouteFileExports>()\n\n await Promise.all(\n files.map(async (filePath) => {\n try {\n const source = await readFile(join(routesDir, filePath), 'utf-8')\n exportsMap.set(filePath, detectRouteExports(source))\n } catch {\n // File can't be read — generator treats this as no metadata\n // and emits the optimal lazy() shape.\n exportsMap.set(filePath, EMPTY_EXPORTS)\n }\n }),\n )\n\n return parseFileRoutes(files, defaultMode, exportsMap)\n}\n"],"mappings":";;;AAgCA,MAAM,mBAAmB;CAAC;CAAQ;CAAQ;CAAO;CAAM;;;;;AAyxCvD,eAAsB,eAAe,WAAsC;CACzE,MAAM,EAAE,YAAY,MAAM,OAAO;CACjC,MAAM,EAAE,aAAa,MAAM,OAAO;CAElC,MAAM,QAAkB,EAAE;CAE1B,eAAe,KAAK,KAAa;EAC/B,MAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,MAAM,CAAC;AAC3D,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,WAAW,KAAK,KAAK,MAAM,KAAK;AACtC,OAAI,MAAM,aAAa,CACrB,OAAM,KAAK,SAAS;YACX,iBAAiB,MAAM,QAAQ,MAAM,KAAK,SAAS,IAAI,CAAC,CACjE,OAAM,KAAK,SAAS,WAAW,SAAS,CAAC;;;AAK/C,OAAM,KAAK,UAAU;AACrB,QAAO"}