@elurjs/kit 2.4.9 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +306 -0
- package/README.md +195 -9
- package/dist/lib/action/index.cjs +1 -1
- package/dist/lib/action/index.js +1 -1
- package/dist/lib/{action-DZHihU3Z.cjs → action-BiE4tEty.cjs} +2 -2
- package/dist/lib/{action-DZHihU3Z.cjs.map → action-BiE4tEty.cjs.map} +1 -1
- package/dist/lib/{action-BB6N5yD-.js → action-CXVk8rWt.js} +3 -3
- package/dist/lib/{action-BB6N5yD-.js.map → action-CXVk8rWt.js.map} +1 -1
- package/dist/lib/adapter-DNsjXvq5.js +160 -0
- package/dist/lib/adapter-DNsjXvq5.js.map +1 -0
- package/dist/lib/adapter-Dz2hzR36.cjs +3 -0
- package/dist/lib/adapter-Dz2hzR36.cjs.map +1 -0
- package/dist/lib/adapters/bun.cjs +2 -2
- package/dist/lib/adapters/bun.cjs.map +1 -1
- package/dist/lib/adapters/bun.js +17 -9
- package/dist/lib/adapters/bun.js.map +1 -1
- package/dist/lib/adapters/index.d.cts +34 -0
- package/dist/lib/adapters/index.d.ts +34 -0
- package/dist/lib/adapters/netlify.cjs +1 -1
- package/dist/lib/adapters/netlify.js +7 -7
- package/dist/lib/adapters/node.cjs +5 -5
- package/dist/lib/adapters/node.cjs.map +1 -1
- package/dist/lib/adapters/node.js +20 -12
- package/dist/lib/adapters/node.js.map +1 -1
- package/dist/lib/adapters/vercel.cjs +1 -1
- package/dist/lib/adapters/vercel.js +8 -8
- package/dist/lib/build/build.d.cts +40 -0
- package/dist/lib/build/build.d.ts +40 -0
- package/dist/lib/build/document-shell.d.cts +47 -1
- package/dist/lib/build/document-shell.d.ts +47 -1
- package/dist/lib/build/vite-build.d.cts +25 -2
- package/dist/lib/build/vite-build.d.ts +25 -2
- package/dist/lib/cache/adapter.d.cts +41 -0
- package/dist/lib/cache/adapter.d.ts +41 -0
- package/dist/lib/cache/index.cjs +2 -0
- package/dist/lib/cache/index.cjs.map +1 -0
- package/dist/lib/cache/index.d.cts +5 -0
- package/dist/lib/cache/index.d.ts +5 -0
- package/dist/lib/cache/index.js +113 -0
- package/dist/lib/cache/index.js.map +1 -0
- package/dist/lib/cache/invalidation.d.cts +36 -0
- package/dist/lib/cache/invalidation.d.ts +36 -0
- package/dist/lib/cache/redis-adapter.d.cts +49 -0
- package/dist/lib/cache/redis-adapter.d.ts +49 -0
- package/dist/lib/cache-DGhTYyqh.js +50 -0
- package/dist/lib/cache-DGhTYyqh.js.map +1 -0
- package/dist/lib/cache-X1n4Ossu.cjs +2 -0
- package/dist/lib/cache-X1n4Ossu.cjs.map +1 -0
- package/dist/lib/cli/output.d.cts +58 -0
- package/dist/lib/cli/output.d.ts +58 -0
- package/dist/lib/cli/ports.d.cts +20 -0
- package/dist/lib/cli/ports.d.ts +20 -0
- package/dist/lib/cli.cjs +2568 -1489
- package/dist/lib/cli.cjs.map +1 -1
- package/dist/lib/cli.d.cts +21 -0
- package/dist/lib/cli.d.ts +21 -0
- package/dist/lib/cli.js +2567 -1489
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/config/index.cjs +1 -1
- package/dist/lib/config/index.cjs.map +1 -1
- package/dist/lib/config/index.d.cts +79 -0
- package/dist/lib/config/index.d.ts +79 -0
- package/dist/lib/config/index.js +21 -14
- package/dist/lib/config/index.js.map +1 -1
- package/dist/lib/{errors-ePYxDlhU.js → errors-j4cg9VN3.js} +24 -30
- package/dist/lib/{errors-ePYxDlhU.js.map → errors-j4cg9VN3.js.map} +1 -1
- package/dist/lib/errors-kSl9yLkg.cjs +2 -0
- package/dist/lib/{errors-0X7pCFNU.cjs.map → errors-kSl9yLkg.cjs.map} +1 -1
- package/dist/lib/hydrate-BSLuI0Nh.cjs +2 -0
- package/dist/lib/hydrate-BSLuI0Nh.cjs.map +1 -0
- package/dist/lib/hydrate-BaS7-d8C.js +133 -0
- package/dist/lib/hydrate-BaS7-d8C.js.map +1 -0
- package/dist/lib/idiomorph.esm-BOOTOg1r.cjs +2 -0
- package/dist/lib/idiomorph.esm-BOOTOg1r.cjs.map +1 -0
- package/dist/lib/idiomorph.esm-CXuY-EHB.js +371 -0
- package/dist/lib/idiomorph.esm-CXuY-EHB.js.map +1 -0
- package/dist/lib/index.cjs +1 -1
- package/dist/lib/index.cjs.map +1 -1
- package/dist/lib/index.d.cts +8 -6
- package/dist/lib/index.d.ts +8 -6
- package/dist/lib/index.js +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/interpolation-plugin-C0dTd69H.cjs +58 -0
- package/dist/lib/interpolation-plugin-C0dTd69H.cjs.map +1 -0
- package/dist/lib/{interpolation-plugin-Wgb1j4pT.js → interpolation-plugin-K2qE69tF.js} +95 -126
- package/dist/lib/interpolation-plugin-K2qE69tF.js.map +1 -0
- package/dist/lib/invalidation-CFpNQUv6.cjs +2 -0
- package/dist/lib/invalidation-CFpNQUv6.cjs.map +1 -0
- package/dist/lib/invalidation-DXhdE9wj.js +64 -0
- package/dist/lib/invalidation-DXhdE9wj.js.map +1 -0
- package/dist/lib/island/generate-entry.d.cts +40 -2
- package/dist/lib/island/generate-entry.d.ts +40 -2
- package/dist/lib/island/hydrate.d.cts +19 -6
- package/dist/lib/island/hydrate.d.ts +19 -6
- package/dist/lib/island/index.cjs +1 -1
- package/dist/lib/island/index.d.cts +2 -1
- package/dist/lib/island/index.d.ts +2 -1
- package/dist/lib/island/index.js +1 -1
- package/dist/lib/island/island.d.cts +15 -0
- package/dist/lib/island/island.d.ts +15 -0
- package/dist/lib/island-CTBnsOhI.js +59 -0
- package/dist/lib/island-CTBnsOhI.js.map +1 -0
- package/dist/lib/island-kfQEKRe_.cjs +2 -0
- package/dist/lib/island-kfQEKRe_.cjs.map +1 -0
- package/dist/lib/node-http-DVFcYeBS.js +723 -0
- package/dist/lib/node-http-DVFcYeBS.js.map +1 -0
- package/dist/lib/node-http-Uct7DhmN.cjs +13 -0
- package/dist/lib/node-http-Uct7DhmN.cjs.map +1 -0
- package/dist/lib/rolldown-runtime-BIInC43l.cjs +1 -0
- package/dist/lib/rolldown-runtime-Dy4uBu1J.js +11 -0
- package/dist/lib/router/client.cjs +1 -1
- package/dist/lib/router/client.cjs.map +1 -1
- package/dist/lib/router/client.d.cts +55 -8
- package/dist/lib/router/client.d.ts +55 -8
- package/dist/lib/router/client.js +298 -97
- package/dist/lib/router/client.js.map +1 -1
- package/dist/lib/router/redirects.d.cts +34 -0
- package/dist/lib/router/redirects.d.ts +34 -0
- package/dist/lib/runtime/handler.d.cts +59 -0
- package/dist/lib/runtime/handler.d.ts +59 -0
- package/dist/lib/runtime/index.cjs +1 -1
- package/dist/lib/runtime/index.d.cts +2 -1
- package/dist/lib/runtime/index.d.ts +2 -1
- package/dist/lib/runtime/index.js +4 -4
- package/dist/lib/runtime/logger.d.cts +47 -0
- package/dist/lib/runtime/logger.d.ts +47 -0
- package/dist/lib/runtime/node-http.d.cts +11 -1
- package/dist/lib/runtime/node-http.d.ts +11 -1
- package/dist/lib/runtime-ChE4flX3.cjs +2 -0
- package/dist/lib/runtime-ChE4flX3.cjs.map +1 -0
- package/dist/lib/runtime-OMyz5yE-.js +905 -0
- package/dist/lib/runtime-OMyz5yE-.js.map +1 -0
- package/dist/lib/seo/index.cjs +1 -10
- package/dist/lib/seo/index.d.cts +1 -0
- package/dist/lib/seo/index.d.ts +1 -0
- package/dist/lib/seo/index.js +2 -41
- package/dist/lib/seo/sitemap-from-routes.d.cts +24 -0
- package/dist/lib/seo/sitemap-from-routes.d.ts +24 -0
- package/dist/lib/{shared-CfqOLsKj.cjs → shared-CP0Dc3cp.cjs} +72 -47
- package/dist/lib/shared-CP0Dc3cp.cjs.map +1 -0
- package/dist/lib/{shared-DU3iQgr7.js → shared-D8w-neq2.js} +87 -58
- package/dist/lib/shared-D8w-neq2.js.map +1 -0
- package/dist/lib/sitemap-from-routes-CaBoiGvE.js +77 -0
- package/dist/lib/sitemap-from-routes-CaBoiGvE.js.map +1 -0
- package/dist/lib/sitemap-from-routes-DbcLLL4G.cjs +13 -0
- package/dist/lib/sitemap-from-routes-DbcLLL4G.cjs.map +1 -0
- package/dist/lib/ssr/render.d.cts +8 -2
- package/dist/lib/ssr/render.d.ts +8 -2
- package/dist/lib/ssr/server.d.cts +7 -0
- package/dist/lib/ssr/server.d.ts +7 -0
- package/dist/lib/ssr/stream-response.d.cts +30 -0
- package/dist/lib/ssr/stream-response.d.ts +30 -0
- package/dist/lib/ssr/stream.d.cts +16 -1
- package/dist/lib/ssr/stream.d.ts +16 -1
- package/dist/lib/{stream-DmZjYqHm.cjs → stream-Cg-xYG82.cjs} +3 -3
- package/dist/lib/stream-Cg-xYG82.cjs.map +1 -0
- package/dist/lib/stream-D-ytogdu.js +106 -0
- package/dist/lib/stream-D-ytogdu.js.map +1 -0
- package/dist/lib/vite/index.cjs +1 -1
- package/dist/lib/vite/index.cjs.map +1 -1
- package/dist/lib/vite/index.d.cts +11 -0
- package/dist/lib/vite/index.d.ts +11 -0
- package/dist/lib/vite/index.js +101 -94
- package/dist/lib/vite/index.js.map +1 -1
- package/package.json +26 -9
- package/dist/lib/errors-0X7pCFNU.cjs +0 -2
- package/dist/lib/hydrate-CJbUWdXu.js +0 -126
- package/dist/lib/hydrate-CJbUWdXu.js.map +0 -1
- package/dist/lib/hydrate-D6MR24mz.cjs +0 -2
- package/dist/lib/hydrate-D6MR24mz.cjs.map +0 -1
- package/dist/lib/interpolation-plugin-TWDch8nn.cjs +0 -39
- package/dist/lib/interpolation-plugin-TWDch8nn.cjs.map +0 -1
- package/dist/lib/interpolation-plugin-Wgb1j4pT.js.map +0 -1
- package/dist/lib/node-http-DRAUhO0c.js +0 -617
- package/dist/lib/node-http-DRAUhO0c.js.map +0 -1
- package/dist/lib/node-http-DhxguYyz.cjs +0 -13
- package/dist/lib/node-http-DhxguYyz.cjs.map +0 -1
- package/dist/lib/runtime-CrH5X-Es.cjs +0 -2
- package/dist/lib/runtime-CrH5X-Es.cjs.map +0 -1
- package/dist/lib/runtime-Ds2-wH6P.js +0 -520
- package/dist/lib/runtime-Ds2-wH6P.js.map +0 -1
- package/dist/lib/seo/index.cjs.map +0 -1
- package/dist/lib/seo/index.js.map +0 -1
- package/dist/lib/shared-CfqOLsKj.cjs.map +0 -1
- package/dist/lib/shared-DU3iQgr7.js.map +0 -1
- package/dist/lib/stream-CpOG052V.js +0 -104
- package/dist/lib/stream-CpOG052V.js.map +0 -1
- package/dist/lib/stream-DmZjYqHm.cjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-DZHihU3Z.cjs","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n /**\n * Optional input validator. Can be a Zod schema, a plain function, or any\n * object with a `.parse()` method. If validation fails, the action returns\n * a 400 ActionFailure with the validation error.\n */\n input?: ActionInputValidator<TInput>;\n /** Concurrency mode when the same action is called multiple times. */\n concurrency?: ActionConcurrencyMode;\n /** Whether the action is idempotent (safe to retry). */\n idempotent?: boolean;\n /** Tags to invalidate from the cache after a successful action (§9.4). */\n invalidateTags?: string[];\n /** Paths to invalidate from the cache after a successful action (§9.4). */\n invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n /** The original Web Request. */\n request: Request;\n /** AbortSignal from the request — aborts if the client disconnects. */\n signal: AbortSignal;\n /** Idempotency key from the request header, if present. */\n idempotencyKey?: string;\n /** Route params (for page-scoped actions). */\n params: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware). */\n locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n input: TInput,\n ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n /** Metadata for the action (used by the runtime/manifest). */\n __elurAction: {\n name: string;\n concurrency: ActionConcurrencyMode;\n idempotent: boolean;\n invalidateTags: readonly string[];\n invalidatePaths: readonly string[];\n };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@elurjs/kit/action\";\n *\n * export const submitContact = defineAction({\n * input: { parse: (v) => v as { name: string; email: string } },\n * invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n * if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n * await saveContact(input);\n * return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n options: DefineActionOptions<TInput>,\n handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n const concurrency = options.concurrency ?? \"latest\";\n const idempotent = options.idempotent ?? false;\n const invalidateTags = options.invalidateTags ?? [];\n const invalidatePaths = options.invalidatePaths ?? [];\n\n // Track in-flight calls for concurrency control.\n let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n const queue: Array<() => void> = [];\n let running = 0;\n\n async function run(\n input: TInput,\n ctx: ActionContext,\n ): Promise<TOutput | ActionFailure<TOutput>> {\n // Validate input if a validator is configured.\n if (options.input) {\n try {\n const validated = options.input.parse(input);\n input = validated;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const { fail } = await import(\"../errors.js\");\n return fail(400, { validation: message }) as ActionFailure<TOutput>;\n }\n }\n\n return handler(input, ctx);\n }\n\n const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n if (concurrency === \"latest\") {\n // Cancel any previous in-flight call.\n if (latestController) latestController.abort();\n latestController = new AbortControllerCtor();\n // Combine the request signal with our cancellation signal.\n const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n return run(input, { ...ctx, signal: combinedSignal });\n }\n\n if (concurrency === \"queue\") {\n // Wait for previous calls to finish.\n if (running > 0) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n running++;\n try {\n return await run(input, ctx);\n } finally {\n running--;\n const next = queue.shift();\n if (next) next();\n }\n }\n\n // parallel: just run it.\n return run(input, ctx);\n };\n\n // Attach metadata.\n (fn as DefinedAction<TInput, TOutput>).__elurAction = {\n name: handler.name || \"anonymous\",\n concurrency,\n idempotent,\n invalidateTags,\n invalidatePaths,\n };\n\n return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n const controller = new AbortControllerCtor();\n for (const signal of signals) {\n if (!signal) continue;\n if (signal.aborted) {\n controller.abort();\n break;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `elurJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@elurjs/kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { elurJsAction } from \"@elurjs/kit/action\";\n *\n * const contact = elurJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@elurjs/core\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n __elur_js_action_failure?: boolean;\n status?: number;\n data?: unknown;\n}\n\ninterface RedirectPayload {\n __elur_js_action_redirect?: boolean;\n status?: number;\n location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __elur_js_action_failure: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __elur_js_action_redirect: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n name: string;\n page?: string;\n args: unknown[];\n}\n\nexport interface CallActionOptions {\n /** Page URL path that scopes the action, e.g. `/contact`. */\n page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__elur-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n name: string,\n args: unknown = [],\n options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n const argsArray = Array.isArray(args) ? args : [args];\n const res = await fetch(\"/__elur-js/actions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n // not JSON, treat as plain error\n }\n if (isActionFailurePayload(payload) && payload.status !== undefined) {\n return new ActionFailure(payload.status, payload.data as T);\n }\n throw new Error(`Action \"${name}\" failed: ${text}`);\n }\n\n const payload: unknown = JSON.parse(text);\n if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n return new RedirectResponse(payload.status, payload.location);\n }\n\n return payload as T;\n}\n\nexport interface ElurJsAction<TInput = unknown, TOutput = unknown> {\n /** Submit the action with the given input. */\n submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n /** Signal that is true while the action is running. */\n pending: { value: boolean };\n /** Signal with the last successful result, action failure, redirect, or null. */\n data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n /** Signal with the last error, or null. */\n error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function elurJsAction<TInput = unknown, TOutput = unknown>(\n name: string,\n options: CallActionOptions = {},\n): ElurJsAction<TInput, TOutput> {\n const pending = signal(false);\n const error = signal<Error | null>(null);\n const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n pending.value = true;\n error.value = null;\n try {\n const result = await callAction<TOutput>(name, input, options);\n data.value = result;\n return result;\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err));\n throw err;\n } finally {\n pending.value = false;\n }\n }\n\n return {\n submit,\n pending,\n data,\n error,\n };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n defineAction,\n type DefineActionOptions,\n type DefinedAction,\n type DefinedActionFn,\n type ActionContext,\n type ActionInputValidator,\n type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"action-BiE4tEty.cjs","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n /**\n * Optional input validator. Can be a Zod schema, a plain function, or any\n * object with a `.parse()` method. If validation fails, the action returns\n * a 400 ActionFailure with the validation error.\n */\n input?: ActionInputValidator<TInput>;\n /** Concurrency mode when the same action is called multiple times. */\n concurrency?: ActionConcurrencyMode;\n /** Whether the action is idempotent (safe to retry). */\n idempotent?: boolean;\n /** Tags to invalidate from the cache after a successful action (§9.4). */\n invalidateTags?: string[];\n /** Paths to invalidate from the cache after a successful action (§9.4). */\n invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n /** The original Web Request. */\n request: Request;\n /** AbortSignal from the request — aborts if the client disconnects. */\n signal: AbortSignal;\n /** Idempotency key from the request header, if present. */\n idempotencyKey?: string;\n /** Route params (for page-scoped actions). */\n params: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware). */\n locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n input: TInput,\n ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n /** Metadata for the action (used by the runtime/manifest). */\n __elurAction: {\n name: string;\n concurrency: ActionConcurrencyMode;\n idempotent: boolean;\n invalidateTags: readonly string[];\n invalidatePaths: readonly string[];\n };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@elurjs/kit/action\";\n *\n * export const submitContact = defineAction({\n * input: { parse: (v) => v as { name: string; email: string } },\n * invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n * if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n * await saveContact(input);\n * return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n options: DefineActionOptions<TInput>,\n handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n const concurrency = options.concurrency ?? \"latest\";\n const idempotent = options.idempotent ?? false;\n const invalidateTags = options.invalidateTags ?? [];\n const invalidatePaths = options.invalidatePaths ?? [];\n\n // Track in-flight calls for concurrency control.\n let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n const queue: Array<() => void> = [];\n let running = 0;\n\n async function run(\n input: TInput,\n ctx: ActionContext,\n ): Promise<TOutput | ActionFailure<TOutput>> {\n // Validate input if a validator is configured.\n if (options.input) {\n try {\n const validated = options.input.parse(input);\n input = validated;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const { fail } = await import(\"../errors.js\");\n return fail(400, { validation: message }) as ActionFailure<TOutput>;\n }\n }\n\n return handler(input, ctx);\n }\n\n const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n if (concurrency === \"latest\") {\n // Cancel any previous in-flight call.\n if (latestController) latestController.abort();\n latestController = new AbortControllerCtor();\n // Combine the request signal with our cancellation signal.\n const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n return run(input, { ...ctx, signal: combinedSignal });\n }\n\n if (concurrency === \"queue\") {\n // Wait for previous calls to finish.\n if (running > 0) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n running++;\n try {\n return await run(input, ctx);\n } finally {\n running--;\n const next = queue.shift();\n if (next) next();\n }\n }\n\n // parallel: just run it.\n return run(input, ctx);\n };\n\n // Attach metadata.\n (fn as DefinedAction<TInput, TOutput>).__elurAction = {\n name: handler.name || \"anonymous\",\n concurrency,\n idempotent,\n invalidateTags,\n invalidatePaths,\n };\n\n return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n const controller = new AbortControllerCtor();\n for (const signal of signals) {\n if (!signal) continue;\n if (signal.aborted) {\n controller.abort();\n break;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `elurJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@elurjs/kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { elurJsAction } from \"@elurjs/kit/action\";\n *\n * const contact = elurJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@elurjs/core\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n __elur_js_action_failure?: boolean;\n status?: number;\n data?: unknown;\n}\n\ninterface RedirectPayload {\n __elur_js_action_redirect?: boolean;\n status?: number;\n location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __elur_js_action_failure: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __elur_js_action_redirect: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n name: string;\n page?: string;\n args: unknown[];\n}\n\nexport interface CallActionOptions {\n /** Page URL path that scopes the action, e.g. `/contact`. */\n page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__elur-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n name: string,\n args: unknown = [],\n options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n const argsArray = Array.isArray(args) ? args : [args];\n const res = await fetch(\"/__elur-js/actions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n // not JSON, treat as plain error\n }\n if (isActionFailurePayload(payload) && payload.status !== undefined) {\n return new ActionFailure(payload.status, payload.data as T);\n }\n throw new Error(`Action \"${name}\" failed: ${text}`);\n }\n\n const payload: unknown = JSON.parse(text);\n if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n return new RedirectResponse(payload.status, payload.location);\n }\n\n return payload as T;\n}\n\nexport interface ElurJsAction<TInput = unknown, TOutput = unknown> {\n /** Submit the action with the given input. */\n submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n /** Signal that is true while the action is running. */\n pending: { value: boolean };\n /** Signal with the last successful result, action failure, redirect, or null. */\n data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n /** Signal with the last error, or null. */\n error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function elurJsAction<TInput = unknown, TOutput = unknown>(\n name: string,\n options: CallActionOptions = {},\n): ElurJsAction<TInput, TOutput> {\n const pending = signal(false);\n const error = signal<Error | null>(null);\n const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n pending.value = true;\n error.value = null;\n try {\n const result = await callAction<TOutput>(name, input, options);\n data.value = result;\n return result;\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err));\n throw err;\n } finally {\n pending.value = false;\n }\n }\n\n return {\n submit,\n pending,\n data,\n error,\n };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n defineAction,\n type DefineActionOptions,\n type DefinedAction,\n type DefinedActionFn,\n type ActionContext,\n type ActionInputValidator,\n type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as e, t } from "./errors-
|
|
1
|
+
import { n as e, t } from "./errors-j4cg9VN3.js";
|
|
2
2
|
import { signal as n } from "@elurjs/core";
|
|
3
3
|
//#region src/action/define.ts
|
|
4
4
|
var r = globalThis.AbortController;
|
|
@@ -8,7 +8,7 @@ function i(e, t) {
|
|
|
8
8
|
if (e.input) try {
|
|
9
9
|
n = e.input.parse(n);
|
|
10
10
|
} catch (e) {
|
|
11
|
-
let t = e instanceof Error ? e.message : String(e), { fail: n } = await import("./errors-
|
|
11
|
+
let t = e instanceof Error ? e.message : String(e), { fail: n } = await import("./errors-j4cg9VN3.js").then((e) => e.r);
|
|
12
12
|
return n(400, { validation: t });
|
|
13
13
|
}
|
|
14
14
|
return t(n, r);
|
|
@@ -108,4 +108,4 @@ function l(e, t = {}) {
|
|
|
108
108
|
//#endregion
|
|
109
109
|
export { l as n, i as r, c as t };
|
|
110
110
|
|
|
111
|
-
//# sourceMappingURL=action-
|
|
111
|
+
//# sourceMappingURL=action-CXVk8rWt.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-BB6N5yD-.js","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n /**\n * Optional input validator. Can be a Zod schema, a plain function, or any\n * object with a `.parse()` method. If validation fails, the action returns\n * a 400 ActionFailure with the validation error.\n */\n input?: ActionInputValidator<TInput>;\n /** Concurrency mode when the same action is called multiple times. */\n concurrency?: ActionConcurrencyMode;\n /** Whether the action is idempotent (safe to retry). */\n idempotent?: boolean;\n /** Tags to invalidate from the cache after a successful action (§9.4). */\n invalidateTags?: string[];\n /** Paths to invalidate from the cache after a successful action (§9.4). */\n invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n /** The original Web Request. */\n request: Request;\n /** AbortSignal from the request — aborts if the client disconnects. */\n signal: AbortSignal;\n /** Idempotency key from the request header, if present. */\n idempotencyKey?: string;\n /** Route params (for page-scoped actions). */\n params: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware). */\n locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n input: TInput,\n ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n /** Metadata for the action (used by the runtime/manifest). */\n __elurAction: {\n name: string;\n concurrency: ActionConcurrencyMode;\n idempotent: boolean;\n invalidateTags: readonly string[];\n invalidatePaths: readonly string[];\n };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@elurjs/kit/action\";\n *\n * export const submitContact = defineAction({\n * input: { parse: (v) => v as { name: string; email: string } },\n * invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n * if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n * await saveContact(input);\n * return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n options: DefineActionOptions<TInput>,\n handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n const concurrency = options.concurrency ?? \"latest\";\n const idempotent = options.idempotent ?? false;\n const invalidateTags = options.invalidateTags ?? [];\n const invalidatePaths = options.invalidatePaths ?? [];\n\n // Track in-flight calls for concurrency control.\n let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n const queue: Array<() => void> = [];\n let running = 0;\n\n async function run(\n input: TInput,\n ctx: ActionContext,\n ): Promise<TOutput | ActionFailure<TOutput>> {\n // Validate input if a validator is configured.\n if (options.input) {\n try {\n const validated = options.input.parse(input);\n input = validated;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const { fail } = await import(\"../errors.js\");\n return fail(400, { validation: message }) as ActionFailure<TOutput>;\n }\n }\n\n return handler(input, ctx);\n }\n\n const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n if (concurrency === \"latest\") {\n // Cancel any previous in-flight call.\n if (latestController) latestController.abort();\n latestController = new AbortControllerCtor();\n // Combine the request signal with our cancellation signal.\n const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n return run(input, { ...ctx, signal: combinedSignal });\n }\n\n if (concurrency === \"queue\") {\n // Wait for previous calls to finish.\n if (running > 0) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n running++;\n try {\n return await run(input, ctx);\n } finally {\n running--;\n const next = queue.shift();\n if (next) next();\n }\n }\n\n // parallel: just run it.\n return run(input, ctx);\n };\n\n // Attach metadata.\n (fn as DefinedAction<TInput, TOutput>).__elurAction = {\n name: handler.name || \"anonymous\",\n concurrency,\n idempotent,\n invalidateTags,\n invalidatePaths,\n };\n\n return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n const controller = new AbortControllerCtor();\n for (const signal of signals) {\n if (!signal) continue;\n if (signal.aborted) {\n controller.abort();\n break;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `elurJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@elurjs/kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { elurJsAction } from \"@elurjs/kit/action\";\n *\n * const contact = elurJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@elurjs/core\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n __elur_js_action_failure?: boolean;\n status?: number;\n data?: unknown;\n}\n\ninterface RedirectPayload {\n __elur_js_action_redirect?: boolean;\n status?: number;\n location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __elur_js_action_failure: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __elur_js_action_redirect: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n name: string;\n page?: string;\n args: unknown[];\n}\n\nexport interface CallActionOptions {\n /** Page URL path that scopes the action, e.g. `/contact`. */\n page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__elur-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n name: string,\n args: unknown = [],\n options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n const argsArray = Array.isArray(args) ? args : [args];\n const res = await fetch(\"/__elur-js/actions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n // not JSON, treat as plain error\n }\n if (isActionFailurePayload(payload) && payload.status !== undefined) {\n return new ActionFailure(payload.status, payload.data as T);\n }\n throw new Error(`Action \"${name}\" failed: ${text}`);\n }\n\n const payload: unknown = JSON.parse(text);\n if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n return new RedirectResponse(payload.status, payload.location);\n }\n\n return payload as T;\n}\n\nexport interface ElurJsAction<TInput = unknown, TOutput = unknown> {\n /** Submit the action with the given input. */\n submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n /** Signal that is true while the action is running. */\n pending: { value: boolean };\n /** Signal with the last successful result, action failure, redirect, or null. */\n data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n /** Signal with the last error, or null. */\n error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function elurJsAction<TInput = unknown, TOutput = unknown>(\n name: string,\n options: CallActionOptions = {},\n): ElurJsAction<TInput, TOutput> {\n const pending = signal(false);\n const error = signal<Error | null>(null);\n const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n pending.value = true;\n error.value = null;\n try {\n const result = await callAction<TOutput>(name, input, options);\n data.value = result;\n return result;\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err));\n throw err;\n } finally {\n pending.value = false;\n }\n }\n\n return {\n submit,\n pending,\n data,\n error,\n };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n defineAction,\n type DefineActionOptions,\n type DefinedAction,\n type DefinedActionFn,\n type ActionContext,\n type ActionInputValidator,\n type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"action-CXVk8rWt.js","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n /**\n * Optional input validator. Can be a Zod schema, a plain function, or any\n * object with a `.parse()` method. If validation fails, the action returns\n * a 400 ActionFailure with the validation error.\n */\n input?: ActionInputValidator<TInput>;\n /** Concurrency mode when the same action is called multiple times. */\n concurrency?: ActionConcurrencyMode;\n /** Whether the action is idempotent (safe to retry). */\n idempotent?: boolean;\n /** Tags to invalidate from the cache after a successful action (§9.4). */\n invalidateTags?: string[];\n /** Paths to invalidate from the cache after a successful action (§9.4). */\n invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n /** The original Web Request. */\n request: Request;\n /** AbortSignal from the request — aborts if the client disconnects. */\n signal: AbortSignal;\n /** Idempotency key from the request header, if present. */\n idempotencyKey?: string;\n /** Route params (for page-scoped actions). */\n params: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware). */\n locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n input: TInput,\n ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n /** Metadata for the action (used by the runtime/manifest). */\n __elurAction: {\n name: string;\n concurrency: ActionConcurrencyMode;\n idempotent: boolean;\n invalidateTags: readonly string[];\n invalidatePaths: readonly string[];\n };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@elurjs/kit/action\";\n *\n * export const submitContact = defineAction({\n * input: { parse: (v) => v as { name: string; email: string } },\n * invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n * if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n * await saveContact(input);\n * return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n options: DefineActionOptions<TInput>,\n handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n const concurrency = options.concurrency ?? \"latest\";\n const idempotent = options.idempotent ?? false;\n const invalidateTags = options.invalidateTags ?? [];\n const invalidatePaths = options.invalidatePaths ?? [];\n\n // Track in-flight calls for concurrency control.\n let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n const queue: Array<() => void> = [];\n let running = 0;\n\n async function run(\n input: TInput,\n ctx: ActionContext,\n ): Promise<TOutput | ActionFailure<TOutput>> {\n // Validate input if a validator is configured.\n if (options.input) {\n try {\n const validated = options.input.parse(input);\n input = validated;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const { fail } = await import(\"../errors.js\");\n return fail(400, { validation: message }) as ActionFailure<TOutput>;\n }\n }\n\n return handler(input, ctx);\n }\n\n const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n if (concurrency === \"latest\") {\n // Cancel any previous in-flight call.\n if (latestController) latestController.abort();\n latestController = new AbortControllerCtor();\n // Combine the request signal with our cancellation signal.\n const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n return run(input, { ...ctx, signal: combinedSignal });\n }\n\n if (concurrency === \"queue\") {\n // Wait for previous calls to finish.\n if (running > 0) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n running++;\n try {\n return await run(input, ctx);\n } finally {\n running--;\n const next = queue.shift();\n if (next) next();\n }\n }\n\n // parallel: just run it.\n return run(input, ctx);\n };\n\n // Attach metadata.\n (fn as DefinedAction<TInput, TOutput>).__elurAction = {\n name: handler.name || \"anonymous\",\n concurrency,\n idempotent,\n invalidateTags,\n invalidatePaths,\n };\n\n return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n const controller = new AbortControllerCtor();\n for (const signal of signals) {\n if (!signal) continue;\n if (signal.aborted) {\n controller.abort();\n break;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `elurJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@elurjs/kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { elurJsAction } from \"@elurjs/kit/action\";\n *\n * const contact = elurJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@elurjs/core\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n __elur_js_action_failure?: boolean;\n status?: number;\n data?: unknown;\n}\n\ninterface RedirectPayload {\n __elur_js_action_redirect?: boolean;\n status?: number;\n location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __elur_js_action_failure: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __elur_js_action_redirect: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n name: string;\n page?: string;\n args: unknown[];\n}\n\nexport interface CallActionOptions {\n /** Page URL path that scopes the action, e.g. `/contact`. */\n page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__elur-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n name: string,\n args: unknown = [],\n options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n const argsArray = Array.isArray(args) ? args : [args];\n const res = await fetch(\"/__elur-js/actions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n // not JSON, treat as plain error\n }\n if (isActionFailurePayload(payload) && payload.status !== undefined) {\n return new ActionFailure(payload.status, payload.data as T);\n }\n throw new Error(`Action \"${name}\" failed: ${text}`);\n }\n\n const payload: unknown = JSON.parse(text);\n if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n return new RedirectResponse(payload.status, payload.location);\n }\n\n return payload as T;\n}\n\nexport interface ElurJsAction<TInput = unknown, TOutput = unknown> {\n /** Submit the action with the given input. */\n submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n /** Signal that is true while the action is running. */\n pending: { value: boolean };\n /** Signal with the last successful result, action failure, redirect, or null. */\n data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n /** Signal with the last error, or null. */\n error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function elurJsAction<TInput = unknown, TOutput = unknown>(\n name: string,\n options: CallActionOptions = {},\n): ElurJsAction<TInput, TOutput> {\n const pending = signal(false);\n const error = signal<Error | null>(null);\n const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n pending.value = true;\n error.value = null;\n try {\n const result = await callAction<TOutput>(name, input, options);\n data.value = result;\n return result;\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err));\n throw err;\n } finally {\n pending.value = false;\n }\n }\n\n return {\n submit,\n pending,\n data,\n error,\n };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n defineAction,\n type DefineActionOptions,\n type DefinedAction,\n type DefinedActionFn,\n type ActionContext,\n type ActionInputValidator,\n type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":"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"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { t as e } from "./rolldown-runtime-Dy4uBu1J.js";
|
|
2
|
+
import { mkdir as t, readFile as n, readdir as r, rename as i, rm as a, stat as o, writeFile as s } from "node:fs/promises";
|
|
3
|
+
import { dirname as c, join as l } from "node:path";
|
|
4
|
+
import { createHash as u, randomUUID as d } from "node:crypto";
|
|
5
|
+
//#region src/cache/adapter.ts
|
|
6
|
+
var f = /* @__PURE__ */ e({
|
|
7
|
+
cacheKey: () => p,
|
|
8
|
+
createFsCacheAdapter: () => m,
|
|
9
|
+
getWithSWR: () => h
|
|
10
|
+
});
|
|
11
|
+
function p(...e) {
|
|
12
|
+
return u("sha256").update(e.join("\n")).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
function m(e) {
|
|
15
|
+
let { cacheDir: u } = e, f = e.maxEntries ?? 1e3, p = e.maxAgeMs ?? 864e5, m = /* @__PURE__ */ new Map(), h = l(u, "_tag-index.json");
|
|
16
|
+
async function g() {
|
|
17
|
+
try {
|
|
18
|
+
let e = await n(h, "utf8");
|
|
19
|
+
return JSON.parse(e);
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function _(e) {
|
|
25
|
+
await t(c(h), { recursive: !0 });
|
|
26
|
+
let n = `${h}.${process.pid}.${d()}.tmp`;
|
|
27
|
+
try {
|
|
28
|
+
await s(n, JSON.stringify(e), "utf8"), await i(n, h);
|
|
29
|
+
} finally {
|
|
30
|
+
await a(n, { force: !0 });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function v(e) {
|
|
34
|
+
return l(u, `${e}.html.json`);
|
|
35
|
+
}
|
|
36
|
+
async function y(e) {
|
|
37
|
+
let t = m.get(e);
|
|
38
|
+
if (t) return t;
|
|
39
|
+
let r = (async () => {
|
|
40
|
+
try {
|
|
41
|
+
let t = await n(v(e), "utf8");
|
|
42
|
+
return JSON.parse(t);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
m.set(e, r);
|
|
48
|
+
try {
|
|
49
|
+
return await r;
|
|
50
|
+
} finally {
|
|
51
|
+
m.delete(e);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function b(e, n, r) {
|
|
55
|
+
let o = v(e);
|
|
56
|
+
await t(c(o), { recursive: !0 });
|
|
57
|
+
let l = {
|
|
58
|
+
html: n.html,
|
|
59
|
+
generatedAt: n.generatedAt ?? Date.now(),
|
|
60
|
+
revalidate: r.revalidate,
|
|
61
|
+
tags: r.tags,
|
|
62
|
+
version: r.version
|
|
63
|
+
}, u = `${o}.${process.pid}.${d()}.tmp`;
|
|
64
|
+
try {
|
|
65
|
+
await s(u, JSON.stringify(l), "utf8"), await i(u, o);
|
|
66
|
+
} finally {
|
|
67
|
+
await a(u, { force: !0 });
|
|
68
|
+
}
|
|
69
|
+
if (r.tags && r.tags.length > 0) {
|
|
70
|
+
let t = await g();
|
|
71
|
+
for (let n of r.tags) t[n] || (t[n] = []), t[n].includes(e) || t[n].push(e);
|
|
72
|
+
await _(t);
|
|
73
|
+
}
|
|
74
|
+
await w();
|
|
75
|
+
}
|
|
76
|
+
async function x(e) {
|
|
77
|
+
await a(v(e), { force: !0 });
|
|
78
|
+
}
|
|
79
|
+
async function S(e) {
|
|
80
|
+
if (e.length === 0) return;
|
|
81
|
+
let t = await g(), n = /* @__PURE__ */ new Set();
|
|
82
|
+
for (let r of e) {
|
|
83
|
+
let e = t[r];
|
|
84
|
+
if (e) {
|
|
85
|
+
for (let t of e) n.add(t);
|
|
86
|
+
delete t[r];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
await Promise.all([...n].map((e) => x(e))), await _(t);
|
|
90
|
+
}
|
|
91
|
+
let C = 0;
|
|
92
|
+
async function w() {
|
|
93
|
+
let e = Date.now();
|
|
94
|
+
if (!(e - C < 6e4)) {
|
|
95
|
+
C = e;
|
|
96
|
+
try {
|
|
97
|
+
let t = await r(u), n = 0, i = [];
|
|
98
|
+
for (let r of t) {
|
|
99
|
+
if (!r.endsWith(".html.json")) continue;
|
|
100
|
+
n++;
|
|
101
|
+
let t = l(u, r);
|
|
102
|
+
try {
|
|
103
|
+
e - (await o(t)).mtimeMs > p && i.push(t);
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
if (n - i.length > f) {
|
|
107
|
+
let e = [];
|
|
108
|
+
for (let n of t) {
|
|
109
|
+
if (!n.endsWith(".html.json")) continue;
|
|
110
|
+
let t = l(u, n);
|
|
111
|
+
if (!i.includes(t)) try {
|
|
112
|
+
let n = await o(t);
|
|
113
|
+
e.push({
|
|
114
|
+
path: t,
|
|
115
|
+
mtime: n.mtimeMs
|
|
116
|
+
});
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
e.sort((e, t) => e.mtime - t.mtime);
|
|
120
|
+
let r = n - i.length - f;
|
|
121
|
+
for (let t = 0; t < r && t < e.length; t++) i.push(e[t].path);
|
|
122
|
+
}
|
|
123
|
+
await Promise.all(i.map((e) => a(e, { force: !0 })));
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
get: y,
|
|
129
|
+
set: b,
|
|
130
|
+
delete: x,
|
|
131
|
+
invalidateTags: S
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function h(e, t, n) {
|
|
135
|
+
let r = await e.get(t);
|
|
136
|
+
return r ? Date.now() - r.generatedAt >= r.revalidate * 1e3 ? (n().then((n) => {
|
|
137
|
+
n && e.set(t, n, {
|
|
138
|
+
revalidate: r.revalidate,
|
|
139
|
+
tags: r.tags,
|
|
140
|
+
version: r.version
|
|
141
|
+
}).catch((e) => {
|
|
142
|
+
console.error("[elur-kit] background cache write failed:", e);
|
|
143
|
+
});
|
|
144
|
+
}, (e) => {
|
|
145
|
+
console.error("[elur-kit] background revalidation failed:", e);
|
|
146
|
+
}), {
|
|
147
|
+
entry: r,
|
|
148
|
+
stale: !0
|
|
149
|
+
}) : {
|
|
150
|
+
entry: r,
|
|
151
|
+
stale: !1
|
|
152
|
+
} : {
|
|
153
|
+
entry: await n(),
|
|
154
|
+
stale: !1
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
export { h as i, p as n, m as r, f as t };
|
|
159
|
+
|
|
160
|
+
//# sourceMappingURL=adapter-DNsjXvq5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-DNsjXvq5.js","names":[],"sources":["../../src/cache/adapter.ts"],"sourcesContent":["// --- CacheAdapter: pluggable cache with single-flight, SWR, tags (§9.2) ---\n//\n// Implements the CacheAdapter interface from the runtime-security design:\n// - SHA-256 keys for normalized identity\n// - temp + atomic rename writes\n// - single-flight per process (deduplicates concurrent gets for same key)\n// - stale-while-revalidate (serves stale, refreshes in background)\n// - tag-based invalidation\n// - size limits and periodic cleanup\n//\n// The filesystem adapter is the default. External adapters (Redis, KV, etc.)\n// can implement the same interface and be plugged in via config.\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { mkdir, readFile, rename, rm, writeFile, readdir, stat } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\n// Types\n\nexport interface CacheEntry {\n html: string;\n generatedAt: number;\n revalidate: number;\n tags?: string[];\n version?: string;\n}\n\nexport interface CacheWriteOptions {\n revalidate: number;\n tags?: string[];\n version?: string;\n}\n\nexport interface CacheAdapter {\n get(key: string): Promise<CacheEntry | null>;\n set(key: string, value: CacheEntry, options: CacheWriteOptions): Promise<void>;\n delete(key: string): Promise<void>;\n invalidateTags(tags: readonly string[]): Promise<void>;\n}\n\n// Helpers\n\n/** Computes a SHA-256 key from a normalized identity string. */\nexport function cacheKey(...parts: string[]): string {\n return createHash(\"sha256\").update(parts.join(\"\\n\")).digest(\"hex\");\n}\n\n// Filesystem CacheAdapter\n\nexport interface FsCacheAdapterOptions {\n cacheDir: string;\n /** Max entries before cleanup runs. Default: 1000. */\n maxEntries?: number;\n /** Max age in ms for entries. Default: 24h. */\n maxAgeMs?: number;\n}\n\nexport function createFsCacheAdapter(options: FsCacheAdapterOptions): CacheAdapter {\n const { cacheDir } = options;\n const maxEntries = options.maxEntries ?? 1000;\n const maxAgeMs = options.maxAgeMs ?? 24 * 60 * 60 * 1000;\n\n // Single-flight: deduplicates concurrent gets for the same key.\n const inFlight = new Map<string, Promise<CacheEntry | null>>();\n\n // Tag index: maps tag -> set of cache keys.\n // Persisted to a JSON file for cross-process visibility.\n const tagIndexPath = join(cacheDir, \"_tag-index.json\");\n\n async function loadTagIndex(): Promise<Record<string, string[]>> {\n try {\n const raw = await readFile(tagIndexPath, \"utf8\");\n return JSON.parse(raw) as Record<string, string[]>;\n } catch {\n return {};\n }\n }\n\n async function saveTagIndex(index: Record<string, string[]>): Promise<void> {\n await mkdir(dirname(tagIndexPath), { recursive: true });\n const tmp = `${tagIndexPath}.${process.pid}.${randomUUID()}.tmp`;\n try {\n await writeFile(tmp, JSON.stringify(index), \"utf8\");\n await rename(tmp, tagIndexPath);\n } finally {\n await rm(tmp, { force: true });\n }\n }\n\n function entryPath(key: string): string {\n return join(cacheDir, `${key}.html.json`);\n }\n\n async function get(key: string): Promise<CacheEntry | null> {\n // Single-flight: if a get is already in progress for this key, wait for it.\n const existing = inFlight.get(key);\n if (existing) return existing;\n\n const promise = (async () => {\n try {\n const raw = await readFile(entryPath(key), \"utf8\");\n const entry = JSON.parse(raw) as CacheEntry;\n return entry;\n } catch {\n return null;\n }\n })();\n\n inFlight.set(key, promise);\n try {\n return await promise;\n } finally {\n inFlight.delete(key);\n }\n }\n\n async function set(key: string, value: CacheEntry, opts: CacheWriteOptions): Promise<void> {\n const path = entryPath(key);\n await mkdir(dirname(path), { recursive: true });\n\n const entry: CacheEntry = {\n html: value.html,\n generatedAt: value.generatedAt ?? Date.now(),\n revalidate: opts.revalidate,\n tags: opts.tags,\n version: opts.version,\n };\n\n // Atomic write: temp + rename.\n const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;\n try {\n await writeFile(tmp, JSON.stringify(entry), \"utf8\");\n await rename(tmp, path);\n } finally {\n await rm(tmp, { force: true });\n }\n\n // Update tag index.\n if (opts.tags && opts.tags.length > 0) {\n const index = await loadTagIndex();\n for (const tag of opts.tags) {\n if (!index[tag]) index[tag] = [];\n if (!index[tag].includes(key)) index[tag].push(key);\n }\n await saveTagIndex(index);\n }\n\n // Periodic cleanup.\n await maybeCleanup();\n }\n\n async function del(key: string): Promise<void> {\n await rm(entryPath(key), { force: true });\n }\n\n async function invalidateTags(tags: readonly string[]): Promise<void> {\n if (tags.length === 0) return;\n const index = await loadTagIndex();\n const keysToDelete = new Set<string>();\n for (const tag of tags) {\n const keys = index[tag];\n if (keys) {\n for (const key of keys) keysToDelete.add(key);\n delete index[tag];\n }\n }\n await Promise.all([...keysToDelete].map((key) => del(key)));\n await saveTagIndex(index);\n }\n\n let lastCleanup = 0;\n async function maybeCleanup(): Promise<void> {\n const now = Date.now();\n if (now - lastCleanup < 60_000) return; // at most once per minute\n lastCleanup = now;\n try {\n const files = await readdir(cacheDir);\n let entryCount = 0;\n const toDelete: string[] = [];\n for (const file of files) {\n if (!file.endsWith(\".html.json\")) continue;\n entryCount++;\n const filePath = join(cacheDir, file);\n try {\n const stats = await stat(filePath);\n if (now - stats.mtimeMs > maxAgeMs) {\n toDelete.push(filePath);\n }\n } catch {\n // stat failed, skip\n }\n }\n // If over limit, delete oldest (by mtime).\n if (entryCount - toDelete.length > maxEntries) {\n const candidates: Array<{ path: string; mtime: number }> = [];\n for (const file of files) {\n if (!file.endsWith(\".html.json\")) continue;\n const filePath = join(cacheDir, file);\n if (toDelete.includes(filePath)) continue;\n try {\n const stats = await stat(filePath);\n candidates.push({ path: filePath, mtime: stats.mtimeMs });\n } catch {\n // skip\n }\n }\n candidates.sort((a, b) => a.mtime - b.mtime);\n const excess = entryCount - toDelete.length - maxEntries;\n for (let i = 0; i < excess && i < candidates.length; i++) {\n toDelete.push(candidates[i].path);\n }\n }\n await Promise.all(toDelete.map((p) => rm(p, { force: true })));\n } catch {\n // cleanup is best-effort\n }\n }\n\n return { get, set, delete: del, invalidateTags };\n}\n\n// Stale-while-revalidate wrapper\n\n/**\n * Gets a cached entry. If the entry is stale (past revalidate), serves it\n * immediately and triggers a background revalidation.\n *\n * @param adapter The cache adapter.\n * @param key The cache key.\n * @param revalidate The revalidation function (called if stale or missing).\n * @returns The cache entry (fresh or stale), or null if missing.\n */\nexport async function getWithSWR(\n adapter: CacheAdapter,\n key: string,\n revalidate: () => Promise<CacheEntry | null>,\n): Promise<{ entry: CacheEntry | null; stale: boolean }> {\n const entry = await adapter.get(key);\n if (!entry) {\n // Cache miss: revalidate synchronously.\n const fresh = await revalidate();\n return { entry: fresh, stale: false };\n }\n\n const ageMs = Date.now() - entry.generatedAt;\n const isStale = ageMs >= entry.revalidate * 1000;\n\n if (isStale) {\n // Serve stale, revalidate in background (fire-and-forget).\n revalidate().then(\n (fresh) => {\n if (fresh) {\n adapter.set(key, fresh, {\n revalidate: entry.revalidate,\n tags: entry.tags,\n version: entry.version,\n }).catch((err) => {\n console.error(\"[elur-kit] background cache write failed:\", err);\n });\n }\n },\n (err) => {\n console.error(\"[elur-kit] background revalidation failed:\", err);\n },\n );\n return { entry, stale: true };\n }\n\n return { entry, stale: false };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
const e=require("./rolldown-runtime-BIInC43l.cjs");let t=require("node:fs/promises"),n=require("node:path"),r=require("node:crypto");var i=e.t({cacheKey:()=>a,createFsCacheAdapter:()=>o,getWithSWR:()=>s});function a(...e){return(0,r.createHash)(`sha256`).update(e.join(`
|
|
2
|
+
`)).digest(`hex`)}function o(e){let{cacheDir:i}=e,a=e.maxEntries??1e3,o=e.maxAgeMs??864e5,s=new Map,c=(0,n.join)(i,`_tag-index.json`);async function l(){try{let e=await(0,t.readFile)(c,`utf8`);return JSON.parse(e)}catch{return{}}}async function u(e){await(0,t.mkdir)((0,n.dirname)(c),{recursive:!0});let i=`${c}.${process.pid}.${(0,r.randomUUID)()}.tmp`;try{await(0,t.writeFile)(i,JSON.stringify(e),`utf8`),await(0,t.rename)(i,c)}finally{await(0,t.rm)(i,{force:!0})}}function d(e){return(0,n.join)(i,`${e}.html.json`)}async function f(e){let n=s.get(e);if(n)return n;let r=(async()=>{try{let n=await(0,t.readFile)(d(e),`utf8`);return JSON.parse(n)}catch{return null}})();s.set(e,r);try{return await r}finally{s.delete(e)}}async function p(e,i,a){let o=d(e);await(0,t.mkdir)((0,n.dirname)(o),{recursive:!0});let s={html:i.html,generatedAt:i.generatedAt??Date.now(),revalidate:a.revalidate,tags:a.tags,version:a.version},c=`${o}.${process.pid}.${(0,r.randomUUID)()}.tmp`;try{await(0,t.writeFile)(c,JSON.stringify(s),`utf8`),await(0,t.rename)(c,o)}finally{await(0,t.rm)(c,{force:!0})}if(a.tags&&a.tags.length>0){let t=await l();for(let n of a.tags)t[n]||(t[n]=[]),t[n].includes(e)||t[n].push(e);await u(t)}await _()}async function m(e){await(0,t.rm)(d(e),{force:!0})}async function h(e){if(e.length===0)return;let t=await l(),n=new Set;for(let r of e){let e=t[r];if(e){for(let t of e)n.add(t);delete t[r]}}await Promise.all([...n].map(e=>m(e))),await u(t)}let g=0;async function _(){let e=Date.now();if(!(e-g<6e4)){g=e;try{let r=await(0,t.readdir)(i),s=0,c=[];for(let a of r){if(!a.endsWith(`.html.json`))continue;s++;let r=(0,n.join)(i,a);try{e-(await(0,t.stat)(r)).mtimeMs>o&&c.push(r)}catch{}}if(s-c.length>a){let e=[];for(let a of r){if(!a.endsWith(`.html.json`))continue;let r=(0,n.join)(i,a);if(!c.includes(r))try{let n=await(0,t.stat)(r);e.push({path:r,mtime:n.mtimeMs})}catch{}}e.sort((e,t)=>e.mtime-t.mtime);let o=s-c.length-a;for(let t=0;t<o&&t<e.length;t++)c.push(e[t].path)}await Promise.all(c.map(e=>(0,t.rm)(e,{force:!0})))}catch{}}}return{get:f,set:p,delete:m,invalidateTags:h}}async function s(e,t,n){let r=await e.get(t);return r?Date.now()-r.generatedAt>=r.revalidate*1e3?(n().then(n=>{n&&e.set(t,n,{revalidate:r.revalidate,tags:r.tags,version:r.version}).catch(e=>{console.error(`[elur-kit] background cache write failed:`,e)})},e=>{console.error(`[elur-kit] background revalidation failed:`,e)}),{entry:r,stale:!0}):{entry:r,stale:!1}:{entry:await n(),stale:!1}}Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return i}});
|
|
3
|
+
//# sourceMappingURL=adapter-Dz2hzR36.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-Dz2hzR36.cjs","names":[],"sources":["../../src/cache/adapter.ts"],"sourcesContent":["// --- CacheAdapter: pluggable cache with single-flight, SWR, tags (§9.2) ---\n//\n// Implements the CacheAdapter interface from the runtime-security design:\n// - SHA-256 keys for normalized identity\n// - temp + atomic rename writes\n// - single-flight per process (deduplicates concurrent gets for same key)\n// - stale-while-revalidate (serves stale, refreshes in background)\n// - tag-based invalidation\n// - size limits and periodic cleanup\n//\n// The filesystem adapter is the default. External adapters (Redis, KV, etc.)\n// can implement the same interface and be plugged in via config.\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { mkdir, readFile, rename, rm, writeFile, readdir, stat } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\n// Types\n\nexport interface CacheEntry {\n html: string;\n generatedAt: number;\n revalidate: number;\n tags?: string[];\n version?: string;\n}\n\nexport interface CacheWriteOptions {\n revalidate: number;\n tags?: string[];\n version?: string;\n}\n\nexport interface CacheAdapter {\n get(key: string): Promise<CacheEntry | null>;\n set(key: string, value: CacheEntry, options: CacheWriteOptions): Promise<void>;\n delete(key: string): Promise<void>;\n invalidateTags(tags: readonly string[]): Promise<void>;\n}\n\n// Helpers\n\n/** Computes a SHA-256 key from a normalized identity string. */\nexport function cacheKey(...parts: string[]): string {\n return createHash(\"sha256\").update(parts.join(\"\\n\")).digest(\"hex\");\n}\n\n// Filesystem CacheAdapter\n\nexport interface FsCacheAdapterOptions {\n cacheDir: string;\n /** Max entries before cleanup runs. Default: 1000. */\n maxEntries?: number;\n /** Max age in ms for entries. Default: 24h. */\n maxAgeMs?: number;\n}\n\nexport function createFsCacheAdapter(options: FsCacheAdapterOptions): CacheAdapter {\n const { cacheDir } = options;\n const maxEntries = options.maxEntries ?? 1000;\n const maxAgeMs = options.maxAgeMs ?? 24 * 60 * 60 * 1000;\n\n // Single-flight: deduplicates concurrent gets for the same key.\n const inFlight = new Map<string, Promise<CacheEntry | null>>();\n\n // Tag index: maps tag -> set of cache keys.\n // Persisted to a JSON file for cross-process visibility.\n const tagIndexPath = join(cacheDir, \"_tag-index.json\");\n\n async function loadTagIndex(): Promise<Record<string, string[]>> {\n try {\n const raw = await readFile(tagIndexPath, \"utf8\");\n return JSON.parse(raw) as Record<string, string[]>;\n } catch {\n return {};\n }\n }\n\n async function saveTagIndex(index: Record<string, string[]>): Promise<void> {\n await mkdir(dirname(tagIndexPath), { recursive: true });\n const tmp = `${tagIndexPath}.${process.pid}.${randomUUID()}.tmp`;\n try {\n await writeFile(tmp, JSON.stringify(index), \"utf8\");\n await rename(tmp, tagIndexPath);\n } finally {\n await rm(tmp, { force: true });\n }\n }\n\n function entryPath(key: string): string {\n return join(cacheDir, `${key}.html.json`);\n }\n\n async function get(key: string): Promise<CacheEntry | null> {\n // Single-flight: if a get is already in progress for this key, wait for it.\n const existing = inFlight.get(key);\n if (existing) return existing;\n\n const promise = (async () => {\n try {\n const raw = await readFile(entryPath(key), \"utf8\");\n const entry = JSON.parse(raw) as CacheEntry;\n return entry;\n } catch {\n return null;\n }\n })();\n\n inFlight.set(key, promise);\n try {\n return await promise;\n } finally {\n inFlight.delete(key);\n }\n }\n\n async function set(key: string, value: CacheEntry, opts: CacheWriteOptions): Promise<void> {\n const path = entryPath(key);\n await mkdir(dirname(path), { recursive: true });\n\n const entry: CacheEntry = {\n html: value.html,\n generatedAt: value.generatedAt ?? Date.now(),\n revalidate: opts.revalidate,\n tags: opts.tags,\n version: opts.version,\n };\n\n // Atomic write: temp + rename.\n const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;\n try {\n await writeFile(tmp, JSON.stringify(entry), \"utf8\");\n await rename(tmp, path);\n } finally {\n await rm(tmp, { force: true });\n }\n\n // Update tag index.\n if (opts.tags && opts.tags.length > 0) {\n const index = await loadTagIndex();\n for (const tag of opts.tags) {\n if (!index[tag]) index[tag] = [];\n if (!index[tag].includes(key)) index[tag].push(key);\n }\n await saveTagIndex(index);\n }\n\n // Periodic cleanup.\n await maybeCleanup();\n }\n\n async function del(key: string): Promise<void> {\n await rm(entryPath(key), { force: true });\n }\n\n async function invalidateTags(tags: readonly string[]): Promise<void> {\n if (tags.length === 0) return;\n const index = await loadTagIndex();\n const keysToDelete = new Set<string>();\n for (const tag of tags) {\n const keys = index[tag];\n if (keys) {\n for (const key of keys) keysToDelete.add(key);\n delete index[tag];\n }\n }\n await Promise.all([...keysToDelete].map((key) => del(key)));\n await saveTagIndex(index);\n }\n\n let lastCleanup = 0;\n async function maybeCleanup(): Promise<void> {\n const now = Date.now();\n if (now - lastCleanup < 60_000) return; // at most once per minute\n lastCleanup = now;\n try {\n const files = await readdir(cacheDir);\n let entryCount = 0;\n const toDelete: string[] = [];\n for (const file of files) {\n if (!file.endsWith(\".html.json\")) continue;\n entryCount++;\n const filePath = join(cacheDir, file);\n try {\n const stats = await stat(filePath);\n if (now - stats.mtimeMs > maxAgeMs) {\n toDelete.push(filePath);\n }\n } catch {\n // stat failed, skip\n }\n }\n // If over limit, delete oldest (by mtime).\n if (entryCount - toDelete.length > maxEntries) {\n const candidates: Array<{ path: string; mtime: number }> = [];\n for (const file of files) {\n if (!file.endsWith(\".html.json\")) continue;\n const filePath = join(cacheDir, file);\n if (toDelete.includes(filePath)) continue;\n try {\n const stats = await stat(filePath);\n candidates.push({ path: filePath, mtime: stats.mtimeMs });\n } catch {\n // skip\n }\n }\n candidates.sort((a, b) => a.mtime - b.mtime);\n const excess = entryCount - toDelete.length - maxEntries;\n for (let i = 0; i < excess && i < candidates.length; i++) {\n toDelete.push(candidates[i].path);\n }\n }\n await Promise.all(toDelete.map((p) => rm(p, { force: true })));\n } catch {\n // cleanup is best-effort\n }\n }\n\n return { get, set, delete: del, invalidateTags };\n}\n\n// Stale-while-revalidate wrapper\n\n/**\n * Gets a cached entry. If the entry is stale (past revalidate), serves it\n * immediately and triggers a background revalidation.\n *\n * @param adapter The cache adapter.\n * @param key The cache key.\n * @param revalidate The revalidation function (called if stale or missing).\n * @returns The cache entry (fresh or stale), or null if missing.\n */\nexport async function getWithSWR(\n adapter: CacheAdapter,\n key: string,\n revalidate: () => Promise<CacheEntry | null>,\n): Promise<{ entry: CacheEntry | null; stale: boolean }> {\n const entry = await adapter.get(key);\n if (!entry) {\n // Cache miss: revalidate synchronously.\n const fresh = await revalidate();\n return { entry: fresh, stale: false };\n }\n\n const ageMs = Date.now() - entry.generatedAt;\n const isStale = ageMs >= entry.revalidate * 1000;\n\n if (isStale) {\n // Serve stale, revalidate in background (fire-and-forget).\n revalidate().then(\n (fresh) => {\n if (fresh) {\n adapter.set(key, fresh, {\n revalidate: entry.revalidate,\n tags: entry.tags,\n version: entry.version,\n }).catch((err) => {\n console.error(\"[elur-kit] background cache write failed:\", err);\n });\n }\n },\n (err) => {\n console.error(\"[elur-kit] background revalidation failed:\", err);\n },\n );\n return { entry, stale: true };\n }\n\n return { entry, stale: false };\n}\n"],"mappings":"6MA2CA,SAAgB,EAAS,GAAG,EAAyB,CACnD,OAAA,EAAO,EAAA,WAAA,CAAW,QAAQ,CAAC,CAAC,OAAO,EAAM,KAAK;CAAI,CAAC,CAAC,CAAC,OAAO,KAAK,CACnE,CAYA,SAAgB,EAAqB,EAA8C,CACjF,GAAM,CAAE,YAAa,EACf,EAAa,EAAQ,YAAc,IACnC,EAAW,EAAQ,UAAY,MAG/B,EAAW,IAAI,IAIf,GAAA,EAAe,EAAA,KAAA,CAAK,EAAU,iBAAiB,EAErD,eAAe,GAAkD,CAC/D,GAAI,CACF,IAAM,EAAM,MAAA,EAAM,EAAA,SAAA,CAAS,EAAc,MAAM,EAC/C,OAAO,KAAK,MAAM,CAAG,CACvB,MAAQ,CACN,MAAO,CAAC,CACV,CACF,CAEA,eAAe,EAAa,EAAgD,CAC1E,MAAA,EAAM,EAAA,MAAA,EAAA,EAAM,EAAA,QAAA,CAAQ,CAAY,EAAG,CAAE,UAAW,EAAK,CAAC,EACtD,IAAM,EAAM,GAAG,EAAa,GAAG,QAAQ,IAAI,IAAA,EAAG,EAAA,WAAA,CAAW,EAAE,MAC3D,GAAI,CACF,MAAA,EAAM,EAAA,UAAA,CAAU,EAAK,KAAK,UAAU,CAAK,EAAG,MAAM,EAClD,MAAA,EAAM,EAAA,OAAA,CAAO,EAAK,CAAY,CAChC,QAAU,CACR,MAAA,EAAM,EAAA,GAAA,CAAG,EAAK,CAAE,MAAO,EAAK,CAAC,CAC/B,CACF,CAEA,SAAS,EAAU,EAAqB,CACtC,OAAA,EAAO,EAAA,KAAA,CAAK,EAAU,GAAG,EAAI,WAAW,CAC1C,CAEA,eAAe,EAAI,EAAyC,CAE1D,IAAM,EAAW,EAAS,IAAI,CAAG,EACjC,GAAI,EAAU,OAAO,EAErB,IAAM,GAAW,SAAY,CAC3B,GAAI,CACF,IAAM,EAAM,MAAA,EAAM,EAAA,SAAA,CAAS,EAAU,CAAG,EAAG,MAAM,EAEjD,OADc,KAAK,MAAM,CAClB,CACT,MAAQ,CACN,OAAO,IACT,CACF,EAAA,CAAG,EAEH,EAAS,IAAI,EAAK,CAAO,EACzB,GAAI,CACF,OAAO,MAAM,CACf,QAAU,CACR,EAAS,OAAO,CAAG,CACrB,CACF,CAEA,eAAe,EAAI,EAAa,EAAmB,EAAwC,CACzF,IAAM,EAAO,EAAU,CAAG,EAC1B,MAAA,EAAM,EAAA,MAAA,EAAA,EAAM,EAAA,QAAA,CAAQ,CAAI,EAAG,CAAE,UAAW,EAAK,CAAC,EAE9C,IAAM,EAAoB,CACxB,KAAM,EAAM,KACZ,YAAa,EAAM,aAAe,KAAK,IAAI,EAC3C,WAAY,EAAK,WACjB,KAAM,EAAK,KACX,QAAS,EAAK,OAChB,EAGM,EAAM,GAAG,EAAK,GAAG,QAAQ,IAAI,IAAA,EAAG,EAAA,WAAA,CAAW,EAAE,MACnD,GAAI,CACF,MAAA,EAAM,EAAA,UAAA,CAAU,EAAK,KAAK,UAAU,CAAK,EAAG,MAAM,EAClD,MAAA,EAAM,EAAA,OAAA,CAAO,EAAK,CAAI,CACxB,QAAU,CACR,MAAA,EAAM,EAAA,GAAA,CAAG,EAAK,CAAE,MAAO,EAAK,CAAC,CAC/B,CAGA,GAAI,EAAK,MAAQ,EAAK,KAAK,OAAS,EAAG,CACrC,IAAM,EAAQ,MAAM,EAAa,EACjC,IAAK,IAAM,KAAO,EAAK,KAChB,EAAM,KAAM,EAAM,GAAO,CAAC,GAC1B,EAAM,EAAI,CAAC,SAAS,CAAG,GAAG,EAAM,EAAI,CAAC,KAAK,CAAG,EAEpD,MAAM,EAAa,CAAK,CAC1B,CAGA,MAAM,EAAa,CACrB,CAEA,eAAe,EAAI,EAA4B,CAC7C,MAAA,EAAM,EAAA,GAAA,CAAG,EAAU,CAAG,EAAG,CAAE,MAAO,EAAK,CAAC,CAC1C,CAEA,eAAe,EAAe,EAAwC,CACpE,GAAI,EAAK,SAAW,EAAG,OACvB,IAAM,EAAQ,MAAM,EAAa,EAC3B,EAAe,IAAI,IACzB,IAAK,IAAM,KAAO,EAAM,CACtB,IAAM,EAAO,EAAM,GACnB,GAAI,EAAM,CACR,IAAK,IAAM,KAAO,EAAM,EAAa,IAAI,CAAG,EAC5C,OAAO,EAAM,EACf,CACF,CACA,MAAM,QAAQ,IAAI,CAAC,GAAG,CAAY,CAAC,CAAC,IAAK,GAAQ,EAAI,CAAG,CAAC,CAAC,EAC1D,MAAM,EAAa,CAAK,CAC1B,CAEA,IAAI,EAAc,EAClB,eAAe,GAA8B,CAC3C,IAAM,EAAM,KAAK,IAAI,EACjB,OAAM,EAAc,KACxB,GAAc,EACd,GAAI,CACF,IAAM,EAAQ,MAAA,EAAM,EAAA,QAAA,CAAQ,CAAQ,EAChC,EAAa,EACX,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAQ,EAAO,CACxB,GAAI,CAAC,EAAK,SAAS,YAAY,EAAG,SAClC,IACA,IAAM,GAAA,EAAW,EAAA,KAAA,CAAK,EAAU,CAAI,EACpC,GAAI,CAEE,GAAM,MAAA,EADU,EAAA,KAAA,CAAK,CAAQ,EAAA,CACjB,QAAU,GACxB,EAAS,KAAK,CAAQ,CAE1B,MAAQ,CAER,CACF,CAEA,GAAI,EAAa,EAAS,OAAS,EAAY,CAC7C,IAAM,EAAqD,CAAC,EAC5D,IAAK,IAAM,KAAQ,EAAO,CACxB,GAAI,CAAC,EAAK,SAAS,YAAY,EAAG,SAClC,IAAM,GAAA,EAAW,EAAA,KAAA,CAAK,EAAU,CAAI,EAChC,MAAS,SAAS,CAAQ,EAC9B,GAAI,CACF,IAAM,EAAQ,MAAA,EAAM,EAAA,KAAA,CAAK,CAAQ,EACjC,EAAW,KAAK,CAAE,KAAM,EAAU,MAAO,EAAM,OAAQ,CAAC,CAC1D,MAAQ,CAER,CACF,CACA,EAAW,MAAM,EAAG,IAAM,EAAE,MAAQ,EAAE,KAAK,EAC3C,IAAM,EAAS,EAAa,EAAS,OAAS,EAC9C,IAAK,IAAI,EAAI,EAAG,EAAI,GAAU,EAAI,EAAW,OAAQ,IACnD,EAAS,KAAK,EAAW,EAAE,CAAC,IAAI,CAEpC,CACA,MAAM,QAAQ,IAAI,EAAS,IAAK,IAAA,EAAM,EAAA,GAAA,CAAG,EAAG,CAAE,MAAO,EAAK,CAAC,CAAC,CAAC,CAC/D,MAAQ,CAER,CAzCc,CA0ChB,CAEA,MAAO,CAAE,MAAK,MAAK,OAAQ,EAAK,gBAAe,CACjD,CAaA,eAAsB,EACpB,EACA,EACA,EACuD,CACvD,IAAM,EAAQ,MAAM,EAAQ,IAAI,CAAG,EA+BnC,OA9BK,EAMS,KAAK,IAAI,EAAI,EAAM,aACR,EAAM,WAAa,KAI1C,EAAW,CAAC,CAAC,KACV,GAAU,CACL,GACF,EAAQ,IAAI,EAAK,EAAO,CACtB,WAAY,EAAM,WAClB,KAAM,EAAM,KACZ,QAAS,EAAM,OACjB,CAAC,CAAC,CAAC,MAAO,GAAQ,CAChB,QAAQ,MAAM,4CAA6C,CAAG,CAChE,CAAC,CAEL,EACC,GAAQ,CACP,QAAQ,MAAM,6CAA8C,CAAG,CACjE,CACF,EACO,CAAE,QAAO,MAAO,EAAK,GAGvB,CAAE,QAAO,MAAO,EAAM,EA3BpB,CAAE,MAAO,MADI,EAAW,EACR,MAAO,EAAM,CA4BxC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../route-scanner-JY6YrNH5.cjs"),t=require("../
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../route-scanner-JY6YrNH5.cjs"),t=require("../capabilities-C0TDQoMY.cjs"),n=require("../shared-CP0Dc3cp.cjs");let r=require("node:fs/promises"),i=require("node:path");var a={name:`bun`,capabilities:t.t,async build(t){let a=(0,i.resolve)(t.root),c=(0,i.resolve)(a,t.outDir),l=(0,i.resolve)(a,`.elur`);try{await(0,r.stat)(c)}catch{throw Error(`Output directory not found: ${c}. Run "elur-kit build" first.`)}await(0,r.rm)(l,{recursive:!0,force:!0}),await(0,r.mkdir)(l,{recursive:!0});let u=(0,i.resolve)(a,t.appDir),d=await e.t(u),f=(0,i.resolve)(l,`bun-index.ts`);await n.n(f,d,t);let p=(0,i.resolve)(l,`bun-server.ts`);await(0,r.writeFile)(p,o(s(l,c),t),`utf8`)}};function o(e,t){let n=t.logLevel?`, logLevel: ${JSON.stringify(t.logLevel)}`:``,r=[`redirects`,`rewrites`,`routeHeaders`].map(e=>{let n=t[e];return n&&n.length>0?`, ${e}: ${JSON.stringify(n)}`:``}).join(``);return`// AUTO-GENERATED by @elurjs/kit. Do not edit.
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { createWebHandler } from "@elurjs/kit/runtime";
|
|
4
4
|
import handler from "./bun-index.ts";
|
|
@@ -9,7 +9,7 @@ const port = Number(process.env.PORT) || ${t.port??3e3};
|
|
|
9
9
|
const webHandler = createWebHandler(
|
|
10
10
|
{ pages: [], api: [], error404: undefined, error500: undefined },
|
|
11
11
|
{},
|
|
12
|
-
{ staticRoot: outDir, lang: ${JSON.stringify(t.lang)}, clientEntry: ${JSON.stringify(t.clientEntry)} },
|
|
12
|
+
{ staticRoot: outDir, lang: ${JSON.stringify(t.lang)}, clientEntry: ${JSON.stringify(t.clientEntry)}${n}${r} },
|
|
13
13
|
);
|
|
14
14
|
|
|
15
15
|
Bun.serve({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bun.cjs","names":[],"sources":["../../../src/adapters/bun.ts"],"sourcesContent":["import { mkdir, rm, stat, writeFile } from \"node:fs/promises\";\nimport { relative, resolve } from \"node:path\";\nimport { scanRoutes } from \"../router/route-scanner.js\";\nimport type { Adapter } from \"./index.js\";\nimport { writeSsrEntry } from \"./shared.js\";\nimport { DEFAULT_CAPABILITIES } from \"../runtime/capabilities.js\";\n\n/**\n * Bun adapter for elur-kit.\n *\n * Produces a self-contained Bun server entry at `.elur/bun-server.ts`.\n * Run it with:\n *\n * bun run .elur/bun-server.ts\n *\n * The server serves static files from `dist/` and renders pages on demand for\n * unmatched routes.\n */\nexport const bunAdapter: Adapter = {\n name: \"bun\",\n capabilities: DEFAULT_CAPABILITIES,\n\n async build(options) {\n const root = resolve(options.root);\n const outDir = resolve(root, options.outDir);\n const generatedDir = resolve(root, \".elur\");\n\n // Verify the production build exists.\n try {\n await stat(outDir);\n } catch {\n throw new Error(\n `Output directory not found: ${outDir}. Run \"elur-kit build\" first.`,\n );\n }\n\n // Clean previous adapter output.\n await rm(generatedDir, { recursive: true, force: true });\n await mkdir(generatedDir, { recursive: true });\n\n // Scan routes and generate a self-contained SSR handler entry.\n const appDir = resolve(root, options.appDir);\n const routes = await scanRoutes(appDir);\n\n const entryPath = resolve(generatedDir, \"bun-index.ts\");\n await writeSsrEntry(entryPath, routes, options);\n\n // Write the Bun server entry.\n const serverPath = resolve(generatedDir, \"bun-server.ts\");\n await writeFile(\n serverPath,\n buildBunServerSource(relativeToUrlPath(generatedDir, outDir), options),\n \"utf8\",\n );\n },\n};\n\nfunction buildBunServerSource(\n outDirUrl: string,\n options: {\n clientEntry: string;\n lang: string;\n port?: number;\n },\n): string {\n return `// AUTO-GENERATED by @elurjs/kit. Do not edit.\nimport { fileURLToPath } from \"node:url\";\nimport { createWebHandler } from \"@elurjs/kit/runtime\";\nimport handler from \"./bun-index.ts\";\n\nconst outDir = fileURLToPath(new URL(${JSON.stringify(outDirUrl)}, import.meta.url));\nconst port = Number(process.env.PORT) || ${options.port ?? 3000};\n\nconst webHandler = createWebHandler(\n { pages: [], api: [], error404: undefined, error500: undefined },\n {},\n { staticRoot: outDir, lang: ${JSON.stringify(options.lang)}, clientEntry: ${JSON.stringify(options.clientEntry)} },\n);\n\nBun.serve({\n port,\n async fetch(request) {\n // Try static files first via the unified handler, then fall back to the\n // bundled SSR handler for dynamic routes.\n let response = await webHandler(request);\n if (response.status === 404) {\n response = await handler(request);\n }\n return response;\n },\n});\n\nconsole.log(\\`Bun server running at http://localhost:\\${port}\\`);\n`;\n}\n\nfunction relativeToUrlPath(from: string, to: string): string {\n const path = relative(from, to).split(\"\\\\\").join(\"/\");\n return `${path.startsWith(\".\") ? path : `./${path}`}/`;\n}\n"],"mappings":"2PAkBA,IAAa,EAAsB,CACjC,KAAM,MACN,aAAc,EAAA,EAEd,MAAM,MAAM,EAAS,CACnB,IAAM,GAAA,EAAO,EAAA,QAAA,CAAQ,EAAQ,IAAI,EAC3B,GAAA,EAAS,EAAA,QAAA,CAAQ,EAAM,EAAQ,MAAM,EACrC,GAAA,EAAe,EAAA,QAAA,CAAQ,EAAM,OAAO,EAG1C,GAAI,CACF,MAAA,EAAM,EAAA,KAAA,CAAK,CAAM,CACnB,MAAQ,CACN,MAAU,MACR,+BAA+B,EAAO,8BACxC,CACF,CAGA,MAAA,EAAM,EAAA,GAAA,CAAG,EAAc,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACvD,MAAA,EAAM,EAAA,MAAA,CAAM,EAAc,CAAE,UAAW,EAAK,CAAC,EAG7C,IAAM,GAAA,EAAS,EAAA,QAAA,CAAQ,EAAM,EAAQ,MAAM,EACrC,EAAS,MAAM,EAAA,EAAW,CAAM,EAEhC,GAAA,EAAY,EAAA,QAAA,CAAQ,EAAc,cAAc,EACtD,MAAM,EAAA,EAAc,EAAW,EAAQ,CAAO,EAG9C,IAAM,GAAA,EAAa,EAAA,QAAA,CAAQ,EAAc,eAAe,EACxD,MAAA,EAAM,EAAA,UAAA,CACJ,EACA,EAAqB,EAAkB,EAAc,CAAM,EAAG,CAAO,EACrE,MACF,CACF,CACF,EAEA,SAAS,EACP,EACA,
|
|
1
|
+
{"version":3,"file":"bun.cjs","names":[],"sources":["../../../src/adapters/bun.ts"],"sourcesContent":["import { mkdir, rm, stat, writeFile } from \"node:fs/promises\";\nimport { relative, resolve } from \"node:path\";\nimport { scanRoutes } from \"../router/route-scanner.js\";\nimport type { Adapter } from \"./index.js\";\nimport { writeSsrEntry } from \"./shared.js\";\nimport { DEFAULT_CAPABILITIES } from \"../runtime/capabilities.js\";\n\n/**\n * Bun adapter for elur-kit.\n *\n * Produces a self-contained Bun server entry at `.elur/bun-server.ts`.\n * Run it with:\n *\n * bun run .elur/bun-server.ts\n *\n * The server serves static files from `dist/` and renders pages on demand for\n * unmatched routes.\n */\nexport const bunAdapter: Adapter = {\n name: \"bun\",\n capabilities: DEFAULT_CAPABILITIES,\n\n async build(options) {\n const root = resolve(options.root);\n const outDir = resolve(root, options.outDir);\n const generatedDir = resolve(root, \".elur\");\n\n // Verify the production build exists.\n try {\n await stat(outDir);\n } catch {\n throw new Error(\n `Output directory not found: ${outDir}. Run \"elur-kit build\" first.`,\n );\n }\n\n // Clean previous adapter output.\n await rm(generatedDir, { recursive: true, force: true });\n await mkdir(generatedDir, { recursive: true });\n\n // Scan routes and generate a self-contained SSR handler entry.\n const appDir = resolve(root, options.appDir);\n const routes = await scanRoutes(appDir);\n\n const entryPath = resolve(generatedDir, \"bun-index.ts\");\n await writeSsrEntry(entryPath, routes, options);\n\n // Write the Bun server entry.\n const serverPath = resolve(generatedDir, \"bun-server.ts\");\n await writeFile(\n serverPath,\n buildBunServerSource(relativeToUrlPath(generatedDir, outDir), options),\n \"utf8\",\n );\n },\n};\n\nfunction buildBunServerSource(\n outDirUrl: string,\n options: {\n clientEntry: string;\n lang: string;\n port?: number;\n logLevel?: string;\n redirects?: import(\"../router/redirects.js\").RedirectRule[];\n rewrites?: import(\"../router/redirects.js\").RewriteRule[];\n routeHeaders?: import(\"../router/redirects.js\").RouteHeadersRule[];\n },\n): string {\n const logLevelOption = options.logLevel ? `, logLevel: ${JSON.stringify(options.logLevel)}` : \"\";\n // Rule arrays are plain data: emit them as JSON literals when defined.\n const routingOptions = ([\"redirects\", \"rewrites\", \"routeHeaders\"] as const)\n .map((key) => {\n const rules = options[key];\n return rules && rules.length > 0 ? `, ${key}: ${JSON.stringify(rules)}` : \"\";\n })\n .join(\"\");\n return `// AUTO-GENERATED by @elurjs/kit. Do not edit.\nimport { fileURLToPath } from \"node:url\";\nimport { createWebHandler } from \"@elurjs/kit/runtime\";\nimport handler from \"./bun-index.ts\";\n\nconst outDir = fileURLToPath(new URL(${JSON.stringify(outDirUrl)}, import.meta.url));\nconst port = Number(process.env.PORT) || ${options.port ?? 3000};\n\nconst webHandler = createWebHandler(\n { pages: [], api: [], error404: undefined, error500: undefined },\n {},\n { staticRoot: outDir, lang: ${JSON.stringify(options.lang)}, clientEntry: ${JSON.stringify(options.clientEntry)}${logLevelOption}${routingOptions} },\n);\n\nBun.serve({\n port,\n async fetch(request) {\n // Try static files first via the unified handler, then fall back to the\n // bundled SSR handler for dynamic routes.\n let response = await webHandler(request);\n if (response.status === 404) {\n response = await handler(request);\n }\n return response;\n },\n});\n\nconsole.log(\\`Bun server running at http://localhost:\\${port}\\`);\n`;\n}\n\nfunction relativeToUrlPath(from: string, to: string): string {\n const path = relative(from, to).split(\"\\\\\").join(\"/\");\n return `${path.startsWith(\".\") ? path : `./${path}`}/`;\n}\n"],"mappings":"2PAkBA,IAAa,EAAsB,CACjC,KAAM,MACN,aAAc,EAAA,EAEd,MAAM,MAAM,EAAS,CACnB,IAAM,GAAA,EAAO,EAAA,QAAA,CAAQ,EAAQ,IAAI,EAC3B,GAAA,EAAS,EAAA,QAAA,CAAQ,EAAM,EAAQ,MAAM,EACrC,GAAA,EAAe,EAAA,QAAA,CAAQ,EAAM,OAAO,EAG1C,GAAI,CACF,MAAA,EAAM,EAAA,KAAA,CAAK,CAAM,CACnB,MAAQ,CACN,MAAU,MACR,+BAA+B,EAAO,8BACxC,CACF,CAGA,MAAA,EAAM,EAAA,GAAA,CAAG,EAAc,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACvD,MAAA,EAAM,EAAA,MAAA,CAAM,EAAc,CAAE,UAAW,EAAK,CAAC,EAG7C,IAAM,GAAA,EAAS,EAAA,QAAA,CAAQ,EAAM,EAAQ,MAAM,EACrC,EAAS,MAAM,EAAA,EAAW,CAAM,EAEhC,GAAA,EAAY,EAAA,QAAA,CAAQ,EAAc,cAAc,EACtD,MAAM,EAAA,EAAc,EAAW,EAAQ,CAAO,EAG9C,IAAM,GAAA,EAAa,EAAA,QAAA,CAAQ,EAAc,eAAe,EACxD,MAAA,EAAM,EAAA,UAAA,CACJ,EACA,EAAqB,EAAkB,EAAc,CAAM,EAAG,CAAO,EACrE,MACF,CACF,CACF,EAEA,SAAS,EACP,EACA,EASQ,CACR,IAAM,EAAiB,EAAQ,SAAW,eAAe,KAAK,UAAU,EAAQ,QAAQ,IAAM,GAExF,EAAkB,CAAC,YAAa,WAAY,cAAc,CAAC,CAC9D,IAAK,GAAQ,CACZ,IAAM,EAAQ,EAAQ,GACtB,OAAO,GAAS,EAAM,OAAS,EAAI,KAAK,EAAI,IAAI,KAAK,UAAU,CAAK,IAAM,EAC5E,CAAC,CAAC,CACD,KAAK,EAAE,EACV,MAAO;;;;;uCAK8B,KAAK,UAAU,CAAS,EAAE;2CACtB,EAAQ,MAAQ,IAAK;;;;;gCAKhC,KAAK,UAAU,EAAQ,IAAI,EAAE,iBAAiB,KAAK,UAAU,EAAQ,WAAW,IAAI,IAAiB,EAAe;;;;;;;;;;;;;;;;;CAkBpJ,CAEA,SAAS,EAAkB,EAAc,EAAoB,CAC3D,IAAM,GAAA,EAAO,EAAA,SAAA,CAAS,EAAM,CAAE,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,GAAG,EACpD,MAAO,GAAG,EAAK,WAAW,GAAG,EAAI,EAAO,KAAK,IAAO,EACtD"}
|
package/dist/lib/adapters/bun.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { t as e } from "../route-scanner-XcLId7lK.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { t } from "../capabilities-C7txofmY.js";
|
|
3
|
+
import { n } from "../shared-D8w-neq2.js";
|
|
4
4
|
import { mkdir as r, rm as i, stat as a, writeFile as o } from "node:fs/promises";
|
|
5
5
|
import { relative as s, resolve as c } from "node:path";
|
|
6
6
|
//#region src/adapters/bun.ts
|
|
7
7
|
var l = {
|
|
8
8
|
name: "bun",
|
|
9
|
-
capabilities:
|
|
10
|
-
async build(
|
|
11
|
-
let s = c(
|
|
9
|
+
capabilities: t,
|
|
10
|
+
async build(t) {
|
|
11
|
+
let s = c(t.root), l = c(s, t.outDir), f = c(s, ".elur");
|
|
12
12
|
try {
|
|
13
13
|
await a(l);
|
|
14
14
|
} catch {
|
|
@@ -18,13 +18,21 @@ var l = {
|
|
|
18
18
|
recursive: !0,
|
|
19
19
|
force: !0
|
|
20
20
|
}), await r(f, { recursive: !0 });
|
|
21
|
-
let p = c(s,
|
|
22
|
-
await
|
|
21
|
+
let p = c(s, t.appDir), m = await e(p), h = c(f, "bun-index.ts");
|
|
22
|
+
await n(h, m, t);
|
|
23
23
|
let g = c(f, "bun-server.ts");
|
|
24
|
-
await o(g, u(d(f, l),
|
|
24
|
+
await o(g, u(d(f, l), t), "utf8");
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
27
|
function u(e, t) {
|
|
28
|
+
let n = t.logLevel ? `, logLevel: ${JSON.stringify(t.logLevel)}` : "", r = [
|
|
29
|
+
"redirects",
|
|
30
|
+
"rewrites",
|
|
31
|
+
"routeHeaders"
|
|
32
|
+
].map((e) => {
|
|
33
|
+
let n = t[e];
|
|
34
|
+
return n && n.length > 0 ? `, ${e}: ${JSON.stringify(n)}` : "";
|
|
35
|
+
}).join("");
|
|
28
36
|
return `// AUTO-GENERATED by @elurjs/kit. Do not edit.
|
|
29
37
|
import { fileURLToPath } from "node:url";
|
|
30
38
|
import { createWebHandler } from "@elurjs/kit/runtime";
|
|
@@ -36,7 +44,7 @@ const port = Number(process.env.PORT) || ${t.port ?? 3e3};
|
|
|
36
44
|
const webHandler = createWebHandler(
|
|
37
45
|
{ pages: [], api: [], error404: undefined, error500: undefined },
|
|
38
46
|
{},
|
|
39
|
-
{ staticRoot: outDir, lang: ${JSON.stringify(t.lang)}, clientEntry: ${JSON.stringify(t.clientEntry)} },
|
|
47
|
+
{ staticRoot: outDir, lang: ${JSON.stringify(t.lang)}, clientEntry: ${JSON.stringify(t.clientEntry)}${n}${r} },
|
|
40
48
|
);
|
|
41
49
|
|
|
42
50
|
Bun.serve({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bun.js","names":[],"sources":["../../../src/adapters/bun.ts"],"sourcesContent":["import { mkdir, rm, stat, writeFile } from \"node:fs/promises\";\nimport { relative, resolve } from \"node:path\";\nimport { scanRoutes } from \"../router/route-scanner.js\";\nimport type { Adapter } from \"./index.js\";\nimport { writeSsrEntry } from \"./shared.js\";\nimport { DEFAULT_CAPABILITIES } from \"../runtime/capabilities.js\";\n\n/**\n * Bun adapter for elur-kit.\n *\n * Produces a self-contained Bun server entry at `.elur/bun-server.ts`.\n * Run it with:\n *\n * bun run .elur/bun-server.ts\n *\n * The server serves static files from `dist/` and renders pages on demand for\n * unmatched routes.\n */\nexport const bunAdapter: Adapter = {\n name: \"bun\",\n capabilities: DEFAULT_CAPABILITIES,\n\n async build(options) {\n const root = resolve(options.root);\n const outDir = resolve(root, options.outDir);\n const generatedDir = resolve(root, \".elur\");\n\n // Verify the production build exists.\n try {\n await stat(outDir);\n } catch {\n throw new Error(\n `Output directory not found: ${outDir}. Run \"elur-kit build\" first.`,\n );\n }\n\n // Clean previous adapter output.\n await rm(generatedDir, { recursive: true, force: true });\n await mkdir(generatedDir, { recursive: true });\n\n // Scan routes and generate a self-contained SSR handler entry.\n const appDir = resolve(root, options.appDir);\n const routes = await scanRoutes(appDir);\n\n const entryPath = resolve(generatedDir, \"bun-index.ts\");\n await writeSsrEntry(entryPath, routes, options);\n\n // Write the Bun server entry.\n const serverPath = resolve(generatedDir, \"bun-server.ts\");\n await writeFile(\n serverPath,\n buildBunServerSource(relativeToUrlPath(generatedDir, outDir), options),\n \"utf8\",\n );\n },\n};\n\nfunction buildBunServerSource(\n outDirUrl: string,\n options: {\n clientEntry: string;\n lang: string;\n port?: number;\n },\n): string {\n return `// AUTO-GENERATED by @elurjs/kit. Do not edit.\nimport { fileURLToPath } from \"node:url\";\nimport { createWebHandler } from \"@elurjs/kit/runtime\";\nimport handler from \"./bun-index.ts\";\n\nconst outDir = fileURLToPath(new URL(${JSON.stringify(outDirUrl)}, import.meta.url));\nconst port = Number(process.env.PORT) || ${options.port ?? 3000};\n\nconst webHandler = createWebHandler(\n { pages: [], api: [], error404: undefined, error500: undefined },\n {},\n { staticRoot: outDir, lang: ${JSON.stringify(options.lang)}, clientEntry: ${JSON.stringify(options.clientEntry)} },\n);\n\nBun.serve({\n port,\n async fetch(request) {\n // Try static files first via the unified handler, then fall back to the\n // bundled SSR handler for dynamic routes.\n let response = await webHandler(request);\n if (response.status === 404) {\n response = await handler(request);\n }\n return response;\n },\n});\n\nconsole.log(\\`Bun server running at http://localhost:\\${port}\\`);\n`;\n}\n\nfunction relativeToUrlPath(from: string, to: string): string {\n const path = relative(from, to).split(\"\\\\\").join(\"/\");\n return `${path.startsWith(\".\") ? path : `./${path}`}/`;\n}\n"],"mappings":";;;;;;AAkBA,IAAa,IAAsB;CACjC,MAAM;CACN,cAAc;CAEd,MAAM,MAAM,GAAS;EACnB,IAAM,IAAO,EAAQ,EAAQ,IAAI,GAC3B,IAAS,EAAQ,GAAM,EAAQ,MAAM,GACrC,IAAe,EAAQ,GAAM,OAAO;EAG1C,IAAI;GACF,MAAM,EAAK,CAAM;EACnB,QAAQ;GACN,MAAU,MACR,+BAA+B,EAAO,8BACxC;EACF;EAIA,AADA,MAAM,EAAG,GAAc;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC,GACvD,MAAM,EAAM,GAAc,EAAE,WAAW,GAAK,CAAC;EAG7C,IAAM,IAAS,EAAQ,GAAM,EAAQ,MAAM,GACrC,IAAS,MAAM,EAAW,CAAM,GAEhC,IAAY,EAAQ,GAAc,cAAc;EACtD,MAAM,EAAc,GAAW,GAAQ,CAAO;EAG9C,IAAM,IAAa,EAAQ,GAAc,eAAe;EACxD,MAAM,EACJ,GACA,EAAqB,EAAkB,GAAc,CAAM,GAAG,CAAO,GACrE,MACF;CACF;AACF;AAEA,SAAS,EACP,GACA,
|
|
1
|
+
{"version":3,"file":"bun.js","names":[],"sources":["../../../src/adapters/bun.ts"],"sourcesContent":["import { mkdir, rm, stat, writeFile } from \"node:fs/promises\";\nimport { relative, resolve } from \"node:path\";\nimport { scanRoutes } from \"../router/route-scanner.js\";\nimport type { Adapter } from \"./index.js\";\nimport { writeSsrEntry } from \"./shared.js\";\nimport { DEFAULT_CAPABILITIES } from \"../runtime/capabilities.js\";\n\n/**\n * Bun adapter for elur-kit.\n *\n * Produces a self-contained Bun server entry at `.elur/bun-server.ts`.\n * Run it with:\n *\n * bun run .elur/bun-server.ts\n *\n * The server serves static files from `dist/` and renders pages on demand for\n * unmatched routes.\n */\nexport const bunAdapter: Adapter = {\n name: \"bun\",\n capabilities: DEFAULT_CAPABILITIES,\n\n async build(options) {\n const root = resolve(options.root);\n const outDir = resolve(root, options.outDir);\n const generatedDir = resolve(root, \".elur\");\n\n // Verify the production build exists.\n try {\n await stat(outDir);\n } catch {\n throw new Error(\n `Output directory not found: ${outDir}. Run \"elur-kit build\" first.`,\n );\n }\n\n // Clean previous adapter output.\n await rm(generatedDir, { recursive: true, force: true });\n await mkdir(generatedDir, { recursive: true });\n\n // Scan routes and generate a self-contained SSR handler entry.\n const appDir = resolve(root, options.appDir);\n const routes = await scanRoutes(appDir);\n\n const entryPath = resolve(generatedDir, \"bun-index.ts\");\n await writeSsrEntry(entryPath, routes, options);\n\n // Write the Bun server entry.\n const serverPath = resolve(generatedDir, \"bun-server.ts\");\n await writeFile(\n serverPath,\n buildBunServerSource(relativeToUrlPath(generatedDir, outDir), options),\n \"utf8\",\n );\n },\n};\n\nfunction buildBunServerSource(\n outDirUrl: string,\n options: {\n clientEntry: string;\n lang: string;\n port?: number;\n logLevel?: string;\n redirects?: import(\"../router/redirects.js\").RedirectRule[];\n rewrites?: import(\"../router/redirects.js\").RewriteRule[];\n routeHeaders?: import(\"../router/redirects.js\").RouteHeadersRule[];\n },\n): string {\n const logLevelOption = options.logLevel ? `, logLevel: ${JSON.stringify(options.logLevel)}` : \"\";\n // Rule arrays are plain data: emit them as JSON literals when defined.\n const routingOptions = ([\"redirects\", \"rewrites\", \"routeHeaders\"] as const)\n .map((key) => {\n const rules = options[key];\n return rules && rules.length > 0 ? `, ${key}: ${JSON.stringify(rules)}` : \"\";\n })\n .join(\"\");\n return `// AUTO-GENERATED by @elurjs/kit. Do not edit.\nimport { fileURLToPath } from \"node:url\";\nimport { createWebHandler } from \"@elurjs/kit/runtime\";\nimport handler from \"./bun-index.ts\";\n\nconst outDir = fileURLToPath(new URL(${JSON.stringify(outDirUrl)}, import.meta.url));\nconst port = Number(process.env.PORT) || ${options.port ?? 3000};\n\nconst webHandler = createWebHandler(\n { pages: [], api: [], error404: undefined, error500: undefined },\n {},\n { staticRoot: outDir, lang: ${JSON.stringify(options.lang)}, clientEntry: ${JSON.stringify(options.clientEntry)}${logLevelOption}${routingOptions} },\n);\n\nBun.serve({\n port,\n async fetch(request) {\n // Try static files first via the unified handler, then fall back to the\n // bundled SSR handler for dynamic routes.\n let response = await webHandler(request);\n if (response.status === 404) {\n response = await handler(request);\n }\n return response;\n },\n});\n\nconsole.log(\\`Bun server running at http://localhost:\\${port}\\`);\n`;\n}\n\nfunction relativeToUrlPath(from: string, to: string): string {\n const path = relative(from, to).split(\"\\\\\").join(\"/\");\n return `${path.startsWith(\".\") ? path : `./${path}`}/`;\n}\n"],"mappings":";;;;;;AAkBA,IAAa,IAAsB;CACjC,MAAM;CACN,cAAc;CAEd,MAAM,MAAM,GAAS;EACnB,IAAM,IAAO,EAAQ,EAAQ,IAAI,GAC3B,IAAS,EAAQ,GAAM,EAAQ,MAAM,GACrC,IAAe,EAAQ,GAAM,OAAO;EAG1C,IAAI;GACF,MAAM,EAAK,CAAM;EACnB,QAAQ;GACN,MAAU,MACR,+BAA+B,EAAO,8BACxC;EACF;EAIA,AADA,MAAM,EAAG,GAAc;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC,GACvD,MAAM,EAAM,GAAc,EAAE,WAAW,GAAK,CAAC;EAG7C,IAAM,IAAS,EAAQ,GAAM,EAAQ,MAAM,GACrC,IAAS,MAAM,EAAW,CAAM,GAEhC,IAAY,EAAQ,GAAc,cAAc;EACtD,MAAM,EAAc,GAAW,GAAQ,CAAO;EAG9C,IAAM,IAAa,EAAQ,GAAc,eAAe;EACxD,MAAM,EACJ,GACA,EAAqB,EAAkB,GAAc,CAAM,GAAG,CAAO,GACrE,MACF;CACF;AACF;AAEA,SAAS,EACP,GACA,GASQ;CACR,IAAM,IAAiB,EAAQ,WAAW,eAAe,KAAK,UAAU,EAAQ,QAAQ,MAAM,IAExF,IAAkB;EAAC;EAAa;EAAY;CAAc,CAAC,CAC9D,KAAK,MAAQ;EACZ,IAAM,IAAQ,EAAQ;EACtB,OAAO,KAAS,EAAM,SAAS,IAAI,KAAK,EAAI,IAAI,KAAK,UAAU,CAAK,MAAM;CAC5E,CAAC,CAAC,CACD,KAAK,EAAE;CACV,OAAO;;;;;uCAK8B,KAAK,UAAU,CAAS,EAAE;2CACtB,EAAQ,QAAQ,IAAK;;;;;gCAKhC,KAAK,UAAU,EAAQ,IAAI,EAAE,iBAAiB,KAAK,UAAU,EAAQ,WAAW,IAAI,IAAiB,EAAe;;;;;;;;;;;;;;;;;;AAkBpJ;AAEA,SAAS,EAAkB,GAAc,GAAoB;CAC3D,IAAM,IAAO,EAAS,GAAM,CAAE,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,GAAG;CACpD,OAAO,GAAG,EAAK,WAAW,GAAG,IAAI,IAAO,KAAK,IAAO;AACtD"}
|
|
@@ -18,6 +18,40 @@ export interface AdapterOptions {
|
|
|
18
18
|
lang: string;
|
|
19
19
|
/** Import specifier for hydrateIslands in the generated client entry. */
|
|
20
20
|
hydrateImport?: string;
|
|
21
|
+
/** Minimum log level for the runtime structured logger. */
|
|
22
|
+
logLevel?: import("../runtime/logger.js").LogLevel;
|
|
23
|
+
/**
|
|
24
|
+
* Pluggable ISR cache adapter (programmatic only). Note: the generated
|
|
25
|
+
* Node/Bun servers cannot serialize adapter instances — they only serve
|
|
26
|
+
* static files and delegate SSR to the bundled handler.
|
|
27
|
+
*/
|
|
28
|
+
cacheAdapter?: import("../cache/adapter.js").CacheAdapter;
|
|
29
|
+
/** Redirect rules evaluated before any routing. */
|
|
30
|
+
redirects?: import("../router/redirects.js").RedirectRule[];
|
|
31
|
+
/** Rewrite rules applied transparently before routing. */
|
|
32
|
+
rewrites?: import("../router/redirects.js").RewriteRule[];
|
|
33
|
+
/** Extra response headers applied to matching request paths. */
|
|
34
|
+
routeHeaders?: import("../router/redirects.js").RouteHeadersRule[];
|
|
35
|
+
/**
|
|
36
|
+
* Opt-in streaming SSR (experimental). Baked into the generated server:
|
|
37
|
+
* routes with a `loading` boundary stream shell-first when the adapter
|
|
38
|
+
* declares `capabilities.streaming: true`.
|
|
39
|
+
*/
|
|
40
|
+
streaming?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Client router flag baked into the generated server. When `enabled` is
|
|
43
|
+
* false the SSR output emits no client scripts and no render-endpoint
|
|
44
|
+
* marker; when a split `/_elur/router.js` chunk exists in the build output
|
|
45
|
+
* it is advertised as the router entry automatically.
|
|
46
|
+
*/
|
|
47
|
+
router?: {
|
|
48
|
+
enabled?: boolean;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Client JS mode baked into the generated server. `"legacy"` restores the
|
|
52
|
+
* unconditional single client entry (pre-0%-JS behavior).
|
|
53
|
+
*/
|
|
54
|
+
js?: "modern" | "legacy";
|
|
21
55
|
}
|
|
22
56
|
/**
|
|
23
57
|
* An adapter turns a elur-kit build into a deployment target output.
|
|
@@ -18,6 +18,40 @@ export interface AdapterOptions {
|
|
|
18
18
|
lang: string;
|
|
19
19
|
/** Import specifier for hydrateIslands in the generated client entry. */
|
|
20
20
|
hydrateImport?: string;
|
|
21
|
+
/** Minimum log level for the runtime structured logger. */
|
|
22
|
+
logLevel?: import("../runtime/logger.js").LogLevel;
|
|
23
|
+
/**
|
|
24
|
+
* Pluggable ISR cache adapter (programmatic only). Note: the generated
|
|
25
|
+
* Node/Bun servers cannot serialize adapter instances — they only serve
|
|
26
|
+
* static files and delegate SSR to the bundled handler.
|
|
27
|
+
*/
|
|
28
|
+
cacheAdapter?: import("../cache/adapter.js").CacheAdapter;
|
|
29
|
+
/** Redirect rules evaluated before any routing. */
|
|
30
|
+
redirects?: import("../router/redirects.js").RedirectRule[];
|
|
31
|
+
/** Rewrite rules applied transparently before routing. */
|
|
32
|
+
rewrites?: import("../router/redirects.js").RewriteRule[];
|
|
33
|
+
/** Extra response headers applied to matching request paths. */
|
|
34
|
+
routeHeaders?: import("../router/redirects.js").RouteHeadersRule[];
|
|
35
|
+
/**
|
|
36
|
+
* Opt-in streaming SSR (experimental). Baked into the generated server:
|
|
37
|
+
* routes with a `loading` boundary stream shell-first when the adapter
|
|
38
|
+
* declares `capabilities.streaming: true`.
|
|
39
|
+
*/
|
|
40
|
+
streaming?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Client router flag baked into the generated server. When `enabled` is
|
|
43
|
+
* false the SSR output emits no client scripts and no render-endpoint
|
|
44
|
+
* marker; when a split `/_elur/router.js` chunk exists in the build output
|
|
45
|
+
* it is advertised as the router entry automatically.
|
|
46
|
+
*/
|
|
47
|
+
router?: {
|
|
48
|
+
enabled?: boolean;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Client JS mode baked into the generated server. `"legacy"` restores the
|
|
52
|
+
* unconditional single client entry (pre-0%-JS behavior).
|
|
53
|
+
*/
|
|
54
|
+
js?: "modern" | "legacy";
|
|
21
55
|
}
|
|
22
56
|
/**
|
|
23
57
|
* An adapter turns a elur-kit build into a deployment target output.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../route-scanner-JY6YrNH5.cjs"),t=require("../
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../route-scanner-JY6YrNH5.cjs"),t=require("../capabilities-C0TDQoMY.cjs"),n=require("../shared-CP0Dc3cp.cjs");let r=require("node:fs/promises"),i=require("node:path"),a=require("vite");var o={name:`netlify`,capabilities:t.r,async build(t){let o=(0,i.resolve)(t.root),s=(0,i.resolve)(o,t.outDir),c=(0,i.resolve)(o,`netlify`),l=(0,i.join)(c,`functions`),u=(0,i.resolve)(o,`.elur`);try{await(0,r.stat)(s)}catch{throw Error(`Output directory not found: ${s}. Run "elur-kit build" first.`)}await(0,r.rm)(l,{recursive:!0,force:!0}),await(0,r.mkdir)(l,{recursive:!0}),await(0,r.mkdir)(u,{recursive:!0});let d=(0,i.resolve)(o,t.appDir),f=await e.t(d),p=(0,i.resolve)(u,`netlify-index.ts`);await n.n(p,f,t),await(0,a.build)({configFile:!1,root:o,build:{outDir:l,emptyOutDir:!0,ssr:!0,lib:{entry:p,formats:[`es`],fileName:()=>`__elur-js-kit.mjs`},rollupOptions:{external:[],output:{inlineDynamicImports:!0}}}});let m=(0,i.join)(l,`netlify-index.js`),h=(0,i.join)(l,`__elur-js-kit.mjs`);try{await(0,r.stat)(m),await(0,r.rename)(m,h)}catch{}await(0,r.writeFile)((0,i.join)(o,`netlify.toml`),`[build]
|
|
2
2
|
command = "elur-kit build"
|
|
3
3
|
publish = "dist"
|
|
4
4
|
|