{"version":3,"file":"route-scanner-8P2TJsuY.cjs","names":[],"sources":["../../src/router/route-scanner.ts"],"sourcesContent":["import { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\n// --- Route scanner ---\n//\n// Walks src/app/ and maps file conventions to URL paths.\n//\n// Supported conventions:\n//   - page.ts          -> URL path\n//   - page.data.ts     -> loader for that page\n//   - layout.ts        -> layout wrapping pages in the same segment\n//   - route.ts         -> API endpoint (collected separately)\n//\n// Dynamic segments:\n//   - [slug]           -> :slug\n//   - [...slug]        -> catch-all (rendered as :slug*)\n//   - [[...slug]]      -> optional catch-all (rendered as :slug* but matches\n//                         the base path too)\n//\n// Route conflicts (two routes with the same path pattern) cause an error\n// during scanRoutes (plan §11.1).\n\n/** A page route discovered by the scanner. */\nexport interface PageRoute {\n  /** URL path, e.g. \"/blog/:slug\". */\n  path: string;\n  /** File system path to the page.ts module. */\n  pagePath: string;\n  /** File system path to the page.data.ts module, if any. */\n  dataPath?: string;\n  /** File system path to the page.action.ts module, if any. */\n  actionPath?: string;\n  /** Ordered list of layout.ts modules from root to leaf. */\n  layouts: string[];\n  /** File system path to the loading.ts module, if any. */\n  loadingPath?: string;\n  /** Dynamic parameter names extracted from the path. */\n  params: string[];\n  /** Whether the route has an optional catch-all segment. */\n  optionalCatchAll?: boolean;\n  /**\n   * Named slot modules discovered in the same directory as the page.\n   * Keyed by slot name (filename without `.slot.ts` suffix).\n   * (v2.1 — Fix #2: Layout Slots)\n   */\n  slots?: Record<string, string>;\n}\n\n/** An API route discovered by the scanner. */\nexport interface ApiRoute {\n  /** URL path, e.g. \"/api/posts\". */\n  path: string;\n  /** File system path to the route.ts module. */\n  routePath: string;\n  /** Dynamic parameter names extracted from the path. */\n  params: string[];\n}\n\n/** Result of scanning the app directory. */\nexport interface ScannedRoutes {\n  pages: PageRoute[];\n  api: ApiRoute[];\n  /** Optional 404 error page. */\n  error404?: PageRoute;\n  /** Optional 500 error page. */\n  error500?: PageRoute;\n}\n\nfunction isRouteGroup(segment: string): boolean {\n  return segment.startsWith(\"(\") && segment.endsWith(\")\");\n}\n\nfunction segmentToUrl(segment: string): string {\n  // Optional catch-all: [[...slug]] -> :slug* (matches base path too)\n  if (segment.startsWith(\"[[...\") && segment.endsWith(\"]]\")) {\n    return `:${segment.slice(5, -2)}*`;\n  }\n  // Catch-all: [...slug] -> :slug*\n  if (segment.startsWith(\"[...\") && segment.endsWith(\"]\")) {\n    return `:${segment.slice(4, -1)}*`;\n  }\n  // Dynamic: [slug] -> :slug\n  if (segment.startsWith(\"[\") && segment.endsWith(\"]\")) {\n    return `:${segment.slice(1, -1)}`;\n  }\n  return segment;\n}\n\nfunction extractParams(segment: string): string[] {\n  if (segment.startsWith(\"[[...\") && segment.endsWith(\"]]\")) {\n    return [segment.slice(5, -2)];\n  }\n  if (segment.startsWith(\"[...\") && segment.endsWith(\"]\")) {\n    return [segment.slice(4, -1)];\n  }\n  if (segment.startsWith(\"[\") && segment.endsWith(\"]\")) {\n    return [segment.slice(1, -1)];\n  }\n  return [];\n}\n\nfunction isOptionalCatchAll(segment: string): boolean {\n  return segment.startsWith(\"[[...\") && segment.endsWith(\"]]\");\n}\n\nasync function collectFiles(dir: string): Promise<string[]> {\n  try {\n    const entries = await readdir(dir, { withFileTypes: true });\n    return entries\n      .filter((e) => e.isFile() && e.name.endsWith(\".ts\"))\n      .map((e) => e.name);\n  } catch {\n    return [];\n  }\n}\n\nasync function collectDirs(dir: string): Promise<string[]> {\n  try {\n    const entries = await readdir(dir, { withFileTypes: true });\n    return entries.filter((e) => e.isDirectory()).map((e) => e.name);\n  } catch {\n    return [];\n  }\n}\n\nasync function scanRecursive(\n  appDir: string,\n  currentDir: string,\n  urlSegments: string[],\n  params: string[],\n  layouts: string[],\n  result: ScannedRoutes,\n  hasOptionalCatchAll = false,\n): Promise<void> {\n  const files = await collectFiles(currentDir);\n  const dirs = await collectDirs(currentDir);\n\n  const pagePath = files.includes(\"page.ts\")\n    ? join(currentDir, \"page.ts\")\n    : undefined;\n  const dataPath = files.includes(\"page.data.ts\")\n    ? join(currentDir, \"page.data.ts\")\n    : undefined;\n  const actionPath = files.includes(\"page.action.ts\")\n    ? join(currentDir, \"page.action.ts\")\n    : undefined;\n  const loadingPath = files.includes(\"loading.ts\")\n    ? join(currentDir, \"loading.ts\")\n    : undefined;\n  const layoutPath = files.includes(\"layout.ts\")\n    ? join(currentDir, \"layout.ts\")\n    : undefined;\n  const routePath = files.includes(\"route.ts\")\n    ? join(currentDir, \"route.ts\")\n    : undefined;\n\n  const currentLayouts = layoutPath\n    ? [...layouts, layoutPath]\n    : [...layouts];\n\n  if (routePath) {\n    result.api.push({\n      path: urlSegments.length === 0 ? \"/\" : \"/\" + urlSegments.join(\"/\"),\n      routePath,\n      params: [...params],\n    });\n  }\n\n  if (pagePath) {\n    const path = urlSegments.length === 0 ? \"/\" : \"/\" + urlSegments.join(\"/\");\n    // Detect named slot files: *.slot.ts (v2.1 — Fix #2: Layout Slots)\n    const slots: Record<string, string> = {};\n    for (const file of files) {\n      const slotMatch = file.match(/^(.+)\\.slot\\.ts$/);\n      if (slotMatch) {\n        slots[slotMatch[1]] = join(currentDir, file);\n      }\n    }\n    result.pages.push({\n      path,\n      pagePath,\n      dataPath,\n      actionPath,\n      layouts: currentLayouts,\n      loadingPath,\n      params: [...params],\n      optionalCatchAll: hasOptionalCatchAll,\n      slots: Object.keys(slots).length > 0 ? slots : undefined,\n    });\n  }\n\n  for (const dir of dirs) {\n    if (isRouteGroup(dir)) {\n      // Route groups do not add a URL segment, but they can add a layout.\n      const groupDir = join(currentDir, dir);\n      const groupFiles = await collectFiles(groupDir);\n      const groupLayout = groupFiles.includes(\"layout.ts\")\n        ? join(groupDir, \"layout.ts\")\n        : undefined;\n      await scanRecursive(\n        appDir,\n        groupDir,\n        urlSegments,\n        params,\n        groupLayout ? [...currentLayouts, groupLayout] : currentLayouts,\n        result,\n      );\n      continue;\n    }\n\n    const optional = isOptionalCatchAll(dir);\n    await scanRecursive(\n      appDir,\n      join(currentDir, dir),\n      [...urlSegments, segmentToUrl(dir)],\n      [...params, ...extractParams(dir)],\n      currentLayouts,\n      result,\n      optional,\n    );\n  }\n}\n\n/**\n * Scans an app directory for Nix.js Kit file-based routes.\n *\n * @param appDir Absolute path to the app directory (e.g. \"src/app\").\n * @returns Discovered page and API routes.\n */\nexport async function scanRoutes(appDir: string): Promise<ScannedRoutes> {\n  const result: ScannedRoutes = { pages: [], api: [] };\n  const rootFiles = await collectFiles(appDir);\n  const rootLayout = rootFiles.includes(\"layout.ts\")\n    ? join(appDir, \"layout.ts\")\n    : undefined;\n\n  if (rootFiles.includes(\"404.page.ts\")) {\n    result.error404 = {\n      path: \"/404\",\n      pagePath: join(appDir, \"404.page.ts\"),\n      dataPath: rootFiles.includes(\"404.page.data.ts\")\n        ? join(appDir, \"404.page.data.ts\")\n        : undefined,\n      layouts: rootLayout ? [rootLayout] : [],\n      params: [],\n    };\n  }\n\n  if (rootFiles.includes(\"500.page.ts\")) {\n    result.error500 = {\n      path: \"/500\",\n      pagePath: join(appDir, \"500.page.ts\"),\n      dataPath: rootFiles.includes(\"500.page.data.ts\")\n        ? join(appDir, \"500.page.data.ts\")\n        : undefined,\n      layouts: rootLayout ? [rootLayout] : [],\n      params: [],\n    };\n  }\n\n  await scanRecursive(appDir, appDir, [], [], [], result);\n\n  // Detect route conflicts (plan §11.1): two routes with the same path\n  // pattern is an error during manifest generation.\n  detectRouteConflicts(result);\n\n  return result;\n}\n\n/**\n * Detects and throws on route conflicts (plan §11.1, runtime-security §10).\n * Two routes with the same path pattern cause an error.\n */\nfunction detectRouteConflicts(routes: ScannedRoutes): void {\n  const pagePaths = new Map<string, string>();\n  for (const page of routes.pages) {\n    const existing = pagePaths.get(page.path);\n    if (existing) {\n      throw new Error(\n        `[nix-js-kit] Route conflict: \"${page.path}\" is defined by both ` +\n        `\"${existing}\" and \"${page.pagePath}\". ` +\n        `Remove one of the conflicting page.ts files.`,\n      );\n    }\n    pagePaths.set(page.path, page.pagePath);\n  }\n\n  // Also check API route conflicts.\n  const apiPaths = new Map<string, string>();\n  for (const api of routes.api) {\n    const existing = apiPaths.get(api.path);\n    if (existing) {\n      throw new Error(\n        `[nix-js-kit] API route conflict: \"${api.path}\" is defined by both ` +\n        `\"${existing}\" and \"${api.routePath}\".`,\n      );\n    }\n    apiPaths.set(api.path, api.routePath);\n  }\n}\n"],"mappings":"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"}