@pyreon/zero 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/lib/api-routes-DANluJic.js +146 -0
  2. package/lib/client.js +3 -1
  3. package/lib/csp.js +19 -9
  4. package/lib/{fs-router-CQ7Zxeca.js → fs-router-ZebyutPa.js} +43 -6
  5. package/lib/image-plugin.js +4 -0
  6. package/lib/image.js +1 -50
  7. package/lib/index.js +1 -50
  8. package/lib/link.js +1 -49
  9. package/lib/script.js +1 -49
  10. package/lib/server.js +6 -688
  11. package/lib/theme.js +1 -50
  12. package/lib/types/i18n-routing.d.ts +4 -4
  13. package/lib/types/index.d.ts +23 -13
  14. package/lib/types/link.d.ts +3 -3
  15. package/lib/types/server.d.ts +28 -5
  16. package/lib/types/theme.d.ts +2 -2
  17. package/lib/vite-plugin-E4BHYvYW.js +855 -0
  18. package/package.json +15 -13
  19. package/src/app.ts +21 -1
  20. package/src/csp.ts +28 -12
  21. package/src/fs-router.ts +53 -3
  22. package/src/ssg-plugin.ts +366 -0
  23. package/src/types.ts +28 -9
  24. package/src/vite-plugin.ts +220 -40
  25. package/lib/actions.js.map +0 -1
  26. package/lib/ai.js.map +0 -1
  27. package/lib/api-routes.js.map +0 -1
  28. package/lib/cache.js.map +0 -1
  29. package/lib/client.js.map +0 -1
  30. package/lib/compression.js.map +0 -1
  31. package/lib/config.js.map +0 -1
  32. package/lib/cors.js.map +0 -1
  33. package/lib/csp.js.map +0 -1
  34. package/lib/env.js.map +0 -1
  35. package/lib/favicon.js.map +0 -1
  36. package/lib/font.js.map +0 -1
  37. package/lib/fs-router-3xzp-4Wj.js.map +0 -1
  38. package/lib/fs-router-CQ7Zxeca.js.map +0 -1
  39. package/lib/i18n-routing.js.map +0 -1
  40. package/lib/image-plugin.js.map +0 -1
  41. package/lib/image.js.map +0 -1
  42. package/lib/index.js.map +0 -1
  43. package/lib/link.js.map +0 -1
  44. package/lib/logger.js.map +0 -1
  45. package/lib/meta.js.map +0 -1
  46. package/lib/middleware.js.map +0 -1
  47. package/lib/og-image.js.map +0 -1
  48. package/lib/rate-limit.js.map +0 -1
  49. package/lib/script.js.map +0 -1
  50. package/lib/seo.js.map +0 -1
  51. package/lib/server.js.map +0 -1
  52. package/lib/testing.js.map +0 -1
  53. package/lib/theme.js.map +0 -1
  54. package/lib/types/actions.d.ts.map +0 -1
  55. package/lib/types/ai.d.ts.map +0 -1
  56. package/lib/types/api-routes.d.ts.map +0 -1
  57. package/lib/types/cache.d.ts.map +0 -1
  58. package/lib/types/client.d.ts.map +0 -1
  59. package/lib/types/compression.d.ts.map +0 -1
  60. package/lib/types/config.d.ts.map +0 -1
  61. package/lib/types/cors.d.ts.map +0 -1
  62. package/lib/types/csp.d.ts.map +0 -1
  63. package/lib/types/env.d.ts.map +0 -1
  64. package/lib/types/favicon.d.ts.map +0 -1
  65. package/lib/types/font.d.ts.map +0 -1
  66. package/lib/types/i18n-routing.d.ts.map +0 -1
  67. package/lib/types/image-plugin.d.ts.map +0 -1
  68. package/lib/types/image.d.ts.map +0 -1
  69. package/lib/types/index.d.ts.map +0 -1
  70. package/lib/types/link.d.ts.map +0 -1
  71. package/lib/types/logger.d.ts.map +0 -1
  72. package/lib/types/meta.d.ts.map +0 -1
  73. package/lib/types/middleware.d.ts.map +0 -1
  74. package/lib/types/og-image.d.ts.map +0 -1
  75. package/lib/types/rate-limit.d.ts.map +0 -1
  76. package/lib/types/script.d.ts.map +0 -1
  77. package/lib/types/seo.d.ts.map +0 -1
  78. package/lib/types/server.d.ts.map +0 -1
  79. package/lib/types/testing.d.ts.map +0 -1
  80. package/lib/types/theme.d.ts.map +0 -1
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"fs-router-CQ7Zxeca.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":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"i18n-routing.js","names":[],"sources":["../src/i18n-routing.ts"],"sourcesContent":["import { createContext } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { Plugin } from 'vite'\n\n// ─── Localized routing ─────────────────────────────────────────────────────\n//\n// Adds locale-prefixed routes to Zero's file-system router:\n// - /about → /en/about, /de/about, /cs/about\n// - / → /en, /de, /cs (or default locale without prefix)\n// - Automatic locale detection from Accept-Language header\n// - Redirect to preferred locale\n// - hreflang link generation\n//\n// Usage:\n// import { i18nRouting } from \"@pyreon/zero\"\n// export default { plugins: [Pyreon], defaultLocale: \"en\" })] }\n\nexport interface I18nRoutingConfig {\n /** Supported locales. e.g. [\"en\", \"de\", \"cs\"] */\n locales: string[]\n /** Default locale — served without prefix (/ instead of /en/). */\n defaultLocale: string\n /** Redirect root to detected locale. Default: true */\n detectLocale?: boolean\n /** Cookie name to persist locale preference. Default: \"locale\" */\n cookieName?: string\n /** URL strategy. Default: \"prefix-except-default\" */\n strategy?: 'prefix' | 'prefix-except-default'\n}\n\nexport interface LocaleContext {\n /** Current locale code. e.g. \"en\", \"de\" */\n locale: string\n /** All supported locales. */\n locales: string[]\n /** Default locale. */\n defaultLocale: string\n /** Build a localized path. e.g. localePath(\"/about\", \"de\") → \"/de/about\" */\n localePath: (path: string, locale?: string) => string\n /** Get hreflang alternates for the current path. */\n alternates: () => Array<{ locale: string; url: string }>\n}\n\n/**\n * Detect preferred locale from Accept-Language header.\n */\nexport function detectLocaleFromHeader(\n acceptLanguage: string | null | undefined,\n locales: string[],\n defaultLocale: string,\n): string {\n if (!acceptLanguage) return defaultLocale\n\n // Parse Accept-Language: en-US,en;q=0.9,de;q=0.8\n const preferred = acceptLanguage\n .split(',')\n .map((part) => {\n const [lang, q] = part.trim().split(';q=')\n return {\n lang: lang?.split('-')[0]?.toLowerCase() ?? '',\n quality: q ? Number.parseFloat(q) : 1,\n }\n })\n .sort((a, b) => b.quality - a.quality)\n\n for (const { lang } of preferred) {\n if (locales.includes(lang)) return lang\n }\n\n return defaultLocale\n}\n\n/**\n * Extract locale from a URL path.\n * Returns { locale, pathWithoutLocale }.\n */\nexport function extractLocaleFromPath(\n path: string,\n locales: string[],\n defaultLocale: string,\n): { locale: string; pathWithoutLocale: string } {\n const segments = path.split('/').filter(Boolean)\n const firstSegment = segments[0]?.toLowerCase()\n\n if (firstSegment && locales.includes(firstSegment)) {\n return {\n locale: firstSegment,\n pathWithoutLocale: '/' + segments.slice(1).join('/') || '/',\n }\n }\n\n return { locale: defaultLocale, pathWithoutLocale: path }\n}\n\n/**\n * Build a localized path.\n */\nexport function buildLocalePath(\n path: string,\n locale: string,\n defaultLocale: string,\n strategy: 'prefix' | 'prefix-except-default',\n): string {\n const clean = path === '/' ? '' : path\n if (strategy === 'prefix-except-default' && locale === defaultLocale) {\n return path\n }\n return `/${locale}${clean}`\n}\n\n/**\n * Create a LocaleContext for use in components and loaders.\n */\nexport function createLocaleContext(\n locale: string,\n path: string,\n config: I18nRoutingConfig,\n): LocaleContext {\n const strategy = config.strategy ?? 'prefix-except-default'\n\n return {\n locale,\n locales: config.locales,\n defaultLocale: config.defaultLocale,\n\n localePath(targetPath: string, targetLocale?: string) {\n return buildLocalePath(\n targetPath,\n targetLocale ?? locale,\n config.defaultLocale,\n strategy,\n )\n },\n\n alternates() {\n const { pathWithoutLocale } = extractLocaleFromPath(\n path,\n config.locales,\n config.defaultLocale,\n )\n return config.locales.map((loc) => ({\n locale: loc,\n url: buildLocalePath(pathWithoutLocale, loc, config.defaultLocale, strategy),\n }))\n },\n }\n}\n\n/**\n * I18n routing middleware for Zero's server.\n *\n * - Detects locale from URL prefix or Accept-Language header\n * - Redirects root to preferred locale (when detectLocale is true)\n * - Sets locale context for loaders and components\n *\n * @example\n * ```ts\n * // zero.config.ts\n * import { i18nRouting } from \"@pyreon/zero\"\n *\n * export default defineConfig({\n * plugins: [\n * i18nRouting({\n * locales: [\"en\", \"de\", \"cs\"],\n * defaultLocale: \"en\",\n * }),\n * ],\n * })\n * ```\n */\nexport function i18nRouting(config: I18nRoutingConfig): Plugin {\n const strategy = config.strategy ?? 'prefix-except-default'\n const detectEnabled = config.detectLocale !== false\n const cookieName = config.cookieName ?? 'locale'\n\n return {\n name: 'pyreon-zero-i18n-routing',\n\n // Route duplication is NOT handled here. The fs-router's `scanRouteFiles`\n // consumes the i18n config to duplicate routes per locale at build time.\n // This plugin only provides: (1) the server middleware for locale detection\n // and (2) the runtime hooks (useLocale, setLocale) for client-side use.\n configResolved() {},\n\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = req.url ?? '/'\n\n // Skip static assets\n if (url.startsWith('/@') || url.startsWith('/__') || url.includes('.')) {\n return next()\n }\n\n const { locale } = extractLocaleFromPath(\n url,\n config.locales,\n config.defaultLocale,\n )\n\n // Redirect root to detected locale\n if (detectEnabled && url === '/') {\n const cookies = parseCookies(req.headers.cookie)\n const preferredFromCookie = cookies[cookieName]\n const preferredFromHeader = detectLocaleFromHeader(\n req.headers['accept-language'],\n config.locales,\n config.defaultLocale,\n )\n const preferred = preferredFromCookie && config.locales.includes(preferredFromCookie)\n ? preferredFromCookie\n : preferredFromHeader\n\n if (strategy === 'prefix' || preferred !== config.defaultLocale) {\n res.writeHead(302, { Location: `/${preferred}/` })\n res.end()\n return\n }\n }\n\n // Attach locale context to request for loaders\n ;(req as any).__locale = locale\n ;(req as any).__localeContext = createLocaleContext(locale, url, config)\n\n // Update the module-level signal so useLocale() returns the correct value\n localeSignal.set(locale)\n\n next()\n })\n },\n }\n}\n\nfunction parseCookies(header: string | undefined): Record<string, string> {\n if (!header) return {}\n const result: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const [key, value] = pair.trim().split('=')\n if (key && value) result[key] = decodeURIComponent(value)\n }\n return result\n}\n\n// ─── Reactive locale hook ───────────────────────────────────────────────────\n\n/** @internal Context for the current locale. */\nexport const LocaleCtx = createContext<string>('en')\n\n/** Current locale signal — set by the server middleware or client-side detection. */\nexport const localeSignal = signal('en')\n\n/**\n * Read the current locale reactively.\n *\n * Returns the locale signal value directly — reactive in both SSR and CSR.\n * The server middleware sets `localeSignal` per-request, and client-side\n * `setLocale()` updates it as well.\n *\n * @example\n * ```tsx\n * const locale = useLocale() // \"en\", \"de\", etc.\n * ```\n */\nexport function useLocale(): string {\n return localeSignal()\n}\n\n/**\n * Set the locale client-side and update the URL.\n *\n * @example\n * ```tsx\n * <button onClick={() => setLocale('de')}>Deutsch</button>\n * ```\n */\nexport function setLocale(\n locale: string,\n config: I18nRoutingConfig,\n): void {\n localeSignal.set(locale)\n\n // Persist to cookie\n if (typeof document !== 'undefined') {\n document.cookie = `${config.cookieName ?? 'locale'}=${locale}; path=/; max-age=31536000`\n }\n\n // Navigate to localized URL — use pushState to avoid full page reload\n if (typeof window !== 'undefined') {\n const strategy = config.strategy ?? 'prefix-except-default'\n const { pathWithoutLocale } = extractLocaleFromPath(\n window.location.pathname,\n config.locales,\n config.defaultLocale,\n )\n const newPath = buildLocalePath(pathWithoutLocale, locale, config.defaultLocale, strategy)\n window.history.pushState(null, '', newPath)\n // Dispatch popstate so @pyreon/router picks up the URL change\n window.dispatchEvent(new PopStateEvent('popstate'))\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"image-plugin.js","names":[],"sources":["../src/image-plugin.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { basename, extname, join } from 'node:path'\nimport type { Plugin } from 'vite'\n\nlet sharpWarned = false\nfunction warnSharpMissing() {\n if (sharpWarned) return\n sharpWarned = true\n // oxlint-disable-next-line no-console\n console.warn(\n '\\n[Pyreon] sharp not installed — images will not be optimized. Install for full support: bun add -D sharp\\n',\n )\n}\n\n// ─── Image processing Vite plugin ──────────────────────────────────────────\n//\n// Processes images at build time:\n// - Generates multiple sizes for responsive srcset\n// - Converts to modern formats (WebP, AVIF)\n// - Creates tiny blur placeholders (base64 inline)\n// - Outputs optimized images to the build directory\n//\n// Usage in code:\n// import heroImg from \"./hero.jpg?optimize\"\n// // → { src, srcset, width, height, placeholder }\n//\n// Or use the component helper:\n// import { Image } from \"@pyreon/zero/image\"\n// <Image src=\"/hero.jpg\" width={1920} height={1080} optimize />\n\n/**\n * CDN provider — rewrites image URLs to CDN endpoints.\n * Return the rewritten URL, or null to use local processing.\n */\nexport type ImageCdnProvider = (src: string, opts: {\n width: number\n quality: number\n format: ImageFormat\n}) => string | null\n\n/** Built-in CDN providers. */\nexport const cdnProviders = {\n /** Cloudinary: `https://res.cloudinary.com/{cloud}/image/upload/...` */\n cloudinary: (cloudName: string): ImageCdnProvider => (src, { width, quality, format }) =>\n `https://res.cloudinary.com/${cloudName}/image/upload/w_${width},q_${quality},f_${format}/${src}`,\n\n /** Imgix: `https://{domain}.imgix.net/...?w=...&q=...&fm=...` */\n imgix: (domain: string): ImageCdnProvider => (src, { width, quality, format }) =>\n `https://${domain}.imgix.net/${src}?w=${width}&q=${quality}&fm=${format}&auto=format`,\n\n /** Vercel Image Optimization: `/_next/image?url=...&w=...&q=...` */\n vercel: (): ImageCdnProvider => (src, { width, quality }) =>\n `/_vercel/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`,\n\n /** Bunny CDN: `https://{pullZone}.b-cdn.net/...?width=...&quality=...` */\n bunny: (pullZone: string): ImageCdnProvider => (src, { width, quality }) =>\n `https://${pullZone}.b-cdn.net/${src}?width=${width}&quality=${quality}`,\n} as const\n\n/** Placeholder generation strategy. */\nexport type PlaceholderStrategy = 'blur' | 'dominant-color' | 'none'\n\n/** SVG processing options for ?component imports. */\nexport interface SvgOptions {\n /** Replace fill/stroke with currentColor. Default: true */\n currentColor?: boolean\n /** Default size (width/height). */\n defaultSize?: number\n}\n\nexport interface ImagePluginConfig {\n /** Output directory for processed images. Default: \"assets/img\" */\n outDir?: string\n /** Default widths for responsive images. Default: [640, 1024, 1920] */\n widths?: number[]\n /** Output formats. Default: [\"webp\"] */\n formats?: ImageFormat[]\n /** Quality for lossy formats (1-100). Default: 80 */\n quality?: number\n /** Blur placeholder size in px. Default: 16 */\n placeholderSize?: number\n /** Placeholder strategy. Default: \"blur\" */\n placeholder?: PlaceholderStrategy\n /** File patterns to process. Default: /\\.(jpe?g|png|webp|avif)$/i */\n include?: RegExp\n /**\n * CDN provider for URL rewriting. When set, images are NOT processed\n * locally — URLs are rewritten to the CDN endpoint.\n *\n * @example\n * ```ts\n * imagePlugin({ cdn: cdnProviders.cloudinary('my-cloud') })\n * imagePlugin({ cdn: cdnProviders.vercel() })\n * ```\n */\n cdn?: ImageCdnProvider\n /**\n * SVG processing options. Enables `?component` import for inline SVGs.\n *\n * @example\n * ```tsx\n * import Logo from './logo.svg?component'\n * <Logo width={24} class=\"text-primary\" />\n * ```\n */\n svg?: SvgOptions | boolean\n}\n\nexport type ImageFormat = 'webp' | 'avif' | 'jpeg' | 'png'\n\n/** Per-format source set for <picture> <source> elements. */\nexport interface FormatSource {\n /** MIME type. e.g. \"image/webp\", \"image/avif\" */\n type: string\n /** srcset string for this format. e.g. \"/img-640.webp 640w, /img-1920.webp 1920w\" */\n srcset: string\n}\n\nexport interface ProcessedImage {\n /** Fallback source path (last format, largest width). */\n src: string\n /** Fallback srcset string (last format). */\n srcset: string\n /** Intrinsic width. */\n width: number\n /** Intrinsic height. */\n height: number\n /** Base64 blur placeholder data URI. */\n placeholder: string\n /** Per-format source sets for <picture> element. Ordered by priority (best format first). */\n formats: FormatSource[]\n /** Flat list of all sources. */\n sources: Array<{ src: string; width: number; format: string }>\n}\n\nconst IMAGE_EXT_RE = /\\.(jpe?g|png|webp|avif)$/i\n\n/**\n * Zero image processing Vite plugin.\n *\n * Transforms image imports with query params into optimized responsive images:\n *\n * @example\n * // vite.config.ts\n * import { imagePlugin } from \"@pyreon/zero/image-plugin\"\n *\n * export default {\n * plugins: [\n * pyreon(),\n * zero(),\n * imagePlugin({ widths: [480, 960, 1440], quality: 85 }),\n * ],\n * }\n *\n * @example\n * // In a component — import with ?optimize\n * import hero from \"./images/hero.jpg?optimize\"\n * // hero = { src, srcset, width, height, placeholder }\n *\n * <Image {...hero} alt=\"Hero\" priority />\n */\nexport function imagePlugin(config: ImagePluginConfig = {}): Plugin {\n const defaultWidths = config.widths ?? [640, 1024, 1920]\n const defaultFormats = config.formats ?? ['webp']\n const quality = config.quality ?? 80\n const placeholderSize = config.placeholderSize ?? 16\n const placeholderStrategy = config.placeholder ?? 'blur'\n const outSubDir = config.outDir ?? 'assets/img'\n const include = config.include ?? IMAGE_EXT_RE\n const cdn = config.cdn\n const svgOpts: SvgOptions | false = config.svg === true\n ? { currentColor: true }\n : config.svg === false || config.svg === undefined\n ? false\n : config.svg\n\n let root = ''\n let outDir = ''\n let isBuild = false\n\n return {\n name: 'pyreon-zero-images',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n outDir = resolvedConfig.build.outDir\n isBuild = resolvedConfig.command === 'build'\n },\n\n async resolveId(id) {\n // SVG as component: import Logo from './logo.svg?component'\n if (svgOpts && id.includes('?component') && id.split('?')[0]!.endsWith('.svg')) {\n return `\\0virtual:zero-svg:${id}`\n }\n // Handle ?optimize query on image imports\n if (id.includes('?optimize') && include.test(id.split('?')[0]!)) {\n return `\\0virtual:zero-image:${id}`\n }\n return null\n },\n\n async load(id) {\n // SVG component loading\n if (id.startsWith('\\0virtual:zero-svg:')) {\n const rawPath = id.replace('\\0virtual:zero-svg:', '').split('?')[0] ?? id\n const absPath = rawPath.startsWith('/') ? join(root, rawPath) : rawPath\n if (!existsSync(absPath)) return null\n\n let svg = await readFile(absPath, 'utf-8')\n\n // Replace fill/stroke with currentColor\n if (svgOpts && (svgOpts as SvgOptions).currentColor !== false) {\n svg = svg\n .replace(/fill=\"(?!none)[^\"]*\"/g, 'fill=\"currentColor\"')\n .replace(/stroke=\"(?!none)[^\"]*\"/g, 'stroke=\"currentColor\"')\n }\n\n // Add default size from config\n const defaultSize = svgOpts && (svgOpts as SvgOptions).defaultSize\n if (defaultSize && !svg.includes('width=')) {\n svg = svg.replace('<svg', `<svg width=\"${defaultSize}\" height=\"${defaultSize}\"`)\n }\n\n // Export as Pyreon component\n return `\nimport { h } from '@pyreon/core'\nconst _svg = ${JSON.stringify(svg)}\nexport default function SvgComponent(props) {\n const el = h('span', {\n ...props,\n dangerouslySetInnerHTML: { __html: _svg },\n style: [\n 'display:inline-flex;align-items:center;justify-content:center',\n props.width ? 'width:' + props.width + 'px' : '',\n props.height ? 'height:' + props.height + 'px' : '',\n props.style || '',\n ].filter(Boolean).join(';'),\n })\n return el\n}\n`\n }\n\n // Image optimization loading\n if (!id.startsWith('\\0virtual:zero-image:')) return null\n\n const rawPath = id.replace('\\0virtual:zero-image:', '').split('?')[0] ?? id\n const absPath = rawPath.startsWith('/') ? join(root, 'public', rawPath) : rawPath\n\n // CDN mode — rewrite URLs, no local processing\n if (cdn) {\n const metadata = await getImageMetadata(absPath)\n const sources = defaultWidths.map((w) => ({\n src: cdn(rawPath, { width: w, quality, format: defaultFormats[0]! }) ?? rawPath,\n width: w,\n format: defaultFormats[0]! as string,\n }))\n const srcset = sources.map((s) => `${s.src} ${s.width}w`).join(', ')\n const result: ProcessedImage = {\n src: sources[sources.length - 1]?.src ?? rawPath,\n srcset,\n width: metadata.width,\n height: metadata.height,\n placeholder: placeholderStrategy === 'none' ? ''\n : await generateBlurPlaceholder(absPath, placeholderSize),\n formats: defaultFormats.map((fmt) => ({\n type: `image/${fmt}`,\n srcset: defaultWidths\n .map((w) => `${cdn(rawPath, { width: w, quality, format: fmt }) ?? rawPath} ${w}w`)\n .join(', '),\n })),\n sources,\n }\n return `export default ${JSON.stringify(result)}`\n }\n\n if (!isBuild) {\n const result = await loadDevImage(absPath, rawPath, placeholderSize)\n return `export default ${JSON.stringify(result)}`\n }\n\n const processed = await processImage(absPath, {\n widths: defaultWidths,\n formats: defaultFormats,\n quality,\n placeholderSize,\n outSubDir,\n outDir: join(root, outDir),\n })\n\n await emitProcessedSources(processed, outSubDir, this)\n rebuildFormatSrcsets(processed, absPath)\n\n return `export default ${JSON.stringify(processed)}`\n },\n }\n}\n\nasync function loadDevImage(\n absPath: string,\n rawPath: string,\n placeholderSize: number,\n): Promise<ProcessedImage> {\n const metadata = await getImageMetadata(absPath)\n const publicPath = rawPath.startsWith('/') ? rawPath : `/@fs/${absPath}`\n\n return {\n src: publicPath,\n srcset: '',\n width: metadata.width,\n height: metadata.height,\n placeholder: await generateBlurPlaceholder(absPath, placeholderSize),\n formats: [],\n sources: [{ src: publicPath, width: metadata.width, format: 'original' }],\n }\n}\n\nasync function emitProcessedSources(\n processed: ProcessedImage,\n outSubDir: string,\n ctx: {\n emitFile: (f: { type: 'asset'; fileName: string; source: Uint8Array }) => void\n },\n) {\n for (const source of processed.sources) {\n const fileName = join(outSubDir, basename(source.src))\n const content = await readFile(source.src)\n ctx.emitFile({ type: 'asset', fileName, source: content })\n source.src = `/${fileName}`\n }\n}\n\nfunction rebuildFormatSrcsets(processed: ProcessedImage, fallbackPath: string) {\n const formatGroups = new Map<string, string[]>()\n for (const s of processed.sources) {\n let group = formatGroups.get(s.format)\n if (!group) {\n group = []\n formatGroups.set(s.format, group)\n }\n group.push(`${s.src} ${s.width}w`)\n }\n processed.formats = [...formatGroups.entries()].map(([fmt, entries]) => ({\n type: `image/${fmt}`,\n srcset: entries.join(', '),\n }))\n\n const lastFormat = processed.formats.at(-1)\n processed.srcset = lastFormat?.srcset ?? ''\n processed.src = processed.sources.at(-1)?.src ?? fallbackPath\n}\n\n// ─── Image processing utilities ─────────────────────────────────────────────\n\ninterface ProcessOptions {\n widths: number[]\n formats: ImageFormat[]\n quality: number\n placeholderSize: number\n outSubDir: string\n outDir: string\n}\n\nasync function processImage(absPath: string, opts: ProcessOptions): Promise<ProcessedImage> {\n const metadata = await getImageMetadata(absPath)\n const ext = extname(absPath)\n const name = basename(absPath, ext)\n const sources: Array<{ src: string; width: number; format: string }> = []\n\n // Ensure output directory exists\n const processedDir = join(opts.outDir, opts.outSubDir)\n if (!existsSync(processedDir)) {\n await mkdir(processedDir, { recursive: true })\n }\n\n // Generate resized variants — iterate formats first so sources are grouped by format\n for (const format of opts.formats) {\n for (const targetWidth of opts.widths) {\n // Don't upscale\n const width = Math.min(targetWidth, metadata.width)\n const outName = `${name}-${width}.${format}`\n const outPath = join(processedDir, outName)\n\n await resizeImage(absPath, outPath, width, format, opts.quality)\n sources.push({ src: outPath, width, format })\n }\n }\n\n // Build per-format source sets for <picture>\n const formatGroups = new Map<string, Array<{ src: string; width: number }>>()\n for (const s of sources) {\n let group = formatGroups.get(s.format)\n if (!group) {\n group = []\n formatGroups.set(s.format, group)\n }\n group.push({ src: s.src, width: s.width })\n }\n\n const formats: FormatSource[] = [...formatGroups.entries()].map(([fmt, group]) => ({\n type: `image/${fmt === 'jpeg' ? 'jpeg' : fmt}`,\n srcset: group.map((s) => `${s.src} ${s.width}w`).join(', '),\n }))\n\n // Fallback: last format's srcset\n const fallbackFormat = formats[formats.length - 1]\n const fallbackSources = formatGroups.get([...formatGroups.keys()].pop()!)!\n\n // Generate blur placeholder\n const placeholder = await generateBlurPlaceholder(absPath, opts.placeholderSize)\n\n return {\n src: fallbackSources[fallbackSources.length - 1]?.src ?? absPath,\n srcset: fallbackFormat?.srcset ?? '',\n width: metadata.width,\n height: metadata.height,\n placeholder,\n formats,\n sources,\n }\n}\n\ninterface ImageMetadata {\n width: number\n height: number\n format: string\n}\n\n/**\n * Read basic image metadata.\n * Uses minimal binary header parsing — no external dependencies.\n */\nasync function getImageMetadata(absPath: string): Promise<ImageMetadata> {\n const buffer = await readFile(absPath)\n const ext = extname(absPath).toLowerCase()\n\n if (ext === '.png') {\n // PNG: width at bytes 16-19, height at 20-23 (big-endian)\n const width = buffer.readUInt32BE(16)\n const height = buffer.readUInt32BE(20)\n return { width, height, format: 'png' }\n }\n\n if (ext === '.jpg' || ext === '.jpeg') {\n // JPEG: scan for SOF markers\n const dimensions = parseJpegDimensions(buffer)\n return { ...dimensions, format: 'jpeg' }\n }\n\n if (ext === '.webp') {\n // WebP: VP8 header\n const dimensions = parseWebPDimensions(buffer)\n return { ...dimensions, format: 'webp' }\n }\n\n // Fallback\n return { width: 0, height: 0, format: ext.slice(1) }\n}\n\n/** @internal Exported for testing */\nexport function parseJpegDimensions(buffer: Buffer): {\n width: number\n height: number\n} {\n let offset = 2 // Skip SOI marker\n while (offset < buffer.length) {\n if (buffer[offset] !== 0xff) break\n const marker = buffer[offset + 1]!\n // SOF markers (0xC0-0xCF except 0xC4, 0xC8, 0xCC)\n if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {\n const height = buffer.readUInt16BE(offset + 5)\n const width = buffer.readUInt16BE(offset + 7)\n return { width, height }\n }\n const length = buffer.readUInt16BE(offset + 2)\n offset += 2 + length\n }\n return { width: 0, height: 0 }\n}\n\n/** @internal Exported for testing */\nexport function parseWebPDimensions(buffer: Buffer): {\n width: number\n height: number\n} {\n // RIFF header: bytes 0-3 = \"RIFF\", 8-11 = \"WEBP\"\n const chunk = buffer.toString('ascii', 12, 16)\n if (chunk === 'VP8 ') {\n // Lossy VP8\n const width = buffer.readUInt16LE(26) & 0x3fff\n const height = buffer.readUInt16LE(28) & 0x3fff\n return { width, height }\n }\n if (chunk === 'VP8L') {\n // Lossless VP8L\n const bits = buffer.readUInt32LE(21)\n const width = (bits & 0x3fff) + 1\n const height = ((bits >> 14) & 0x3fff) + 1\n return { width, height }\n }\n if (chunk === 'VP8X') {\n // Extended VP8X\n const width = 1 + ((buffer[24]! | (buffer[25]! << 8) | (buffer[26]! << 16)) & 0xffffff)\n const height = 1 + ((buffer[27]! | (buffer[28]! << 8) | (buffer[29]! << 16)) & 0xffffff)\n return { width, height }\n }\n return { width: 0, height: 0 }\n}\n\n/**\n * Resize an image using native platform capabilities.\n * Uses sharp if available, falls back to canvas API.\n */\nasync function resizeImage(\n input: string,\n output: string,\n width: number,\n format: ImageFormat,\n quality: number,\n): Promise<void> {\n try {\n // Try sharp (the standard Node.js image processing library)\n const sharp = await import('sharp').then((m) => m.default ?? m)\n let pipeline = sharp(input).resize(width)\n\n switch (format) {\n case 'webp':\n pipeline = pipeline.webp({ quality })\n break\n case 'avif':\n pipeline = pipeline.avif({ quality })\n break\n case 'jpeg':\n pipeline = pipeline.jpeg({ quality, mozjpeg: true })\n break\n case 'png':\n pipeline = pipeline.png({ compressionLevel: 9 })\n break\n }\n\n await pipeline.toFile(output)\n } catch {\n // sharp not available — copy original as fallback\n warnSharpMissing()\n const content = await readFile(input)\n await writeFile(output, content)\n }\n}\n\n/**\n * Generate a tiny blur placeholder as a base64 data URI.\n */\nasync function generateBlurPlaceholder(input: string, size: number): Promise<string> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n const buffer = await sharp(input)\n .resize(size, size, { fit: 'inside' })\n .blur(2)\n .webp({ quality: 20 })\n .toBuffer()\n\n return `data:image/webp;base64,${buffer.toString('base64')}`\n } catch {\n // sharp not available — return a transparent placeholder\n return \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3C/svg%3E\"\n }\n}\n"],"mappings":"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"}
package/lib/image.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"image.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../core/core/lib/jsx-runtime.js","../src/image.tsx"],"sourcesContent":["import { onMount, onUnmount } from '@pyreon/core'\n\n/**\n * Observes an element and calls `onIntersect` once it enters the viewport.\n * Automatically disconnects after the first intersection.\n *\n * @param getElement - Getter for the target element (may be undefined before mount).\n * @param onIntersect - Callback fired when the element becomes visible.\n * @param rootMargin - IntersectionObserver rootMargin. Default: \"200px\".\n */\nexport function useIntersectionObserver(\n getElement: () => HTMLElement | undefined,\n onIntersect: () => void,\n rootMargin = '200px',\n) {\n onMount(() => {\n const el = getElement()\n if (!el) return undefined\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n onIntersect()\n observer.disconnect()\n }\n }\n },\n { rootMargin },\n )\n\n observer.observe(el)\n onUnmount(() => observer.disconnect())\n return undefined\n })\n}\n","//#region src/h.ts\n/** Marker for fragment nodes — renders children without a wrapper element */\nconst Fragment = Symbol(\"Pyreon.Fragment\");\n/**\n* Hyperscript function — the compiled output of JSX.\n* `<div class=\"x\">hello</div>` → `h(\"div\", { class: \"x\" }, \"hello\")`\n*\n* Generic on P so TypeScript validates props match the component's signature\n* at the call site, then stores the result in the loosely-typed VNode.\n*/\n/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */\nconst EMPTY_PROPS = {};\nfunction h(type, props, ...children) {\n\treturn {\n\t\ttype,\n\t\tprops: props ?? EMPTY_PROPS,\n\t\tchildren: normalizeChildren(children),\n\t\tkey: props?.key ?? null\n\t};\n}\nfunction normalizeChildren(children) {\n\tfor (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);\n\treturn children;\n}\nfunction flattenChildren(children) {\n\tconst result = [];\n\tfor (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));\n\telse result.push(child);\n\treturn result;\n}\n\n//#endregion\n//#region src/jsx-runtime.ts\n/**\n* JSX automatic runtime.\n*\n* When tsconfig has `\"jsxImportSource\": \"@pyreon/core\"`, the TS/bundler compiler\n* rewrites JSX to imports from this file automatically:\n* <div class=\"x\" /> → jsx(\"div\", { class: \"x\" })\n*/\nfunction jsx(type, props, key) {\n\tconst { children, ...rest } = props;\n\tconst propsWithKey = key != null ? {\n\t\t...rest,\n\t\tkey\n\t} : rest;\n\tif (typeof type === \"function\") return h(type, children !== void 0 ? {\n\t\t...propsWithKey,\n\t\tchildren\n\t} : propsWithKey);\n\treturn h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);\n}\nconst jsxs = jsx;\n\n//#endregion\nexport { Fragment, jsx, jsxs };\n//# sourceMappingURL=jsx-runtime.js.map","import type { VNodeChild } from '@pyreon/core'\nimport { createRef } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { FormatSource } from './image-plugin'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Image optimization component ───────────────────────────────────────────\n//\n// <Image> provides:\n// - Lazy loading via IntersectionObserver (loads when near viewport)\n// - Automatic width/height to prevent CLS (Cumulative Layout Shift)\n// - Responsive srcset generation from width descriptors\n// - Multi-format support via <picture> (WebP/AVIF with fallback)\n// - Blur-up placeholder while loading\n// - Priority loading for above-the-fold images\n\nexport interface ImageProps {\n /** Image source URL. */\n src: string\n /** Alt text (required for accessibility). */\n alt: string\n /** Intrinsic width of the image. */\n width: number\n /** Intrinsic height of the image. */\n height: number\n /** Responsive sizes attribute. Default: \"100vw\" */\n sizes?: string\n /** Responsive srcset string or source array. */\n srcset?: string | ImageSource[]\n /** Per-format source sets for <picture>. Provided automatically by imagePlugin. */\n formats?: FormatSource[]\n /** Loading strategy. \"lazy\" uses IntersectionObserver, \"eager\" loads immediately. Default: \"lazy\" */\n loading?: 'lazy' | 'eager'\n /** Mark as priority (LCP image). Disables lazy loading, adds fetchPriority=\"high\". */\n priority?: boolean\n /** Low-quality placeholder image URL or base64 data URI for blur-up effect. */\n placeholder?: string\n /** CSS class name. */\n class?: string\n /** Inline styles. */\n style?: string\n /** CSS object-fit. Default: \"cover\" */\n fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'\n /** Decode async. Default: true */\n decoding?: 'sync' | 'async' | 'auto'\n /**\n * Raw mode — renders a plain `<img>` without the container div,\n * aspect-ratio, max-width, or lazy loading wrapper.\n * Use when the Image is inside a custom layout (absolute positioning, etc.).\n */\n raw?: boolean\n}\n\nexport interface ImageSource {\n src: string\n width: number\n}\n\n/**\n * Optimized image component with lazy loading, responsive images,\n * multi-format <picture> support, and blur-up placeholders.\n *\n * @example\n * // With imagePlugin — spread the import directly\n * import hero from \"./hero.jpg?optimize\"\n * <Image {...hero} alt=\"Hero\" priority />\n *\n * @example\n * // Manual usage\n * <Image src=\"/hero.jpg\" alt=\"Hero\" width={1200} height={630} />\n */\nexport function Image(props: ImageProps): VNodeChild {\n // Raw mode: plain <img> without container, lazy loading, or layout constraints\n if (props.raw) {\n return (\n <img\n src={props.src}\n alt={props.alt}\n width={props.width}\n height={props.height}\n class={props.class}\n style={props.style}\n decoding={props.decoding ?? 'async'}\n loading={props.loading ?? 'lazy'}\n fetchPriority={props.priority ? 'high' : undefined}\n />\n ) as any\n }\n\n const isEager = props.priority || props.loading === 'eager'\n const loaded = signal(isEager)\n const inView = signal(isEager)\n const containerRef = createRef<HTMLElement>()\n\n // Resolve srcset from string or array\n const resolvedSrcset =\n typeof props.srcset === 'string'\n ? props.srcset\n : props.srcset?.map((s) => `${s.src} ${s.width}w`).join(', ')\n\n const sizes = props.sizes ?? '100vw'\n const fit = props.fit ?? 'cover'\n const hasFormats = props.formats && props.formats.length > 0\n const aspectRatio = `${props.width} / ${props.height}`\n\n if (!isEager) {\n useIntersectionObserver(\n () => containerRef.current ?? undefined,\n () => inView.set(true),\n )\n }\n\n // Static styles (don't depend on signals)\n const containerStyle = [\n 'position: relative',\n 'overflow: hidden',\n `aspect-ratio: ${aspectRatio}`,\n `max-width: ${props.width}px`,\n 'width: 100%',\n props.style,\n ]\n .filter(Boolean)\n .join('; ')\n\n const imgEl = (\n <img\n src={() => (inView() ? props.src : '')}\n srcSet={() => (!hasFormats && inView() && resolvedSrcset ? resolvedSrcset : '')}\n sizes={resolvedSrcset ? sizes : undefined}\n alt={props.alt}\n width={props.width}\n height={props.height}\n loading={isEager ? 'eager' : 'lazy'}\n decoding={props.decoding ?? 'async'}\n fetchPriority={props.priority ? 'high' : undefined}\n onLoad={() => loaded.set(true)}\n style={() =>\n [\n 'display: block',\n 'width: 100%',\n 'height: 100%',\n `object-fit: ${fit}`,\n 'transition: opacity 0.3s ease',\n props.placeholder && !loaded() ? 'opacity: 0' : 'opacity: 1',\n ].join('; ')\n }\n />\n )\n\n return (\n <div ref={containerRef} class={props.class} style={containerStyle}>\n {props.placeholder && (\n <img\n src={props.placeholder}\n alt=\"\"\n aria-hidden=\"true\"\n loading=\"eager\"\n style={() =>\n [\n 'position: absolute',\n 'inset: 0',\n 'width: 100%',\n 'height: 100%',\n 'object-fit: cover',\n 'filter: blur(20px)',\n 'transform: scale(1.1)',\n 'transition: opacity 0.4s ease',\n loaded() ? 'opacity: 0; pointer-events: none' : 'opacity: 1',\n ].join('; ')\n }\n />\n )}\n {hasFormats ? (\n <picture>\n {props.formats?.map((fmt) => (\n <source\n type={fmt.type}\n srcSet={() => (inView() ? (fmt.srcset ?? '') : '')}\n sizes={sizes}\n />\n ))}\n {imgEl}\n </picture>\n ) : (\n imgEl\n )}\n </div>\n )\n}\n"],"mappings":"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"}