@elurjs/kit 2.4.5 → 2.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -1
- package/README.md +9 -3
- package/dist/lib/cli.cjs +9 -1
- package/dist/lib/cli.cjs.map +1 -1
- package/dist/lib/cli.js +9 -1
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/index.cjs +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/{interpolation-plugin-CMPCDlVJ.cjs → interpolation-plugin-TWDch8nn.cjs} +10 -2
- package/dist/lib/{interpolation-plugin-CMPCDlVJ.cjs.map → interpolation-plugin-TWDch8nn.cjs.map} +1 -1
- package/dist/lib/{interpolation-plugin-Bt-KRKVY.js → interpolation-plugin-Wgb1j4pT.js} +10 -2
- package/dist/lib/{interpolation-plugin-Bt-KRKVY.js.map → interpolation-plugin-Wgb1j4pT.js.map} +1 -1
- package/dist/lib/node-http-DRAUhO0c.js.map +1 -1
- package/dist/lib/node-http-DhxguYyz.cjs.map +1 -1
- package/dist/lib/vite/index.cjs +1 -1
- package/dist/lib/vite/index.js +1 -1
- package/package.json +5 -4
package/dist/lib/{interpolation-plugin-Bt-KRKVY.js.map → interpolation-plugin-Wgb1j4pT.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interpolation-plugin-Bt-KRKVY.js","names":[],"sources":["../../src/island/generate-entry.ts","../../src/middleware/index.ts","../../src/vite/interpolation-plugin.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, relative, sep } from \"node:path\";\nimport type { IslandModule } from \"./scan.js\";\n\n// --- Client entry generator ---\n//\n// Turns a list of scanned islands into a client entry module that imports each\n// island and registers it with `hydrateIslands`. This removes the need to hand-\n// maintain `entry-client.ts` as islands are added or removed.\n//\n// The generated file imports island default exports and passes them to\n// `hydrateIslands` keyed by their registry name.\n\n/** Options for generating the client entry module. */\nexport interface GenerateEntryOptions {\n /** Islands to register, from `scanIslands`. */\n islands: IslandModule[];\n /** Absolute path of the entry file to write (e.g. \".elur/entry-client.ts\"). */\n outFile: string;\n /**\n * Import specifier for the kit's client island helpers.\n * Defaults to the published subpath `@elurjs/kit/island`.\n */\n hydrateImport?: string;\n /**\n * Import specifier for the kit's client router.\n * Defaults to the published subpath `@elurjs/kit/router`.\n */\n routerImport?: string;\n}\n\n/** Turns a registry name into a safe JS identifier for the import binding. */\nfunction toIdentifier(name: string, index: number): string {\n const cleaned = name.replace(/[^a-zA-Z0-9_$]/g, \"_\");\n return /^[a-zA-Z_$]/.test(cleaned) ? `${cleaned}_${index}` : `_${cleaned}_${index}`;\n}\n\n/** Builds the source code of the client entry module. */\nexport function buildEntrySource(\n islands: IslandModule[],\n outFile: string,\n hydrateImport = \"@elurjs/kit/island\",\n routerImport = \"@elurjs/kit/router\",\n): string {\n const bindings = islands.map((island, i) => ({\n ident: toIdentifier(island.name, i),\n name: island.name,\n // Relative import specifier from the entry file to the island module.\n spec: toImportSpecifier(outFile, island.filePath),\n }));\n\n // Lazy registry: each island is loaded on-demand via dynamic import().\n // This enables code-splitting — islands not on the current page (or not yet\n // triggered by their directive) stay out of the initial bundle.\n //\n // The registry maps island name → discriminated lazy loader `{ load }`.\n // hydrateIslands() awaits `entry.load()` before hydrating, so the first\n // paint only needs the small entry chunk + the islands on the page. The\n // discriminated form lets the hydrator tell eager components from lazy\n // loaders without executing a probe.\n const registryLines = bindings\n .map((b) => ` ${JSON.stringify(b.name)}: { load: () => import(${JSON.stringify(b.spec)}).then(m => m.default) },`)\n .join(\"\\n\");\n\n const islandHydration = registryLines\n ? `const registry = {\n${registryLines}\n};\nhydrateIslands(registry);\ndocument.addEventListener(\"elur:rendered\", () => {\n cleanupHydratedIslands();\n hydrateIslands(registry);\n});\n\n// Vite HMR: when an island module (or the entry itself) updates, dispose the\n// current islands and re-hydrate from the updated modules — the registry's\n// dynamic import() resolves to the fresh modules, so no full page reload is\n// needed (progressive enhancement, audit §10.2 / §12.2).\nif (import.meta.hot) {\n import.meta.hot.accept((newModule) => {\n cleanupHydratedIslands();\n hydrateIslands(registry);\n if (newModule) {\n // Re-run the module so its side effects (router, listeners) apply.\n }\n });\n}`\n : \"\";\n\n return `// AUTO-GENERATED by @elurjs/kit. Do not edit.\nimport { startClientRouter } from ${JSON.stringify(routerImport)};\nimport { hydrateIslands, cleanupHydratedIslands } from ${JSON.stringify(hydrateImport)};\n\nstartClientRouter();\n${islandHydration}\n`;\n}\n\n/** Computes a POSIX-style relative import specifier between two files. */\nfunction toImportSpecifier(fromFile: string, toFile: string): string {\n let spec = relative(dirname(fromFile), toFile).split(sep).join(\"/\");\n if (!spec.startsWith(\".\")) spec = `./${spec}`;\n return spec;\n}\n\n/**\n * Generates and writes the client entry module for the given islands.\n *\n * @param options Generation options.\n * @returns The absolute path of the written entry file.\n */\nexport async function generateClientEntry(\n options: GenerateEntryOptions,\n): Promise<string> {\n const source = buildEntrySource(\n options.islands,\n options.outFile,\n options.hydrateImport,\n options.routerImport,\n );\n await mkdir(dirname(options.outFile), { recursive: true });\n await writeFile(options.outFile, source, \"utf8\");\n return options.outFile;\n}\n","// --- Middleware ---\n//\n// Convention: `src/middleware.ts` in the project root exports a default\n// function and an optional `config` with a `matcher` array.\n//\n// import type { Middleware } from \"@elurjs/kit\";\n//\n// export default function middleware(request: Request) {\n// if (!request.headers.get(\"Cookie\")?.includes(\"session=\")) {\n// return Response.redirect(new URL(\"/login\", request.url), 307);\n// }\n// }\n//\n// export const config = { matcher: [\"/dashboard/:path*\", \"/admin/:path*\"] };\n//\n// The middleware runs before routing. Return a `Response` to short-circuit\n// (redirect, rewrite, 401, etc.). Return `undefined` or nothing to continue.\n// Use `next()` to pass headers to the loader.\n\nimport { matchRoute } from \"../ssr/match.js\";\nimport type { PageRoute } from \"../router/route-scanner.js\";\n\n/** The middleware function signature. */\nexport type Middleware = (request: Request, context: MiddlewareContext) =>\n | Response\n | void\n | Promise<Response | void>;\n\n/** Context passed to the middleware function. */\nexport interface MiddlewareContext {\n /** Helper to continue to the next handler. Can attach headers, params, and locals. */\n next(options?: {\n headers?: Record<string, string>;\n params?: Record<string, string | string[]>;\n locals?: Record<string, unknown>;\n }): void;\n /** Matched route params (only available if the path matches a page route). */\n params?: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware, available to loaders/actions). */\n locals?: Record<string, unknown>;\n}\n\n/** Configuration for the middleware module. */\nexport interface MiddlewareConfig {\n /** Path patterns that trigger the middleware. Supports `:param` and `:param*`. */\n matcher?: string[];\n}\n\nexport interface LoadedMiddleware {\n handler: Middleware;\n config: MiddlewareConfig;\n}\n\n/** Result of running middleware: either a response to short-circuit with, or continue. */\nexport type MiddlewareResult =\n | { kind: \"response\"; response: Response }\n | {\n kind: \"continue\";\n headers?: Record<string, string>;\n params?: Record<string, string | string[]>;\n locals?: Record<string, unknown>;\n };\n\n/**\n * Loads the user's `src/middleware.ts` module. Returns `null` if no middleware\n * file exists. Distinguishes \"file not found\" from \"file has errors\" (§6):\n * an import error is not silently treated as \"no middleware\".\n */\nexport async function loadMiddleware(root: string): Promise<LoadedMiddleware | null> {\n const candidates = [\n `${root}/src/middleware.ts`,\n `${root}/middleware.ts`,\n ];\n\n for (const path of candidates) {\n try {\n const mod = await import(path);\n const handler = (mod.default ?? mod.middleware) as Middleware | undefined;\n if (typeof handler !== \"function\") continue;\n const config = (mod.config ?? {}) as MiddlewareConfig;\n return { handler, config };\n } catch (err) {\n // Distinguish \"module not found\" from actual errors.\n // If the error is a module resolution error for this specific file,\n // it means the file doesn't exist — try the next candidate.\n // If it's a syntax/runtime error, rethrow so the user sees it.\n // Note: Bun's ResolveMessage is not `instanceof Error`, so match on the\n // message property instead of relying on the class hierarchy.\n const msg =\n typeof err === \"object\" && err !== null && \"message\" in err\n ? String((err as { message: unknown }).message)\n : String(err);\n if (\n msg.includes(\"Cannot find module\") ||\n msg.includes(\"Cannot find package\") ||\n msg.includes(\"ENOENT\") ||\n msg.includes(\"Module not found\")\n ) {\n // File doesn't exist — try next candidate.\n continue;\n }\n // Actual error in the middleware file — rethrow (§6).\n throw new Error(`[elur-kit] Error loading middleware: ${msg}`, { cause: err });\n }\n }\n\n return null;\n}\n\n/**\n * Checks if a pathname matches any of the middleware's matcher patterns.\n * If no matcher is configured, the middleware runs for every request.\n *\n * Catch-all patterns (`:param*`) match both the base path and any sub-paths,\n * e.g. `/dashboard/:path*` matches `/dashboard` and `/dashboard/settings/users`.\n */\nexport function matchesMiddleware(pathname: string, config: MiddlewareConfig): boolean {\n if (!config.matcher || config.matcher.length === 0) return true;\n\n const cleanPath = pathname.split(\"?\")[0];\n\n for (const pattern of config.matcher) {\n // Exact match.\n if (pattern === cleanPath) return true;\n\n // Check for catch-all: `/foo/:bar*` should also match `/foo`.\n const catchAllMatch = pattern.match(/^(.*)\\/:[\\w]+\\*$/);\n if (catchAllMatch) {\n const base = catchAllMatch[1];\n if (cleanPath === base) return true;\n }\n\n // Use matchRoute for param matching.\n const pseudoRoutes: PageRoute[] = [{\n path: pattern,\n pagePath: \"\",\n params: [],\n layouts: [],\n }];\n if (matchRoute(cleanPath, pseudoRoutes)) return true;\n }\n\n return false;\n}\n\n/**\n * Runs the middleware for a request. Returns the result indicating whether to\n * short-circuit with a response or continue with propagated headers/params/locals.\n *\n * Per §6: cleanup runs in `finally`, response short-circuits the pipeline,\n * headers/params/locals are propagated to downstream handlers.\n */\nexport async function runMiddleware(\n middleware: LoadedMiddleware,\n request: Request,\n params?: Record<string, string | string[]>,\n): Promise<MiddlewareResult> {\n let nextHeaders: Record<string, string> | undefined;\n let nextParams: Record<string, string | string[]> | undefined;\n let nextLocals: Record<string, unknown> | undefined;\n const cleanups: Array<() => void | Promise<void>> = [];\n\n const context: MiddlewareContext = {\n next(options) {\n if (options?.headers) nextHeaders = options.headers;\n if (options?.params) nextParams = options.params;\n if (options?.locals) nextLocals = options.locals;\n },\n params,\n locals: {},\n };\n\n try {\n const result = await middleware.handler(request, context);\n\n if (result instanceof Response) {\n return { kind: \"response\", response: result };\n }\n\n return {\n kind: \"continue\",\n headers: nextHeaders,\n params: nextParams ?? params,\n locals: nextLocals,\n };\n } finally {\n // Run any cleanup functions (§6). Errors in cleanup are logged but\n // do not propagate to the caller.\n for (const cleanup of cleanups) {\n try {\n await cleanup();\n } catch (err) {\n console.error(\"[elur-kit] middleware cleanup error:\", err);\n }\n }\n }\n}\n","import { createRequire } from \"node:module\";\nimport type { Plugin } from \"vite\";\n\n/**\n * How the legacy interpolation transform is handled relative to the installed\n * Elur core and Vite plugin:\n *\n * - `\"auto\"` (default): the kit's legacy transform is only applied when the\n * Vite plugin (`@elurjs/vite-plugin-elur` >= 1.1.0) is NOT installed.\n * The plugin has a more powerful state-machine lexer and takes precedence.\n * - `\"legacy\"`: always apply the kit's transform (for migrations), with a\n * one-time deprecation warning.\n * - `\"off\"`: never apply the kit's transform. Recommended when the Vite\n * plugin is installed.\n */\nexport type InterpolationMode = \"auto\" | \"legacy\" | \"off\";\n\nconst require = createRequire(import.meta.url);\n\nlet _warnedLegacy = false;\n\nfunction warnLegacyOnce(): void {\n if (_warnedLegacy) return;\n _warnedLegacy = true;\n console.warn(\n \"[elur-kit] The legacy interpolation transform is deprecated. \" +\n \"Install @elurjs/vite-plugin-elur >= 1.1.0 for compile-time \" +\n \"partial attribute interpolation. Remove `interpolation: \\\"legacy\\\"` \" +\n \"once migration is complete.\",\n );\n}\n\n/**\n * Detects whether the Vite plugin (`@elurjs/vite-plugin-elur`) is\n * installed and provides compile-time partial attribute interpolation.\n */\nexport function pluginSupportsPartialInterpolation(): boolean {\n try {\n const pkg = require(\"@elurjs/vite-plugin-elur/package.json\") as {\n version?: string;\n };\n // >= 1.1.0 has the interpolation lexer\n const [major, minor] = (pkg.version ?? \"0.0.0\").split(\".\").map(Number);\n return major > 1 || (major === 1 && minor >= 1);\n } catch {\n return false;\n }\n}\n\n/**\n * Detects whether the installed Elur core supports partial attribute\n * interpolation natively (via the public `templateFeatures` capability).\n * Note: as of core v3.4.0, this is always false — the lexer moved to the\n * Vite plugin.\n */\nexport function coreSupportsPartialInterpolation(): boolean {\n try {\n const core = require(\"@elurjs/core\") as {\n templateFeatures?: { partialAttributeInterpolation?: boolean };\n };\n return core?.templateFeatures?.partialAttributeInterpolation === true;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolves whether the kit's legacy transform should be applied.\n *\n * In `\"auto\"` mode, the kit's transform runs only when neither the Vite\n * plugin nor the core provides partial interpolation. When the Vite plugin\n * is installed (>= 1.1.0), it takes precedence and the kit's transform is\n * skipped to avoid double-processing.\n */\nexport function shouldUseLegacyInterpolation(mode: InterpolationMode): boolean {\n if (mode === \"off\") return false;\n if (mode === \"legacy\") {\n warnLegacyOnce();\n return true;\n }\n // auto: skip if the Vite plugin handles it\n if (pluginSupportsPartialInterpolation()) return false;\n // fallback: use legacy if core doesn't support it natively\n return !coreSupportsPartialInterpolation();\n}\n\n/**\n * Transforms Elur `html\\`\\`` templates so that attributes with partial\n * interpolation become a single interpolation expression.\n *\n * Elur requires every dynamic attribute to be a single interpolation covering\n * the whole value. This plugin rewrites patterns such as:\n *\n * html\\`<a href=\"/blog/${slug}\">...</a>\\`\n *\n * into:\n *\n * html\\`<a href=${\"/blog/\" + slug}>...</a>\\`\n *\n * Only files inside the app and islands directories are processed.\n *\n * @deprecated Elur core supports partial attribute interpolation natively.\n * Keep this transform only for migrations against older cores\n * (`interpolation: \"legacy\"`).\n */\nexport interface InterpolationPluginOptions {\n appDir?: string;\n islandsDir?: string;\n}\n\nconst HTML_TAG = \"html\";\nconst TEMPLATE_START = \"`\";\n\n/**\n * Scans a `${...}` interpolation starting at `start` (where content[start] is\n * `$` and content[start + 1] is `{`), honoring nested braces, strings and\n * escape sequences. Returns the index just past the closing `}`.\n */\nfunction scanInterpolation(content: string, start: number): number {\n let depth = 1;\n let i = start + 2;\n while (i < content.length && depth > 0) {\n const c = content[i];\n if (c === \"\\\\\") {\n i += 2;\n continue;\n }\n if (c === '\"' || c === \"'\" || c === \"`\") {\n const q = c;\n i++;\n while (i < content.length) {\n if (content[i] === \"\\\\\") {\n i += 2;\n continue;\n }\n if (content[i] === q) break;\n i++;\n }\n i++;\n continue;\n }\n if (c === \"{\") depth++;\n else if (c === \"}\") depth--;\n i++;\n }\n return i;\n}\n\n/**\n * Scans a quoted attribute value starting at `start` (where content[start] is\n * the quote character). Handles escapes, `${...}` interpolations with nested\n * braces, and nested quotes. Returns the index just past the closing quote,\n * the raw inner text (escapes preserved as in the source) and whether the\n * value contains at least one interpolation.\n */\nfunction scanQuotedValue(\n content: string,\n start: number,\n quote: string,\n): { end: number; inside: string; hasInterp: boolean } {\n let i = start + 1;\n let inside = \"\";\n let hasInterp = false;\n while (i < content.length) {\n const c = content[i];\n if (c === \"\\\\\") {\n inside += c + (content[i + 1] ?? \"\");\n i += 2;\n continue;\n }\n if (c === quote) {\n i++;\n break;\n }\n if (c === \"$\" && content[i + 1] === \"{\") {\n const end = scanInterpolation(content, i);\n inside += content.slice(i, end);\n i = end;\n hasInterp = true;\n continue;\n }\n inside += c;\n i++;\n }\n return { end: i, inside, hasInterp };\n}\n\n/**\n * Converts the inner text of a quoted attribute value (which may contain\n * `${...}` interpolations) into a JS expression. Literal parts are JSON\n * encoded; interpolations keep their raw expression text.\n *\n * Examples:\n * /blog/${slug} -> \"/blog/\" + (slug)\n * ${slug} -> (slug)\n * tag ${cls({a:1})} -> \"tag \" + (cls({a:1}))\n */\nfunction valueToExpression(value: string): string {\n const parts: string[] = [];\n let i = 0;\n let literal = \"\";\n const flush = () => {\n if (literal) {\n parts.push(JSON.stringify(unescapeAttributeLiteral(literal)));\n literal = \"\";\n }\n };\n\n while (i < value.length) {\n if (value[i] === \"\\\\\") {\n literal += value[i] + (value[i + 1] ?? \"\");\n i += 2;\n continue;\n }\n if (value[i] === \"$\" && value[i + 1] === \"{\") {\n flush();\n const end = scanInterpolation(value, i);\n const expr = value.slice(i + 2, end - 1).trim();\n if (expr) parts.push(`(${expr})`);\n i = end;\n continue;\n }\n literal += value[i];\n i++;\n }\n flush();\n\n if (parts.length === 0) return '\"\"';\n if (parts.length === 1) return parts[0] as string;\n return parts.join(\" + \");\n}\n\n/**\n * Unescapes escape sequences that appear inside a JS template literal so the\n * JSON.stringify output matches the runtime string value.\n */\nfunction unescapeAttributeLiteral(literal: string): string {\n const escapes: Record<string, string> = {\n n: \"\\n\",\n t: \"\\t\",\n r: \"\\r\",\n };\n let out = \"\";\n let i = 0;\n while (i < literal.length) {\n const c = literal[i];\n if (c === \"\\\\\" && i + 1 < literal.length) {\n const next = literal[i + 1];\n if (next in escapes) {\n out += escapes[next];\n i += 2;\n continue;\n }\n out += next;\n i += 2;\n continue;\n }\n out += c;\n i++;\n }\n return out;\n}\n\n/**\n * Rewrites quoted attribute values that contain interpolations inside html``\n * templates, leaving everything else untouched.\n */\nfunction transformTemplateContent(content: string): string {\n let out = \"\";\n let i = 0;\n const n = content.length;\n\n while (i < n) {\n const lt = content.indexOf(\"<\", i);\n if (lt === -1) {\n out += content.slice(i);\n break;\n }\n out += content.slice(i, lt);\n i = lt;\n\n // HTML comments: copy verbatim.\n if (content.startsWith(\"<!--\", i)) {\n const end = content.indexOf(\"-->\", i + 4);\n if (end === -1) {\n out += content.slice(i);\n break;\n }\n out += content.slice(i, end + 3);\n i = end + 3;\n continue;\n }\n\n // Closing tags, doctype, CDATA, processing instructions: copy verbatim.\n if (content[i + 1] === \"/\" || content[i + 1] === \"!\" || content[i + 1] === \"?\") {\n const gt = content.indexOf(\">\", i + 1);\n if (gt === -1) {\n out += content.slice(i);\n break;\n }\n out += content.slice(i, gt + 1);\n i = gt + 1;\n continue;\n }\n\n // Opening tag. Copy the tag name, then walk its attributes.\n let j = i + 1;\n while (j < n && /[a-zA-Z0-9-]/.test(content[j])) j++;\n out += content.slice(i, j);\n i = j;\n\n while (i < n) {\n let ws = \"\";\n while (i < n && /\\s/.test(content[i])) {\n ws += content[i];\n i++;\n }\n if (i >= n) {\n out += ws;\n break;\n }\n if (content[i] === \">\") {\n out += ws + \">\";\n i++;\n break;\n }\n if (content[i] === \"/\" && content[i + 1] === \">\") {\n out += ws + \"/>\";\n i += 2;\n break;\n }\n // Interpolation in the tag body (dynamic attrs/spread): copy verbatim.\n if (content[i] === \"$\" && content[i + 1] === \"{\") {\n const end = scanInterpolation(content, i);\n out += ws + content.slice(i, end);\n i = end;\n continue;\n }\n\n // Attribute name.\n let nameStart = i;\n while (i < n && !/[\\s=/>\"'$]/.test(content[i])) i++;\n const name = content.slice(nameStart, i);\n if (!name) {\n out += ws + content[i];\n i++;\n continue;\n }\n\n let eqWs = \"\";\n while (i < n && /\\s/.test(content[i])) {\n eqWs += content[i];\n i++;\n }\n\n if (content[i] !== \"=\") {\n out += ws + name + eqWs;\n continue;\n }\n\n i++; // consume \"=\"\n let valWs = \"\";\n while (i < n && /\\s/.test(content[i])) {\n valWs += content[i];\n i++;\n }\n\n const quote = content[i];\n if (quote === '\"' || quote === \"'\") {\n const { end, inside, hasInterp } = scanQuotedValue(content, i, quote);\n if (hasInterp) {\n // Skip values that are a single full interpolation: Elur handles\n // `attr=\"${expr}\"` natively, so only partial interpolations need the\n // rewrite.\n const first = scanInterpolation(inside, 0);\n const fullValue =\n inside.startsWith(\"${\") &&\n first === inside.length &&\n !inside.slice(2, first - 1).includes(\"${\");\n if (!fullValue) {\n // Elur needs the interpolation to start right after \"=\" (no space),\n // so the whitespace before the original value is dropped.\n out += ws + name + eqWs + \"=\" + \"${\" + valueToExpression(inside) + \"}\";\n i = end;\n continue;\n }\n out += ws + name + eqWs + \"=\" + valWs + content.slice(i, end);\n } else {\n out += ws + name + eqWs + \"=\" + valWs + content.slice(i, end);\n }\n i = end;\n continue;\n }\n\n // Unquoted value: copy up to whitespace, \">\" or \"/>\".\n let v = \"\";\n while (\n i < n &&\n !/\\s/.test(content[i]) &&\n content[i] !== \">\" &&\n !(content[i] === \"/\" && content[i + 1] === \">\")\n ) {\n v += content[i];\n i++;\n }\n out += ws + name + eqWs + \"=\" + valWs + v;\n }\n }\n\n return out;\n}\n\n/**\n * @deprecated Use the native partial attribute interpolation of Elur core\n * (core >= 3.3). Kept for legacy migrations and direct consumers.\n */\nexport function transformPartialInterpolations(source: string): string {\n let result = \"\";\n let i = 0;\n while (i < source.length) {\n // Find the next html` sequence.\n const htmlIndex = source.indexOf(HTML_TAG, i);\n if (htmlIndex === -1) {\n result += source.slice(i);\n break;\n }\n result += source.slice(i, htmlIndex + HTML_TAG.length);\n i = htmlIndex + HTML_TAG.length;\n\n // Skip whitespace before the backtick.\n while (i < source.length && /\\s/.test(source[i])) {\n result += source[i];\n i++;\n }\n if (i >= source.length || source[i] !== TEMPLATE_START) {\n continue;\n }\n result += source[i];\n i++;\n\n // Parse the template literal until the matching backtick.\n let depth = 1;\n let templateContent = \"\";\n while (i < source.length && depth > 0) {\n const char = source[i];\n if (char === \"\\\\\") {\n templateContent += char + source[i + 1];\n i += 2;\n continue;\n }\n if (char === TEMPLATE_START) {\n depth--;\n if (depth === 0) {\n i++;\n break;\n }\n }\n if (char === \"$\") {\n // Look ahead for ${...}\n if (source[i + 1] === \"{\") {\n const end = scanInterpolation(source, i);\n templateContent += source.slice(i, end);\n i = end;\n continue;\n }\n }\n templateContent += char;\n i++;\n }\n\n const transformed = transformTemplateContent(templateContent);\n result += transformed;\n result += TEMPLATE_START;\n }\n return result;\n}\n\nexport function elurJsInterpolationPlugin(options: InterpolationPluginOptions = {}): Plugin {\n const appDir = options.appDir ?? \"src/app\";\n const islandsDir = options.islandsDir ?? \"src/islands\";\n return {\n name: \"elur-kit-interpolation\",\n enforce: \"pre\",\n transform(code, id) {\n if (!id.endsWith(\".ts\") && !id.endsWith(\".js\")) return;\n if (!id.includes(appDir) && !id.includes(islandsDir)) return;\n if (!code.includes(\"html`\")) return;\n const transformed = transformPartialInterpolations(code);\n if (transformed === code) return;\n return { code: transformed, map: null };\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"interpolation-plugin-Wgb1j4pT.js","names":[],"sources":["../../src/island/generate-entry.ts","../../src/middleware/index.ts","../../src/vite/interpolation-plugin.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, relative, sep } from \"node:path\";\nimport type { IslandModule } from \"./scan.js\";\n\n// --- Client entry generator ---\n//\n// Turns a list of scanned islands into a client entry module that imports each\n// island and registers it with `hydrateIslands`. This removes the need to hand-\n// maintain `entry-client.ts` as islands are added or removed.\n//\n// The generated file imports island default exports and passes them to\n// `hydrateIslands` keyed by their registry name.\n\n/** Options for generating the client entry module. */\nexport interface GenerateEntryOptions {\n /** Islands to register, from `scanIslands`. */\n islands: IslandModule[];\n /** Absolute path of the entry file to write (e.g. \".elur/entry-client.ts\"). */\n outFile: string;\n /**\n * Import specifier for the kit's client island helpers.\n * Defaults to the published subpath `@elurjs/kit/island`.\n */\n hydrateImport?: string;\n /**\n * Import specifier for the kit's client router.\n * Defaults to the published subpath `@elurjs/kit/router`.\n */\n routerImport?: string;\n}\n\n/** Turns a registry name into a safe JS identifier for the import binding. */\nfunction toIdentifier(name: string, index: number): string {\n const cleaned = name.replace(/[^a-zA-Z0-9_$]/g, \"_\");\n return /^[a-zA-Z_$]/.test(cleaned) ? `${cleaned}_${index}` : `_${cleaned}_${index}`;\n}\n\n/** Builds the source code of the client entry module. */\nexport function buildEntrySource(\n islands: IslandModule[],\n outFile: string,\n hydrateImport = \"@elurjs/kit/island\",\n routerImport = \"@elurjs/kit/router\",\n): string {\n const bindings = islands.map((island, i) => ({\n ident: toIdentifier(island.name, i),\n name: island.name,\n // Relative import specifier from the entry file to the island module.\n spec: toImportSpecifier(outFile, island.filePath),\n }));\n\n // Lazy registry: each island is loaded on-demand via dynamic import().\n // This enables code-splitting — islands not on the current page (or not yet\n // triggered by their directive) stay out of the initial bundle.\n //\n // The registry maps island name → discriminated lazy loader `{ load }`.\n // hydrateIslands() awaits `entry.load()` before hydrating, so the first\n // paint only needs the small entry chunk + the islands on the page. The\n // discriminated form lets the hydrator tell eager components from lazy\n // loaders without executing a probe.\n const registryLines = bindings\n .map((b) => ` ${JSON.stringify(b.name)}: { load: () => import(${JSON.stringify(b.spec)}).then(m => m.default) },`)\n .join(\"\\n\");\n\n const islandHydration = registryLines\n ? `const registry = {\n${registryLines}\n};\n// Defer hydration until the browser is idle so the first paint is not\n// blocked. Islands with directive \"load\" already have SSR-rendered DOM,\n// so the user sees content immediately — hydration only adds interactivity.\nconst hydrate = () => hydrateIslands(registry);\nif (\"requestIdleCallback\" in window) {\n requestIdleCallback(hydrate, { timeout: 2000 });\n} else {\n setTimeout(hydrate, 0);\n}\ndocument.addEventListener(\"elur:rendered\", () => {\n cleanupHydratedIslands();\n hydrateIslands(registry);\n});\n\n// Vite HMR: when an island module (or the entry itself) updates, dispose the\n// current islands and re-hydrate from the updated modules — the registry's\n// dynamic import() resolves to the fresh modules, so no full page reload is\n// needed (progressive enhancement, audit §10.2 / §12.2).\nif (import.meta.hot) {\n import.meta.hot.accept((newModule) => {\n cleanupHydratedIslands();\n hydrateIslands(registry);\n if (newModule) {\n // Re-run the module so its side effects (router, listeners) apply.\n }\n });\n}`\n : \"\";\n\n return `// AUTO-GENERATED by @elurjs/kit. Do not edit.\nimport { startClientRouter } from ${JSON.stringify(routerImport)};\nimport { hydrateIslands, cleanupHydratedIslands } from ${JSON.stringify(hydrateImport)};\n\nstartClientRouter();\n${islandHydration}\n`;\n}\n\n/** Computes a POSIX-style relative import specifier between two files. */\nfunction toImportSpecifier(fromFile: string, toFile: string): string {\n let spec = relative(dirname(fromFile), toFile).split(sep).join(\"/\");\n if (!spec.startsWith(\".\")) spec = `./${spec}`;\n return spec;\n}\n\n/**\n * Generates and writes the client entry module for the given islands.\n *\n * @param options Generation options.\n * @returns The absolute path of the written entry file.\n */\nexport async function generateClientEntry(\n options: GenerateEntryOptions,\n): Promise<string> {\n const source = buildEntrySource(\n options.islands,\n options.outFile,\n options.hydrateImport,\n options.routerImport,\n );\n await mkdir(dirname(options.outFile), { recursive: true });\n await writeFile(options.outFile, source, \"utf8\");\n return options.outFile;\n}\n","// --- Middleware ---\n//\n// Convention: `src/middleware.ts` in the project root exports a default\n// function and an optional `config` with a `matcher` array.\n//\n// import type { Middleware } from \"@elurjs/kit\";\n//\n// export default function middleware(request: Request) {\n// if (!request.headers.get(\"Cookie\")?.includes(\"session=\")) {\n// return Response.redirect(new URL(\"/login\", request.url), 307);\n// }\n// }\n//\n// export const config = { matcher: [\"/dashboard/:path*\", \"/admin/:path*\"] };\n//\n// The middleware runs before routing. Return a `Response` to short-circuit\n// (redirect, rewrite, 401, etc.). Return `undefined` or nothing to continue.\n// Use `next()` to pass headers to the loader.\n\nimport { matchRoute } from \"../ssr/match.js\";\nimport type { PageRoute } from \"../router/route-scanner.js\";\n\n/** The middleware function signature. */\nexport type Middleware = (request: Request, context: MiddlewareContext) =>\n | Response\n | void\n | Promise<Response | void>;\n\n/** Context passed to the middleware function. */\nexport interface MiddlewareContext {\n /** Helper to continue to the next handler. Can attach headers, params, and locals. */\n next(options?: {\n headers?: Record<string, string>;\n params?: Record<string, string | string[]>;\n locals?: Record<string, unknown>;\n }): void;\n /** Matched route params (only available if the path matches a page route). */\n params?: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware, available to loaders/actions). */\n locals?: Record<string, unknown>;\n}\n\n/** Configuration for the middleware module. */\nexport interface MiddlewareConfig {\n /** Path patterns that trigger the middleware. Supports `:param` and `:param*`. */\n matcher?: string[];\n}\n\nexport interface LoadedMiddleware {\n handler: Middleware;\n config: MiddlewareConfig;\n}\n\n/** Result of running middleware: either a response to short-circuit with, or continue. */\nexport type MiddlewareResult =\n | { kind: \"response\"; response: Response }\n | {\n kind: \"continue\";\n headers?: Record<string, string>;\n params?: Record<string, string | string[]>;\n locals?: Record<string, unknown>;\n };\n\n/**\n * Loads the user's `src/middleware.ts` module. Returns `null` if no middleware\n * file exists. Distinguishes \"file not found\" from \"file has errors\" (§6):\n * an import error is not silently treated as \"no middleware\".\n */\nexport async function loadMiddleware(root: string): Promise<LoadedMiddleware | null> {\n const candidates = [\n `${root}/src/middleware.ts`,\n `${root}/middleware.ts`,\n ];\n\n for (const path of candidates) {\n try {\n const mod = await import(path);\n const handler = (mod.default ?? mod.middleware) as Middleware | undefined;\n if (typeof handler !== \"function\") continue;\n const config = (mod.config ?? {}) as MiddlewareConfig;\n return { handler, config };\n } catch (err) {\n // Distinguish \"module not found\" from actual errors.\n // If the error is a module resolution error for this specific file,\n // it means the file doesn't exist — try the next candidate.\n // If it's a syntax/runtime error, rethrow so the user sees it.\n // Note: Bun's ResolveMessage is not `instanceof Error`, so match on the\n // message property instead of relying on the class hierarchy.\n const msg =\n typeof err === \"object\" && err !== null && \"message\" in err\n ? String((err as { message: unknown }).message)\n : String(err);\n if (\n msg.includes(\"Cannot find module\") ||\n msg.includes(\"Cannot find package\") ||\n msg.includes(\"ENOENT\") ||\n msg.includes(\"Module not found\")\n ) {\n // File doesn't exist — try next candidate.\n continue;\n }\n // Actual error in the middleware file — rethrow (§6).\n throw new Error(`[elur-kit] Error loading middleware: ${msg}`, { cause: err });\n }\n }\n\n return null;\n}\n\n/**\n * Checks if a pathname matches any of the middleware's matcher patterns.\n * If no matcher is configured, the middleware runs for every request.\n *\n * Catch-all patterns (`:param*`) match both the base path and any sub-paths,\n * e.g. `/dashboard/:path*` matches `/dashboard` and `/dashboard/settings/users`.\n */\nexport function matchesMiddleware(pathname: string, config: MiddlewareConfig): boolean {\n if (!config.matcher || config.matcher.length === 0) return true;\n\n const cleanPath = pathname.split(\"?\")[0];\n\n for (const pattern of config.matcher) {\n // Exact match.\n if (pattern === cleanPath) return true;\n\n // Check for catch-all: `/foo/:bar*` should also match `/foo`.\n const catchAllMatch = pattern.match(/^(.*)\\/:[\\w]+\\*$/);\n if (catchAllMatch) {\n const base = catchAllMatch[1];\n if (cleanPath === base) return true;\n }\n\n // Use matchRoute for param matching.\n const pseudoRoutes: PageRoute[] = [{\n path: pattern,\n pagePath: \"\",\n params: [],\n layouts: [],\n }];\n if (matchRoute(cleanPath, pseudoRoutes)) return true;\n }\n\n return false;\n}\n\n/**\n * Runs the middleware for a request. Returns the result indicating whether to\n * short-circuit with a response or continue with propagated headers/params/locals.\n *\n * Per §6: cleanup runs in `finally`, response short-circuits the pipeline,\n * headers/params/locals are propagated to downstream handlers.\n */\nexport async function runMiddleware(\n middleware: LoadedMiddleware,\n request: Request,\n params?: Record<string, string | string[]>,\n): Promise<MiddlewareResult> {\n let nextHeaders: Record<string, string> | undefined;\n let nextParams: Record<string, string | string[]> | undefined;\n let nextLocals: Record<string, unknown> | undefined;\n const cleanups: Array<() => void | Promise<void>> = [];\n\n const context: MiddlewareContext = {\n next(options) {\n if (options?.headers) nextHeaders = options.headers;\n if (options?.params) nextParams = options.params;\n if (options?.locals) nextLocals = options.locals;\n },\n params,\n locals: {},\n };\n\n try {\n const result = await middleware.handler(request, context);\n\n if (result instanceof Response) {\n return { kind: \"response\", response: result };\n }\n\n return {\n kind: \"continue\",\n headers: nextHeaders,\n params: nextParams ?? params,\n locals: nextLocals,\n };\n } finally {\n // Run any cleanup functions (§6). Errors in cleanup are logged but\n // do not propagate to the caller.\n for (const cleanup of cleanups) {\n try {\n await cleanup();\n } catch (err) {\n console.error(\"[elur-kit] middleware cleanup error:\", err);\n }\n }\n }\n}\n","import { createRequire } from \"node:module\";\nimport type { Plugin } from \"vite\";\n\n/**\n * How the legacy interpolation transform is handled relative to the installed\n * Elur core and Vite plugin:\n *\n * - `\"auto\"` (default): the kit's legacy transform is only applied when the\n * Vite plugin (`@elurjs/vite-plugin-elur` >= 1.1.0) is NOT installed.\n * The plugin has a more powerful state-machine lexer and takes precedence.\n * - `\"legacy\"`: always apply the kit's transform (for migrations), with a\n * one-time deprecation warning.\n * - `\"off\"`: never apply the kit's transform. Recommended when the Vite\n * plugin is installed.\n */\nexport type InterpolationMode = \"auto\" | \"legacy\" | \"off\";\n\nconst require = createRequire(import.meta.url);\n\nlet _warnedLegacy = false;\n\nfunction warnLegacyOnce(): void {\n if (_warnedLegacy) return;\n _warnedLegacy = true;\n console.warn(\n \"[elur-kit] The legacy interpolation transform is deprecated. \" +\n \"Install @elurjs/vite-plugin-elur >= 1.1.0 for compile-time \" +\n \"partial attribute interpolation. Remove `interpolation: \\\"legacy\\\"` \" +\n \"once migration is complete.\",\n );\n}\n\n/**\n * Detects whether the Vite plugin (`@elurjs/vite-plugin-elur`) is\n * installed and provides compile-time partial attribute interpolation.\n */\nexport function pluginSupportsPartialInterpolation(): boolean {\n try {\n const pkg = require(\"@elurjs/vite-plugin-elur/package.json\") as {\n version?: string;\n };\n // >= 1.1.0 has the interpolation lexer\n const [major, minor] = (pkg.version ?? \"0.0.0\").split(\".\").map(Number);\n return major > 1 || (major === 1 && minor >= 1);\n } catch {\n return false;\n }\n}\n\n/**\n * Detects whether the installed Elur core supports partial attribute\n * interpolation natively (via the public `templateFeatures` capability).\n * Note: as of core v3.4.0, this is always false — the lexer moved to the\n * Vite plugin.\n */\nexport function coreSupportsPartialInterpolation(): boolean {\n try {\n const core = require(\"@elurjs/core\") as {\n templateFeatures?: { partialAttributeInterpolation?: boolean };\n };\n return core?.templateFeatures?.partialAttributeInterpolation === true;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolves whether the kit's legacy transform should be applied.\n *\n * In `\"auto\"` mode, the kit's transform runs only when neither the Vite\n * plugin nor the core provides partial interpolation. When the Vite plugin\n * is installed (>= 1.1.0), it takes precedence and the kit's transform is\n * skipped to avoid double-processing.\n */\nexport function shouldUseLegacyInterpolation(mode: InterpolationMode): boolean {\n if (mode === \"off\") return false;\n if (mode === \"legacy\") {\n warnLegacyOnce();\n return true;\n }\n // auto: skip if the Vite plugin handles it\n if (pluginSupportsPartialInterpolation()) return false;\n // fallback: use legacy if core doesn't support it natively\n return !coreSupportsPartialInterpolation();\n}\n\n/**\n * Transforms Elur `html\\`\\`` templates so that attributes with partial\n * interpolation become a single interpolation expression.\n *\n * Elur requires every dynamic attribute to be a single interpolation covering\n * the whole value. This plugin rewrites patterns such as:\n *\n * html\\`<a href=\"/blog/${slug}\">...</a>\\`\n *\n * into:\n *\n * html\\`<a href=${\"/blog/\" + slug}>...</a>\\`\n *\n * Only files inside the app and islands directories are processed.\n *\n * @deprecated Elur core supports partial attribute interpolation natively.\n * Keep this transform only for migrations against older cores\n * (`interpolation: \"legacy\"`).\n */\nexport interface InterpolationPluginOptions {\n appDir?: string;\n islandsDir?: string;\n}\n\nconst HTML_TAG = \"html\";\nconst TEMPLATE_START = \"`\";\n\n/**\n * Scans a `${...}` interpolation starting at `start` (where content[start] is\n * `$` and content[start + 1] is `{`), honoring nested braces, strings and\n * escape sequences. Returns the index just past the closing `}`.\n */\nfunction scanInterpolation(content: string, start: number): number {\n let depth = 1;\n let i = start + 2;\n while (i < content.length && depth > 0) {\n const c = content[i];\n if (c === \"\\\\\") {\n i += 2;\n continue;\n }\n if (c === '\"' || c === \"'\" || c === \"`\") {\n const q = c;\n i++;\n while (i < content.length) {\n if (content[i] === \"\\\\\") {\n i += 2;\n continue;\n }\n if (content[i] === q) break;\n i++;\n }\n i++;\n continue;\n }\n if (c === \"{\") depth++;\n else if (c === \"}\") depth--;\n i++;\n }\n return i;\n}\n\n/**\n * Scans a quoted attribute value starting at `start` (where content[start] is\n * the quote character). Handles escapes, `${...}` interpolations with nested\n * braces, and nested quotes. Returns the index just past the closing quote,\n * the raw inner text (escapes preserved as in the source) and whether the\n * value contains at least one interpolation.\n */\nfunction scanQuotedValue(\n content: string,\n start: number,\n quote: string,\n): { end: number; inside: string; hasInterp: boolean } {\n let i = start + 1;\n let inside = \"\";\n let hasInterp = false;\n while (i < content.length) {\n const c = content[i];\n if (c === \"\\\\\") {\n inside += c + (content[i + 1] ?? \"\");\n i += 2;\n continue;\n }\n if (c === quote) {\n i++;\n break;\n }\n if (c === \"$\" && content[i + 1] === \"{\") {\n const end = scanInterpolation(content, i);\n inside += content.slice(i, end);\n i = end;\n hasInterp = true;\n continue;\n }\n inside += c;\n i++;\n }\n return { end: i, inside, hasInterp };\n}\n\n/**\n * Converts the inner text of a quoted attribute value (which may contain\n * `${...}` interpolations) into a JS expression. Literal parts are JSON\n * encoded; interpolations keep their raw expression text.\n *\n * Examples:\n * /blog/${slug} -> \"/blog/\" + (slug)\n * ${slug} -> (slug)\n * tag ${cls({a:1})} -> \"tag \" + (cls({a:1}))\n */\nfunction valueToExpression(value: string): string {\n const parts: string[] = [];\n let i = 0;\n let literal = \"\";\n const flush = () => {\n if (literal) {\n parts.push(JSON.stringify(unescapeAttributeLiteral(literal)));\n literal = \"\";\n }\n };\n\n while (i < value.length) {\n if (value[i] === \"\\\\\") {\n literal += value[i] + (value[i + 1] ?? \"\");\n i += 2;\n continue;\n }\n if (value[i] === \"$\" && value[i + 1] === \"{\") {\n flush();\n const end = scanInterpolation(value, i);\n const expr = value.slice(i + 2, end - 1).trim();\n if (expr) parts.push(`(${expr})`);\n i = end;\n continue;\n }\n literal += value[i];\n i++;\n }\n flush();\n\n if (parts.length === 0) return '\"\"';\n if (parts.length === 1) return parts[0] as string;\n return parts.join(\" + \");\n}\n\n/**\n * Unescapes escape sequences that appear inside a JS template literal so the\n * JSON.stringify output matches the runtime string value.\n */\nfunction unescapeAttributeLiteral(literal: string): string {\n const escapes: Record<string, string> = {\n n: \"\\n\",\n t: \"\\t\",\n r: \"\\r\",\n };\n let out = \"\";\n let i = 0;\n while (i < literal.length) {\n const c = literal[i];\n if (c === \"\\\\\" && i + 1 < literal.length) {\n const next = literal[i + 1];\n if (next in escapes) {\n out += escapes[next];\n i += 2;\n continue;\n }\n out += next;\n i += 2;\n continue;\n }\n out += c;\n i++;\n }\n return out;\n}\n\n/**\n * Rewrites quoted attribute values that contain interpolations inside html``\n * templates, leaving everything else untouched.\n */\nfunction transformTemplateContent(content: string): string {\n let out = \"\";\n let i = 0;\n const n = content.length;\n\n while (i < n) {\n const lt = content.indexOf(\"<\", i);\n if (lt === -1) {\n out += content.slice(i);\n break;\n }\n out += content.slice(i, lt);\n i = lt;\n\n // HTML comments: copy verbatim.\n if (content.startsWith(\"<!--\", i)) {\n const end = content.indexOf(\"-->\", i + 4);\n if (end === -1) {\n out += content.slice(i);\n break;\n }\n out += content.slice(i, end + 3);\n i = end + 3;\n continue;\n }\n\n // Closing tags, doctype, CDATA, processing instructions: copy verbatim.\n if (content[i + 1] === \"/\" || content[i + 1] === \"!\" || content[i + 1] === \"?\") {\n const gt = content.indexOf(\">\", i + 1);\n if (gt === -1) {\n out += content.slice(i);\n break;\n }\n out += content.slice(i, gt + 1);\n i = gt + 1;\n continue;\n }\n\n // Opening tag. Copy the tag name, then walk its attributes.\n let j = i + 1;\n while (j < n && /[a-zA-Z0-9-]/.test(content[j])) j++;\n out += content.slice(i, j);\n i = j;\n\n while (i < n) {\n let ws = \"\";\n while (i < n && /\\s/.test(content[i])) {\n ws += content[i];\n i++;\n }\n if (i >= n) {\n out += ws;\n break;\n }\n if (content[i] === \">\") {\n out += ws + \">\";\n i++;\n break;\n }\n if (content[i] === \"/\" && content[i + 1] === \">\") {\n out += ws + \"/>\";\n i += 2;\n break;\n }\n // Interpolation in the tag body (dynamic attrs/spread): copy verbatim.\n if (content[i] === \"$\" && content[i + 1] === \"{\") {\n const end = scanInterpolation(content, i);\n out += ws + content.slice(i, end);\n i = end;\n continue;\n }\n\n // Attribute name.\n let nameStart = i;\n while (i < n && !/[\\s=/>\"'$]/.test(content[i])) i++;\n const name = content.slice(nameStart, i);\n if (!name) {\n out += ws + content[i];\n i++;\n continue;\n }\n\n let eqWs = \"\";\n while (i < n && /\\s/.test(content[i])) {\n eqWs += content[i];\n i++;\n }\n\n if (content[i] !== \"=\") {\n out += ws + name + eqWs;\n continue;\n }\n\n i++; // consume \"=\"\n let valWs = \"\";\n while (i < n && /\\s/.test(content[i])) {\n valWs += content[i];\n i++;\n }\n\n const quote = content[i];\n if (quote === '\"' || quote === \"'\") {\n const { end, inside, hasInterp } = scanQuotedValue(content, i, quote);\n if (hasInterp) {\n // Skip values that are a single full interpolation: Elur handles\n // `attr=\"${expr}\"` natively, so only partial interpolations need the\n // rewrite.\n const first = scanInterpolation(inside, 0);\n const fullValue =\n inside.startsWith(\"${\") &&\n first === inside.length &&\n !inside.slice(2, first - 1).includes(\"${\");\n if (!fullValue) {\n // Elur needs the interpolation to start right after \"=\" (no space),\n // so the whitespace before the original value is dropped.\n out += ws + name + eqWs + \"=\" + \"${\" + valueToExpression(inside) + \"}\";\n i = end;\n continue;\n }\n out += ws + name + eqWs + \"=\" + valWs + content.slice(i, end);\n } else {\n out += ws + name + eqWs + \"=\" + valWs + content.slice(i, end);\n }\n i = end;\n continue;\n }\n\n // Unquoted value: copy up to whitespace, \">\" or \"/>\".\n let v = \"\";\n while (\n i < n &&\n !/\\s/.test(content[i]) &&\n content[i] !== \">\" &&\n !(content[i] === \"/\" && content[i + 1] === \">\")\n ) {\n v += content[i];\n i++;\n }\n out += ws + name + eqWs + \"=\" + valWs + v;\n }\n }\n\n return out;\n}\n\n/**\n * @deprecated Use the native partial attribute interpolation of Elur core\n * (core >= 3.3). Kept for legacy migrations and direct consumers.\n */\nexport function transformPartialInterpolations(source: string): string {\n let result = \"\";\n let i = 0;\n while (i < source.length) {\n // Find the next html` sequence.\n const htmlIndex = source.indexOf(HTML_TAG, i);\n if (htmlIndex === -1) {\n result += source.slice(i);\n break;\n }\n result += source.slice(i, htmlIndex + HTML_TAG.length);\n i = htmlIndex + HTML_TAG.length;\n\n // Skip whitespace before the backtick.\n while (i < source.length && /\\s/.test(source[i])) {\n result += source[i];\n i++;\n }\n if (i >= source.length || source[i] !== TEMPLATE_START) {\n continue;\n }\n result += source[i];\n i++;\n\n // Parse the template literal until the matching backtick.\n let depth = 1;\n let templateContent = \"\";\n while (i < source.length && depth > 0) {\n const char = source[i];\n if (char === \"\\\\\") {\n templateContent += char + source[i + 1];\n i += 2;\n continue;\n }\n if (char === TEMPLATE_START) {\n depth--;\n if (depth === 0) {\n i++;\n break;\n }\n }\n if (char === \"$\") {\n // Look ahead for ${...}\n if (source[i + 1] === \"{\") {\n const end = scanInterpolation(source, i);\n templateContent += source.slice(i, end);\n i = end;\n continue;\n }\n }\n templateContent += char;\n i++;\n }\n\n const transformed = transformTemplateContent(templateContent);\n result += transformed;\n result += TEMPLATE_START;\n }\n return result;\n}\n\nexport function elurJsInterpolationPlugin(options: InterpolationPluginOptions = {}): Plugin {\n const appDir = options.appDir ?? \"src/app\";\n const islandsDir = options.islandsDir ?? \"src/islands\";\n return {\n name: \"elur-kit-interpolation\",\n enforce: \"pre\",\n transform(code, id) {\n if (!id.endsWith(\".ts\") && !id.endsWith(\".js\")) return;\n if (!id.includes(appDir) && !id.includes(islandsDir)) return;\n if (!code.includes(\"html`\")) return;\n const transformed = transformPartialInterpolations(code);\n if (transformed === code) return;\n return { code: transformed, map: null };\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-http-DRAUhO0c.js","names":[],"sources":["../../src/render/ssr-flag.ts","../../src/render/render-to-string.ts","../../src/build/document-shell.ts","../../src/action/error-store.ts","../../src/cache/policy.ts","../../src/ssr/render.ts","../../src/ssr/match.ts","../../src/action/origin.ts","../../src/action/server.ts","../../src/runtime/node-http.ts"],"sourcesContent":["// --- SSR flag utility ---\n//\n// Elur 2.6.0 (published on npm) does not export `_setSSR`/`_isSSR`. The\n// reactivity state lives on `globalThis[Symbol.for(\"@elurjs/core/reactivity-state\")]`\n// and exposes an `ssr` boolean that, when true, makes effects run a single\n// pass without subscribing — exactly what we need during server rendering.\n//\n// This module manipulates that flag directly so the kit does not depend on\n// private exports that may or may not be present in a given elur release.\n\nconst STATE_KEY = Symbol.for(\"@elurjs/core/reactivity-state\");\n\ntype ReactivityState = { ssr: boolean };\n\nfunction getState(): ReactivityState | undefined {\n return (globalThis as Record<symbol, unknown>)[STATE_KEY] as\n | ReactivityState\n | undefined;\n}\n\n/** Sets the SSR flag on the Elur reactivity state. No-op if state is absent. */\nexport function setSSR(value: boolean): void {\n const state = getState();\n if (state) state.ssr = value;\n}\n\n/** Reads the SSR flag from the Elur reactivity state. Defaults to false. */\nexport function isSSR(): boolean {\n return getState()?.ssr ?? false;\n}\n","import type { ElurTemplate } from \"@elurjs/core\";\nimport { renderToString as renderCoreTemplate } from \"@elurjs/core/server\";\nimport { setSSR } from \"./ssr-flag\";\n\n// --- Build-time / server rendering ---\n//\n// The Elur core ships a DOM-free `renderToString` (`@elurjs/core/server`)\n// that streams template output without ever touching a `document`. The kit used\n// to inject a Node-side DOM (happy-dom) as a fallback for legacy compatibility;\n// that fallback has been removed together with the happy-dom dependency.\n\n/**\n * Renders a Elur template to an HTML string in Node.\n *\n * Accepts a *factory* (not a template) because `html`` evaluates at call time.\n *\n * @param factory Thunk that builds the template, e.g. `() => Page({ data })`.\n * @returns Serialized HTML of the rendered template.\n */\nexport async function renderToString(\n factory: () => ElurTemplate,\n options: { markers?: \"none\" | \"hydration\" } = {},\n): Promise<string> {\n setSSR(true);\n try {\n return await renderCoreTemplate(factory(), {\n markers: options.markers ?? \"hydration\",\n });\n } finally {\n setSSR(false);\n }\n}\n","//\n// The <!DOCTYPE>, <head> and <body> wrapper — plus the serialized loader data\n// and the client entry — are injected here at build time.\n\nimport type { PageMetadata } from \"../types.js\";\nexport interface ShellOptions {\n /** Rendered inner HTML that goes inside `#app`. */\n body: string;\n /** `<title>` text. */\n title?: string;\n /** `<html lang>` attribute. */\n lang?: string;\n /** Additional attributes for the `<html>` element, e.g. `{ \"data-theme\": \"dark\" }`. */\n htmlAttributes?: Record<string, string>;\n /**\n * Inline scripts injected into `<head>`. They run synchronously while the\n * document parses — before the first paint and before the (deferred) client\n * bundle — so they are the right place for no-flash bootstrapping (e.g.\n * applying a stored theme before the page becomes visible).\n */\n headScripts?: string[];\n /**\n * Raw HTML strings injected into `<head>` — e.g. `<link rel=\"icon\">`,\n * `<link rel=\"manifest\">`, `<meta name=\"theme-color\">`. Each string is\n * rendered as-is inside `<head>`.\n */\n headLinks?: string[];\n /** Loader data serialized into `<script id=\"elur-data\">`. */\n data?: unknown;\n /** Per-page action names serialized into `<script id=\"elur-actions\">`. */\n actions?: Record<string, string[]>;\n /** Path to the client entry module, e.g. `/_elur/entry-client.js`. */\n clientEntry?: string;\n /** Page metadata emitted as `<meta>`, `<link>` and OG/Twitter tags in `<head>`. */\n metadata?: PageMetadata;\n /**\n * Whether the SSR render endpoint (`/__elur-js/render`) is available at\n * runtime. Defaults to `true`. When `false` (static deployments), the shell\n * emits `<meta name=\"elur:render-endpoint\" content=\"off\" />` so the client\n * router skips probing the endpoint entirely — preventing a storm of 404\n * requests on fully static sites.\n */\n renderEndpoint?: boolean;\n}\n\nconst HTML_ESCAPES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (c) => HTML_ESCAPES[c]);\n}\n\n/**\n * Serializes data for embedding inside a `<script>` tag. Escapes `<` so a\n * `</script>` sequence in the data cannot break out of the tag.\n */\nfunction serializeData(data: unknown): string {\n return JSON.stringify(data ?? null).replace(/</g, \"\\\\u003c\");\n}\n\n/**\n * Builds the `<head>` tags for a `PageMetadata` object. Every tag is marked with\n * `data-elur-head` so the client-side router can replace them on navigation\n * without touching charset/viewport or user-supplied `headScripts`.\n */\nexport function buildHeadTags(metadata: PageMetadata, fallbackTitle: string): string {\n const tags: string[] = [];\n const title = metadata.title ?? fallbackTitle;\n if (metadata.title) {\n tags.push(`<title data-elur-head>${escapeHtml(title)}</title>`);\n }\n\n if (metadata.description) {\n tags.push(`<meta data-elur-head name=\"description\" content=\"${escapeHtml(metadata.description)}\" />`);\n }\n\n if (metadata.canonical) {\n tags.push(`<link data-elur-head rel=\"canonical\" href=\"${escapeHtml(metadata.canonical)}\" />`);\n }\n\n if (metadata.robots) {\n tags.push(`<meta data-elur-head name=\"robots\" content=\"${escapeHtml(metadata.robots)}\" />`);\n }\n\n const og = metadata.openGraph;\n if (og) {\n if (og.type) tags.push(`<meta data-elur-head property=\"og:type\" content=\"${escapeHtml(og.type)}\" />`);\n tags.push(`<meta data-elur-head property=\"og:title\" content=\"${escapeHtml(og.title ?? title)}\" />`);\n if (og.description ?? metadata.description) {\n tags.push(`<meta data-elur-head property=\"og:description\" content=\"${escapeHtml(og.description ?? metadata.description!)}\" />`);\n }\n if (og.url ?? metadata.canonical) {\n tags.push(`<meta data-elur-head property=\"og:url\" content=\"${escapeHtml(og.url ?? metadata.canonical!)}\" />`);\n }\n if (og.image) tags.push(`<meta data-elur-head property=\"og:image\" content=\"${escapeHtml(og.image)}\" />`);\n if (og.image && og.imageAlt) tags.push(`<meta data-elur-head property=\"og:image:alt\" content=\"${escapeHtml(og.imageAlt)}\" />`);\n if (og.image && og.imageWidth) tags.push(`<meta data-elur-head property=\"og:image:width\" content=\"${String(og.imageWidth)}\" />`);\n if (og.image && og.imageHeight) tags.push(`<meta data-elur-head property=\"og:image:height\" content=\"${String(og.imageHeight)}\" />`);\n if (og.image && og.imageType) tags.push(`<meta data-elur-head property=\"og:image:type\" content=\"${escapeHtml(og.imageType)}\" />`);\n if (og.siteName) tags.push(`<meta data-elur-head property=\"og:site_name\" content=\"${escapeHtml(og.siteName)}\" />`);\n if (og.locale) tags.push(`<meta data-elur-head property=\"og:locale\" content=\"${escapeHtml(og.locale)}\" />`);\n }\n\n const tw = metadata.twitter;\n if (tw) {\n if (tw.card) tags.push(`<meta data-elur-head name=\"twitter:card\" content=\"${escapeHtml(tw.card)}\" />`);\n if (tw.title ?? title) tags.push(`<meta data-elur-head name=\"twitter:title\" content=\"${escapeHtml(tw.title ?? title)}\" />`);\n if (tw.description ?? metadata.description) {\n tags.push(`<meta data-elur-head name=\"twitter:description\" content=\"${escapeHtml(tw.description ?? metadata.description!)}\" />`);\n }\n if (tw.image) tags.push(`<meta data-elur-head name=\"twitter:image\" content=\"${escapeHtml(tw.image)}\" />`);\n if (tw.image && tw.imageAlt) tags.push(`<meta data-elur-head name=\"twitter:image:alt\" content=\"${escapeHtml(tw.imageAlt)}\" />`);\n }\n\n if (metadata.other) {\n for (const [name, content] of Object.entries(metadata.other)) {\n tags.push(`<meta data-elur-head name=\"${escapeHtml(name)}\" content=\"${escapeHtml(content)}\" />`);\n }\n }\n\n return tags.map((t) => `\\n ${t}`).join(\"\");\n}\n\n/** Wraps rendered body HTML into a full HTML document. */\nexport function documentShell(opts: ShellOptions): string {\n const { body, title = \"Elur Kit App\", lang = \"es\", data, actions, clientEntry, htmlAttributes, headScripts, headLinks, metadata } = opts;\n\n const dataScript =\n data !== undefined\n ? `\\n <script type=\"application/json\" id=\"elur-data\">${serializeData(data)}</script>`\n : \"\";\n\n const actionsScript = actions && Object.keys(actions).length > 0\n ? `\\n <script type=\"application/json\" id=\"elur-actions\">${serializeData(actions)}</script>`\n : \"\";\n\n const entryScript = clientEntry\n ? `\\n <script type=\"module\" src=\"${escapeHtml(clientEntry)}\"></script>`\n : \"\";\n\n const htmlAttrs = htmlAttributes\n ? Object.entries(htmlAttributes)\n .filter(([, value]) => value !== undefined && value !== null && value !== \"\")\n .map(([key, value]) => ` ${escapeHtml(key)}=\"${escapeHtml(String(value))}\"`)\n .join(\"\")\n : \"\";\n\n const headScriptsHtml = headScripts\n ? headScripts\n .filter((script) => typeof script === \"string\" && script.trim().length > 0)\n .map((script) => {\n // If the script is already a complete <script> tag (e.g. JSON-LD),\n // render it as-is without wrapping.\n if (script.trimStart().startsWith(\"<script\")) {\n return `\\n ${script}`;\n }\n return `\\n <script>${script.replace(/<\\/script>/gi, \"<\\\\/script>\")}</script>`;\n })\n .join(\"\")\n : \"\";\n\n const headTags = metadata ? buildHeadTags(metadata, title) : \"\";\n const titleTag = metadata?.title\n ? \"\" // already emitted by buildHeadTags\n : `\\n <title>${escapeHtml(title)}</title>`;\n\n const headLinksHtml = headLinks\n ? headLinks\n .filter((link) => typeof link === \"string\" && link.trim().length > 0)\n .map((link) => `\\n ${link}`)\n .join(\"\")\n : \"\";\n\n const renderEndpointMeta =\n opts.renderEndpoint === false\n ? '\\n <meta name=\"elur:render-endpoint\" content=\"off\" />'\n : \"\";\n\n return `<!DOCTYPE html>\n<html lang=\"${escapeHtml(lang)}\"${htmlAttrs}>\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />${renderEndpointMeta}${titleTag}${headTags}${headLinksHtml}${headScriptsHtml}\n </head>\n <body>\n <div id=\"app\">${body}</div>${dataScript}${actionsScript}${entryScript}\n </body>\n</html>\n`;\n}\n","// --- Ephemeral action error store ---\n//\n// Action failures submitted via plain HTML forms (progressive enhancement)\n// need to be relayed back to the page so the user sees validation errors.\n//\n// Previously the failure data was serialized into a `?__elur_js_action_error=`\n// query param on the redirect. That leaks errors into browser history,\n// server logs and third-party Referer headers.\n//\n// Now we stash the failure in a short-lived in-memory store keyed by a random\n// id, set a small cookie `__elur_js_action_error=<id>` (Max-Age=15s, SameSite=Lax),\n// and the next render reads the cookie, fetches the payload, exposes it as\n// `props.form`, and clears the entry.\n//\n// The store is process-local, which is fine for the single-process SSR server\n// and the dev server. For multi-instance deployments the cookie carries the\n// payload directly when it fits (see `encodeActionErrorCookie`); the store is\n// only the overflow path for large payloads.\n\nimport { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\n\nconst COOKIE_NAME = \"__elur_js_action_error\";\nconst MAX_COOKIE_SIZE = 3500; // bytes; leaves headroom under the 4KB cookie limit\nconst TTL_MS = 15_000;\n\n// HMAC key for signing action error cookies. In production this should be\n// set via ELUR_JS_ACTION_SECRET env var; otherwise we derive a per-process\n// key (sufficient for single-process dev/preview, but NOT for multi-instance).\nconst ACTION_SECRET =\n process.env.ELUR_JS_ACTION_SECRET ?? randomBytes(32).toString(\"hex\");\n\ninterface StoredError {\n data: unknown;\n status: number;\n expiresAt: number;\n}\n\nconst store = new Map<string, StoredError>();\n\n// Periodically purge expired entries so the map does not grow unbounded.\nlet sweepScheduled = false;\nfunction scheduleSweep(): void {\n if (sweepScheduled) return;\n sweepScheduled = true;\n setTimeout(() => {\n sweepScheduled = false;\n const now = Date.now();\n for (const [key, entry] of store) {\n if (entry.expiresAt <= now) store.delete(key);\n }\n }, TTL_MS).unref?.();\n}\n\n/**\n * Signs a payload with HMAC-SHA256 using the action secret.\n * Returns `signature.payload` (both hex/base64url).\n */\nfunction sign(payload: string): string {\n const sig = createHmac(\"sha256\", ACTION_SECRET).update(payload).digest(\"hex\");\n return `${sig}.${payload}`;\n}\n\n/**\n * Verifies a signed value and returns the payload if valid, or undefined.\n * Uses timingSafeEqual to prevent timing attacks.\n */\nfunction verify(value: string): string | undefined {\n const dotIndex = value.indexOf(\".\");\n if (dotIndex === -1) return undefined;\n const sig = value.slice(0, dotIndex);\n const payload = value.slice(dotIndex + 1);\n const expectedSig = createHmac(\"sha256\", ACTION_SECRET).update(payload).digest(\"hex\");\n if (sig.length !== expectedSig.length) return undefined;\n try {\n if (timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) {\n return payload;\n }\n } catch {\n // Length mismatch — invalid.\n }\n return undefined;\n}\n\n/**\n * Encodes an action failure for the redirect cookie. When the payload fits\n * inside the cookie limit, it is embedded directly as a signed base64url JSON\n * value. When it is too large, it is stored in memory and only a short signed\n * id is written to the cookie.\n *\n * The cookie is signed with HMAC-SHA256 to prevent forgery (A-20).\n *\n * @returns The cookie value to set on the redirect response.\n */\nexport function encodeActionErrorCookie(\n data: unknown,\n status: number,\n): { value: string; storeId?: string } {\n const payload = JSON.stringify({ d: data, s: status });\n const encoded = Buffer.from(payload, \"utf8\").toString(\"base64url\");\n const signed = sign(encoded);\n if (signed.length <= MAX_COOKIE_SIZE) {\n return { value: signed };\n }\n\n // Overflow: stash in memory and reference by signed id.\n const id = randomBytes(12).toString(\"hex\");\n store.set(id, { data, status, expiresAt: Date.now() + TTL_MS });\n scheduleSweep();\n return { value: sign(`id:${id}`), storeId: id };\n}\n\n/**\n * Decodes a cookie value (previously produced by `encodeActionErrorCookie`)\n * into the failure payload. Verifies the HMAC signature first, then resolves\n * in-memory overflow entries and deletes them after reading.\n */\nexport function decodeActionErrorCookie(value: string | undefined | null):\n | { data: unknown; status: number }\n | undefined {\n if (!value) return undefined;\n\n // Verify signature first.\n const verifiedPayload = verify(value);\n if (verifiedPayload === undefined) return undefined;\n\n // Check if it's an in-memory store reference.\n if (verifiedPayload.startsWith(\"id:\")) {\n const id = verifiedPayload.slice(3);\n const entry = store.get(id);\n if (!entry) return undefined;\n store.delete(id);\n if (entry.expiresAt <= Date.now()) return undefined;\n return { data: entry.data, status: entry.status };\n }\n\n try {\n const json = Buffer.from(verifiedPayload, \"base64url\").toString(\"utf8\");\n const parsed = JSON.parse(json) as { d: unknown; s: number };\n return { data: parsed.d, status: parsed.s };\n } catch {\n return undefined;\n }\n}\n\n/** Name of the cookie used to relay action errors. */\nexport const ACTION_ERROR_COOKIE = COOKIE_NAME;\n\n/** Builds the Set-Cookie header value that clears the error cookie. */\nexport function clearActionErrorCookieHeader(): string {\n return `${COOKIE_NAME}=; Path=/; Max-Age=0; SameSite=Lax`;\n}\n\n/** Builds the Set-Cookie header value that sets the error cookie. */\nexport function setActionErrorCookieHeader(value: string): string {\n return `${COOKIE_NAME}=${value}; Path=/; Max-Age=15; SameSite=Lax; HttpOnly`;\n}\n","// --- Cache policy per route (runtime-security §9.1) ---\n//\n// Authors can declare a cache policy in their page.data.ts:\n//\n// export const cache = {\n// mode: \"public\", // \"public\" | \"private\" | \"dynamic\"\n// revalidate: 60, // seconds\n// tags: [\"products\"], // for tag-based invalidation\n// };\n//\n// Default policy: \"dynamic\" (no public ISR caching).\n// Requests with Cookie/Authorization are never cached publicly.\n// Responses with Set-Cookie/private/no-store are never cached publicly.\n\n/** Cache mode for a route. */\nexport type CacheMode = \"public\" | \"private\" | \"dynamic\";\n\n/** Cache policy declared by the route's data module. */\nexport interface CachePolicy {\n mode: CacheMode;\n revalidate: number;\n tags?: string[];\n}\n\n/** Default cache policy when none is declared. */\nexport const DEFAULT_CACHE_POLICY: CachePolicy = {\n mode: \"dynamic\",\n revalidate: 0,\n};\n\n/**\n * Normalizes a raw cache export from a data module into a CachePolicy.\n * Returns the default policy if the input is invalid or missing.\n */\nexport function normalizeCachePolicy(raw: unknown): CachePolicy {\n if (!raw || typeof raw !== \"object\") return DEFAULT_CACHE_POLICY;\n const obj = raw as Record<string, unknown>;\n const mode = obj.mode;\n if (mode !== \"public\" && mode !== \"private\" && mode !== \"dynamic\") {\n return DEFAULT_CACHE_POLICY;\n }\n const revalidate = typeof obj.revalidate === \"number\" ? obj.revalidate : 0;\n const tags = Array.isArray(obj.tags) ? obj.tags.filter((t) => typeof t === \"string\") : undefined;\n return { mode, revalidate, tags };\n}\n\n/**\n * Determines whether a route's cache policy allows public caching for the\n * given request.\n *\n * Per §9.1:\n * - \"dynamic\" → never cache\n * - \"private\" → never cache publicly (requires private adapter)\n * - \"public\" → cache only if request has no Cookie/Authorization\n */\nexport function shouldCachePublic(\n policy: CachePolicy,\n request: Request,\n): boolean {\n if (policy.mode !== \"public\") return false;\n if (policy.revalidate <= 0) return false;\n if (request.headers.get(\"Cookie\")) return false;\n if (request.headers.get(\"Authorization\")) return false;\n return true;\n}\n","import type { ElurTemplate } from \"@elurjs/core\";\nimport { renderToString } from \"../render/render-to-string.js\";\nimport { documentShell, buildHeadTags } from \"../build/document-shell.js\";\nimport type { PageRoute, ScannedRoutes } from \"../router/route-scanner.js\";\nimport type { BuildConfig } from \"../build/build.js\";\nimport type { PageDataLoad, PageProps, RouteParams, PageMetadata, GenerateMetadata } from \"../types.js\";\nimport { existsSync } from \"node:fs\";\nimport { decodeActionErrorCookie, ACTION_ERROR_COOKIE } from \"../action/error-store.js\";\nimport { normalizeCachePolicy, type CachePolicy } from \"../cache/policy.js\";\n\nexport interface RenderPageOptions {\n route: PageRoute;\n params?: RouteParams;\n searchParams?: URLSearchParams;\n config: Pick<BuildConfig, \"lang\" | \"clientEntry\" | \"renderEndpoint\">;\n /** Custom module loader. Defaults to native dynamic import. */\n importer?: (path: string) => Promise<unknown>;\n /** Per-page action names exposed in the HTML shell. */\n actions?: Record<string, string[]>;\n /** Current request, used to hydrate data loaders that need cookies/headers. */\n request?: Request;\n}\n\nexport interface RenderPageResult {\n html: string;\n revalidate?: number;\n /**\n * `Set-Cookie` header value that clears the action error cookie, when the\n * page consumed a relayed action failure. The SSR server should append it to\n * the outgoing response so the cookie does not persist.\n */\n clearActionErrorCookie?: string;\n /** `<head>` tags (title, meta, OG, twitter) for the SPA router to merge. */\n head?: string;\n /** Resolved page title (from metadata or fallback). */\n resolvedTitle?: string;\n /**\n * When a loader or layout throws a `Response` (e.g. `throw new Response(...,\n * { status: 404 })`), it is captured here as a first-class response instead\n * of being treated as an internal error (A-22).\n */\n response?: Response;\n /** HTTP status code for the rendered page (e.g. 404 for not-found pages). */\n status?: number;\n /** Cache policy declared by the route (§9.1). */\n cachePolicy?: CachePolicy;\n}\n\nconst defaultImport = (path: string) => import(path);\n\n/**\n * Collects `<html>` attributes and head scripts declared by data loaders\n * (page and layouts) via top-level `htmlAttributes` / `headScripts` fields.\n */\nexport function collectShellExtras(\n pageData: unknown,\n layoutDataList: unknown[],\n): { htmlAttributes: Record<string, string>; headScripts: string[]; headLinks: string[] } {\n const htmlAttributes: Record<string, string> = {};\n const headScripts: string[] = [];\n const headLinks: string[] = [];\n const merge = (value: unknown) => {\n if (!value || typeof value !== \"object\") return;\n const attrs = (value as { htmlAttributes?: Record<string, string> }).htmlAttributes;\n if (attrs) Object.assign(htmlAttributes, attrs);\n const scripts = (value as { headScripts?: string[] }).headScripts;\n if (Array.isArray(scripts)) headScripts.push(...scripts);\n const links = (value as { headLinks?: string[] }).headLinks;\n if (Array.isArray(links)) headLinks.push(...links);\n };\n for (const layoutData of layoutDataList) merge(layoutData);\n merge(pageData);\n // Deduplicate headScripts and headLinks (e.g. from both layout and page data)\n const uniqueScripts = [...new Set(headScripts)];\n const uniqueLinks = [...new Set(headLinks)];\n return { htmlAttributes, headScripts: uniqueScripts, headLinks: uniqueLinks };\n}\n\nexport async function renderPage(options: RenderPageOptions): Promise<RenderPageResult> {\n const { route, params = {}, searchParams = new URLSearchParams(), config, importer = defaultImport, actions, request } = options;\n\n const pageModule = await importer(route.pagePath) as {\n default: (props: PageProps<unknown>) => ElurTemplate;\n generateMetadata?: GenerateMetadata;\n };\n const { default: PageComponent, generateMetadata } = pageModule;\n\n let data: unknown;\n let revalidate: number | undefined;\n let cachePolicy: import(\"../cache/policy.js\").CachePolicy | undefined;\n // Use a mutable container so TypeScript doesn't narrow the type after\n // the first `if (thrownResponse)` check.\n const thrown: { response: Response | undefined } = { response: undefined };\n if (route.dataPath) {\n const mod = await importer(route.dataPath) as {\n load?: PageDataLoad;\n revalidate?: number;\n cache?: unknown;\n };\n if (mod.load) {\n try {\n data = await mod.load({ params, searchParams, request });\n } catch (err) {\n if (err instanceof Response) {\n thrown.response = err;\n } else {\n throw err;\n }\n }\n }\n if (typeof mod.revalidate === \"number\") {\n revalidate = mod.revalidate;\n }\n // Read cache policy from the data module (§9.1).\n if (mod.cache) {\n cachePolicy = normalizeCachePolicy(mod.cache);\n if (cachePolicy.revalidate > 0) {\n revalidate = cachePolicy.revalidate;\n }\n }\n }\n\n // If a loader threw a Response (redirect, 404, etc.), return it as a\n // first-class response instead of rendering the page (A-22).\n if (thrown.response) {\n return { html: \"\", response: thrown.response, status: thrown.response.status };\n }\n\n // Relay an action failure previously stored in the ephemeral cookie so the\n // page can render validation errors via `props.form`. The cookie is cleared\n // on the outgoing response (see `clearActionErrorCookie` in the result).\n let form: unknown;\n let clearActionErrorCookie: string | undefined;\n if (request) {\n const cookieHeader = request.headers.get(\"Cookie\") ?? \"\";\n const match = cookieHeader.match(new RegExp(`(?:^|;\\\\s*)${ACTION_ERROR_COOKIE}=([^;]+)`));\n if (match) {\n const decoded = decodeActionErrorCookie(match[1]);\n if (decoded) {\n form = { __elur_js_action_error: true, status: decoded.status, data: decoded.data };\n clearActionErrorCookie = `${ACTION_ERROR_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax`;\n }\n }\n }\n\n const props: PageProps<unknown> = {\n data: data ?? {},\n params,\n searchParams,\n form,\n };\n\n const layoutModules = await Promise.all(\n route.layouts.map(async (layoutPath) => importer(layoutPath)),\n );\n const layoutDataList = await Promise.all(\n route.layouts.map(async (layoutPath) => {\n const dataPath = layoutPath.replace(/layout\\.ts$/, \"layout.data.ts\");\n if (!existsSync(dataPath)) return undefined;\n const mod = (await importer(dataPath)) as { load?: PageDataLoad };\n if (mod.load) {\n try {\n return await mod.load({ params, searchParams, request });\n } catch (err) {\n if (err instanceof Response) {\n thrown.response = err;\n return undefined;\n }\n throw err;\n }\n }\n return undefined;\n }),\n );\n\n // If a layout loader threw a Response, return it as first-class (A-22).\n const layoutThrown = thrown.response as Response | undefined;\n if (layoutThrown) {\n return { html: \"\", response: layoutThrown, status: layoutThrown.status };\n }\n\n // Load slot modules if the route has them (v2.1 — Fix #2: Layout Slots).\n let slotTemplates: Record<string, ElurTemplate> | undefined;\n if (route.slots) {\n slotTemplates = {};\n for (const [slotName, slotPath] of Object.entries(route.slots)) {\n const slotMod = await importer(slotPath) as { default: (props: PageProps<unknown>) => ElurTemplate };\n slotTemplates[slotName] = slotMod.default(props);\n }\n }\n\n const body = await renderToString(() => {\n let template = PageComponent(props);\n for (let i = layoutModules.length - 1; i >= 0; i--) {\n const { default: Layout } = layoutModules[i] as {\n default: (props: { children: ElurTemplate; data?: unknown; slots?: Record<string, ElurTemplate> }) => ElurTemplate;\n };\n template = Layout({ children: template, data: layoutDataList[i], slots: slotTemplates });\n }\n return template;\n });\n\n const title = typeof data === \"object\" && data && \"title\" in data\n ? String((data as { title?: unknown }).title ?? \"Elur Kit\")\n : \"Elur Kit\";\n\n const { htmlAttributes, headScripts, headLinks } = collectShellExtras(data, layoutDataList);\n\n // Resolve page metadata. Priority: `generateMetadata` from page.ts > `metadata`\n // field in the page loader data > `metadata` field in layout loader data.\n let metadata: PageMetadata | undefined;\n if (typeof generateMetadata === \"function\") {\n metadata = await generateMetadata({ params, searchParams, request, data });\n }\n if (!metadata) {\n metadata = extractMetadata(data) ?? extractMetadataFromList(layoutDataList);\n }\n // The title from metadata takes precedence over the data.title fallback.\n const resolvedTitle = metadata?.title ?? title;\n\n const html = documentShell({\n title: resolvedTitle,\n lang: config.lang,\n body,\n data,\n actions,\n htmlAttributes,\n headScripts,\n headLinks,\n metadata,\n clientEntry: config.clientEntry,\n renderEndpoint: config.renderEndpoint,\n });\n\n const head = metadata ? buildHeadTags(metadata, resolvedTitle) : \"\";\n return { html, revalidate, clearActionErrorCookie, head, resolvedTitle, cachePolicy };\n}\n\n/** Extracts a `metadata` field from a loader data object, if present. */\nfunction extractMetadata(value: unknown): PageMetadata | undefined {\n if (value && typeof value === \"object\" && \"metadata\" in value) {\n const meta = (value as { metadata?: unknown }).metadata;\n if (meta && typeof meta === \"object\") return meta as PageMetadata;\n }\n return undefined;\n}\n\n/** Extracts metadata from the first layout data object that has one. */\nfunction extractMetadataFromList(list: unknown[]): PageMetadata | undefined {\n for (const item of list) {\n const meta = extractMetadata(item);\n if (meta) return meta;\n }\n return undefined;\n}\n\nexport interface RenderErrorPageOptions {\n routes: ScannedRoutes;\n status: 404 | 500;\n error?: unknown;\n config: Pick<BuildConfig, \"lang\" | \"clientEntry\" | \"renderEndpoint\">;\n actions?: Record<string, string[]>;\n importer?: (path: string) => Promise<unknown>;\n}\n\nexport async function renderErrorPage(\n options: RenderErrorPageOptions,\n): Promise<{ html: string; status: number } | undefined> {\n const route = options.status === 404 ? options.routes.error404 : options.routes.error500;\n if (!route) return undefined;\n\n try {\n const { html } = await renderPage({\n route,\n params: {},\n searchParams: new URLSearchParams(),\n config: options.config,\n actions: options.actions,\n importer: options.importer,\n });\n return { html, status: options.status };\n } catch (err) {\n console.error(`[render] error ${options.status} page failed`, err);\n return undefined;\n }\n}\n","import type { ApiRoute, PageRoute } from \"../router/route-scanner.js\";\n\nexport interface MatchResult {\n route: PageRoute;\n params: Record<string, string | string[]>;\n searchParams: URLSearchParams;\n}\n\n/**\n * Match a request pathname against a list of page routes.\n *\n * Routes are sorted by specificity (static > dynamic > catch-all) before\n * matching, so `/about` wins over `/:slug` even if the catch-all appears first.\n *\n * URL segments are safely decoded (plan §11.1, runtime-security §10).\n */\nexport function matchRoute(\n pathname: string,\n routes: PageRoute[],\n): MatchResult | undefined {\n const cleanPath = pathname.split(\"?\")[0];\n const requestSegments = cleanPath.split(\"/\").filter(Boolean).map(safeDecodeURIComponent);\n\n const sorted = [...routes].sort((a, b) => specificity(b.path) - specificity(a.path));\n\n for (const route of sorted) {\n const routeSegments = route.path.split(\"/\").filter(Boolean);\n const match = tryMatch(requestSegments, routeSegments, route.optionalCatchAll);\n if (match) {\n return { route, params: match, searchParams: new URLSearchParams() };\n }\n }\n\n return undefined;\n}\n\nexport interface ApiMatchResult<T = ApiRoute> {\n route: T;\n params: Record<string, string | string[]>;\n}\n\n/**\n * Match a request pathname against a list of API routes.\n */\nexport function matchApiRoute<T extends { path: string }>(pathname: string, routes: T[]): ApiMatchResult<T> | undefined {\n const cleanPath = pathname.split(\"?\")[0];\n const requestSegments = cleanPath.split(\"/\").filter(Boolean).map(safeDecodeURIComponent);\n\n const sorted = [...routes].sort((a, b) => specificity(b.path) - specificity(a.path));\n\n for (const route of sorted) {\n const routeSegments = route.path.split(\"/\").filter(Boolean);\n const match = tryMatch(requestSegments, routeSegments);\n if (match) {\n return { route, params: match };\n }\n }\n\n return undefined;\n}\n\n/**\n * Safely decodes a URI component. If decoding fails (malformed % sequences),\n * returns the original string rather than throwing (runtime-security §10).\n */\nfunction safeDecodeURIComponent(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n return segment;\n }\n}\n\nfunction specificity(path: string): number {\n return path.split(\"/\").filter(Boolean).reduce((score, segment) => {\n if (segment.endsWith(\"*\")) return score;\n if (segment.startsWith(\":\")) return score + 1;\n return score + 2;\n }, 0);\n}\n\nfunction tryMatch(\n requestSegments: string[],\n routeSegments: string[],\n optionalCatchAll = false,\n): Record<string, string | string[]> | undefined {\n const params: Record<string, string | string[]> = {};\n\n let i = 0;\n for (let r = 0; r < routeSegments.length; r++) {\n const routeSeg = routeSegments[r];\n\n if (routeSeg.endsWith(\"*\")) {\n // Catch-all consumes the rest of the request segments.\n const name = routeSeg.slice(1, -1);\n const rest = requestSegments.slice(i);\n // For optional catch-all, empty rest is OK.\n if (rest.length === 0 && !optionalCatchAll) return undefined;\n params[name] = rest.length > 0 ? rest : [];\n return params;\n }\n\n if (routeSeg.startsWith(\":\")) {\n const requestSeg = requestSegments[i];\n if (requestSeg === undefined) return undefined;\n params[routeSeg.slice(1)] = requestSeg;\n i++;\n continue;\n }\n\n if (routeSeg !== requestSegments[i]) {\n return undefined;\n }\n i++;\n }\n\n if (i !== requestSegments.length) return undefined;\n return params;\n}\n","// --- Origin verification (CSRF protection for server actions) ---\n//\n// Server actions accept POST requests from the browser. Without origin\n// verification, any third-party site could submit forged requests to\n// `/__elur-js/actions` on behalf of a logged-in user (CSRF).\n//\n// Strategy: compare the request's `Origin` (or `Referer` fallback) host against\n// the target `Host` header. Same-origin requests pass; cross-origin requests\n// are rejected with 403 unless the origin is explicitly allow-listed.\n//\n// Requests without `Origin` AND without `Referer` (e.g. curl, server-to-server)\n// are accepted by default for DX, unless `strictOrigin: true` is configured.\n\nexport interface OriginCheckOptions {\n /** Extra origins allowed to call actions (e.g. preview deployments). */\n allowedOrigins?: string[];\n /**\n * When true, requests missing both `Origin` and `Referer` are rejected.\n * Defaults to false so curl/server-to-server calls keep working.\n */\n strictOrigin?: boolean;\n}\n\n/**\n * Returns the host:port of a URL string, or undefined if it cannot be parsed.\n */\nfunction originOf(urlString: string | null | undefined): string | undefined {\n if (!urlString) return undefined;\n try {\n const url = new URL(urlString);\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return undefined;\n return url.origin;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Verifies that a request originates from the same host (or an allow-listed\n * origin). Returns an error message when the request must be rejected, or\n * undefined when it is allowed.\n *\n * @param request The incoming Request to actions.\n * @param options Origin check configuration.\n */\nexport function verifyOrigin(\n request: Request,\n options: OriginCheckOptions = {},\n): string | undefined {\n const targetOrigin = originOf(request.url);\n if (!targetOrigin) return \"Invalid target URL\";\n\n const origin = request.headers.get(\"Origin\");\n const referer = request.headers.get(\"Referer\");\n if (!origin && !referer) {\n return options.strictOrigin\n ? \"Missing Origin and Referer headers\"\n : undefined;\n }\n\n const sourceOrigin = origin ? originOf(origin) : originOf(referer);\n if (!sourceOrigin) return origin ? \"Invalid Origin header\" : \"Invalid Referer header\";\n if (sourceOrigin === targetOrigin) return undefined;\n\n if (options.allowedOrigins?.some((allowed) => originOf(allowed) === sourceOrigin)) return undefined;\n\n return `Cross-origin request blocked: source \"${sourceOrigin}\" != target \"${targetOrigin}\"`;\n}\n\n/** Builds a 403 Response for a rejected origin. */\nexport function originForbidden(message: string): Response {\n return new Response(message, {\n status: 403,\n headers: { \"Content-Type\": \"text/plain; charset=utf-8\" },\n });\n}\n","import type { ActionRequest } from \"./index.js\";\nimport { isActionFailure, isRedirectResponse, publicErrorResponse } from \"../errors.js\";\nimport { verifyOrigin, originForbidden, type OriginCheckOptions } from \"./origin.js\";\nimport {\n encodeActionErrorCookie,\n setActionErrorCookieHeader,\n} from \"./error-store.js\";\n\n/**\n * Resolves a server action by name and optional page scope.\n */\nexport type ActionResolver = (\n name: string,\n page?: string,\n) => Promise<((...args: unknown[]) => unknown) | undefined>;\n\n/** Options shared by `handleActionRequest` callers for CSRF protection. */\nexport interface ActionSecurityOptions extends OriginCheckOptions {\n /** Maximum body size in bytes. Defaults to 1MB (1_048_576). */\n bodyLimit?: number;\n}\n\n/** Default body size limit: 1MB. */\nconst DEFAULT_BODY_LIMIT = 1_048_576;\n\n/**\n * Reads the request body as text, enforcing a maximum size.\n * Returns a 413 response if the body exceeds the limit.\n */\nasync function readBodyWithLimit(\n request: Request,\n limit: number,\n): Promise<{ ok: true; text: string } | { ok: false; response: Response }> {\n const contentLength = request.headers.get(\"Content-Length\");\n if (contentLength && parseInt(contentLength, 10) > limit) {\n return {\n ok: false,\n response: new Response(\"Request body too large\", {\n status: 413,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n // Read the body as a stream with a size cap to prevent memory exhaustion\n // from chunked transfer encoding without Content-Length.\n const reader = request.body?.getReader();\n if (!reader) {\n return { ok: true, text: \"\" };\n }\n const chunks: Uint8Array[] = [];\n let totalSize = 0;\n try {\n for (; ;) {\n const { done, value } = await reader.read();\n if (done) break;\n totalSize += value.byteLength;\n if (totalSize > limit) {\n try { reader.cancel(); } catch { /* ignore */ }\n return {\n ok: false,\n response: new Response(\"Request body too large\", {\n status: 413,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n chunks.push(value);\n }\n } finally {\n try { reader.releaseLock(); } catch { /* ignore */ }\n }\n const total = new Uint8Array(totalSize);\n let offset = 0;\n for (const chunk of chunks) {\n total.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return { ok: true, text: new TextDecoder().decode(total) };\n}\n\nfunction parseFormBody(body: string): Record<string, unknown> {\n const params = new URLSearchParams(body);\n const result: Record<string, unknown> = {};\n for (const [key, value] of params) {\n if (result[key] === undefined) {\n result[key] = value;\n } else if (Array.isArray(result[key])) {\n (result[key] as unknown[]).push(value);\n } else {\n result[key] = [result[key], value];\n }\n }\n return result;\n}\n\nasync function parseActionRequest(\n request: Request,\n bodyLimit: number = DEFAULT_BODY_LIMIT,\n): Promise<\n | { ok: true; name: string; page?: string; args: unknown[]; wantsJson: boolean }\n | { ok: false; response: Response }\n> {\n if (request.method !== \"POST\") {\n return {\n ok: false,\n response: new Response(\"Method not allowed\", {\n status: 405,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n\n const contentType = request.headers.get(\"Content-Type\") ?? \"\";\n const wantsJson = (request.headers.get(\"Accept\") ?? \"\").includes(\"application/json\");\n\n let name: string | undefined;\n let page: string | undefined;\n let args: unknown[] = [];\n\n if (contentType.includes(\"application/json\")) {\n const bodyResult = await readBodyWithLimit(request, bodyLimit);\n if (!bodyResult.ok) return { ok: false, response: bodyResult.response };\n let body: ActionRequest;\n try {\n body = JSON.parse(bodyResult.text) as ActionRequest;\n } catch {\n return {\n ok: false,\n response: new Response(\"Invalid JSON body\", {\n status: 400,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n name = body.name;\n page = body.page;\n args = Array.isArray(body.args) ? body.args : [];\n } else if (\n contentType.includes(\"application/x-www-form-urlencoded\") ||\n contentType.includes(\"multipart/form-data\")\n ) {\n // For multipart, use the native formData() parser after checking\n // Content-Length against the limit. For urlencoded, use our size-capped\n // reader to handle chunked encoding without Content-Length.\n if (contentType.includes(\"multipart/form-data\")) {\n const contentLength = request.headers.get(\"Content-Length\");\n if (contentLength && parseInt(contentLength, 10) > bodyLimit) {\n return {\n ok: false,\n response: new Response(\"Request body too large\", {\n status: 413,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n let form: FormData;\n try {\n form = await request.formData();\n } catch {\n return {\n ok: false,\n response: new Response(\"Invalid form body\", {\n status: 400,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n name = form.get(\"__elur_js_action_name\") as string | null ?? undefined;\n page = form.get(\"__elur_js_action_page\") as string | null ?? undefined;\n const input: Record<string, unknown> = {};\n for (const [key, value] of form) {\n if (key === \"__elur_js_action_name\" || key === \"__elur_js_action_page\") continue;\n input[key] = value;\n }\n args = [input];\n } else {\n const bodyResult = await readBodyWithLimit(request, bodyLimit);\n if (!bodyResult.ok) return { ok: false, response: bodyResult.response };\n const form = parseFormBody(bodyResult.text);\n name = form.__elur_js_action_name as string | undefined;\n page = form.__elur_js_action_page as string | undefined;\n const input: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(form)) {\n if (key === \"__elur_js_action_name\" || key === \"__elur_js_action_page\") continue;\n input[key] = value;\n }\n args = [input];\n }\n } else {\n // Try to parse a plain form body as a fallback for progressive enhancement.\n const bodyResult = await readBodyWithLimit(request, bodyLimit);\n if (!bodyResult.ok) return { ok: false, response: bodyResult.response };\n const form = parseFormBody(bodyResult.text);\n name = form.__elur_js_action_name as string | undefined;\n page = form.__elur_js_action_page as string | undefined;\n const input: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(form)) {\n if (key === \"__elur_js_action_name\" || key === \"__elur_js_action_page\") continue;\n input[key] = value;\n }\n args = [input];\n }\n\n if (!name || typeof name !== \"string\") {\n return {\n ok: false,\n response: new Response(\"Missing action name\", {\n status: 400,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n\n return { ok: true, name, page, args, wantsJson };\n}\n\n/**\n * Handles a POST request to the server action endpoint.\n *\n * Accepts both JSON requests (`{ name, page?, args }`) and HTML form submissions\n * for progressive enhancement. The provided resolver looks up the action\n * implementation, invokes it with the supplied arguments and returns the result\n * as JSON or redirects back to the request origin for form submissions.\n *\n * Origin verification (CSRF protection) runs before parsing the body: any\n * cross-origin POST is rejected with 403 unless its origin is allow-listed via\n * `security.allowedOrigins`.\n *\n * For progressive-enhancement form submissions that fail, the failure payload\n * is relayed back via a short-lived `__elur_js_action_error` cookie (SameSite=Lax,\n * Max-Age=15s) instead of a query param, so errors do not leak into browser\n * history, server logs or third-party Referer headers.\n */\nexport async function handleActionRequest(\n request: Request,\n resolveAction: ActionResolver,\n security: ActionSecurityOptions = {},\n): Promise<Response> {\n // CSRF: verify same-origin (or allow-listed) before doing any work.\n const originError = verifyOrigin(request, security);\n if (originError) return originForbidden(originError);\n\n const parsed = await parseActionRequest(request, security.bodyLimit ?? DEFAULT_BODY_LIMIT);\n if (!parsed.ok) return parsed.response;\n\n const { name, page, args, wantsJson } = parsed;\n\n try {\n const action = await resolveAction(name, page);\n if (!action) {\n const message = page ? `Action not found: ${name} (page: ${page})` : `Action not found: ${name}`;\n return new Response(message, {\n status: 404,\n headers: { \"Content-Type\": \"text/plain\" },\n });\n }\n\n const result = await action(...args);\n\n if (isActionFailure(result)) {\n if (wantsJson) {\n return new Response(JSON.stringify({ __elur_js_action_failure: true, status: result.status, data: result.data }), {\n status: result.status,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n // Progressive enhancement: redirect back with the failure in a cookie.\n const referer = request.headers.get(\"Referer\") ?? \"/\";\n const url = new URL(referer, \"http://localhost\");\n const { value } = encodeActionErrorCookie(result.data, result.status);\n return new Response(null, {\n status: 303,\n headers: {\n Location: url.pathname + url.search,\n \"Content-Type\": \"text/plain\",\n \"Set-Cookie\": setActionErrorCookieHeader(value),\n },\n });\n }\n\n if (isRedirectResponse(result)) {\n if (wantsJson) {\n return new Response(\n JSON.stringify({ __elur_js_action_redirect: true, status: result.status, location: result.location }),\n {\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n },\n );\n }\n return new Response(null, {\n status: result.status,\n headers: { Location: result.location, \"Content-Type\": \"text/plain\" },\n });\n }\n\n if (wantsJson) {\n return new Response(JSON.stringify(result ?? null), {\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n // For progressive enhancement (plain form POST), redirect back.\n const referer = request.headers.get(\"Referer\") ?? \"/\";\n return new Response(null, {\n status: 303,\n headers: {\n Location: typeof result === \"string\" ? result : referer,\n \"Content-Type\": \"text/plain\",\n },\n });\n } catch (err) {\n console.error(\"[elur-kit] Action error:\", err);\n return publicErrorResponse(err, { includeDetail: false });\n }\n}\n\nexport { verifyOrigin, originForbidden, type OriginCheckOptions } from \"./origin.js\";\nexport {\n decodeActionErrorCookie,\n clearActionErrorCookieHeader,\n setActionErrorCookieHeader,\n ACTION_ERROR_COOKIE,\n} from \"./error-store.js\";\n","import type { IncomingMessage } from \"node:http\";\n\n// Capture the global AbortController at module load time so it's immune to\n// test frameworks that replace or delete globalThis.AbortController.\nconst GlobalAbortController =\n (globalThis as { AbortController?: typeof AbortController }).AbortController ?? AbortController;\n\nexport function incomingMessageToRequest(req: IncomingMessage, body?: BodyInit | null): Request {\n const headers = new Headers();\n for (let index = 0; index < req.rawHeaders.length; index += 2) {\n headers.append(req.rawHeaders[index], req.rawHeaders[index + 1]);\n }\n\n const controller = new GlobalAbortController();\n req.once(\"aborted\", () => controller.abort());\n req.once(\"close\", () => {\n if (!req.complete) controller.abort();\n });\n\n const protocol = (req.socket as typeof req.socket & { encrypted?: boolean }).encrypted ? \"https\" : \"http\";\n const init: RequestInit = {\n method: req.method ?? \"GET\",\n headers,\n signal: controller.signal,\n };\n if (body !== undefined && body !== null && init.method !== \"GET\" && init.method !== \"HEAD\") init.body = body;\n\n return new Request(`${protocol}://${headers.get(\"host\") ?? \"localhost\"}${req.url ?? \"/\"}`, init);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"node-http-DRAUhO0c.js","names":[],"sources":["../../src/render/ssr-flag.ts","../../src/render/render-to-string.ts","../../src/build/document-shell.ts","../../src/action/error-store.ts","../../src/cache/policy.ts","../../src/ssr/render.ts","../../src/ssr/match.ts","../../src/action/origin.ts","../../src/action/server.ts","../../src/runtime/node-http.ts"],"sourcesContent":["// --- SSR flag utility ---\n//\n// `@elurjs/core` does not export `_setSSR`/`_isSSR`. The reactivity state lives\n// on `globalThis[Symbol.for(\"@elurjs/core/reactivity-state\")]` and the kit owns\n// the `ssr` boolean on it: `renderToString` sets it to `true` while server\n// rendering so `isSSR()` reflects the current render mode for user code\n// (environment reads, client-only guards, ...).\n//\n// This module manipulates that flag directly so the kit does not depend on\n// private exports that may or may not be present in a given elur release.\n\nconst STATE_KEY = Symbol.for(\"@elurjs/core/reactivity-state\");\n\ntype ReactivityState = { ssr?: boolean };\n\nfunction getState(): ReactivityState | undefined {\n return (globalThis as Record<symbol, unknown>)[STATE_KEY] as\n | ReactivityState\n | undefined;\n}\n\n/** Sets the SSR flag on the Elur reactivity state. No-op if state is absent. */\nexport function setSSR(value: boolean): void {\n const state = getState();\n if (state) state.ssr = value;\n}\n\n/** Reads the SSR flag from the Elur reactivity state. Defaults to false. */\nexport function isSSR(): boolean {\n return getState()?.ssr ?? false;\n}\n","import type { ElurTemplate } from \"@elurjs/core\";\nimport { renderToString as renderCoreTemplate } from \"@elurjs/core/server\";\nimport { setSSR } from \"./ssr-flag\";\n\n// --- Build-time / server rendering ---\n//\n// The Elur core ships a DOM-free `renderToString` (`@elurjs/core/server`)\n// that streams template output without ever touching a `document`. The kit used\n// to inject a Node-side DOM (happy-dom) as a fallback for legacy compatibility;\n// that fallback has been removed together with the happy-dom dependency.\n\n/**\n * Renders a Elur template to an HTML string in Node.\n *\n * Accepts a *factory* (not a template) because `html`` evaluates at call time.\n *\n * @param factory Thunk that builds the template, e.g. `() => Page({ data })`.\n * @returns Serialized HTML of the rendered template.\n */\nexport async function renderToString(\n factory: () => ElurTemplate,\n options: { markers?: \"none\" | \"hydration\" } = {},\n): Promise<string> {\n setSSR(true);\n try {\n return await renderCoreTemplate(factory(), {\n markers: options.markers ?? \"hydration\",\n });\n } finally {\n setSSR(false);\n }\n}\n","//\n// The <!DOCTYPE>, <head> and <body> wrapper — plus the serialized loader data\n// and the client entry — are injected here at build time.\n\nimport type { PageMetadata } from \"../types.js\";\nexport interface ShellOptions {\n /** Rendered inner HTML that goes inside `#app`. */\n body: string;\n /** `<title>` text. */\n title?: string;\n /** `<html lang>` attribute. */\n lang?: string;\n /** Additional attributes for the `<html>` element, e.g. `{ \"data-theme\": \"dark\" }`. */\n htmlAttributes?: Record<string, string>;\n /**\n * Inline scripts injected into `<head>`. They run synchronously while the\n * document parses — before the first paint and before the (deferred) client\n * bundle — so they are the right place for no-flash bootstrapping (e.g.\n * applying a stored theme before the page becomes visible).\n */\n headScripts?: string[];\n /**\n * Raw HTML strings injected into `<head>` — e.g. `<link rel=\"icon\">`,\n * `<link rel=\"manifest\">`, `<meta name=\"theme-color\">`. Each string is\n * rendered as-is inside `<head>`.\n */\n headLinks?: string[];\n /** Loader data serialized into `<script id=\"elur-data\">`. */\n data?: unknown;\n /** Per-page action names serialized into `<script id=\"elur-actions\">`. */\n actions?: Record<string, string[]>;\n /** Path to the client entry module, e.g. `/_elur/entry-client.js`. */\n clientEntry?: string;\n /** Page metadata emitted as `<meta>`, `<link>` and OG/Twitter tags in `<head>`. */\n metadata?: PageMetadata;\n /**\n * Whether the SSR render endpoint (`/__elur-js/render`) is available at\n * runtime. Defaults to `true`. When `false` (static deployments), the shell\n * emits `<meta name=\"elur:render-endpoint\" content=\"off\" />` so the client\n * router skips probing the endpoint entirely — preventing a storm of 404\n * requests on fully static sites.\n */\n renderEndpoint?: boolean;\n}\n\nconst HTML_ESCAPES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (c) => HTML_ESCAPES[c]);\n}\n\n/**\n * Serializes data for embedding inside a `<script>` tag. Escapes `<` so a\n * `</script>` sequence in the data cannot break out of the tag.\n */\nfunction serializeData(data: unknown): string {\n return JSON.stringify(data ?? null).replace(/</g, \"\\\\u003c\");\n}\n\n/**\n * Builds the `<head>` tags for a `PageMetadata` object. Every tag is marked with\n * `data-elur-head` so the client-side router can replace them on navigation\n * without touching charset/viewport or user-supplied `headScripts`.\n */\nexport function buildHeadTags(metadata: PageMetadata, fallbackTitle: string): string {\n const tags: string[] = [];\n const title = metadata.title ?? fallbackTitle;\n if (metadata.title) {\n tags.push(`<title data-elur-head>${escapeHtml(title)}</title>`);\n }\n\n if (metadata.description) {\n tags.push(`<meta data-elur-head name=\"description\" content=\"${escapeHtml(metadata.description)}\" />`);\n }\n\n if (metadata.canonical) {\n tags.push(`<link data-elur-head rel=\"canonical\" href=\"${escapeHtml(metadata.canonical)}\" />`);\n }\n\n if (metadata.robots) {\n tags.push(`<meta data-elur-head name=\"robots\" content=\"${escapeHtml(metadata.robots)}\" />`);\n }\n\n const og = metadata.openGraph;\n if (og) {\n if (og.type) tags.push(`<meta data-elur-head property=\"og:type\" content=\"${escapeHtml(og.type)}\" />`);\n tags.push(`<meta data-elur-head property=\"og:title\" content=\"${escapeHtml(og.title ?? title)}\" />`);\n if (og.description ?? metadata.description) {\n tags.push(`<meta data-elur-head property=\"og:description\" content=\"${escapeHtml(og.description ?? metadata.description!)}\" />`);\n }\n if (og.url ?? metadata.canonical) {\n tags.push(`<meta data-elur-head property=\"og:url\" content=\"${escapeHtml(og.url ?? metadata.canonical!)}\" />`);\n }\n if (og.image) tags.push(`<meta data-elur-head property=\"og:image\" content=\"${escapeHtml(og.image)}\" />`);\n if (og.image && og.imageAlt) tags.push(`<meta data-elur-head property=\"og:image:alt\" content=\"${escapeHtml(og.imageAlt)}\" />`);\n if (og.image && og.imageWidth) tags.push(`<meta data-elur-head property=\"og:image:width\" content=\"${String(og.imageWidth)}\" />`);\n if (og.image && og.imageHeight) tags.push(`<meta data-elur-head property=\"og:image:height\" content=\"${String(og.imageHeight)}\" />`);\n if (og.image && og.imageType) tags.push(`<meta data-elur-head property=\"og:image:type\" content=\"${escapeHtml(og.imageType)}\" />`);\n if (og.siteName) tags.push(`<meta data-elur-head property=\"og:site_name\" content=\"${escapeHtml(og.siteName)}\" />`);\n if (og.locale) tags.push(`<meta data-elur-head property=\"og:locale\" content=\"${escapeHtml(og.locale)}\" />`);\n }\n\n const tw = metadata.twitter;\n if (tw) {\n if (tw.card) tags.push(`<meta data-elur-head name=\"twitter:card\" content=\"${escapeHtml(tw.card)}\" />`);\n if (tw.title ?? title) tags.push(`<meta data-elur-head name=\"twitter:title\" content=\"${escapeHtml(tw.title ?? title)}\" />`);\n if (tw.description ?? metadata.description) {\n tags.push(`<meta data-elur-head name=\"twitter:description\" content=\"${escapeHtml(tw.description ?? metadata.description!)}\" />`);\n }\n if (tw.image) tags.push(`<meta data-elur-head name=\"twitter:image\" content=\"${escapeHtml(tw.image)}\" />`);\n if (tw.image && tw.imageAlt) tags.push(`<meta data-elur-head name=\"twitter:image:alt\" content=\"${escapeHtml(tw.imageAlt)}\" />`);\n }\n\n if (metadata.other) {\n for (const [name, content] of Object.entries(metadata.other)) {\n tags.push(`<meta data-elur-head name=\"${escapeHtml(name)}\" content=\"${escapeHtml(content)}\" />`);\n }\n }\n\n return tags.map((t) => `\\n ${t}`).join(\"\");\n}\n\n/** Wraps rendered body HTML into a full HTML document. */\nexport function documentShell(opts: ShellOptions): string {\n const { body, title = \"Elur Kit App\", lang = \"es\", data, actions, clientEntry, htmlAttributes, headScripts, headLinks, metadata } = opts;\n\n const dataScript =\n data !== undefined\n ? `\\n <script type=\"application/json\" id=\"elur-data\">${serializeData(data)}</script>`\n : \"\";\n\n const actionsScript = actions && Object.keys(actions).length > 0\n ? `\\n <script type=\"application/json\" id=\"elur-actions\">${serializeData(actions)}</script>`\n : \"\";\n\n const entryScript = clientEntry\n ? `\\n <script type=\"module\" src=\"${escapeHtml(clientEntry)}\"></script>`\n : \"\";\n\n const htmlAttrs = htmlAttributes\n ? Object.entries(htmlAttributes)\n .filter(([, value]) => value !== undefined && value !== null && value !== \"\")\n .map(([key, value]) => ` ${escapeHtml(key)}=\"${escapeHtml(String(value))}\"`)\n .join(\"\")\n : \"\";\n\n const headScriptsHtml = headScripts\n ? headScripts\n .filter((script) => typeof script === \"string\" && script.trim().length > 0)\n .map((script) => {\n // If the script is already a complete <script> tag (e.g. JSON-LD),\n // render it as-is without wrapping.\n if (script.trimStart().startsWith(\"<script\")) {\n return `\\n ${script}`;\n }\n return `\\n <script>${script.replace(/<\\/script>/gi, \"<\\\\/script>\")}</script>`;\n })\n .join(\"\")\n : \"\";\n\n const headTags = metadata ? buildHeadTags(metadata, title) : \"\";\n const titleTag = metadata?.title\n ? \"\" // already emitted by buildHeadTags\n : `\\n <title>${escapeHtml(title)}</title>`;\n\n const headLinksHtml = headLinks\n ? headLinks\n .filter((link) => typeof link === \"string\" && link.trim().length > 0)\n .map((link) => `\\n ${link}`)\n .join(\"\")\n : \"\";\n\n const renderEndpointMeta =\n opts.renderEndpoint === false\n ? '\\n <meta name=\"elur:render-endpoint\" content=\"off\" />'\n : \"\";\n\n return `<!DOCTYPE html>\n<html lang=\"${escapeHtml(lang)}\"${htmlAttrs}>\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />${renderEndpointMeta}${titleTag}${headTags}${headLinksHtml}${headScriptsHtml}\n </head>\n <body>\n <div id=\"app\">${body}</div>${dataScript}${actionsScript}${entryScript}\n </body>\n</html>\n`;\n}\n","// --- Ephemeral action error store ---\n//\n// Action failures submitted via plain HTML forms (progressive enhancement)\n// need to be relayed back to the page so the user sees validation errors.\n//\n// Previously the failure data was serialized into a `?__elur_js_action_error=`\n// query param on the redirect. That leaks errors into browser history,\n// server logs and third-party Referer headers.\n//\n// Now we stash the failure in a short-lived in-memory store keyed by a random\n// id, set a small cookie `__elur_js_action_error=<id>` (Max-Age=15s, SameSite=Lax),\n// and the next render reads the cookie, fetches the payload, exposes it as\n// `props.form`, and clears the entry.\n//\n// The store is process-local, which is fine for the single-process SSR server\n// and the dev server. For multi-instance deployments the cookie carries the\n// payload directly when it fits (see `encodeActionErrorCookie`); the store is\n// only the overflow path for large payloads.\n\nimport { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\n\nconst COOKIE_NAME = \"__elur_js_action_error\";\nconst MAX_COOKIE_SIZE = 3500; // bytes; leaves headroom under the 4KB cookie limit\nconst TTL_MS = 15_000;\n\n// HMAC key for signing action error cookies. In production this should be\n// set via ELUR_JS_ACTION_SECRET env var; otherwise we derive a per-process\n// key (sufficient for single-process dev/preview, but NOT for multi-instance).\nconst ACTION_SECRET =\n process.env.ELUR_JS_ACTION_SECRET ?? randomBytes(32).toString(\"hex\");\n\ninterface StoredError {\n data: unknown;\n status: number;\n expiresAt: number;\n}\n\nconst store = new Map<string, StoredError>();\n\n// Periodically purge expired entries so the map does not grow unbounded.\nlet sweepScheduled = false;\nfunction scheduleSweep(): void {\n if (sweepScheduled) return;\n sweepScheduled = true;\n setTimeout(() => {\n sweepScheduled = false;\n const now = Date.now();\n for (const [key, entry] of store) {\n if (entry.expiresAt <= now) store.delete(key);\n }\n }, TTL_MS).unref?.();\n}\n\n/**\n * Signs a payload with HMAC-SHA256 using the action secret.\n * Returns `signature.payload` (both hex/base64url).\n */\nfunction sign(payload: string): string {\n const sig = createHmac(\"sha256\", ACTION_SECRET).update(payload).digest(\"hex\");\n return `${sig}.${payload}`;\n}\n\n/**\n * Verifies a signed value and returns the payload if valid, or undefined.\n * Uses timingSafeEqual to prevent timing attacks.\n */\nfunction verify(value: string): string | undefined {\n const dotIndex = value.indexOf(\".\");\n if (dotIndex === -1) return undefined;\n const sig = value.slice(0, dotIndex);\n const payload = value.slice(dotIndex + 1);\n const expectedSig = createHmac(\"sha256\", ACTION_SECRET).update(payload).digest(\"hex\");\n if (sig.length !== expectedSig.length) return undefined;\n try {\n if (timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) {\n return payload;\n }\n } catch {\n // Length mismatch — invalid.\n }\n return undefined;\n}\n\n/**\n * Encodes an action failure for the redirect cookie. When the payload fits\n * inside the cookie limit, it is embedded directly as a signed base64url JSON\n * value. When it is too large, it is stored in memory and only a short signed\n * id is written to the cookie.\n *\n * The cookie is signed with HMAC-SHA256 to prevent forgery (A-20).\n *\n * @returns The cookie value to set on the redirect response.\n */\nexport function encodeActionErrorCookie(\n data: unknown,\n status: number,\n): { value: string; storeId?: string } {\n const payload = JSON.stringify({ d: data, s: status });\n const encoded = Buffer.from(payload, \"utf8\").toString(\"base64url\");\n const signed = sign(encoded);\n if (signed.length <= MAX_COOKIE_SIZE) {\n return { value: signed };\n }\n\n // Overflow: stash in memory and reference by signed id.\n const id = randomBytes(12).toString(\"hex\");\n store.set(id, { data, status, expiresAt: Date.now() + TTL_MS });\n scheduleSweep();\n return { value: sign(`id:${id}`), storeId: id };\n}\n\n/**\n * Decodes a cookie value (previously produced by `encodeActionErrorCookie`)\n * into the failure payload. Verifies the HMAC signature first, then resolves\n * in-memory overflow entries and deletes them after reading.\n */\nexport function decodeActionErrorCookie(value: string | undefined | null):\n | { data: unknown; status: number }\n | undefined {\n if (!value) return undefined;\n\n // Verify signature first.\n const verifiedPayload = verify(value);\n if (verifiedPayload === undefined) return undefined;\n\n // Check if it's an in-memory store reference.\n if (verifiedPayload.startsWith(\"id:\")) {\n const id = verifiedPayload.slice(3);\n const entry = store.get(id);\n if (!entry) return undefined;\n store.delete(id);\n if (entry.expiresAt <= Date.now()) return undefined;\n return { data: entry.data, status: entry.status };\n }\n\n try {\n const json = Buffer.from(verifiedPayload, \"base64url\").toString(\"utf8\");\n const parsed = JSON.parse(json) as { d: unknown; s: number };\n return { data: parsed.d, status: parsed.s };\n } catch {\n return undefined;\n }\n}\n\n/** Name of the cookie used to relay action errors. */\nexport const ACTION_ERROR_COOKIE = COOKIE_NAME;\n\n/** Builds the Set-Cookie header value that clears the error cookie. */\nexport function clearActionErrorCookieHeader(): string {\n return `${COOKIE_NAME}=; Path=/; Max-Age=0; SameSite=Lax`;\n}\n\n/** Builds the Set-Cookie header value that sets the error cookie. */\nexport function setActionErrorCookieHeader(value: string): string {\n return `${COOKIE_NAME}=${value}; Path=/; Max-Age=15; SameSite=Lax; HttpOnly`;\n}\n","// --- Cache policy per route (runtime-security §9.1) ---\n//\n// Authors can declare a cache policy in their page.data.ts:\n//\n// export const cache = {\n// mode: \"public\", // \"public\" | \"private\" | \"dynamic\"\n// revalidate: 60, // seconds\n// tags: [\"products\"], // for tag-based invalidation\n// };\n//\n// Default policy: \"dynamic\" (no public ISR caching).\n// Requests with Cookie/Authorization are never cached publicly.\n// Responses with Set-Cookie/private/no-store are never cached publicly.\n\n/** Cache mode for a route. */\nexport type CacheMode = \"public\" | \"private\" | \"dynamic\";\n\n/** Cache policy declared by the route's data module. */\nexport interface CachePolicy {\n mode: CacheMode;\n revalidate: number;\n tags?: string[];\n}\n\n/** Default cache policy when none is declared. */\nexport const DEFAULT_CACHE_POLICY: CachePolicy = {\n mode: \"dynamic\",\n revalidate: 0,\n};\n\n/**\n * Normalizes a raw cache export from a data module into a CachePolicy.\n * Returns the default policy if the input is invalid or missing.\n */\nexport function normalizeCachePolicy(raw: unknown): CachePolicy {\n if (!raw || typeof raw !== \"object\") return DEFAULT_CACHE_POLICY;\n const obj = raw as Record<string, unknown>;\n const mode = obj.mode;\n if (mode !== \"public\" && mode !== \"private\" && mode !== \"dynamic\") {\n return DEFAULT_CACHE_POLICY;\n }\n const revalidate = typeof obj.revalidate === \"number\" ? obj.revalidate : 0;\n const tags = Array.isArray(obj.tags) ? obj.tags.filter((t) => typeof t === \"string\") : undefined;\n return { mode, revalidate, tags };\n}\n\n/**\n * Determines whether a route's cache policy allows public caching for the\n * given request.\n *\n * Per §9.1:\n * - \"dynamic\" → never cache\n * - \"private\" → never cache publicly (requires private adapter)\n * - \"public\" → cache only if request has no Cookie/Authorization\n */\nexport function shouldCachePublic(\n policy: CachePolicy,\n request: Request,\n): boolean {\n if (policy.mode !== \"public\") return false;\n if (policy.revalidate <= 0) return false;\n if (request.headers.get(\"Cookie\")) return false;\n if (request.headers.get(\"Authorization\")) return false;\n return true;\n}\n","import type { ElurTemplate } from \"@elurjs/core\";\nimport { renderToString } from \"../render/render-to-string.js\";\nimport { documentShell, buildHeadTags } from \"../build/document-shell.js\";\nimport type { PageRoute, ScannedRoutes } from \"../router/route-scanner.js\";\nimport type { BuildConfig } from \"../build/build.js\";\nimport type { PageDataLoad, PageProps, RouteParams, PageMetadata, GenerateMetadata } from \"../types.js\";\nimport { existsSync } from \"node:fs\";\nimport { decodeActionErrorCookie, ACTION_ERROR_COOKIE } from \"../action/error-store.js\";\nimport { normalizeCachePolicy, type CachePolicy } from \"../cache/policy.js\";\n\nexport interface RenderPageOptions {\n route: PageRoute;\n params?: RouteParams;\n searchParams?: URLSearchParams;\n config: Pick<BuildConfig, \"lang\" | \"clientEntry\" | \"renderEndpoint\">;\n /** Custom module loader. Defaults to native dynamic import. */\n importer?: (path: string) => Promise<unknown>;\n /** Per-page action names exposed in the HTML shell. */\n actions?: Record<string, string[]>;\n /** Current request, used to hydrate data loaders that need cookies/headers. */\n request?: Request;\n}\n\nexport interface RenderPageResult {\n html: string;\n revalidate?: number;\n /**\n * `Set-Cookie` header value that clears the action error cookie, when the\n * page consumed a relayed action failure. The SSR server should append it to\n * the outgoing response so the cookie does not persist.\n */\n clearActionErrorCookie?: string;\n /** `<head>` tags (title, meta, OG, twitter) for the SPA router to merge. */\n head?: string;\n /** Resolved page title (from metadata or fallback). */\n resolvedTitle?: string;\n /**\n * When a loader or layout throws a `Response` (e.g. `throw new Response(...,\n * { status: 404 })`), it is captured here as a first-class response instead\n * of being treated as an internal error (A-22).\n */\n response?: Response;\n /** HTTP status code for the rendered page (e.g. 404 for not-found pages). */\n status?: number;\n /** Cache policy declared by the route (§9.1). */\n cachePolicy?: CachePolicy;\n}\n\nconst defaultImport = (path: string) => import(path);\n\n/**\n * Collects `<html>` attributes and head scripts declared by data loaders\n * (page and layouts) via top-level `htmlAttributes` / `headScripts` fields.\n */\nexport function collectShellExtras(\n pageData: unknown,\n layoutDataList: unknown[],\n): { htmlAttributes: Record<string, string>; headScripts: string[]; headLinks: string[] } {\n const htmlAttributes: Record<string, string> = {};\n const headScripts: string[] = [];\n const headLinks: string[] = [];\n const merge = (value: unknown) => {\n if (!value || typeof value !== \"object\") return;\n const attrs = (value as { htmlAttributes?: Record<string, string> }).htmlAttributes;\n if (attrs) Object.assign(htmlAttributes, attrs);\n const scripts = (value as { headScripts?: string[] }).headScripts;\n if (Array.isArray(scripts)) headScripts.push(...scripts);\n const links = (value as { headLinks?: string[] }).headLinks;\n if (Array.isArray(links)) headLinks.push(...links);\n };\n for (const layoutData of layoutDataList) merge(layoutData);\n merge(pageData);\n // Deduplicate headScripts and headLinks (e.g. from both layout and page data)\n const uniqueScripts = [...new Set(headScripts)];\n const uniqueLinks = [...new Set(headLinks)];\n return { htmlAttributes, headScripts: uniqueScripts, headLinks: uniqueLinks };\n}\n\nexport async function renderPage(options: RenderPageOptions): Promise<RenderPageResult> {\n const { route, params = {}, searchParams = new URLSearchParams(), config, importer = defaultImport, actions, request } = options;\n\n const pageModule = await importer(route.pagePath) as {\n default: (props: PageProps<unknown>) => ElurTemplate;\n generateMetadata?: GenerateMetadata;\n };\n const { default: PageComponent, generateMetadata } = pageModule;\n\n let data: unknown;\n let revalidate: number | undefined;\n let cachePolicy: import(\"../cache/policy.js\").CachePolicy | undefined;\n // Use a mutable container so TypeScript doesn't narrow the type after\n // the first `if (thrownResponse)` check.\n const thrown: { response: Response | undefined } = { response: undefined };\n if (route.dataPath) {\n const mod = await importer(route.dataPath) as {\n load?: PageDataLoad;\n revalidate?: number;\n cache?: unknown;\n };\n if (mod.load) {\n try {\n data = await mod.load({ params, searchParams, request });\n } catch (err) {\n if (err instanceof Response) {\n thrown.response = err;\n } else {\n throw err;\n }\n }\n }\n if (typeof mod.revalidate === \"number\") {\n revalidate = mod.revalidate;\n }\n // Read cache policy from the data module (§9.1).\n if (mod.cache) {\n cachePolicy = normalizeCachePolicy(mod.cache);\n if (cachePolicy.revalidate > 0) {\n revalidate = cachePolicy.revalidate;\n }\n }\n }\n\n // If a loader threw a Response (redirect, 404, etc.), return it as a\n // first-class response instead of rendering the page (A-22).\n if (thrown.response) {\n return { html: \"\", response: thrown.response, status: thrown.response.status };\n }\n\n // Relay an action failure previously stored in the ephemeral cookie so the\n // page can render validation errors via `props.form`. The cookie is cleared\n // on the outgoing response (see `clearActionErrorCookie` in the result).\n let form: unknown;\n let clearActionErrorCookie: string | undefined;\n if (request) {\n const cookieHeader = request.headers.get(\"Cookie\") ?? \"\";\n const match = cookieHeader.match(new RegExp(`(?:^|;\\\\s*)${ACTION_ERROR_COOKIE}=([^;]+)`));\n if (match) {\n const decoded = decodeActionErrorCookie(match[1]);\n if (decoded) {\n form = { __elur_js_action_error: true, status: decoded.status, data: decoded.data };\n clearActionErrorCookie = `${ACTION_ERROR_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax`;\n }\n }\n }\n\n const props: PageProps<unknown> = {\n data: data ?? {},\n params,\n searchParams,\n form,\n };\n\n const layoutModules = await Promise.all(\n route.layouts.map(async (layoutPath) => importer(layoutPath)),\n );\n const layoutDataList = await Promise.all(\n route.layouts.map(async (layoutPath) => {\n const dataPath = layoutPath.replace(/layout\\.ts$/, \"layout.data.ts\");\n if (!existsSync(dataPath)) return undefined;\n const mod = (await importer(dataPath)) as { load?: PageDataLoad };\n if (mod.load) {\n try {\n return await mod.load({ params, searchParams, request });\n } catch (err) {\n if (err instanceof Response) {\n thrown.response = err;\n return undefined;\n }\n throw err;\n }\n }\n return undefined;\n }),\n );\n\n // If a layout loader threw a Response, return it as first-class (A-22).\n const layoutThrown = thrown.response as Response | undefined;\n if (layoutThrown) {\n return { html: \"\", response: layoutThrown, status: layoutThrown.status };\n }\n\n // Load slot modules if the route has them (v2.1 — Fix #2: Layout Slots).\n let slotTemplates: Record<string, ElurTemplate> | undefined;\n if (route.slots) {\n slotTemplates = {};\n for (const [slotName, slotPath] of Object.entries(route.slots)) {\n const slotMod = await importer(slotPath) as { default: (props: PageProps<unknown>) => ElurTemplate };\n slotTemplates[slotName] = slotMod.default(props);\n }\n }\n\n const body = await renderToString(() => {\n let template = PageComponent(props);\n for (let i = layoutModules.length - 1; i >= 0; i--) {\n const { default: Layout } = layoutModules[i] as {\n default: (props: { children: ElurTemplate; data?: unknown; slots?: Record<string, ElurTemplate> }) => ElurTemplate;\n };\n template = Layout({ children: template, data: layoutDataList[i], slots: slotTemplates });\n }\n return template;\n });\n\n const title = typeof data === \"object\" && data && \"title\" in data\n ? String((data as { title?: unknown }).title ?? \"Elur Kit\")\n : \"Elur Kit\";\n\n const { htmlAttributes, headScripts, headLinks } = collectShellExtras(data, layoutDataList);\n\n // Resolve page metadata. Priority: `generateMetadata` from page.ts > `metadata`\n // field in the page loader data > `metadata` field in layout loader data.\n let metadata: PageMetadata | undefined;\n if (typeof generateMetadata === \"function\") {\n metadata = await generateMetadata({ params, searchParams, request, data });\n }\n if (!metadata) {\n metadata = extractMetadata(data) ?? extractMetadataFromList(layoutDataList);\n }\n // The title from metadata takes precedence over the data.title fallback.\n const resolvedTitle = metadata?.title ?? title;\n\n const html = documentShell({\n title: resolvedTitle,\n lang: config.lang,\n body,\n data,\n actions,\n htmlAttributes,\n headScripts,\n headLinks,\n metadata,\n clientEntry: config.clientEntry,\n renderEndpoint: config.renderEndpoint,\n });\n\n const head = metadata ? buildHeadTags(metadata, resolvedTitle) : \"\";\n return { html, revalidate, clearActionErrorCookie, head, resolvedTitle, cachePolicy };\n}\n\n/** Extracts a `metadata` field from a loader data object, if present. */\nfunction extractMetadata(value: unknown): PageMetadata | undefined {\n if (value && typeof value === \"object\" && \"metadata\" in value) {\n const meta = (value as { metadata?: unknown }).metadata;\n if (meta && typeof meta === \"object\") return meta as PageMetadata;\n }\n return undefined;\n}\n\n/** Extracts metadata from the first layout data object that has one. */\nfunction extractMetadataFromList(list: unknown[]): PageMetadata | undefined {\n for (const item of list) {\n const meta = extractMetadata(item);\n if (meta) return meta;\n }\n return undefined;\n}\n\nexport interface RenderErrorPageOptions {\n routes: ScannedRoutes;\n status: 404 | 500;\n error?: unknown;\n config: Pick<BuildConfig, \"lang\" | \"clientEntry\" | \"renderEndpoint\">;\n actions?: Record<string, string[]>;\n importer?: (path: string) => Promise<unknown>;\n}\n\nexport async function renderErrorPage(\n options: RenderErrorPageOptions,\n): Promise<{ html: string; status: number } | undefined> {\n const route = options.status === 404 ? options.routes.error404 : options.routes.error500;\n if (!route) return undefined;\n\n try {\n const { html } = await renderPage({\n route,\n params: {},\n searchParams: new URLSearchParams(),\n config: options.config,\n actions: options.actions,\n importer: options.importer,\n });\n return { html, status: options.status };\n } catch (err) {\n console.error(`[render] error ${options.status} page failed`, err);\n return undefined;\n }\n}\n","import type { ApiRoute, PageRoute } from \"../router/route-scanner.js\";\n\nexport interface MatchResult {\n route: PageRoute;\n params: Record<string, string | string[]>;\n searchParams: URLSearchParams;\n}\n\n/**\n * Match a request pathname against a list of page routes.\n *\n * Routes are sorted by specificity (static > dynamic > catch-all) before\n * matching, so `/about` wins over `/:slug` even if the catch-all appears first.\n *\n * URL segments are safely decoded (plan §11.1, runtime-security §10).\n */\nexport function matchRoute(\n pathname: string,\n routes: PageRoute[],\n): MatchResult | undefined {\n const cleanPath = pathname.split(\"?\")[0];\n const requestSegments = cleanPath.split(\"/\").filter(Boolean).map(safeDecodeURIComponent);\n\n const sorted = [...routes].sort((a, b) => specificity(b.path) - specificity(a.path));\n\n for (const route of sorted) {\n const routeSegments = route.path.split(\"/\").filter(Boolean);\n const match = tryMatch(requestSegments, routeSegments, route.optionalCatchAll);\n if (match) {\n return { route, params: match, searchParams: new URLSearchParams() };\n }\n }\n\n return undefined;\n}\n\nexport interface ApiMatchResult<T = ApiRoute> {\n route: T;\n params: Record<string, string | string[]>;\n}\n\n/**\n * Match a request pathname against a list of API routes.\n */\nexport function matchApiRoute<T extends { path: string }>(pathname: string, routes: T[]): ApiMatchResult<T> | undefined {\n const cleanPath = pathname.split(\"?\")[0];\n const requestSegments = cleanPath.split(\"/\").filter(Boolean).map(safeDecodeURIComponent);\n\n const sorted = [...routes].sort((a, b) => specificity(b.path) - specificity(a.path));\n\n for (const route of sorted) {\n const routeSegments = route.path.split(\"/\").filter(Boolean);\n const match = tryMatch(requestSegments, routeSegments);\n if (match) {\n return { route, params: match };\n }\n }\n\n return undefined;\n}\n\n/**\n * Safely decodes a URI component. If decoding fails (malformed % sequences),\n * returns the original string rather than throwing (runtime-security §10).\n */\nfunction safeDecodeURIComponent(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n return segment;\n }\n}\n\nfunction specificity(path: string): number {\n return path.split(\"/\").filter(Boolean).reduce((score, segment) => {\n if (segment.endsWith(\"*\")) return score;\n if (segment.startsWith(\":\")) return score + 1;\n return score + 2;\n }, 0);\n}\n\nfunction tryMatch(\n requestSegments: string[],\n routeSegments: string[],\n optionalCatchAll = false,\n): Record<string, string | string[]> | undefined {\n const params: Record<string, string | string[]> = {};\n\n let i = 0;\n for (let r = 0; r < routeSegments.length; r++) {\n const routeSeg = routeSegments[r];\n\n if (routeSeg.endsWith(\"*\")) {\n // Catch-all consumes the rest of the request segments.\n const name = routeSeg.slice(1, -1);\n const rest = requestSegments.slice(i);\n // For optional catch-all, empty rest is OK.\n if (rest.length === 0 && !optionalCatchAll) return undefined;\n params[name] = rest.length > 0 ? rest : [];\n return params;\n }\n\n if (routeSeg.startsWith(\":\")) {\n const requestSeg = requestSegments[i];\n if (requestSeg === undefined) return undefined;\n params[routeSeg.slice(1)] = requestSeg;\n i++;\n continue;\n }\n\n if (routeSeg !== requestSegments[i]) {\n return undefined;\n }\n i++;\n }\n\n if (i !== requestSegments.length) return undefined;\n return params;\n}\n","// --- Origin verification (CSRF protection for server actions) ---\n//\n// Server actions accept POST requests from the browser. Without origin\n// verification, any third-party site could submit forged requests to\n// `/__elur-js/actions` on behalf of a logged-in user (CSRF).\n//\n// Strategy: compare the request's `Origin` (or `Referer` fallback) host against\n// the target `Host` header. Same-origin requests pass; cross-origin requests\n// are rejected with 403 unless the origin is explicitly allow-listed.\n//\n// Requests without `Origin` AND without `Referer` (e.g. curl, server-to-server)\n// are accepted by default for DX, unless `strictOrigin: true` is configured.\n\nexport interface OriginCheckOptions {\n /** Extra origins allowed to call actions (e.g. preview deployments). */\n allowedOrigins?: string[];\n /**\n * When true, requests missing both `Origin` and `Referer` are rejected.\n * Defaults to false so curl/server-to-server calls keep working.\n */\n strictOrigin?: boolean;\n}\n\n/**\n * Returns the host:port of a URL string, or undefined if it cannot be parsed.\n */\nfunction originOf(urlString: string | null | undefined): string | undefined {\n if (!urlString) return undefined;\n try {\n const url = new URL(urlString);\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return undefined;\n return url.origin;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Verifies that a request originates from the same host (or an allow-listed\n * origin). Returns an error message when the request must be rejected, or\n * undefined when it is allowed.\n *\n * @param request The incoming Request to actions.\n * @param options Origin check configuration.\n */\nexport function verifyOrigin(\n request: Request,\n options: OriginCheckOptions = {},\n): string | undefined {\n const targetOrigin = originOf(request.url);\n if (!targetOrigin) return \"Invalid target URL\";\n\n const origin = request.headers.get(\"Origin\");\n const referer = request.headers.get(\"Referer\");\n if (!origin && !referer) {\n return options.strictOrigin\n ? \"Missing Origin and Referer headers\"\n : undefined;\n }\n\n const sourceOrigin = origin ? originOf(origin) : originOf(referer);\n if (!sourceOrigin) return origin ? \"Invalid Origin header\" : \"Invalid Referer header\";\n if (sourceOrigin === targetOrigin) return undefined;\n\n if (options.allowedOrigins?.some((allowed) => originOf(allowed) === sourceOrigin)) return undefined;\n\n return `Cross-origin request blocked: source \"${sourceOrigin}\" != target \"${targetOrigin}\"`;\n}\n\n/** Builds a 403 Response for a rejected origin. */\nexport function originForbidden(message: string): Response {\n return new Response(message, {\n status: 403,\n headers: { \"Content-Type\": \"text/plain; charset=utf-8\" },\n });\n}\n","import type { ActionRequest } from \"./index.js\";\nimport { isActionFailure, isRedirectResponse, publicErrorResponse } from \"../errors.js\";\nimport { verifyOrigin, originForbidden, type OriginCheckOptions } from \"./origin.js\";\nimport {\n encodeActionErrorCookie,\n setActionErrorCookieHeader,\n} from \"./error-store.js\";\n\n/**\n * Resolves a server action by name and optional page scope.\n */\nexport type ActionResolver = (\n name: string,\n page?: string,\n) => Promise<((...args: unknown[]) => unknown) | undefined>;\n\n/** Options shared by `handleActionRequest` callers for CSRF protection. */\nexport interface ActionSecurityOptions extends OriginCheckOptions {\n /** Maximum body size in bytes. Defaults to 1MB (1_048_576). */\n bodyLimit?: number;\n}\n\n/** Default body size limit: 1MB. */\nconst DEFAULT_BODY_LIMIT = 1_048_576;\n\n/**\n * Reads the request body as text, enforcing a maximum size.\n * Returns a 413 response if the body exceeds the limit.\n */\nasync function readBodyWithLimit(\n request: Request,\n limit: number,\n): Promise<{ ok: true; text: string } | { ok: false; response: Response }> {\n const contentLength = request.headers.get(\"Content-Length\");\n if (contentLength && parseInt(contentLength, 10) > limit) {\n return {\n ok: false,\n response: new Response(\"Request body too large\", {\n status: 413,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n // Read the body as a stream with a size cap to prevent memory exhaustion\n // from chunked transfer encoding without Content-Length.\n const reader = request.body?.getReader();\n if (!reader) {\n return { ok: true, text: \"\" };\n }\n const chunks: Uint8Array[] = [];\n let totalSize = 0;\n try {\n for (; ;) {\n const { done, value } = await reader.read();\n if (done) break;\n totalSize += value.byteLength;\n if (totalSize > limit) {\n try { reader.cancel(); } catch { /* ignore */ }\n return {\n ok: false,\n response: new Response(\"Request body too large\", {\n status: 413,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n chunks.push(value);\n }\n } finally {\n try { reader.releaseLock(); } catch { /* ignore */ }\n }\n const total = new Uint8Array(totalSize);\n let offset = 0;\n for (const chunk of chunks) {\n total.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return { ok: true, text: new TextDecoder().decode(total) };\n}\n\nfunction parseFormBody(body: string): Record<string, unknown> {\n const params = new URLSearchParams(body);\n const result: Record<string, unknown> = {};\n for (const [key, value] of params) {\n if (result[key] === undefined) {\n result[key] = value;\n } else if (Array.isArray(result[key])) {\n (result[key] as unknown[]).push(value);\n } else {\n result[key] = [result[key], value];\n }\n }\n return result;\n}\n\nasync function parseActionRequest(\n request: Request,\n bodyLimit: number = DEFAULT_BODY_LIMIT,\n): Promise<\n | { ok: true; name: string; page?: string; args: unknown[]; wantsJson: boolean }\n | { ok: false; response: Response }\n> {\n if (request.method !== \"POST\") {\n return {\n ok: false,\n response: new Response(\"Method not allowed\", {\n status: 405,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n\n const contentType = request.headers.get(\"Content-Type\") ?? \"\";\n const wantsJson = (request.headers.get(\"Accept\") ?? \"\").includes(\"application/json\");\n\n let name: string | undefined;\n let page: string | undefined;\n let args: unknown[] = [];\n\n if (contentType.includes(\"application/json\")) {\n const bodyResult = await readBodyWithLimit(request, bodyLimit);\n if (!bodyResult.ok) return { ok: false, response: bodyResult.response };\n let body: ActionRequest;\n try {\n body = JSON.parse(bodyResult.text) as ActionRequest;\n } catch {\n return {\n ok: false,\n response: new Response(\"Invalid JSON body\", {\n status: 400,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n name = body.name;\n page = body.page;\n args = Array.isArray(body.args) ? body.args : [];\n } else if (\n contentType.includes(\"application/x-www-form-urlencoded\") ||\n contentType.includes(\"multipart/form-data\")\n ) {\n // For multipart, use the native formData() parser after checking\n // Content-Length against the limit. For urlencoded, use our size-capped\n // reader to handle chunked encoding without Content-Length.\n if (contentType.includes(\"multipart/form-data\")) {\n const contentLength = request.headers.get(\"Content-Length\");\n if (contentLength && parseInt(contentLength, 10) > bodyLimit) {\n return {\n ok: false,\n response: new Response(\"Request body too large\", {\n status: 413,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n let form: FormData;\n try {\n form = await request.formData();\n } catch {\n return {\n ok: false,\n response: new Response(\"Invalid form body\", {\n status: 400,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n name = form.get(\"__elur_js_action_name\") as string | null ?? undefined;\n page = form.get(\"__elur_js_action_page\") as string | null ?? undefined;\n const input: Record<string, unknown> = {};\n for (const [key, value] of form) {\n if (key === \"__elur_js_action_name\" || key === \"__elur_js_action_page\") continue;\n input[key] = value;\n }\n args = [input];\n } else {\n const bodyResult = await readBodyWithLimit(request, bodyLimit);\n if (!bodyResult.ok) return { ok: false, response: bodyResult.response };\n const form = parseFormBody(bodyResult.text);\n name = form.__elur_js_action_name as string | undefined;\n page = form.__elur_js_action_page as string | undefined;\n const input: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(form)) {\n if (key === \"__elur_js_action_name\" || key === \"__elur_js_action_page\") continue;\n input[key] = value;\n }\n args = [input];\n }\n } else {\n // Try to parse a plain form body as a fallback for progressive enhancement.\n const bodyResult = await readBodyWithLimit(request, bodyLimit);\n if (!bodyResult.ok) return { ok: false, response: bodyResult.response };\n const form = parseFormBody(bodyResult.text);\n name = form.__elur_js_action_name as string | undefined;\n page = form.__elur_js_action_page as string | undefined;\n const input: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(form)) {\n if (key === \"__elur_js_action_name\" || key === \"__elur_js_action_page\") continue;\n input[key] = value;\n }\n args = [input];\n }\n\n if (!name || typeof name !== \"string\") {\n return {\n ok: false,\n response: new Response(\"Missing action name\", {\n status: 400,\n headers: { \"Content-Type\": \"text/plain\" },\n }),\n };\n }\n\n return { ok: true, name, page, args, wantsJson };\n}\n\n/**\n * Handles a POST request to the server action endpoint.\n *\n * Accepts both JSON requests (`{ name, page?, args }`) and HTML form submissions\n * for progressive enhancement. The provided resolver looks up the action\n * implementation, invokes it with the supplied arguments and returns the result\n * as JSON or redirects back to the request origin for form submissions.\n *\n * Origin verification (CSRF protection) runs before parsing the body: any\n * cross-origin POST is rejected with 403 unless its origin is allow-listed via\n * `security.allowedOrigins`.\n *\n * For progressive-enhancement form submissions that fail, the failure payload\n * is relayed back via a short-lived `__elur_js_action_error` cookie (SameSite=Lax,\n * Max-Age=15s) instead of a query param, so errors do not leak into browser\n * history, server logs or third-party Referer headers.\n */\nexport async function handleActionRequest(\n request: Request,\n resolveAction: ActionResolver,\n security: ActionSecurityOptions = {},\n): Promise<Response> {\n // CSRF: verify same-origin (or allow-listed) before doing any work.\n const originError = verifyOrigin(request, security);\n if (originError) return originForbidden(originError);\n\n const parsed = await parseActionRequest(request, security.bodyLimit ?? DEFAULT_BODY_LIMIT);\n if (!parsed.ok) return parsed.response;\n\n const { name, page, args, wantsJson } = parsed;\n\n try {\n const action = await resolveAction(name, page);\n if (!action) {\n const message = page ? `Action not found: ${name} (page: ${page})` : `Action not found: ${name}`;\n return new Response(message, {\n status: 404,\n headers: { \"Content-Type\": \"text/plain\" },\n });\n }\n\n const result = await action(...args);\n\n if (isActionFailure(result)) {\n if (wantsJson) {\n return new Response(JSON.stringify({ __elur_js_action_failure: true, status: result.status, data: result.data }), {\n status: result.status,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n // Progressive enhancement: redirect back with the failure in a cookie.\n const referer = request.headers.get(\"Referer\") ?? \"/\";\n const url = new URL(referer, \"http://localhost\");\n const { value } = encodeActionErrorCookie(result.data, result.status);\n return new Response(null, {\n status: 303,\n headers: {\n Location: url.pathname + url.search,\n \"Content-Type\": \"text/plain\",\n \"Set-Cookie\": setActionErrorCookieHeader(value),\n },\n });\n }\n\n if (isRedirectResponse(result)) {\n if (wantsJson) {\n return new Response(\n JSON.stringify({ __elur_js_action_redirect: true, status: result.status, location: result.location }),\n {\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n },\n );\n }\n return new Response(null, {\n status: result.status,\n headers: { Location: result.location, \"Content-Type\": \"text/plain\" },\n });\n }\n\n if (wantsJson) {\n return new Response(JSON.stringify(result ?? null), {\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n // For progressive enhancement (plain form POST), redirect back.\n const referer = request.headers.get(\"Referer\") ?? \"/\";\n return new Response(null, {\n status: 303,\n headers: {\n Location: typeof result === \"string\" ? result : referer,\n \"Content-Type\": \"text/plain\",\n },\n });\n } catch (err) {\n console.error(\"[elur-kit] Action error:\", err);\n return publicErrorResponse(err, { includeDetail: false });\n }\n}\n\nexport { verifyOrigin, originForbidden, type OriginCheckOptions } from \"./origin.js\";\nexport {\n decodeActionErrorCookie,\n clearActionErrorCookieHeader,\n setActionErrorCookieHeader,\n ACTION_ERROR_COOKIE,\n} from \"./error-store.js\";\n","import type { IncomingMessage } from \"node:http\";\n\n// Capture the global AbortController at module load time so it's immune to\n// test frameworks that replace or delete globalThis.AbortController.\nconst GlobalAbortController =\n (globalThis as { AbortController?: typeof AbortController }).AbortController ?? AbortController;\n\nexport function incomingMessageToRequest(req: IncomingMessage, body?: BodyInit | null): Request {\n const headers = new Headers();\n for (let index = 0; index < req.rawHeaders.length; index += 2) {\n headers.append(req.rawHeaders[index], req.rawHeaders[index + 1]);\n }\n\n const controller = new GlobalAbortController();\n req.once(\"aborted\", () => controller.abort());\n req.once(\"close\", () => {\n if (!req.complete) controller.abort();\n });\n\n const protocol = (req.socket as typeof req.socket & { encrypted?: boolean }).encrypted ? \"https\" : \"http\";\n const init: RequestInit = {\n method: req.method ?? \"GET\",\n headers,\n signal: controller.signal,\n };\n if (body !== undefined && body !== null && init.method !== \"GET\" && init.method !== \"HEAD\") init.body = body;\n\n return new Request(`${protocol}://${headers.get(\"host\") ?? \"localhost\"}${req.url ?? \"/\"}`, init);\n}\n"],"mappings":"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"}
|