@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
package/dist/lib/cli.js
CHANGED
|
@@ -6,10 +6,12 @@ import { existsSync, watch } from "node:fs";
|
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { renderToString } from "@elurjs/core/server";
|
|
9
|
-
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
10
9
|
import "@elurjs/core";
|
|
10
|
+
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
11
11
|
import { consumeImageRegistry, setImageManifest } from "./image/registry.js";
|
|
12
12
|
import { build, loadConfigFromFile } from "vite";
|
|
13
|
+
import { networkInterfaces } from "node:os";
|
|
14
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
15
|
//#region \0rolldown/runtime.js
|
|
14
16
|
var __defProp = Object.defineProperty;
|
|
15
17
|
var __esmMin = (fn, res, err) => () => {
|
|
@@ -201,8 +203,35 @@ function toIdentifier(name, index) {
|
|
|
201
203
|
const cleaned = name.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
202
204
|
return /^[a-zA-Z_$]/.test(cleaned) ? `${cleaned}_${index}` : `_${cleaned}_${index}`;
|
|
203
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Source for the standalone router module emitted in split builds. Router
|
|
208
|
+
* options are baked in so the public module takes no arguments.
|
|
209
|
+
*/
|
|
210
|
+
function buildRouterEntrySource(routerImport = "@elurjs/kit/router", options = {}) {
|
|
211
|
+
const opts = {};
|
|
212
|
+
if (options.prefetch === false) opts.prefetch = false;
|
|
213
|
+
if (options.morph === true) opts.morph = true;
|
|
214
|
+
if (options.loadingIndicator === true) opts.loadingIndicator = true;
|
|
215
|
+
const args = Object.keys(opts).length > 0 ? JSON.stringify(opts) : "";
|
|
216
|
+
return `// AUTO-GENERATED by @elurjs/kit. Do not edit.
|
|
217
|
+
import { startClientRouter } from ${JSON.stringify(routerImport)};
|
|
218
|
+
|
|
219
|
+
startClientRouter(${args});
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Serializes the startClientRouter options embedded in a combined entry.
|
|
224
|
+
* Only non-default flags are emitted to keep the generated code minimal.
|
|
225
|
+
*/
|
|
226
|
+
function routerCallArgs(router) {
|
|
227
|
+
const opts = {};
|
|
228
|
+
if (router?.prefetch === false) opts.prefetch = false;
|
|
229
|
+
if (router?.morph === true) opts.morph = true;
|
|
230
|
+
if (router?.loadingIndicator === true) opts.loadingIndicator = true;
|
|
231
|
+
return Object.keys(opts).length > 0 ? JSON.stringify(opts) : "";
|
|
232
|
+
}
|
|
204
233
|
/** Builds the source code of the client entry module. */
|
|
205
|
-
function buildEntrySource(islands, outFile, hydrateImport = "@elurjs/kit/island", routerImport = "@elurjs/kit/router") {
|
|
234
|
+
function buildEntrySource(islands, outFile, hydrateImport = "@elurjs/kit/island", routerImport = "@elurjs/kit/router", router) {
|
|
206
235
|
const registryLines = islands.map((island, i) => ({
|
|
207
236
|
ident: toIdentifier(island.name, i),
|
|
208
237
|
name: island.name,
|
|
@@ -211,18 +240,36 @@ function buildEntrySource(islands, outFile, hydrateImport = "@elurjs/kit/island"
|
|
|
211
240
|
const islandHydration = registryLines ? `const registry = {
|
|
212
241
|
${registryLines}
|
|
213
242
|
};
|
|
214
|
-
// Defer hydration until the browser is idle so the first paint is not
|
|
215
|
-
// blocked. Islands with directive "load" already have SSR-rendered DOM,
|
|
216
|
-
// so the user sees content immediately — hydration only adds interactivity.
|
|
217
243
|
const hydrate = () => hydrateIslands(registry);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
244
|
+
// Islands inside persisted nodes must not be disposed across navigations.
|
|
245
|
+
// The router announces the survivors in the elur:before-render detail; the
|
|
246
|
+
// DOM query fallback covers hosts that dispatch it without detail.
|
|
247
|
+
const cleanup = (persisted) =>
|
|
248
|
+
cleanupHydratedIslands({
|
|
249
|
+
except: persisted ?? document.querySelectorAll("[data-elur-persist]"),
|
|
250
|
+
});
|
|
251
|
+
let sawBeforeRender = false;
|
|
252
|
+
|
|
253
|
+
// Hydrate right after parse: module scripts are deferred, so this already
|
|
254
|
+
// runs once the DOM is ready. Directives schedule themselves inside
|
|
255
|
+
// hydrateIslands — "load"/"only" run immediately (real Astro semantics),
|
|
256
|
+
// "idle"/"visible" keep their deferred scheduling.
|
|
257
|
+
hydrate();
|
|
258
|
+
|
|
259
|
+
// The router dispatches elur:before-render BEFORE swapping #app: islands are
|
|
260
|
+
// disposed while still attached (Fase A2), except persisted subtrees.
|
|
261
|
+
document.addEventListener("elur:before-render", (event) => {
|
|
262
|
+
sawBeforeRender = true;
|
|
263
|
+
cleanup(event.detail?.persisted);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Re-hydrate after SPA navigations.
|
|
223
267
|
document.addEventListener("elur:rendered", () => {
|
|
224
|
-
|
|
225
|
-
|
|
268
|
+
// Compat fallback: hosts that only dispatch elur:rendered (streaming swap
|
|
269
|
+
// script, hand-rolled integrations) still need the cleanup pass.
|
|
270
|
+
if (!sawBeforeRender) cleanup();
|
|
271
|
+
sawBeforeRender = false;
|
|
272
|
+
hydrate();
|
|
226
273
|
});
|
|
227
274
|
|
|
228
275
|
// Vite HMR: when an island module (or the entry itself) updates, dispose the
|
|
@@ -230,20 +277,15 @@ document.addEventListener("elur:rendered", () => {
|
|
|
230
277
|
// dynamic import() resolves to the fresh modules, so no full page reload is
|
|
231
278
|
// needed (progressive enhancement, audit §10.2 / §12.2).
|
|
232
279
|
if (import.meta.hot) {
|
|
233
|
-
import.meta.hot.accept((
|
|
280
|
+
import.meta.hot.accept(() => {
|
|
234
281
|
cleanupHydratedIslands();
|
|
235
282
|
hydrateIslands(registry);
|
|
236
|
-
if (newModule) {
|
|
237
|
-
// Re-run the module so its side effects (router, listeners) apply.
|
|
238
|
-
}
|
|
239
283
|
});
|
|
240
284
|
}` : "";
|
|
285
|
+
const embedRouter = router ? router.enabled !== false && !router.separate : true;
|
|
241
286
|
return `// AUTO-GENERATED by @elurjs/kit. Do not edit.
|
|
242
|
-
import { startClientRouter } from ${JSON.stringify(routerImport)};
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
startClientRouter();
|
|
246
|
-
${islandHydration}
|
|
287
|
+
${embedRouter ? `import { startClientRouter } from ${JSON.stringify(routerImport)};\n` : ""}import { hydrateIslands, cleanupHydratedIslands } from ${JSON.stringify(hydrateImport)};
|
|
288
|
+
${embedRouter ? `\nstartClientRouter(${routerCallArgs(router)});\n` : ""}${islandHydration}
|
|
247
289
|
`;
|
|
248
290
|
}
|
|
249
291
|
/** Computes a POSIX-style relative import specifier between two files. */
|
|
@@ -253,15 +295,23 @@ function toImportSpecifier(fromFile, toFile) {
|
|
|
253
295
|
return spec;
|
|
254
296
|
}
|
|
255
297
|
/**
|
|
256
|
-
* Generates and writes the client entry module for the given islands.
|
|
298
|
+
* Generates and writes the client entry module for the given islands. When
|
|
299
|
+
* `options.router.separate` is set (and the router is enabled), a standalone
|
|
300
|
+
* router module is written alongside it (default: `router.ts` next to the
|
|
301
|
+
* entry — bundle it as a second input to emit `/_elur/router.js`).
|
|
257
302
|
*
|
|
258
303
|
* @param options Generation options.
|
|
259
304
|
* @returns The absolute path of the written entry file.
|
|
260
305
|
*/
|
|
261
306
|
async function generateClientEntry(options) {
|
|
262
|
-
const source = buildEntrySource(options.islands, options.outFile, options.hydrateImport, options.routerImport);
|
|
307
|
+
const source = buildEntrySource(options.islands, options.outFile, options.hydrateImport, options.routerImport, options.router);
|
|
263
308
|
await mkdir(dirname(options.outFile), { recursive: true });
|
|
264
309
|
await writeFile(options.outFile, source, "utf8");
|
|
310
|
+
if (options.router) {
|
|
311
|
+
const routerFile = options.router.outFile ?? join(dirname(options.outFile), "router.ts");
|
|
312
|
+
const routerSource = options.router.enabled === false ? `// AUTO-GENERATED by @elurjs/kit. Do not edit.\n// router.enabled: false — intentionally empty.\nexport {};\n` : buildRouterEntrySource(options.routerImport ?? "@elurjs/kit/router", options.router);
|
|
313
|
+
await writeFile(routerFile, routerSource, "utf8");
|
|
314
|
+
}
|
|
265
315
|
return options.outFile;
|
|
266
316
|
}
|
|
267
317
|
//#endregion
|
|
@@ -301,6 +351,18 @@ var init_render_to_string = __esmMin((() => {
|
|
|
301
351
|
}));
|
|
302
352
|
//#endregion
|
|
303
353
|
//#region src/build/document-shell.ts
|
|
354
|
+
/**
|
|
355
|
+
* Extracts the inner HTML of `#app` from a full document produced by
|
|
356
|
+
* `documentShell`. Returns `undefined` when the markers are missing (e.g. a
|
|
357
|
+
* hand-written document).
|
|
358
|
+
*/
|
|
359
|
+
function extractAppBody(html) {
|
|
360
|
+
const start = html.indexOf(APP_START_MARKER);
|
|
361
|
+
if (start < 0) return void 0;
|
|
362
|
+
const end = html.indexOf(APP_END_MARKER, start + 21);
|
|
363
|
+
if (end < 0) return void 0;
|
|
364
|
+
return html.slice(start + 21, end);
|
|
365
|
+
}
|
|
304
366
|
function escapeHtml(value) {
|
|
305
367
|
return value.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c]);
|
|
306
368
|
}
|
|
@@ -348,12 +410,33 @@ function buildHeadTags(metadata, fallbackTitle) {
|
|
|
348
410
|
if (metadata.other) for (const [name, content] of Object.entries(metadata.other)) tags.push(`<meta data-elur-head name="${escapeHtml(name)}" content="${escapeHtml(content)}" />`);
|
|
349
411
|
return tags.map((t) => `\n ${t}`).join("");
|
|
350
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Document-level Speculation Rules (Chromium-only, ignored elsewhere).
|
|
415
|
+
*
|
|
416
|
+
* `href_matches: "/*"` scopes the rule to same-origin path links; the
|
|
417
|
+
* `selector_matches` exclusions keep downloads, new-tab links, router-opt-outs
|
|
418
|
+
* and explicit `data-no-speculation` links out of speculation. Actions are
|
|
419
|
+
* POST endpoints reached through forms/`callAction`, never through document
|
|
420
|
+
* links, so they are not speculated. `eagerness: "moderate"` speculates on
|
|
421
|
+
* hover — the same trigger as the client router's prefetch.
|
|
422
|
+
*/
|
|
423
|
+
function speculationRulesScript(mode) {
|
|
424
|
+
return `\n <script type="speculationrules">${JSON.stringify({ [mode]: [{
|
|
425
|
+
source: "document",
|
|
426
|
+
where: { and: [{ href_matches: "/*" }, { not: { selector_matches: "a[download], a[target], a[data-no-router], a[data-no-speculation]" } }] },
|
|
427
|
+
eagerness: "moderate"
|
|
428
|
+
}] })}<\/script>`;
|
|
429
|
+
}
|
|
351
430
|
/** Wraps rendered body HTML into a full HTML document. */
|
|
352
431
|
function documentShell(opts) {
|
|
353
|
-
const { body, title = "Elur Kit App", lang = "es", data, actions, clientEntry, htmlAttributes, headScripts, headLinks, metadata } = opts;
|
|
432
|
+
const { body, title = "Elur Kit App", lang = "es", data, actions, clientEntry, routerEntry, htmlAttributes, headScripts, headLinks, metadata } = opts;
|
|
354
433
|
const dataScript = data !== void 0 ? `\n <script type="application/json" id="elur-data">${serializeData(data)}<\/script>` : "";
|
|
355
434
|
const actionsScript = actions && Object.keys(actions).length > 0 ? `\n <script type="application/json" id="elur-actions">${serializeData(actions)}<\/script>` : "";
|
|
435
|
+
const modulePreload = (src) => `\n <link rel="modulepreload" href="${escapeHtml(src)}" />`;
|
|
436
|
+
const preloads = (clientEntry ? modulePreload(clientEntry) : "") + (routerEntry ? modulePreload(routerEntry) : "");
|
|
356
437
|
const entryScript = clientEntry ? `\n <script type="module" src="${escapeHtml(clientEntry)}"><\/script>` : "";
|
|
438
|
+
const routerScript = routerEntry ? `\n <script type="module" src="${escapeHtml(routerEntry)}"><\/script>` : "";
|
|
439
|
+
const speculationScript = opts.speculation ? speculationRulesScript(opts.speculation) : "";
|
|
357
440
|
const htmlAttrs = htmlAttributes ? Object.entries(htmlAttributes).filter(([, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => ` ${escapeHtml(key)}="${escapeHtml(String(value))}"`).join("") : "";
|
|
358
441
|
const headScriptsHtml = headScripts ? headScripts.filter((script) => typeof script === "string" && script.trim().length > 0).map((script) => {
|
|
359
442
|
if (script.trimStart().startsWith("<script")) return `\n ${script}`;
|
|
@@ -362,20 +445,20 @@ function documentShell(opts) {
|
|
|
362
445
|
const headTags = metadata ? buildHeadTags(metadata, title) : "";
|
|
363
446
|
const titleTag = metadata?.title ? "" : `\n <title>${escapeHtml(title)}</title>`;
|
|
364
447
|
const headLinksHtml = headLinks ? headLinks.filter((link) => typeof link === "string" && link.trim().length > 0).map((link) => `\n ${link}`).join("") : "";
|
|
365
|
-
const renderEndpointMeta = opts.renderEndpoint === false ? "\n <meta name=\"elur:render-endpoint\" content=\"off\" />" : "";
|
|
448
|
+
const renderEndpointMeta = opts.renderEndpoint === false && opts.routerEnabled !== false ? "\n <meta name=\"elur:render-endpoint\" content=\"off\" />" : "";
|
|
366
449
|
return `<!DOCTYPE html>
|
|
367
450
|
<html lang="${escapeHtml(lang)}"${htmlAttrs}>
|
|
368
451
|
<head>
|
|
369
452
|
<meta charset="utf-8" />
|
|
370
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />${renderEndpointMeta}${titleTag}${headTags}${headLinksHtml}${headScriptsHtml}
|
|
453
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />${renderEndpointMeta}${titleTag}${headTags}${headLinksHtml}${headScriptsHtml}${preloads}${speculationScript}
|
|
371
454
|
</head>
|
|
372
455
|
<body>
|
|
373
|
-
<div id="app">${body}</div>${dataScript}${actionsScript}${entryScript}
|
|
456
|
+
<div id="app">${APP_START_MARKER}${body}${APP_END_MARKER}</div>${dataScript}${actionsScript}${entryScript}${routerScript}
|
|
374
457
|
</body>
|
|
375
458
|
</html>
|
|
376
459
|
`;
|
|
377
460
|
}
|
|
378
|
-
var HTML_ESCAPES;
|
|
461
|
+
var HTML_ESCAPES, APP_START_MARKER, APP_END_MARKER;
|
|
379
462
|
var init_document_shell = __esmMin((() => {
|
|
380
463
|
HTML_ESCAPES = {
|
|
381
464
|
"&": "&",
|
|
@@ -384,6 +467,14 @@ var init_document_shell = __esmMin((() => {
|
|
|
384
467
|
"\"": """,
|
|
385
468
|
"'": "'"
|
|
386
469
|
};
|
|
470
|
+
APP_START_MARKER = "<!--elur:app:start-->";
|
|
471
|
+
APP_END_MARKER = "<!--elur:app:end-->";
|
|
472
|
+
}));
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/island/island.ts
|
|
475
|
+
var ISLAND_MARKER_ATTR;
|
|
476
|
+
var init_island = __esmMin((() => {
|
|
477
|
+
ISLAND_MARKER_ATTR = "data-elur-island";
|
|
387
478
|
}));
|
|
388
479
|
//#endregion
|
|
389
480
|
//#region src/action/error-store.ts
|
|
@@ -560,7 +651,7 @@ function collectShellExtras(pageData, layoutDataList) {
|
|
|
560
651
|
};
|
|
561
652
|
}
|
|
562
653
|
async function renderPage(options) {
|
|
563
|
-
const { route, params = {}, searchParams = new URLSearchParams(), config, importer = defaultImport$
|
|
654
|
+
const { route, params = {}, searchParams = new URLSearchParams(), config, importer = defaultImport$2, actions, request } = options;
|
|
564
655
|
const { default: PageComponent, generateMetadata } = await importer(route.pagePath);
|
|
565
656
|
let data;
|
|
566
657
|
let revalidate;
|
|
@@ -667,6 +758,16 @@ async function renderPage(options) {
|
|
|
667
758
|
});
|
|
668
759
|
if (!metadata) metadata = extractMetadata(data) ?? extractMetadataFromList(layoutDataList);
|
|
669
760
|
const resolvedTitle = metadata?.title ?? title;
|
|
761
|
+
const hasIslands = body.includes(ISLAND_MARKER_ATTR);
|
|
762
|
+
const routerCfg = config.router;
|
|
763
|
+
const routerEnabled = routerCfg?.enabled !== false;
|
|
764
|
+
let clientEntry;
|
|
765
|
+
let routerEntry;
|
|
766
|
+
if (!routerCfg || config.js === "legacy") clientEntry = config.clientEntry;
|
|
767
|
+
else if (routerCfg.entry) {
|
|
768
|
+
if (hasIslands) clientEntry = config.clientEntry;
|
|
769
|
+
if (routerEnabled) routerEntry = routerCfg.entry;
|
|
770
|
+
} else if (hasIslands || routerEnabled) clientEntry = config.clientEntry;
|
|
670
771
|
const html = documentShell({
|
|
671
772
|
title: resolvedTitle,
|
|
672
773
|
lang: config.lang,
|
|
@@ -677,7 +778,10 @@ async function renderPage(options) {
|
|
|
677
778
|
headScripts,
|
|
678
779
|
headLinks,
|
|
679
780
|
metadata,
|
|
680
|
-
clientEntry
|
|
781
|
+
clientEntry,
|
|
782
|
+
routerEntry,
|
|
783
|
+
routerEnabled: routerCfg ? routerEnabled : void 0,
|
|
784
|
+
speculation: routerCfg?.speculation,
|
|
681
785
|
renderEndpoint: config.renderEndpoint
|
|
682
786
|
});
|
|
683
787
|
const head = metadata ? buildHeadTags(metadata, resolvedTitle) : "";
|
|
@@ -687,7 +791,8 @@ async function renderPage(options) {
|
|
|
687
791
|
clearActionErrorCookie,
|
|
688
792
|
head,
|
|
689
793
|
resolvedTitle,
|
|
690
|
-
cachePolicy
|
|
794
|
+
cachePolicy,
|
|
795
|
+
data
|
|
691
796
|
};
|
|
692
797
|
}
|
|
693
798
|
/** Extracts a `metadata` field from a loader data object, if present. */
|
|
@@ -725,13 +830,14 @@ async function renderErrorPage(options) {
|
|
|
725
830
|
return;
|
|
726
831
|
}
|
|
727
832
|
}
|
|
728
|
-
var defaultImport$
|
|
833
|
+
var defaultImport$2;
|
|
729
834
|
var init_render = __esmMin((() => {
|
|
730
835
|
init_render_to_string();
|
|
731
836
|
init_document_shell();
|
|
837
|
+
init_island();
|
|
732
838
|
init_error_store();
|
|
733
839
|
init_policy();
|
|
734
|
-
defaultImport$
|
|
840
|
+
defaultImport$2 = (path) => import(path);
|
|
735
841
|
}));
|
|
736
842
|
//#endregion
|
|
737
843
|
//#region src/action/scan.ts
|
|
@@ -770,6 +876,14 @@ function actionNames(actions) {
|
|
|
770
876
|
}
|
|
771
877
|
var init_scan = __esmMin((() => {}));
|
|
772
878
|
//#endregion
|
|
879
|
+
//#region __vite-optional-peer-dep:sharp:@elurjs/kit
|
|
880
|
+
var kit_exports = /* @__PURE__ */ __exportAll({ default: () => kit_default });
|
|
881
|
+
var kit_default;
|
|
882
|
+
var init_kit = __esmMin((() => {
|
|
883
|
+
kit_default = {};
|
|
884
|
+
throw new Error(`Could not resolve "sharp" imported by "@elurjs/kit". Is it installed?`);
|
|
885
|
+
}));
|
|
886
|
+
//#endregion
|
|
773
887
|
//#region src/image/service.ts
|
|
774
888
|
init_render();
|
|
775
889
|
init_scan();
|
|
@@ -783,7 +897,7 @@ async function loadSharp() {
|
|
|
783
897
|
if (sharpLoader === null) return null;
|
|
784
898
|
if (sharpLoader) return sharpLoader();
|
|
785
899
|
try {
|
|
786
|
-
const sharp = (await
|
|
900
|
+
const sharp = (await Promise.resolve().then(() => (init_kit(), kit_exports))).default;
|
|
787
901
|
if (typeof sharp !== "function") {
|
|
788
902
|
sharpLoader = null;
|
|
789
903
|
return null;
|
|
@@ -1041,6 +1155,132 @@ async function runIntegrationHook(integrations, hook, args) {
|
|
|
1041
1155
|
}
|
|
1042
1156
|
}
|
|
1043
1157
|
//#endregion
|
|
1158
|
+
//#region src/seo/index.ts
|
|
1159
|
+
/**
|
|
1160
|
+
* SEO utilities — sitemap.xml and robots.txt generation.
|
|
1161
|
+
*
|
|
1162
|
+
* @module
|
|
1163
|
+
*/
|
|
1164
|
+
/**
|
|
1165
|
+
* Generates a `sitemap.xml` file from a list of URLs.
|
|
1166
|
+
*
|
|
1167
|
+
* @example
|
|
1168
|
+
* ```ts
|
|
1169
|
+
* import { generateSitemap } from "@elurjs/kit/seo";
|
|
1170
|
+
*
|
|
1171
|
+
* await generateSitemap({
|
|
1172
|
+
* siteUrl: "https://elur-kit.dev",
|
|
1173
|
+
* outDir: "./dist",
|
|
1174
|
+
* urls: [
|
|
1175
|
+
* "/",
|
|
1176
|
+
* "/docs/introduction",
|
|
1177
|
+
* { url: "/docs/routing", changefreq: "weekly", priority: 0.8 },
|
|
1178
|
+
* ],
|
|
1179
|
+
* });
|
|
1180
|
+
* ```
|
|
1181
|
+
*/
|
|
1182
|
+
async function generateSitemap(config) {
|
|
1183
|
+
const { siteUrl, urls, outDir } = config;
|
|
1184
|
+
const base = siteUrl.replace(/\/$/, "");
|
|
1185
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1186
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
1187
|
+
${urls.map((entry) => {
|
|
1188
|
+
const e = typeof entry === "string" ? { url: entry } : entry;
|
|
1189
|
+
const lines = [` <url>`, ` <loc>${escapeXml$1(`${base}${e.url.startsWith("/") ? "" : "/"}${e.url}`)}</loc>`];
|
|
1190
|
+
if (e.lastmod) lines.push(` <lastmod>${e.lastmod}</lastmod>`);
|
|
1191
|
+
if (e.changefreq) lines.push(` <changefreq>${e.changefreq}</changefreq>`);
|
|
1192
|
+
if (e.priority !== void 0) lines.push(` <priority>${e.priority.toFixed(1)}</priority>`);
|
|
1193
|
+
lines.push(` </url>`);
|
|
1194
|
+
return lines.join("\n");
|
|
1195
|
+
}).join("\n")}
|
|
1196
|
+
</urlset>
|
|
1197
|
+
`;
|
|
1198
|
+
const filePath = join(outDir, "sitemap.xml");
|
|
1199
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
1200
|
+
await writeFile(filePath, xml, "utf8");
|
|
1201
|
+
return filePath;
|
|
1202
|
+
}
|
|
1203
|
+
function escapeXml$1(str) {
|
|
1204
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1205
|
+
}
|
|
1206
|
+
//#endregion
|
|
1207
|
+
//#region src/seo/sitemap-from-routes.ts
|
|
1208
|
+
/**
|
|
1209
|
+
* Generates a sitemap.xml from the route manifest.
|
|
1210
|
+
*
|
|
1211
|
+
* Static routes are included automatically. Dynamic routes require the author
|
|
1212
|
+
* to provide URLs via `extraUrls` or a `generateSitemapUrls()` export.
|
|
1213
|
+
*
|
|
1214
|
+
* For large sites (>50,000 URLs), a sitemap index is generated.
|
|
1215
|
+
*/
|
|
1216
|
+
async function generateSitemapFromRoutes(options) {
|
|
1217
|
+
const { siteUrl, outDir, routes, extraUrls = [], maxUrlsPerSitemap = 5e4 } = options;
|
|
1218
|
+
const routeUrls = [];
|
|
1219
|
+
for (const page of routes.pages) {
|
|
1220
|
+
if (page.params.length > 0) continue;
|
|
1221
|
+
if (page.path === "/404" || page.path === "/500") continue;
|
|
1222
|
+
if (page.path.startsWith("/_elur") || page.path.startsWith("/__elur-js")) continue;
|
|
1223
|
+
routeUrls.push({
|
|
1224
|
+
url: page.path,
|
|
1225
|
+
changefreq: options.defaultChangefreq,
|
|
1226
|
+
priority: options.defaultPriority
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
const allUrls = [...routeUrls, ...extraUrls];
|
|
1230
|
+
if (allUrls.length <= maxUrlsPerSitemap) return [await generateSitemap({
|
|
1231
|
+
siteUrl,
|
|
1232
|
+
outDir,
|
|
1233
|
+
urls: allUrls
|
|
1234
|
+
})];
|
|
1235
|
+
return generateSitemapIndex({
|
|
1236
|
+
siteUrl,
|
|
1237
|
+
outDir,
|
|
1238
|
+
urls: allUrls,
|
|
1239
|
+
maxUrlsPerSitemap
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Generates a sitemap index file that references multiple sitemap files.
|
|
1244
|
+
* Used for large sites (>50,000 URLs).
|
|
1245
|
+
*/
|
|
1246
|
+
async function generateSitemapIndex(options) {
|
|
1247
|
+
const { siteUrl, outDir, urls, maxUrlsPerSitemap } = options;
|
|
1248
|
+
const base = siteUrl.replace(/\/$/, "");
|
|
1249
|
+
const files = [];
|
|
1250
|
+
const sitemapUrls = [];
|
|
1251
|
+
for (let i = 0; i < urls.length; i += maxUrlsPerSitemap) {
|
|
1252
|
+
const chunk = urls.slice(i, i + maxUrlsPerSitemap);
|
|
1253
|
+
const filename = `sitemap-${Math.floor(i / maxUrlsPerSitemap) + 1}.xml`;
|
|
1254
|
+
const { writeFile, mkdir } = await import("node:fs/promises");
|
|
1255
|
+
const { join, dirname } = await import("node:path");
|
|
1256
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${chunk.map((entry) => {
|
|
1257
|
+
const e = typeof entry === "string" ? { url: entry } : entry;
|
|
1258
|
+
const lines = [` <url>`, ` <loc>${escapeXml(`${base}${e.url.startsWith("/") ? "" : "/"}${e.url}`)}</loc>`];
|
|
1259
|
+
if (e.lastmod) lines.push(` <lastmod>${e.lastmod}</lastmod>`);
|
|
1260
|
+
if (e.changefreq) lines.push(` <changefreq>${e.changefreq}</changefreq>`);
|
|
1261
|
+
if (e.priority !== void 0) lines.push(` <priority>${e.priority.toFixed(1)}</priority>`);
|
|
1262
|
+
lines.push(` </url>`);
|
|
1263
|
+
return lines.join("\n");
|
|
1264
|
+
}).join("\n")}\n</urlset>\n`;
|
|
1265
|
+
const filePath = join(outDir, filename);
|
|
1266
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
1267
|
+
await writeFile(filePath, xml, "utf8");
|
|
1268
|
+
files.push(filePath);
|
|
1269
|
+
sitemapUrls.push(`${base}/${filename}`);
|
|
1270
|
+
}
|
|
1271
|
+
const { writeFile, mkdir } = await import("node:fs/promises");
|
|
1272
|
+
const { join, dirname } = await import("node:path");
|
|
1273
|
+
const indexXml = `<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${sitemapUrls.map((url) => ` <sitemap>\n <loc>${escapeXml(url)}</loc>\n </sitemap>`).join("\n")}\n</sitemapindex>\n`;
|
|
1274
|
+
const indexPath = join(outDir, "sitemap.xml");
|
|
1275
|
+
await mkdir(dirname(indexPath), { recursive: true });
|
|
1276
|
+
await writeFile(indexPath, indexXml, "utf8");
|
|
1277
|
+
files.push(indexPath);
|
|
1278
|
+
return files;
|
|
1279
|
+
}
|
|
1280
|
+
function escapeXml(str) {
|
|
1281
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1282
|
+
}
|
|
1283
|
+
//#endregion
|
|
1044
1284
|
//#region src/build/build.ts
|
|
1045
1285
|
init_route_scanner();
|
|
1046
1286
|
function urlToFilePath(outDir, urlPath) {
|
|
@@ -1077,8 +1317,14 @@ async function build$1(config) {
|
|
|
1077
1317
|
} catch (error) {
|
|
1078
1318
|
if (error.code !== "ENOENT") throw error;
|
|
1079
1319
|
}
|
|
1320
|
+
const reportPhase = (name, start) => {
|
|
1321
|
+
config.onPhase?.(name, performance.now() - start);
|
|
1322
|
+
};
|
|
1323
|
+
let phaseStart = performance.now();
|
|
1080
1324
|
const routes = await scanRoutes(config.appDir);
|
|
1081
|
-
const
|
|
1325
|
+
const actions = await scanActions(config.appDir);
|
|
1326
|
+
reportPhase("scan", phaseStart);
|
|
1327
|
+
const publicActions = actionNames(actions);
|
|
1082
1328
|
const result = {
|
|
1083
1329
|
pages: 0,
|
|
1084
1330
|
skipped: [],
|
|
@@ -1092,8 +1338,17 @@ async function build$1(config) {
|
|
|
1092
1338
|
islands: result.islands,
|
|
1093
1339
|
outFile: config.generatedEntry,
|
|
1094
1340
|
hydrateImport: config.hydrateImport,
|
|
1095
|
-
routerImport: config.routerImport
|
|
1341
|
+
routerImport: config.routerImport,
|
|
1342
|
+
router: config.router ? {
|
|
1343
|
+
enabled: config.router.enabled !== false,
|
|
1344
|
+
prefetch: config.router.prefetch,
|
|
1345
|
+
morph: config.router.morph,
|
|
1346
|
+
loadingIndicator: config.router.loadingIndicator,
|
|
1347
|
+
separate: config.router.separate === true && config.js !== "legacy",
|
|
1348
|
+
outFile: config.router.outFile
|
|
1349
|
+
} : void 0
|
|
1096
1350
|
});
|
|
1351
|
+
phaseStart = performance.now();
|
|
1097
1352
|
for (const route of routes.pages) {
|
|
1098
1353
|
if (!isDynamic(route.path)) {
|
|
1099
1354
|
const filePath = await buildPage(config, route, publicActions);
|
|
@@ -1111,7 +1366,9 @@ async function build$1(config) {
|
|
|
1111
1366
|
const errorConfig = {
|
|
1112
1367
|
lang: config.lang,
|
|
1113
1368
|
clientEntry: config.clientEntry,
|
|
1114
|
-
renderEndpoint: false
|
|
1369
|
+
renderEndpoint: false,
|
|
1370
|
+
router: pageRouterConfig(config),
|
|
1371
|
+
js: config.js
|
|
1115
1372
|
};
|
|
1116
1373
|
if (routes.error404) {
|
|
1117
1374
|
const result404 = await renderErrorPage({
|
|
@@ -1141,9 +1398,11 @@ async function build$1(config) {
|
|
|
1141
1398
|
result.files.push(filePath);
|
|
1142
1399
|
}
|
|
1143
1400
|
}
|
|
1401
|
+
reportPhase("pages", phaseStart);
|
|
1144
1402
|
const registeredImages = consumeImageRegistry();
|
|
1145
1403
|
let manifest = null;
|
|
1146
1404
|
if (registeredImages.length > 0 && config.publicDir) {
|
|
1405
|
+
phaseStart = performance.now();
|
|
1147
1406
|
const manifestPath = join(config.outDir, ".elur", "image-manifest.json");
|
|
1148
1407
|
const processResult = await processImageBatch(registeredImages, {
|
|
1149
1408
|
publicDir: config.publicDir,
|
|
@@ -1200,12 +1459,33 @@ async function build$1(config) {
|
|
|
1200
1459
|
}
|
|
1201
1460
|
}
|
|
1202
1461
|
}
|
|
1462
|
+
reportPhase("images", phaseStart);
|
|
1203
1463
|
}
|
|
1204
1464
|
setImageManifest(null);
|
|
1205
|
-
if (config.integrations && config.integrations.length > 0)
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1465
|
+
if (config.integrations && config.integrations.length > 0) {
|
|
1466
|
+
phaseStart = performance.now();
|
|
1467
|
+
await runIntegrationHook(config.integrations, "build", [result, {
|
|
1468
|
+
root: config.root ?? config.outDir,
|
|
1469
|
+
command: "build"
|
|
1470
|
+
}]);
|
|
1471
|
+
reportPhase("integrations", phaseStart);
|
|
1472
|
+
}
|
|
1473
|
+
if (config.site) {
|
|
1474
|
+
phaseStart = performance.now();
|
|
1475
|
+
let sitemapExists = false;
|
|
1476
|
+
try {
|
|
1477
|
+
sitemapExists = (await stat(join(config.outDir, "sitemap.xml"))).isFile();
|
|
1478
|
+
} catch {}
|
|
1479
|
+
if (!sitemapExists) {
|
|
1480
|
+
const sitemapFiles = await generateSitemapFromRoutes({
|
|
1481
|
+
siteUrl: config.site,
|
|
1482
|
+
outDir: config.outDir,
|
|
1483
|
+
routes
|
|
1484
|
+
});
|
|
1485
|
+
result.files.push(...sitemapFiles);
|
|
1486
|
+
}
|
|
1487
|
+
reportPhase("sitemap", phaseStart);
|
|
1488
|
+
}
|
|
1209
1489
|
return result;
|
|
1210
1490
|
}
|
|
1211
1491
|
async function buildPage(config, route, actions) {
|
|
@@ -1228,7 +1508,9 @@ async function buildConcretePage(config, route, params, actions) {
|
|
|
1228
1508
|
config: {
|
|
1229
1509
|
lang: config.lang,
|
|
1230
1510
|
clientEntry: config.clientEntry,
|
|
1231
|
-
renderEndpoint: false
|
|
1511
|
+
renderEndpoint: false,
|
|
1512
|
+
router: pageRouterConfig(config),
|
|
1513
|
+
js: config.js
|
|
1232
1514
|
},
|
|
1233
1515
|
actions
|
|
1234
1516
|
});
|
|
@@ -1238,6 +1520,20 @@ async function buildConcretePage(config, route, params, actions) {
|
|
|
1238
1520
|
await writeFile(filePath, htmlOut, "utf8");
|
|
1239
1521
|
return filePath;
|
|
1240
1522
|
}
|
|
1523
|
+
/**
|
|
1524
|
+
* The router slice of the per-page render config: `enabled` gates script/meta
|
|
1525
|
+
* emission, `entry` is advertised only for split bundles, and `speculation`
|
|
1526
|
+
* produces the Speculation Rules block on static pages.
|
|
1527
|
+
*/
|
|
1528
|
+
function pageRouterConfig(config) {
|
|
1529
|
+
if (!config.router) return void 0;
|
|
1530
|
+
const separate = config.router.separate === true && config.js !== "legacy";
|
|
1531
|
+
return {
|
|
1532
|
+
enabled: config.router.enabled !== false,
|
|
1533
|
+
entry: separate ? config.router.entry ?? "/_elur/router.js" : void 0,
|
|
1534
|
+
speculation: config.router.speculation
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1241
1537
|
//#endregion
|
|
1242
1538
|
//#region src/vite/interpolation-plugin.ts
|
|
1243
1539
|
function warnLegacyOnce() {
|
|
@@ -1792,402 +2088,830 @@ async function transformProjectFiles(options) {
|
|
|
1792
2088
|
}
|
|
1793
2089
|
}
|
|
1794
2090
|
//#endregion
|
|
1795
|
-
//#region src/
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
/**
|
|
1811
|
-
* Maps an arbitrary thrown value to a public-safe error info. By default the
|
|
1812
|
-
* public message is generic; `includeDetail` (dev/verbose mode) appends the
|
|
1813
|
-
* original `Error.message` for local debugging.
|
|
1814
|
-
*/
|
|
1815
|
-
function toPublicErrorInfo(error, options = {}) {
|
|
1816
|
-
const code = "INTERNAL_SERVER_ERROR";
|
|
1817
|
-
const status = 500;
|
|
1818
|
-
if (options.includeDetail && error instanceof Error && error.message) return {
|
|
1819
|
-
code,
|
|
1820
|
-
status,
|
|
1821
|
-
message: error.message
|
|
1822
|
-
};
|
|
1823
|
-
return {
|
|
1824
|
-
code,
|
|
1825
|
-
status,
|
|
1826
|
-
message: "Internal Server Error"
|
|
2091
|
+
//#region src/runtime/node-http.ts
|
|
2092
|
+
var GlobalAbortController = globalThis.AbortController ?? AbortController;
|
|
2093
|
+
function incomingMessageToRequest(req, body) {
|
|
2094
|
+
const headers = new Headers();
|
|
2095
|
+
for (let index = 0; index < req.rawHeaders.length; index += 2) headers.append(req.rawHeaders[index], req.rawHeaders[index + 1]);
|
|
2096
|
+
const controller = new GlobalAbortController();
|
|
2097
|
+
req.once("aborted", () => controller.abort());
|
|
2098
|
+
req.once("close", () => {
|
|
2099
|
+
if (!req.complete) controller.abort();
|
|
2100
|
+
});
|
|
2101
|
+
const protocol = req.socket.encrypted ? "https" : "http";
|
|
2102
|
+
const init = {
|
|
2103
|
+
method: req.method ?? "GET",
|
|
2104
|
+
headers,
|
|
2105
|
+
signal: controller.signal
|
|
1827
2106
|
};
|
|
2107
|
+
if (body !== void 0 && body !== null && init.method !== "GET" && init.method !== "HEAD") init.body = body;
|
|
2108
|
+
return new Request(`${protocol}://${headers.get("host") ?? "localhost"}${req.url ?? "/"}`, init);
|
|
1828
2109
|
}
|
|
1829
2110
|
/**
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
2111
|
+
* Writes a Web `Response` to a Node `ServerResponse`, streaming the body.
|
|
2112
|
+
*
|
|
2113
|
+
* Unlike `res.end(Buffer.from(await response.arrayBuffer()))`, this forwards
|
|
2114
|
+
* chunks as they are produced — required for streaming SSR responses to reach
|
|
2115
|
+
* the client progressively. Buffered (non-stream) bodies behave exactly as
|
|
2116
|
+
* before. Honors backpressure (`drain`) and cancels the upstream stream when
|
|
2117
|
+
* the client disconnects (`close` before `finish`).
|
|
1832
2118
|
*/
|
|
1833
|
-
function
|
|
1834
|
-
const
|
|
1835
|
-
|
|
1836
|
-
"
|
|
1837
|
-
|
|
2119
|
+
async function sendWebResponse(res, response) {
|
|
2120
|
+
const headers = Object.fromEntries(response.headers.entries());
|
|
2121
|
+
if (response.body !== null) {
|
|
2122
|
+
delete headers["content-length"];
|
|
2123
|
+
delete headers["transfer-encoding"];
|
|
2124
|
+
}
|
|
2125
|
+
res.writeHead(response.status, headers);
|
|
2126
|
+
const body = response.body;
|
|
2127
|
+
if (!body) {
|
|
2128
|
+
res.end();
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
const reader = body.getReader();
|
|
2132
|
+
let done = false;
|
|
2133
|
+
const onClose = () => {
|
|
2134
|
+
if (!done) reader.cancel().catch(() => {});
|
|
1838
2135
|
};
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2136
|
+
res.once("close", onClose);
|
|
2137
|
+
try {
|
|
2138
|
+
for (;;) {
|
|
2139
|
+
const { done: readDone, value } = await reader.read();
|
|
2140
|
+
if (readDone) break;
|
|
2141
|
+
if (value && value.byteLength > 0 && !res.write(value)) await new Promise((resolveDrain) => res.once("drain", resolveDrain));
|
|
2142
|
+
}
|
|
2143
|
+
done = true;
|
|
2144
|
+
res.end();
|
|
2145
|
+
} catch {
|
|
2146
|
+
done = true;
|
|
2147
|
+
res.destroy();
|
|
2148
|
+
} finally {
|
|
2149
|
+
res.removeListener("close", onClose);
|
|
2150
|
+
}
|
|
1844
2151
|
}
|
|
1845
|
-
var init_errors = __esmMin((() => {}));
|
|
1846
2152
|
//#endregion
|
|
1847
|
-
//#region src/
|
|
2153
|
+
//#region src/runtime/logger.ts
|
|
1848
2154
|
/**
|
|
1849
|
-
*
|
|
2155
|
+
* Redacts sensitive fields from a log fields object.
|
|
2156
|
+
* Recursively redacts keys that match sensitive header names.
|
|
1850
2157
|
*/
|
|
1851
|
-
function
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
const
|
|
1855
|
-
if (
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
return;
|
|
2158
|
+
function redactSensitive(fields) {
|
|
2159
|
+
const result = {};
|
|
2160
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
2161
|
+
const lowerKey = key.toLowerCase();
|
|
2162
|
+
if (SENSITIVE_HEADERS.has(lowerKey)) result[key] = "[REDACTED]";
|
|
2163
|
+
else if (typeof value === "object" && value !== null && !Array.isArray(value)) result[key] = redactSensitive(value);
|
|
2164
|
+
else result[key] = value;
|
|
1859
2165
|
}
|
|
2166
|
+
return result;
|
|
1860
2167
|
}
|
|
1861
2168
|
/**
|
|
1862
|
-
*
|
|
1863
|
-
*
|
|
1864
|
-
* undefined when it is allowed.
|
|
1865
|
-
*
|
|
1866
|
-
* @param request The incoming Request to actions.
|
|
1867
|
-
* @param options Origin check configuration.
|
|
2169
|
+
* Creates a logger for a request, optionally using the request's
|
|
2170
|
+
* X-Request-ID header if present.
|
|
1868
2171
|
*/
|
|
1869
|
-
function
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
if (!origin && !referer) return options.strictOrigin ? "Missing Origin and Referer headers" : void 0;
|
|
1875
|
-
const sourceOrigin = origin ? originOf(origin) : originOf(referer);
|
|
1876
|
-
if (!sourceOrigin) return origin ? "Invalid Origin header" : "Invalid Referer header";
|
|
1877
|
-
if (sourceOrigin === targetOrigin) return void 0;
|
|
1878
|
-
if (options.allowedOrigins?.some((allowed) => originOf(allowed) === sourceOrigin)) return void 0;
|
|
1879
|
-
return `Cross-origin request blocked: source "${sourceOrigin}" != target "${targetOrigin}"`;
|
|
1880
|
-
}
|
|
1881
|
-
/** Builds a 403 Response for a rejected origin. */
|
|
1882
|
-
function originForbidden(message) {
|
|
1883
|
-
return new Response(message, {
|
|
1884
|
-
status: 403,
|
|
1885
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
2172
|
+
function createRequestLogger(request, minLevel) {
|
|
2173
|
+
const requestId = request?.headers.get("X-Request-ID") ?? void 0;
|
|
2174
|
+
return new StructuredLogger({
|
|
2175
|
+
minLevel,
|
|
2176
|
+
requestId
|
|
1886
2177
|
});
|
|
1887
2178
|
}
|
|
1888
|
-
var
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
async function readBodyWithLimit(request, limit) {
|
|
1896
|
-
const contentLength = request.headers.get("Content-Length");
|
|
1897
|
-
if (contentLength && parseInt(contentLength, 10) > limit) return {
|
|
1898
|
-
ok: false,
|
|
1899
|
-
response: new Response("Request body too large", {
|
|
1900
|
-
status: 413,
|
|
1901
|
-
headers: { "Content-Type": "text/plain" }
|
|
1902
|
-
})
|
|
1903
|
-
};
|
|
1904
|
-
const reader = request.body?.getReader();
|
|
1905
|
-
if (!reader) return {
|
|
1906
|
-
ok: true,
|
|
1907
|
-
text: ""
|
|
2179
|
+
var LEVEL_PRIORITY, SENSITIVE_HEADERS, StructuredLogger;
|
|
2180
|
+
var init_logger = __esmMin((() => {
|
|
2181
|
+
LEVEL_PRIORITY = {
|
|
2182
|
+
debug: 10,
|
|
2183
|
+
info: 20,
|
|
2184
|
+
warn: 30,
|
|
2185
|
+
error: 40
|
|
1908
2186
|
};
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2187
|
+
SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
|
|
2188
|
+
"cookie",
|
|
2189
|
+
"authorization",
|
|
2190
|
+
"set-cookie",
|
|
2191
|
+
"x-api-key",
|
|
2192
|
+
"x-auth-token"
|
|
2193
|
+
]);
|
|
2194
|
+
StructuredLogger = class {
|
|
2195
|
+
minLevel;
|
|
2196
|
+
requestId;
|
|
2197
|
+
timings = [];
|
|
2198
|
+
entries = [];
|
|
2199
|
+
constructor(options = {}) {
|
|
2200
|
+
this.minLevel = options.minLevel ?? (process.env.NODE_ENV === "production" ? "info" : "debug");
|
|
2201
|
+
this.requestId = options.requestId ?? randomUUID();
|
|
2202
|
+
}
|
|
2203
|
+
/** Returns the request ID for this logger instance. */
|
|
2204
|
+
getRequestId() {
|
|
2205
|
+
return this.requestId;
|
|
2206
|
+
}
|
|
2207
|
+
/** Logs a debug message. */
|
|
2208
|
+
debug(message, fields) {
|
|
2209
|
+
this.log("debug", message, fields);
|
|
2210
|
+
}
|
|
2211
|
+
/** Logs an info message. */
|
|
2212
|
+
info(message, fields) {
|
|
2213
|
+
this.log("info", message, fields);
|
|
2214
|
+
}
|
|
2215
|
+
/** Logs a warning. */
|
|
2216
|
+
warn(message, fields) {
|
|
2217
|
+
this.log("warn", message, fields);
|
|
2218
|
+
}
|
|
2219
|
+
/** Logs an error. */
|
|
2220
|
+
error(message, fields) {
|
|
2221
|
+
this.log("error", message, fields);
|
|
2222
|
+
}
|
|
2223
|
+
/** Records a Server-Timing metric. */
|
|
2224
|
+
timing(name, durationMs, description) {
|
|
2225
|
+
this.timings.push({
|
|
2226
|
+
name,
|
|
2227
|
+
durationMs,
|
|
2228
|
+
description
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
/** Starts a timer and returns a function to stop it and record the timing. */
|
|
2232
|
+
startTimer(name, description) {
|
|
2233
|
+
const start = performance.now();
|
|
2234
|
+
return () => {
|
|
2235
|
+
this.timing(name, performance.now() - start, description);
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
/** Returns the Server-Timing header value. */
|
|
2239
|
+
getServerTimingHeader() {
|
|
2240
|
+
return this.timings.map((t) => {
|
|
2241
|
+
const desc = t.description ? `;desc="${t.description}"` : "";
|
|
2242
|
+
return `${t.name};dur=${t.durationMs.toFixed(1)}${desc}`;
|
|
2243
|
+
}).join(", ");
|
|
2244
|
+
}
|
|
2245
|
+
/** Returns all log entries collected so far. */
|
|
2246
|
+
getEntries() {
|
|
2247
|
+
return this.entries;
|
|
2248
|
+
}
|
|
2249
|
+
log(level, message, fields) {
|
|
2250
|
+
if (LEVEL_PRIORITY[level] < LEVEL_PRIORITY[this.minLevel]) return;
|
|
2251
|
+
const entry = {
|
|
2252
|
+
level,
|
|
2253
|
+
message,
|
|
2254
|
+
requestId: this.requestId,
|
|
2255
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2256
|
+
fields: fields ? redactSensitive(fields) : void 0
|
|
2257
|
+
};
|
|
2258
|
+
this.entries.push(entry);
|
|
2259
|
+
if (process.env.NODE_ENV === "production") {
|
|
2260
|
+
const output = JSON.stringify(entry);
|
|
2261
|
+
if (level === "error") console.error(output);
|
|
2262
|
+
else if (level === "warn") console.warn(output);
|
|
2263
|
+
else console.log(output);
|
|
2264
|
+
} else {
|
|
2265
|
+
const output = `${`[${level.toUpperCase()}]`} ${message}${entry.fields ? " " + JSON.stringify(entry.fields) : ""}`;
|
|
2266
|
+
if (level === "error") console.error(output);
|
|
2267
|
+
else if (level === "warn") console.warn(output);
|
|
2268
|
+
else console.log(output);
|
|
1927
2269
|
}
|
|
1928
|
-
chunks.push(value);
|
|
1929
2270
|
}
|
|
1930
|
-
} finally {
|
|
1931
|
-
try {
|
|
1932
|
-
reader.releaseLock();
|
|
1933
|
-
} catch {}
|
|
1934
|
-
}
|
|
1935
|
-
const total = new Uint8Array(totalSize);
|
|
1936
|
-
let offset = 0;
|
|
1937
|
-
for (const chunk of chunks) {
|
|
1938
|
-
total.set(chunk, offset);
|
|
1939
|
-
offset += chunk.byteLength;
|
|
1940
|
-
}
|
|
1941
|
-
return {
|
|
1942
|
-
ok: true,
|
|
1943
|
-
text: new TextDecoder().decode(total)
|
|
1944
2271
|
};
|
|
2272
|
+
}));
|
|
2273
|
+
//#endregion
|
|
2274
|
+
//#region src/config/index.ts
|
|
2275
|
+
init_logger();
|
|
2276
|
+
async function loadElurConfig(options = {}) {
|
|
2277
|
+
const initialRoot = resolve(options.root ?? process.cwd());
|
|
2278
|
+
const configFile = options.configFile ? resolve(initialRoot, options.configFile) : await findConfigFile(initialRoot);
|
|
2279
|
+
let loaded = {};
|
|
2280
|
+
if (configFile) {
|
|
2281
|
+
const result = await loadConfigFromFile({
|
|
2282
|
+
command: options.command === "build" ? "build" : "serve",
|
|
2283
|
+
mode: options.mode ?? "development"
|
|
2284
|
+
}, configFile, initialRoot);
|
|
2285
|
+
if (!result) throw new Error(`[elur-kit] Could not load config: ${configFile}`);
|
|
2286
|
+
loaded = result.config;
|
|
2287
|
+
}
|
|
2288
|
+
const merged = mergeConfig(loaded, options.overrides ?? {});
|
|
2289
|
+
const root = resolve(initialRoot, merged.root ?? ".");
|
|
2290
|
+
const resolved = resolveConfig(root, merged, configFile);
|
|
2291
|
+
await runIntegrationHook(resolved.integrations, "config", [resolved, {
|
|
2292
|
+
root,
|
|
2293
|
+
command: options.command ?? "dev"
|
|
2294
|
+
}]);
|
|
2295
|
+
return resolved;
|
|
1945
2296
|
}
|
|
1946
|
-
function
|
|
1947
|
-
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2297
|
+
function resolveConfig(root, config, configFile) {
|
|
2298
|
+
if (config.site) new URL(config.site);
|
|
2299
|
+
const base = normalizeBase(config.base ?? "/");
|
|
2300
|
+
const imageQuality = config.images?.quality ?? 80;
|
|
2301
|
+
if (!Number.isFinite(imageQuality) || imageQuality < 1 || imageQuality > 100) throw new Error("[elur-kit] images.quality must be between 1 and 100");
|
|
2302
|
+
return {
|
|
2303
|
+
root,
|
|
2304
|
+
appDir: resolveInside(root, config.appDir ?? "src/app", "appDir"),
|
|
2305
|
+
islandsDir: resolveInside(root, config.islandsDir ?? "src/islands", "islandsDir"),
|
|
2306
|
+
contentDir: resolveInside(root, config.contentDir ?? "src/content", "contentDir"),
|
|
2307
|
+
publicDir: resolveInside(root, config.publicDir ?? "public", "publicDir"),
|
|
2308
|
+
outDir: resolveInside(root, config.outDir ?? "dist", "outDir"),
|
|
2309
|
+
site: config.site,
|
|
2310
|
+
base,
|
|
2311
|
+
trailingSlash: config.trailingSlash ?? "ignore",
|
|
2312
|
+
output: config.output ?? "static",
|
|
2313
|
+
adapter: config.adapter,
|
|
2314
|
+
images: {
|
|
2315
|
+
formats: config.images?.formats ?? ["webp", "avif"],
|
|
2316
|
+
quality: imageQuality,
|
|
2317
|
+
strict: config.images?.strict ?? false
|
|
2318
|
+
},
|
|
2319
|
+
cache: {
|
|
2320
|
+
dir: resolveInside(root, config.cache?.dir ?? ".elur/cache", "cache.dir"),
|
|
2321
|
+
defaultRevalidate: config.cache?.defaultRevalidate,
|
|
2322
|
+
adapter: config.cache?.adapter
|
|
2323
|
+
},
|
|
2324
|
+
security: {
|
|
2325
|
+
allowedOrigins: config.security?.allowedOrigins ?? [],
|
|
2326
|
+
strictOrigin: config.security?.strictOrigin ?? false,
|
|
2327
|
+
bodyLimit: config.security?.bodyLimit ?? 1048576,
|
|
2328
|
+
headers: config.security?.headers === false ? false : config.security?.headers ?? {}
|
|
2329
|
+
},
|
|
2330
|
+
router: {
|
|
2331
|
+
enabled: config.router?.enabled ?? true,
|
|
2332
|
+
prefetch: config.router?.prefetch ?? true,
|
|
2333
|
+
morph: config.router?.morph ?? false,
|
|
2334
|
+
speculation: config.router?.speculation,
|
|
2335
|
+
loadingIndicator: config.router?.loadingIndicator ?? false
|
|
2336
|
+
},
|
|
2337
|
+
js: config.js ?? "modern",
|
|
2338
|
+
logger: { level: config.logger?.level },
|
|
2339
|
+
redirects: config.redirects ?? [],
|
|
2340
|
+
rewrites: config.rewrites ?? [],
|
|
2341
|
+
streaming: config.streaming ?? false,
|
|
2342
|
+
headers: config.headers ?? [],
|
|
2343
|
+
integrations: config.integrations ?? [],
|
|
2344
|
+
configFile
|
|
2345
|
+
};
|
|
1953
2346
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2347
|
+
function mergeConfig(base, override) {
|
|
2348
|
+
return {
|
|
2349
|
+
...base,
|
|
2350
|
+
...override,
|
|
2351
|
+
images: {
|
|
2352
|
+
...base.images,
|
|
2353
|
+
...override.images
|
|
2354
|
+
},
|
|
2355
|
+
cache: {
|
|
2356
|
+
...base.cache,
|
|
2357
|
+
...override.cache
|
|
2358
|
+
},
|
|
2359
|
+
security: {
|
|
2360
|
+
...base.security,
|
|
2361
|
+
...override.security
|
|
2362
|
+
},
|
|
2363
|
+
router: {
|
|
2364
|
+
...base.router,
|
|
2365
|
+
...override.router
|
|
2366
|
+
},
|
|
2367
|
+
logger: {
|
|
2368
|
+
...base.logger,
|
|
2369
|
+
...override.logger
|
|
2370
|
+
},
|
|
2371
|
+
redirects: [...override.redirects ?? [], ...base.redirects ?? []],
|
|
2372
|
+
rewrites: [...override.rewrites ?? [], ...base.rewrites ?? []],
|
|
2373
|
+
headers: [...override.headers ?? [], ...base.headers ?? []],
|
|
2374
|
+
integrations: override.integrations ?? base.integrations
|
|
1961
2375
|
};
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
2376
|
+
}
|
|
2377
|
+
function resolveInside(root, path, name) {
|
|
2378
|
+
const resolved = isAbsolute(path) ? resolve(path) : resolve(root, path);
|
|
2379
|
+
const rel = relative(root, resolved);
|
|
2380
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error(`[elur-kit] ${name} must stay inside root: ${resolved}`);
|
|
2381
|
+
return resolved;
|
|
2382
|
+
}
|
|
2383
|
+
function normalizeBase(base) {
|
|
2384
|
+
if (!base.startsWith("/")) throw new Error("[elur-kit] base must start with /");
|
|
2385
|
+
return base === "/" ? base : `${base.replace(/\/+$/, "")}/`;
|
|
2386
|
+
}
|
|
2387
|
+
var PREFERRED_CONFIG_FILES = [
|
|
2388
|
+
"elur.config.ts",
|
|
2389
|
+
"elur.config.js",
|
|
2390
|
+
"elur.config.mjs"
|
|
2391
|
+
];
|
|
2392
|
+
async function findConfigFile(root) {
|
|
2393
|
+
for (const name of PREFERRED_CONFIG_FILES) {
|
|
2394
|
+
const path = resolve(root, name);
|
|
1974
2395
|
try {
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2396
|
+
await access(path);
|
|
2397
|
+
return path;
|
|
2398
|
+
} catch {}
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
//#endregion
|
|
2402
|
+
//#region src/manifest/index.ts
|
|
2403
|
+
init_scan();
|
|
2404
|
+
init_route_scanner();
|
|
2405
|
+
async function createAppManifest(config) {
|
|
2406
|
+
const [routes, actions, islands] = await Promise.all([
|
|
2407
|
+
scanRoutes(config.appDir),
|
|
2408
|
+
scanActions(config.appDir),
|
|
2409
|
+
scanIslands(config.islandsDir)
|
|
2410
|
+
]);
|
|
2411
|
+
validateManifestRoutes(routes);
|
|
2412
|
+
validateIslands(islands);
|
|
2413
|
+
const manifest = {
|
|
2414
|
+
version: 1,
|
|
2415
|
+
root: config.root,
|
|
2416
|
+
routes,
|
|
2417
|
+
actions,
|
|
2418
|
+
islands,
|
|
2419
|
+
base: config.base,
|
|
2420
|
+
output: config.output
|
|
2421
|
+
};
|
|
2422
|
+
await runIntegrationHook(config.integrations, "routes", [manifest, {
|
|
2423
|
+
root: config.root,
|
|
2424
|
+
command: "build"
|
|
2425
|
+
}]);
|
|
2426
|
+
return manifest;
|
|
2427
|
+
}
|
|
2428
|
+
async function writeAppManifest(manifest, path) {
|
|
2429
|
+
await mkdir(dirname(path), { recursive: true });
|
|
2430
|
+
await writeFile(path, JSON.stringify(toPortableManifest(manifest), null, 2), "utf8");
|
|
2431
|
+
}
|
|
2432
|
+
async function writeRouteTypes(manifest, path) {
|
|
2433
|
+
const routePaths = manifest.routes.pages.map((route) => JSON.stringify(route.path));
|
|
2434
|
+
const actionNames = Object.values(manifest.actions).flatMap((actions) => Object.keys(actions)).filter((name, index, names) => names.indexOf(name) === index).map((name) => JSON.stringify(name));
|
|
2435
|
+
const source = [
|
|
2436
|
+
`export type ElurRoutePath = ${routePaths.length ? routePaths.join(" | ") : "never"};`,
|
|
2437
|
+
`export type ElurActionName = ${actionNames.length ? actionNames.join(" | ") : "never"};`,
|
|
2438
|
+
"export interface ElurRouteParams { [name: string]: string | string[] | undefined }",
|
|
2439
|
+
""
|
|
2440
|
+
].join("\n");
|
|
2441
|
+
await mkdir(dirname(path), { recursive: true });
|
|
2442
|
+
await writeFile(path, source, "utf8");
|
|
2443
|
+
}
|
|
2444
|
+
function validateManifestRoutes(routes) {
|
|
2445
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2446
|
+
for (const route of routes.pages) {
|
|
2447
|
+
registerRoute(seen, route.path, route.pagePath, "page");
|
|
2448
|
+
assertNotReserved(route.path, route.pagePath);
|
|
2449
|
+
}
|
|
2450
|
+
for (const route of routes.api) {
|
|
2451
|
+
registerRoute(seen, route.path, route.routePath, "API");
|
|
2452
|
+
assertNotReserved(route.path, route.routePath);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
function registerRoute(seen, path, file, kind) {
|
|
2456
|
+
const existing = seen.get(path);
|
|
2457
|
+
if (existing) throw new Error(`[elur-kit] Duplicate ${kind} route "${path}": ${existing} and ${file}`);
|
|
2458
|
+
seen.set(path, file);
|
|
2459
|
+
}
|
|
2460
|
+
function assertNotReserved(path, file) {
|
|
2461
|
+
if (path === "/__elur-js" || path.startsWith("/__elur-js/") || path === "/_elur" || path.startsWith("/_elur/")) throw new Error(`[elur-kit] Reserved route "${path}" declared by ${file}`);
|
|
2462
|
+
}
|
|
2463
|
+
function validateIslands(islands) {
|
|
2464
|
+
const names = /* @__PURE__ */ new Set();
|
|
2465
|
+
for (const island of islands) {
|
|
2466
|
+
if (names.has(island.name)) throw new Error(`[elur-kit] Duplicate island name: ${island.name}`);
|
|
2467
|
+
names.add(island.name);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
function toPortableManifest(manifest) {
|
|
2471
|
+
const relativePath = (path) => path ? relative(manifest.root, path).split("\\").join("/") : void 0;
|
|
2472
|
+
const routes = {
|
|
2473
|
+
pages: manifest.routes.pages.map((route) => ({
|
|
2474
|
+
...route,
|
|
2475
|
+
pagePath: relativePath(route.pagePath),
|
|
2476
|
+
dataPath: relativePath(route.dataPath),
|
|
2477
|
+
actionPath: relativePath(route.actionPath),
|
|
2478
|
+
loadingPath: relativePath(route.loadingPath),
|
|
2479
|
+
layouts: route.layouts.map((layout) => relativePath(layout))
|
|
2480
|
+
})),
|
|
2481
|
+
api: manifest.routes.api.map((route) => ({
|
|
2482
|
+
...route,
|
|
2483
|
+
routePath: relativePath(route.routePath)
|
|
2484
|
+
})),
|
|
2485
|
+
error404: manifest.routes.error404 ? {
|
|
2486
|
+
...manifest.routes.error404,
|
|
2487
|
+
pagePath: relativePath(manifest.routes.error404.pagePath),
|
|
2488
|
+
dataPath: relativePath(manifest.routes.error404.dataPath),
|
|
2489
|
+
actionPath: relativePath(manifest.routes.error404.actionPath),
|
|
2490
|
+
loadingPath: relativePath(manifest.routes.error404.loadingPath),
|
|
2491
|
+
layouts: manifest.routes.error404.layouts.map((layout) => relativePath(layout))
|
|
2492
|
+
} : void 0,
|
|
2493
|
+
error500: manifest.routes.error500 ? {
|
|
2494
|
+
...manifest.routes.error500,
|
|
2495
|
+
pagePath: relativePath(manifest.routes.error500.pagePath),
|
|
2496
|
+
dataPath: relativePath(manifest.routes.error500.dataPath),
|
|
2497
|
+
actionPath: relativePath(manifest.routes.error500.actionPath),
|
|
2498
|
+
loadingPath: relativePath(manifest.routes.error500.loadingPath),
|
|
2499
|
+
layouts: manifest.routes.error500.layouts.map((layout) => relativePath(layout))
|
|
2500
|
+
} : void 0
|
|
2501
|
+
};
|
|
2502
|
+
const actions = {};
|
|
2503
|
+
for (const [page, pageActions] of Object.entries(manifest.actions)) actions[page] = Object.fromEntries(Object.entries(pageActions).map(([name, path]) => [name, relativePath(path)]));
|
|
2504
|
+
return {
|
|
2505
|
+
...manifest,
|
|
2506
|
+
root: ".",
|
|
2507
|
+
routes,
|
|
2508
|
+
actions,
|
|
2509
|
+
islands: manifest.islands.map((island) => ({
|
|
2510
|
+
...island,
|
|
2511
|
+
filePath: relativePath(island.filePath)
|
|
2512
|
+
}))
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
//#endregion
|
|
2516
|
+
//#region src/runtime/capabilities.ts
|
|
2517
|
+
/** True when the host supports streaming responses (streaming !== false). */
|
|
2518
|
+
function supportsStreaming(capabilities = { streaming: true }) {
|
|
2519
|
+
return capabilities.streaming !== false;
|
|
2520
|
+
}
|
|
2521
|
+
/** True when the host can write to persistent storage (for ISR/cache/image writes). */
|
|
2522
|
+
function supportsPersistentStorage(capabilities) {
|
|
2523
|
+
return capabilities.filesystem === "persistent";
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Validates a capability declaration and reports incompatible combinations.
|
|
2527
|
+
* Used by the build pipeline so invalid hosts fail at build time instead of
|
|
2528
|
+
* producing a broken runtime.
|
|
2529
|
+
*/
|
|
2530
|
+
function validateCapabilities(capabilities, features = {}) {
|
|
2531
|
+
const problems = [];
|
|
2532
|
+
if (features.isr && !supportsPersistentStorage(capabilities)) problems.push(`ISR requires a persistent filesystem; the host declares filesystem="${capabilities.filesystem}".`);
|
|
2533
|
+
if (features.images && capabilities.imageRuntime === false && capabilities.filesystem === "none") problems.push("On-demand image transforms require either imageRuntime=true or a readable filesystem; the host has neither.");
|
|
2534
|
+
if (features.streaming && capabilities.streaming === false) problems.push("Streaming was requested but the host declares streaming=false.");
|
|
2535
|
+
return {
|
|
2536
|
+
ok: problems.length === 0,
|
|
2537
|
+
problems
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
var DEFAULT_CAPABILITIES, SERVERLESS_CAPABILITIES;
|
|
2541
|
+
var init_capabilities = __esmMin((() => {
|
|
2542
|
+
DEFAULT_CAPABILITIES = {
|
|
2543
|
+
streaming: true,
|
|
2544
|
+
filesystem: "persistent",
|
|
2545
|
+
imageRuntime: true,
|
|
2546
|
+
backgroundWork: true
|
|
2547
|
+
};
|
|
2548
|
+
SERVERLESS_CAPABILITIES = {
|
|
2549
|
+
streaming: true,
|
|
2550
|
+
filesystem: "ephemeral",
|
|
2551
|
+
imageRuntime: false,
|
|
2552
|
+
backgroundWork: false,
|
|
2553
|
+
maxBodySize: 1048576
|
|
2554
|
+
};
|
|
2555
|
+
}));
|
|
2556
|
+
//#endregion
|
|
2557
|
+
//#region src/cli/output.ts
|
|
2558
|
+
var useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
2559
|
+
function paint(code, text) {
|
|
2560
|
+
return useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
2561
|
+
}
|
|
2562
|
+
var bold = (text) => paint(1, text);
|
|
2563
|
+
var dim = (text) => paint(2, text);
|
|
2564
|
+
var red = (text) => paint(31, text);
|
|
2565
|
+
var green = (text) => paint(32, text);
|
|
2566
|
+
var yellow = (text) => paint(33, text);
|
|
2567
|
+
var cyan = (text) => paint(36, text);
|
|
2568
|
+
var quiet = false;
|
|
2569
|
+
/** Enables quiet mode: only errors are printed. */
|
|
2570
|
+
function setQuiet(value) {
|
|
2571
|
+
quiet = value;
|
|
2572
|
+
}
|
|
2573
|
+
/** Success message with a green check. */
|
|
2574
|
+
function success(message) {
|
|
2575
|
+
if (quiet) return;
|
|
2576
|
+
console.log(`${green("✓")} ${message}`);
|
|
2577
|
+
}
|
|
2578
|
+
/** Indented info line with a cyan arrow. */
|
|
2579
|
+
function info(message) {
|
|
2580
|
+
if (quiet) return;
|
|
2581
|
+
console.log(` ${cyan("→")} ${message}`);
|
|
2582
|
+
}
|
|
2583
|
+
/** Indented detail line (file lists, sub-items). */
|
|
2584
|
+
function detail(message) {
|
|
2585
|
+
if (quiet) return;
|
|
2586
|
+
console.log(dim(` - ${message}`));
|
|
2587
|
+
}
|
|
2588
|
+
/** Lifecycle event with a dim bracket tag, e.g. [dev], [change]. */
|
|
2589
|
+
function event(tag, message) {
|
|
2590
|
+
if (quiet) return;
|
|
2591
|
+
console.log(`${dim(`[${tag}]`)} ${message}`);
|
|
2592
|
+
}
|
|
2593
|
+
/** Warning; suppressed in quiet mode. */
|
|
2594
|
+
function warn(message) {
|
|
2595
|
+
if (quiet) return;
|
|
2596
|
+
console.warn(`${yellow("!")} ${message}`);
|
|
2597
|
+
}
|
|
2598
|
+
/** Error; always printed, even in quiet mode. */
|
|
2599
|
+
function error(message) {
|
|
2600
|
+
console.error(`${red("✗")} ${message}`);
|
|
2601
|
+
}
|
|
2602
|
+
/** Formats a byte count as "640 B", "1.5 kB", "2.3 MB". */
|
|
2603
|
+
function formatBytes(bytes) {
|
|
2604
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
2605
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} kB`;
|
|
2606
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
2607
|
+
}
|
|
2608
|
+
/** Formats a duration as "45ms" or "1.23s". */
|
|
2609
|
+
function formatDuration(ms) {
|
|
2610
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
2611
|
+
return `${(ms / 1e3).toFixed(2)}s`;
|
|
2612
|
+
}
|
|
2613
|
+
/** Build phase line: checkmark + label + dim duration. */
|
|
2614
|
+
function phase(label, durationMs) {
|
|
2615
|
+
if (quiet) return;
|
|
2616
|
+
console.log(` ${green("✓")} ${label} ${dim(formatDuration(durationMs))}`);
|
|
2617
|
+
}
|
|
2618
|
+
var MAX_FILE_ROWS = 20;
|
|
2619
|
+
var SHOWN_FILE_ROWS = 10;
|
|
2620
|
+
/**
|
|
2621
|
+
* Renders the generated-file list as aligned "path size" rows (plain text,
|
|
2622
|
+
* no color). With more than MAX_FILE_ROWS entries, shows the SHOWN_FILE_ROWS
|
|
2623
|
+
* largest plus a "… and N more" line.
|
|
2624
|
+
*/
|
|
2625
|
+
function fileRows(files, max = MAX_FILE_ROWS, shown = SHOWN_FILE_ROWS) {
|
|
2626
|
+
const sorted = files.length > max ? [...files].sort((a, b) => b.bytes - a.bytes) : files;
|
|
2627
|
+
const visible = sorted.slice(0, files.length > max ? shown : sorted.length);
|
|
2628
|
+
const pathWidth = Math.max(...visible.map((f) => f.path.length), 0);
|
|
2629
|
+
const sizeWidth = Math.max(...visible.map((f) => formatBytes(f.bytes).length), 0);
|
|
2630
|
+
const rows = visible.map((f) => `${f.path.padEnd(pathWidth)} ${formatBytes(f.bytes).padStart(sizeWidth)}`);
|
|
2631
|
+
const hidden = files.length - visible.length;
|
|
2632
|
+
if (hidden > 0) rows.push(`… and ${hidden} more`);
|
|
2633
|
+
return rows;
|
|
2634
|
+
}
|
|
2635
|
+
/** Prints the generated-file list: dim paths, aligned sizes. */
|
|
2636
|
+
function fileList(files) {
|
|
2637
|
+
if (quiet || files.length === 0) return;
|
|
2638
|
+
for (const row of fileRows(files)) {
|
|
2639
|
+
const sizeMatch = /^(.*?)( \S+)$/.exec(row);
|
|
2640
|
+
if (sizeMatch) console.log(` ${dim(sizeMatch[1])} ${dim(sizeMatch[2])}`);
|
|
2641
|
+
else console.log(` ${dim(row)}`);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* Plain-text lines of the server startup banner (Astro-style, no box):
|
|
2646
|
+
*
|
|
2647
|
+
* elur-kit v2.4.10 dev server running at:
|
|
2648
|
+
* → Local: http://localhost:3000/
|
|
2649
|
+
* → Network: http://192.168.1.20:3000/
|
|
2650
|
+
*/
|
|
2651
|
+
function serverBannerLines(options) {
|
|
2652
|
+
const lines = [`${options.name} ${options.version} ${options.command} server running at:`, ""];
|
|
2653
|
+
const labelWidth = options.networkUrl ? 8 : 6;
|
|
2654
|
+
lines.push(` → ${"Local:".padEnd(labelWidth)} ${options.localUrl}`);
|
|
2655
|
+
if (options.networkUrl) lines.push(` → ${"Network:".padEnd(labelWidth)} ${options.networkUrl}`);
|
|
2656
|
+
return lines;
|
|
2657
|
+
}
|
|
2658
|
+
/** Prints the server startup banner with brand colors. */
|
|
2659
|
+
function serverBanner(options) {
|
|
2660
|
+
if (quiet) return;
|
|
2661
|
+
const [title, blank, ...urls] = serverBannerLines(options);
|
|
2662
|
+
console.log();
|
|
2663
|
+
console.log(` ${bold(cyan(title))}`);
|
|
2664
|
+
console.log(blank);
|
|
2665
|
+
for (const line of urls) {
|
|
2666
|
+
const arrowEnd = line.indexOf("→") + 1;
|
|
2667
|
+
console.log(` ${cyan("→")}${dim(line.slice(arrowEnd))}`);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
/** First external (LAN) IPv4 address, for the Network URL. */
|
|
2671
|
+
function getNetworkAddress() {
|
|
2672
|
+
for (const infos of Object.values(networkInterfaces())) for (const info of infos ?? []) if (info.family === "IPv4" && !info.internal) return info.address;
|
|
2673
|
+
}
|
|
2674
|
+
/**
|
|
2675
|
+
* Listens on host:port, falling back to the next port while the failure is
|
|
2676
|
+
* EADDRINUSE. Resolves with the port actually bound. Rejects with the
|
|
2677
|
+
* original error for non-port failures or when the range is exhausted.
|
|
2678
|
+
*/
|
|
2679
|
+
function listenWithFallback(server, host, port, options = {}) {
|
|
2680
|
+
const maxTries = options.maxTries ?? 20;
|
|
2681
|
+
return new Promise((resolvePromise, reject) => {
|
|
2682
|
+
let attempt = 0;
|
|
2683
|
+
let candidate = port;
|
|
2684
|
+
const onListening = () => {
|
|
2685
|
+
server.removeListener("error", onError);
|
|
2686
|
+
resolvePromise(candidate);
|
|
2687
|
+
};
|
|
2688
|
+
const onError = (err) => {
|
|
2689
|
+
server.removeListener("listening", onListening);
|
|
2690
|
+
if (err.code === "EADDRINUSE" && attempt < maxTries) {
|
|
2691
|
+
attempt++;
|
|
2692
|
+
const nextPort = port + attempt;
|
|
2693
|
+
options.onFallback?.(nextPort - 1, nextPort);
|
|
2694
|
+
tryListen(nextPort);
|
|
2695
|
+
return;
|
|
2031
2696
|
}
|
|
2032
|
-
|
|
2033
|
-
}
|
|
2034
|
-
} else {
|
|
2035
|
-
const bodyResult = await readBodyWithLimit(request, bodyLimit);
|
|
2036
|
-
if (!bodyResult.ok) return {
|
|
2037
|
-
ok: false,
|
|
2038
|
-
response: bodyResult.response
|
|
2697
|
+
reject(err);
|
|
2039
2698
|
};
|
|
2040
|
-
const
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2699
|
+
const tryListen = (next) => {
|
|
2700
|
+
candidate = next;
|
|
2701
|
+
server.once("error", onError);
|
|
2702
|
+
server.once("listening", onListening);
|
|
2703
|
+
server.listen(candidate, host);
|
|
2704
|
+
};
|
|
2705
|
+
tryListen(port);
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
//#endregion
|
|
2709
|
+
//#region src/build/vite-build.ts
|
|
2710
|
+
var vite_build_exports = /* @__PURE__ */ __exportAll({
|
|
2711
|
+
beginAtomicStage: () => beginAtomicStage,
|
|
2712
|
+
buildClientBundle: () => buildClientBundle,
|
|
2713
|
+
copyPublicAssets: () => copyPublicAssets,
|
|
2714
|
+
loadUserConfig: () => loadUserConfig,
|
|
2715
|
+
resolveClientInputs: () => resolveClientInputs
|
|
2716
|
+
});
|
|
2717
|
+
/**
|
|
2718
|
+
* Build the client hydration bundle using the Vite JavaScript API.
|
|
2719
|
+
*
|
|
2720
|
+
* The user's config is loaded programmatically and the elur interpolation
|
|
2721
|
+
* plugin is injected so partial attribute interpolations inside islands are
|
|
2722
|
+
* transformed before reaching the browser.
|
|
2723
|
+
*/
|
|
2724
|
+
async function buildClientBundle(options) {
|
|
2725
|
+
const log = options.logPrefix ?? "[client]";
|
|
2726
|
+
const quiet = options.quiet ?? false;
|
|
2727
|
+
if (!quiet) console.log(`${log} Building hydration bundle...`);
|
|
2728
|
+
const userConfig = options.userConfigPath ? await loadUserConfig(options.userConfigPath, options.root) : defaultClientConfig(options.defaultInputs);
|
|
2729
|
+
const pluginOptions = shouldUseLegacyInterpolation(options.interpolation ?? "auto") ? elurJsInterpolationPlugin({
|
|
2730
|
+
appDir: options.appDir,
|
|
2731
|
+
islandsDir: options.islandsDir
|
|
2732
|
+
}) : [];
|
|
2733
|
+
const config = {
|
|
2734
|
+
...userConfig,
|
|
2735
|
+
root: options.root,
|
|
2736
|
+
base: options.base ?? userConfig.base ?? "/",
|
|
2737
|
+
logLevel: quiet ? "silent" : userConfig.logLevel,
|
|
2738
|
+
build: {
|
|
2739
|
+
...userConfig.build ?? {},
|
|
2740
|
+
outDir: options.outDir,
|
|
2741
|
+
emptyOutDir: true
|
|
2742
|
+
},
|
|
2743
|
+
plugins: [...userConfig.plugins ?? [], pluginOptions],
|
|
2744
|
+
configFile: false
|
|
2745
|
+
};
|
|
2746
|
+
const result = await build(config);
|
|
2747
|
+
const outputCount = (Array.isArray(result) ? result : [result]).reduce((n, r) => n + ("output" in r ? r.output?.length ?? 0 : 0), 0);
|
|
2748
|
+
if (!quiet) console.log(`${log} ✓ ${outputCount} asset(s) emitted → ${relative(options.root, options.outDir)}`);
|
|
2749
|
+
return {
|
|
2750
|
+
outDir: options.outDir,
|
|
2751
|
+
outputCount
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
async function loadUserConfig(path, _root) {
|
|
2755
|
+
const mod = await import(path);
|
|
2756
|
+
const raw = mod.default ?? mod;
|
|
2757
|
+
const resolved = typeof raw === "function" ? await raw({
|
|
2758
|
+
command: "build",
|
|
2759
|
+
mode: "production"
|
|
2760
|
+
}) : raw;
|
|
2761
|
+
return (resolved && typeof resolved.then === "function" ? await resolved : resolved) ?? {};
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Resolves a user Vite config's `build.rollupOptions.input` to a list of
|
|
2765
|
+
* absolute entry paths. Used to detect whether the bundle will emit the
|
|
2766
|
+
* generated router module as its own chunk (split build) or not (legacy
|
|
2767
|
+
* single-entry bundle — the router stays embedded in the entry).
|
|
2768
|
+
*/
|
|
2769
|
+
async function resolveClientInputs(userConfigPath, root) {
|
|
2770
|
+
const input = (await loadUserConfig(userConfigPath, root)).build?.rollupOptions?.input;
|
|
2771
|
+
if (!input || typeof input === "string") return input ? [resolve(root, input)] : [];
|
|
2772
|
+
return (Array.isArray(input) ? input : Object.values(input)).filter((v) => typeof v === "string").map((v) => resolve(root, v));
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* The synthesized client bundle config used when the project does not ship
|
|
2776
|
+
* its own `vite.client.config.*`: named inputs with `[name].js` filenames in
|
|
2777
|
+
* ES format — the same contract the docs give to hand-written client
|
|
2778
|
+
* configs, so `entry-client` → `entry-client.js` and `router` → `router.js`.
|
|
2779
|
+
*/
|
|
2780
|
+
function defaultClientConfig(defaultInputs) {
|
|
2781
|
+
return { build: { rollupOptions: {
|
|
2782
|
+
input: defaultInputs ?? {},
|
|
2783
|
+
output: {
|
|
2784
|
+
entryFileNames: "[name].js",
|
|
2785
|
+
format: "es"
|
|
2047
2786
|
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2787
|
+
} } };
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* Prepare an atomic staging directory for build output.
|
|
2791
|
+
*
|
|
2792
|
+
* Usage:
|
|
2793
|
+
* const stage = await beginAtomicStage({ outDir });
|
|
2794
|
+
* try {
|
|
2795
|
+
* await buildInto(stage.tempDir);
|
|
2796
|
+
* await stage.commit();
|
|
2797
|
+
* } catch (err) {
|
|
2798
|
+
* await stage.rollback();
|
|
2799
|
+
* throw err;
|
|
2800
|
+
* }
|
|
2801
|
+
*/
|
|
2802
|
+
async function beginAtomicStage(options) {
|
|
2803
|
+
const outDir = resolve(options.outDir);
|
|
2804
|
+
const tempDir = resolve(options.tempDir ?? join(dirname(outDir), `.${basename$1(outDir)}.tmp-${process.pid}`));
|
|
2805
|
+
await rm(tempDir, {
|
|
2806
|
+
recursive: true,
|
|
2807
|
+
force: true
|
|
2808
|
+
});
|
|
2809
|
+
await mkdir(tempDir, { recursive: true });
|
|
2810
|
+
const commit = async () => {
|
|
2811
|
+
const backup = options.keepExisting && existsSync(outDir) ? `${outDir}.bak-${process.pid}` : void 0;
|
|
2812
|
+
if (backup) {
|
|
2813
|
+
await rm(backup, {
|
|
2814
|
+
recursive: true,
|
|
2815
|
+
force: true
|
|
2816
|
+
});
|
|
2817
|
+
await safeRename(outDir, backup);
|
|
2818
|
+
}
|
|
2819
|
+
try {
|
|
2820
|
+
await safeRename(tempDir, outDir);
|
|
2821
|
+
} catch (err) {
|
|
2822
|
+
if (isCrossDevice(err)) {
|
|
2823
|
+
await cp(tempDir, outDir, {
|
|
2824
|
+
recursive: true,
|
|
2825
|
+
force: true
|
|
2826
|
+
});
|
|
2827
|
+
await rm(tempDir, {
|
|
2828
|
+
recursive: true,
|
|
2829
|
+
force: true
|
|
2830
|
+
});
|
|
2831
|
+
} else {
|
|
2832
|
+
if (backup) await safeRename(backup, outDir);
|
|
2833
|
+
throw err;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
if (backup) await rm(backup, {
|
|
2837
|
+
recursive: true,
|
|
2838
|
+
force: true
|
|
2839
|
+
});
|
|
2840
|
+
};
|
|
2841
|
+
const rollback = async () => {
|
|
2842
|
+
await rm(tempDir, {
|
|
2843
|
+
recursive: true,
|
|
2844
|
+
force: true
|
|
2845
|
+
});
|
|
2056
2846
|
};
|
|
2057
2847
|
return {
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
args,
|
|
2062
|
-
wantsJson
|
|
2848
|
+
tempDir,
|
|
2849
|
+
commit,
|
|
2850
|
+
rollback
|
|
2063
2851
|
};
|
|
2064
2852
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
* cross-origin POST is rejected with 403 unless its origin is allow-listed via
|
|
2075
|
-
* `security.allowedOrigins`.
|
|
2076
|
-
*
|
|
2077
|
-
* For progressive-enhancement form submissions that fail, the failure payload
|
|
2078
|
-
* is relayed back via a short-lived `__elur_js_action_error` cookie (SameSite=Lax,
|
|
2079
|
-
* Max-Age=15s) instead of a query param, so errors do not leak into browser
|
|
2080
|
-
* history, server logs or third-party Referer headers.
|
|
2081
|
-
*/
|
|
2082
|
-
async function handleActionRequest(request, resolveAction, security = {}) {
|
|
2083
|
-
const originError = verifyOrigin(request, security);
|
|
2084
|
-
if (originError) return originForbidden(originError);
|
|
2085
|
-
const parsed = await parseActionRequest(request, security.bodyLimit ?? DEFAULT_BODY_LIMIT);
|
|
2086
|
-
if (!parsed.ok) return parsed.response;
|
|
2087
|
-
const { name, page, args, wantsJson } = parsed;
|
|
2853
|
+
function basename$1(path) {
|
|
2854
|
+
const parts = path.split(/[\\/]+/).filter(Boolean);
|
|
2855
|
+
return parts[parts.length - 1] ?? "output";
|
|
2856
|
+
}
|
|
2857
|
+
async function safeRename(src, dest) {
|
|
2858
|
+
await rm(dest, {
|
|
2859
|
+
recursive: true,
|
|
2860
|
+
force: true
|
|
2861
|
+
});
|
|
2088
2862
|
try {
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
});
|
|
2096
|
-
}
|
|
2097
|
-
const result = await action(...args);
|
|
2098
|
-
if (isActionFailure(result)) {
|
|
2099
|
-
if (wantsJson) return new Response(JSON.stringify({
|
|
2100
|
-
__elur_js_action_failure: true,
|
|
2101
|
-
status: result.status,
|
|
2102
|
-
data: result.data
|
|
2103
|
-
}), {
|
|
2104
|
-
status: result.status,
|
|
2105
|
-
headers: { "Content-Type": "application/json" }
|
|
2106
|
-
});
|
|
2107
|
-
const referer = request.headers.get("Referer") ?? "/";
|
|
2108
|
-
const url = new URL(referer, "http://localhost");
|
|
2109
|
-
const { value } = encodeActionErrorCookie(result.data, result.status);
|
|
2110
|
-
return new Response(null, {
|
|
2111
|
-
status: 303,
|
|
2112
|
-
headers: {
|
|
2113
|
-
Location: url.pathname + url.search,
|
|
2114
|
-
"Content-Type": "text/plain",
|
|
2115
|
-
"Set-Cookie": setActionErrorCookieHeader(value)
|
|
2116
|
-
}
|
|
2117
|
-
});
|
|
2118
|
-
}
|
|
2119
|
-
if (isRedirectResponse(result)) {
|
|
2120
|
-
if (wantsJson) return new Response(JSON.stringify({
|
|
2121
|
-
__elur_js_action_redirect: true,
|
|
2122
|
-
status: result.status,
|
|
2123
|
-
location: result.location
|
|
2124
|
-
}), {
|
|
2125
|
-
status: 200,
|
|
2126
|
-
headers: { "Content-Type": "application/json" }
|
|
2863
|
+
await rename(src, dest);
|
|
2864
|
+
} catch (err) {
|
|
2865
|
+
if (isCrossDevice(err)) {
|
|
2866
|
+
await cp(src, dest, {
|
|
2867
|
+
recursive: true,
|
|
2868
|
+
force: true
|
|
2127
2869
|
});
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
Location: result.location,
|
|
2132
|
-
"Content-Type": "text/plain"
|
|
2133
|
-
}
|
|
2870
|
+
await rm(src, {
|
|
2871
|
+
recursive: true,
|
|
2872
|
+
force: true
|
|
2134
2873
|
});
|
|
2135
|
-
}
|
|
2136
|
-
if (wantsJson) return new Response(JSON.stringify(result ?? null), {
|
|
2137
|
-
status: 200,
|
|
2138
|
-
headers: { "Content-Type": "application/json" }
|
|
2139
|
-
});
|
|
2140
|
-
const referer = request.headers.get("Referer") ?? "/";
|
|
2141
|
-
return new Response(null, {
|
|
2142
|
-
status: 303,
|
|
2143
|
-
headers: {
|
|
2144
|
-
Location: typeof result === "string" ? result : referer,
|
|
2145
|
-
"Content-Type": "text/plain"
|
|
2146
|
-
}
|
|
2147
|
-
});
|
|
2148
|
-
} catch (err) {
|
|
2149
|
-
console.error("[elur-kit] Action error:", err);
|
|
2150
|
-
return publicErrorResponse(err, { includeDetail: false });
|
|
2874
|
+
} else throw err;
|
|
2151
2875
|
}
|
|
2152
2876
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
init_errors();
|
|
2156
|
-
init_origin();
|
|
2157
|
-
init_error_store();
|
|
2158
|
-
DEFAULT_BODY_LIMIT = 1048576;
|
|
2159
|
-
}));
|
|
2160
|
-
//#endregion
|
|
2161
|
-
//#region src/cache.ts
|
|
2162
|
-
function cachePath(cacheDir, pathname) {
|
|
2163
|
-
const key = createHash("sha256").update(pathname).digest("hex");
|
|
2164
|
-
return join(cacheDir, `${key}.html.json`);
|
|
2877
|
+
function isCrossDevice(err) {
|
|
2878
|
+
return err?.code === "EXDEV";
|
|
2165
2879
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2880
|
+
/**
|
|
2881
|
+
* Copy the public directory into the output directory.
|
|
2882
|
+
* Returns the number of files copied.
|
|
2883
|
+
*/
|
|
2884
|
+
async function copyPublicAssets(options) {
|
|
2168
2885
|
try {
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2886
|
+
await access(options.publicDir);
|
|
2887
|
+
if (!(await stat(options.publicDir)).isDirectory()) return 0;
|
|
2888
|
+
} catch {
|
|
2889
|
+
return 0;
|
|
2890
|
+
}
|
|
2891
|
+
await mkdir(options.outDir, { recursive: true });
|
|
2892
|
+
await cp(options.publicDir, options.outDir, {
|
|
2893
|
+
recursive: true,
|
|
2894
|
+
force: true
|
|
2895
|
+
});
|
|
2896
|
+
return countFiles(options.outDir);
|
|
2173
2897
|
}
|
|
2174
|
-
async function
|
|
2175
|
-
const
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
await writeFile(temporaryPath, JSON.stringify(entry), "utf8");
|
|
2185
|
-
await rename(temporaryPath, path);
|
|
2186
|
-
} finally {
|
|
2187
|
-
await rm(temporaryPath, { force: true });
|
|
2898
|
+
async function countFiles(dir) {
|
|
2899
|
+
const { readdir } = await import("node:fs/promises");
|
|
2900
|
+
let count = 0;
|
|
2901
|
+
async function walk(d) {
|
|
2902
|
+
const entries = await readdir(d, { withFileTypes: true });
|
|
2903
|
+
for (const entry of entries) {
|
|
2904
|
+
const path = join(d, entry.name);
|
|
2905
|
+
if (entry.isDirectory()) await walk(path);
|
|
2906
|
+
else count++;
|
|
2907
|
+
}
|
|
2188
2908
|
}
|
|
2909
|
+
await walk(dir);
|
|
2910
|
+
return count;
|
|
2189
2911
|
}
|
|
2190
|
-
var
|
|
2912
|
+
var init_vite_build = __esmMin((() => {
|
|
2913
|
+
init_interpolation_plugin();
|
|
2914
|
+
}));
|
|
2191
2915
|
//#endregion
|
|
2192
2916
|
//#region src/ssr/match.ts
|
|
2193
2917
|
/**
|
|
@@ -2228,174 +2952,53 @@ function matchApiRoute(pathname, routes) {
|
|
|
2228
2952
|
* Safely decodes a URI component. If decoding fails (malformed % sequences),
|
|
2229
2953
|
* returns the original string rather than throwing (runtime-security §10).
|
|
2230
2954
|
*/
|
|
2231
|
-
function safeDecodeURIComponent(segment) {
|
|
2232
|
-
try {
|
|
2233
|
-
return decodeURIComponent(segment);
|
|
2234
|
-
} catch {
|
|
2235
|
-
return segment;
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
function specificity(path) {
|
|
2239
|
-
return path.split("/").filter(Boolean).reduce((score, segment) => {
|
|
2240
|
-
if (segment.endsWith("*")) return score;
|
|
2241
|
-
if (segment.startsWith(":")) return score + 1;
|
|
2242
|
-
return score + 2;
|
|
2243
|
-
}, 0);
|
|
2244
|
-
}
|
|
2245
|
-
function tryMatch(requestSegments, routeSegments, optionalCatchAll = false) {
|
|
2246
|
-
const params = {};
|
|
2247
|
-
let i = 0;
|
|
2248
|
-
for (let r = 0; r < routeSegments.length; r++) {
|
|
2249
|
-
const routeSeg = routeSegments[r];
|
|
2250
|
-
if (routeSeg.endsWith("*")) {
|
|
2251
|
-
const name = routeSeg.slice(1, -1);
|
|
2252
|
-
const rest = requestSegments.slice(i);
|
|
2253
|
-
if (rest.length === 0 && !optionalCatchAll) return void 0;
|
|
2254
|
-
params[name] = rest.length > 0 ? rest : [];
|
|
2255
|
-
return params;
|
|
2256
|
-
}
|
|
2257
|
-
if (routeSeg.startsWith(":")) {
|
|
2258
|
-
const requestSeg = requestSegments[i];
|
|
2259
|
-
if (requestSeg === void 0) return void 0;
|
|
2260
|
-
params[routeSeg.slice(1)] = requestSeg;
|
|
2261
|
-
i++;
|
|
2262
|
-
continue;
|
|
2263
|
-
}
|
|
2264
|
-
if (routeSeg !== requestSegments[i]) return;
|
|
2265
|
-
i++;
|
|
2266
|
-
}
|
|
2267
|
-
if (i !== requestSegments.length) return void 0;
|
|
2268
|
-
return params;
|
|
2269
|
-
}
|
|
2270
|
-
var init_match = __esmMin((() => {}));
|
|
2271
|
-
//#endregion
|
|
2272
|
-
//#region src/ssr/stream.ts
|
|
2273
|
-
/** Builds the concrete URL path for a route pattern given matched params. */
|
|
2274
|
-
function buildConcretePath(routePath, params) {
|
|
2275
|
-
return routePath.replace(/:([a-zA-Z0-9_]+)(\*)?/g, (_m, name, catchAll) => {
|
|
2276
|
-
const value = params[name];
|
|
2277
|
-
if (value === void 0 || value === null) return "";
|
|
2278
|
-
return catchAll ? Array.isArray(value) ? value.join("/") : String(value) : String(value);
|
|
2279
|
-
});
|
|
2280
|
-
}
|
|
2281
|
-
function streamingScript(page, search) {
|
|
2282
|
-
return `<script type="module">${`
|
|
2283
|
-
async function __elurJsStreamRender() {
|
|
2284
|
-
try {
|
|
2285
|
-
const url = "/__elur-js/render?page=" + encodeURIComponent(${JSON.stringify(page)}) + "&search=" + encodeURIComponent(${JSON.stringify(search)});
|
|
2286
|
-
const res = await fetch(url);
|
|
2287
|
-
if (!res.ok) throw new Error("Streaming render failed: " + res.status);
|
|
2288
|
-
const html = await res.text();
|
|
2289
|
-
const app = document.getElementById("app");
|
|
2290
|
-
if (app) app.innerHTML = html;
|
|
2291
|
-
document.dispatchEvent(new CustomEvent("elur:rendered"));
|
|
2292
|
-
} catch (err) {
|
|
2293
|
-
console.error("[elur-kit] streaming render failed", err);
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
|
-
__elurJsStreamRender();
|
|
2297
|
-
`}<\/script>`;
|
|
2298
|
-
}
|
|
2299
|
-
/**
|
|
2300
|
-
* Render a page shell that shows the loading boundary while the real content
|
|
2301
|
-
* is fetched and injected by the client.
|
|
2302
|
-
*/
|
|
2303
|
-
async function renderStreamingPage(options) {
|
|
2304
|
-
const { route, params, searchParams, config, importer = defaultImport, actions } = options;
|
|
2305
|
-
if (!route.loadingPath) throw new Error("Cannot stream a page without a loading.ts boundary");
|
|
2306
|
-
const { default: Loading } = await importer(route.loadingPath);
|
|
2307
|
-
const body = `<div id="elur-loading">${await renderToString$1(() => Loading())}</div>${streamingScript(buildConcretePath(route.path, params), searchParams.toString())}`;
|
|
2308
|
-
const htmlAttributes = {};
|
|
2309
|
-
const headScripts = [];
|
|
2310
|
-
const headLinks = [];
|
|
2311
|
-
if (route.layouts.length > 0) {
|
|
2312
|
-
const rootLayout = route.layouts[0];
|
|
2313
|
-
const dataPath = rootLayout.replace(/layout\.ts$/, "layout.data.ts");
|
|
2314
|
-
if (dataPath !== rootLayout) try {
|
|
2315
|
-
const mod = await importer(dataPath);
|
|
2316
|
-
const layoutData = mod.load ? await mod.load({
|
|
2317
|
-
params,
|
|
2318
|
-
searchParams,
|
|
2319
|
-
request: options.request
|
|
2320
|
-
}) : void 0;
|
|
2321
|
-
if (layoutData && typeof layoutData === "object") {
|
|
2322
|
-
const attrs = layoutData.htmlAttributes;
|
|
2323
|
-
if (attrs) Object.assign(htmlAttributes, attrs);
|
|
2324
|
-
const scripts = layoutData.headScripts;
|
|
2325
|
-
if (Array.isArray(scripts)) headScripts.push(...scripts);
|
|
2326
|
-
const links = layoutData.headLinks;
|
|
2327
|
-
if (Array.isArray(links)) headLinks.push(...links);
|
|
2328
|
-
}
|
|
2329
|
-
} catch {}
|
|
2330
|
-
}
|
|
2331
|
-
return documentShell({
|
|
2332
|
-
title: "Loading...",
|
|
2333
|
-
lang: config.lang,
|
|
2334
|
-
body,
|
|
2335
|
-
data: {
|
|
2336
|
-
__elur_js_streaming: true,
|
|
2337
|
-
page: route.path
|
|
2338
|
-
},
|
|
2339
|
-
actions,
|
|
2340
|
-
htmlAttributes,
|
|
2341
|
-
headScripts,
|
|
2342
|
-
headLinks,
|
|
2343
|
-
clientEntry: config.clientEntry
|
|
2344
|
-
});
|
|
2345
|
-
}
|
|
2346
|
-
/**
|
|
2347
|
-
* Render only the inner HTML body for a page. Used by the streaming endpoint
|
|
2348
|
-
* to inject the real content into the shell.
|
|
2349
|
-
*/
|
|
2350
|
-
async function renderPageBody(options) {
|
|
2351
|
-
const { routes, pathname, searchParams, config, actions, importer = defaultImport, request } = options;
|
|
2352
|
-
const match = matchRoute(pathname, routes.pages);
|
|
2353
|
-
if (!match) throw new RouteNotFoundError(pathname);
|
|
2354
|
-
const result = await renderPage({
|
|
2355
|
-
route: match.route,
|
|
2356
|
-
params: match.params,
|
|
2357
|
-
searchParams,
|
|
2358
|
-
config,
|
|
2359
|
-
actions,
|
|
2360
|
-
importer,
|
|
2361
|
-
request
|
|
2362
|
-
});
|
|
2363
|
-
if (result.response) return {
|
|
2364
|
-
body: "",
|
|
2365
|
-
title: "",
|
|
2366
|
-
response: result.response
|
|
2367
|
-
};
|
|
2368
|
-
const bodyMatch = result.html.match(/<div id="app">([\s\S]*)<\/div>\s*(<script|$)/);
|
|
2369
|
-
const body = bodyMatch ? bodyMatch[1].trim() : result.html;
|
|
2370
|
-
const titleMatch = result.html.match(/<title[^>]*>([^<]*)<\/title>/);
|
|
2371
|
-
return {
|
|
2372
|
-
body,
|
|
2373
|
-
title: titleMatch ? titleMatch[1] : result.resolvedTitle ?? "",
|
|
2374
|
-
fullHtml: result.html,
|
|
2375
|
-
clearActionErrorCookie: result.clearActionErrorCookie,
|
|
2376
|
-
head: result.head
|
|
2377
|
-
};
|
|
2955
|
+
function safeDecodeURIComponent(segment) {
|
|
2956
|
+
try {
|
|
2957
|
+
return decodeURIComponent(segment);
|
|
2958
|
+
} catch {
|
|
2959
|
+
return segment;
|
|
2960
|
+
}
|
|
2378
2961
|
}
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2962
|
+
function specificity(path) {
|
|
2963
|
+
return path.split("/").filter(Boolean).reduce((score, segment) => {
|
|
2964
|
+
if (segment.endsWith("*")) return score;
|
|
2965
|
+
if (segment.startsWith(":")) return score + 1;
|
|
2966
|
+
return score + 2;
|
|
2967
|
+
}, 0);
|
|
2968
|
+
}
|
|
2969
|
+
function tryMatch(requestSegments, routeSegments, optionalCatchAll = false) {
|
|
2970
|
+
const params = {};
|
|
2971
|
+
let i = 0;
|
|
2972
|
+
for (let r = 0; r < routeSegments.length; r++) {
|
|
2973
|
+
const routeSeg = routeSegments[r];
|
|
2974
|
+
if (routeSeg.endsWith("*")) {
|
|
2975
|
+
const name = routeSeg.slice(1, -1);
|
|
2976
|
+
const rest = requestSegments.slice(i);
|
|
2977
|
+
if (rest.length === 0 && !optionalCatchAll) return void 0;
|
|
2978
|
+
params[name] = rest.length > 0 ? rest : [];
|
|
2979
|
+
return params;
|
|
2390
2980
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2981
|
+
if (routeSeg.startsWith(":")) {
|
|
2982
|
+
const requestSeg = requestSegments[i];
|
|
2983
|
+
if (requestSeg === void 0) return void 0;
|
|
2984
|
+
params[routeSeg.slice(1)] = requestSeg;
|
|
2985
|
+
i++;
|
|
2986
|
+
continue;
|
|
2987
|
+
}
|
|
2988
|
+
if (routeSeg !== requestSegments[i]) return;
|
|
2989
|
+
i++;
|
|
2990
|
+
}
|
|
2991
|
+
if (i !== requestSegments.length) return void 0;
|
|
2992
|
+
return params;
|
|
2993
|
+
}
|
|
2994
|
+
var init_match = __esmMin((() => {}));
|
|
2393
2995
|
//#endregion
|
|
2394
2996
|
//#region src/middleware/index.ts
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2997
|
+
var middleware_exports = /* @__PURE__ */ __exportAll({
|
|
2998
|
+
loadMiddleware: () => loadMiddleware,
|
|
2999
|
+
matchesMiddleware: () => matchesMiddleware,
|
|
3000
|
+
runMiddleware: () => runMiddleware
|
|
3001
|
+
});
|
|
2399
3002
|
/**
|
|
2400
3003
|
* Loads the user's `src/middleware.ts` module. Returns `null` if no middleware
|
|
2401
3004
|
* file exists. Distinguishes "file not found" from "file has errors" (§6):
|
|
@@ -2484,854 +3087,731 @@ async function runMiddleware(middleware, request, params) {
|
|
|
2484
3087
|
}
|
|
2485
3088
|
}
|
|
2486
3089
|
}
|
|
3090
|
+
var init_middleware = __esmMin((() => {
|
|
3091
|
+
init_match();
|
|
3092
|
+
}));
|
|
2487
3093
|
//#endregion
|
|
2488
|
-
//#region src/
|
|
2489
|
-
var GlobalAbortController = globalThis.AbortController ?? AbortController;
|
|
2490
|
-
function incomingMessageToRequest(req, body) {
|
|
2491
|
-
const headers = new Headers();
|
|
2492
|
-
for (let index = 0; index < req.rawHeaders.length; index += 2) headers.append(req.rawHeaders[index], req.rawHeaders[index + 1]);
|
|
2493
|
-
const controller = new GlobalAbortController();
|
|
2494
|
-
req.once("aborted", () => controller.abort());
|
|
2495
|
-
req.once("close", () => {
|
|
2496
|
-
if (!req.complete) controller.abort();
|
|
2497
|
-
});
|
|
2498
|
-
const protocol = req.socket.encrypted ? "https" : "http";
|
|
2499
|
-
const init = {
|
|
2500
|
-
method: req.method ?? "GET",
|
|
2501
|
-
headers,
|
|
2502
|
-
signal: controller.signal
|
|
2503
|
-
};
|
|
2504
|
-
if (body !== void 0 && body !== null && init.method !== "GET" && init.method !== "HEAD") init.body = body;
|
|
2505
|
-
return new Request(`${protocol}://${headers.get("host") ?? "localhost"}${req.url ?? "/"}`, init);
|
|
2506
|
-
}
|
|
2507
|
-
//#endregion
|
|
2508
|
-
//#region src/runtime/static.ts
|
|
2509
|
-
function isInside(root, candidate) {
|
|
2510
|
-
return candidate === root || candidate.startsWith(`${root}${sep}`);
|
|
2511
|
-
}
|
|
2512
|
-
function decodePathname(pathname) {
|
|
2513
|
-
try {
|
|
2514
|
-
const decoded = decodeURIComponent(pathname);
|
|
2515
|
-
if (decoded.includes("\0") || decoded.includes("\\") || /%(?:00|2e|2f|5c)/i.test(decoded)) return null;
|
|
2516
|
-
if (decoded.split("/").some((segment) => segment === "..")) return null;
|
|
2517
|
-
return decoded;
|
|
2518
|
-
} catch {
|
|
2519
|
-
return null;
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
async function resolveStaticFile(root, pathname) {
|
|
2523
|
-
const decoded = decodePathname(pathname);
|
|
2524
|
-
if (decoded === null) return null;
|
|
2525
|
-
const resolvedRoot = resolve(root);
|
|
2526
|
-
const relativePath = decoded.replace(/^\/+/, "");
|
|
2527
|
-
let candidate = resolve(resolvedRoot, relativePath);
|
|
2528
|
-
if (!isInside(resolvedRoot, candidate)) return null;
|
|
2529
|
-
try {
|
|
2530
|
-
if ((await stat(candidate)).isDirectory()) candidate = resolve(candidate, "index.html");
|
|
2531
|
-
} catch {
|
|
2532
|
-
if (decoded.endsWith("/") || extname(decoded) === "") candidate = resolve(candidate, "index.html");
|
|
2533
|
-
}
|
|
2534
|
-
if (!isInside(resolvedRoot, candidate)) return null;
|
|
2535
|
-
try {
|
|
2536
|
-
const [canonicalRoot, canonicalCandidate, candidateStat] = await Promise.all([
|
|
2537
|
-
realpath(resolvedRoot),
|
|
2538
|
-
realpath(candidate),
|
|
2539
|
-
stat(candidate)
|
|
2540
|
-
]);
|
|
2541
|
-
if (!candidateStat.isFile() || !isInside(canonicalRoot, canonicalCandidate)) return null;
|
|
2542
|
-
return canonicalCandidate;
|
|
2543
|
-
} catch {
|
|
2544
|
-
return null;
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
var init_static = __esmMin((() => {}));
|
|
2548
|
-
//#endregion
|
|
2549
|
-
//#region src/ssr/server.ts
|
|
2550
|
-
init_route_scanner();
|
|
2551
|
-
init_scan();
|
|
2552
|
-
init_match();
|
|
2553
|
-
init_render();
|
|
2554
|
-
init_static();
|
|
2555
|
-
init_errors();
|
|
3094
|
+
//#region src/errors.ts
|
|
2556
3095
|
/**
|
|
2557
|
-
*
|
|
3096
|
+
* Type guard for action failures. Uses the marker field so it works across
|
|
3097
|
+
* bundling boundaries where `instanceof` fails.
|
|
2558
3098
|
*/
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
const actions = await scanActions(options.appDir);
|
|
2562
|
-
const publicActions = actionNames(actions);
|
|
2563
|
-
const middleware = options.root ? await loadMiddleware(options.root) : null;
|
|
2564
|
-
const resolveAction = async (name, page) => {
|
|
2565
|
-
const pageKey = resolveActionPageKey(page, routes);
|
|
2566
|
-
const pageActions = pageKey ? actions[pageKey] : Object.values(actions).find((p) => p[name]) ?? void 0;
|
|
2567
|
-
const actionPath = pageActions ? pageActions[name] : void 0;
|
|
2568
|
-
if (!actionPath) return void 0;
|
|
2569
|
-
const action = (await import(actionPath))[name];
|
|
2570
|
-
if (typeof action === "function") return action;
|
|
2571
|
-
};
|
|
2572
|
-
const server = createServer(async (req, res) => {
|
|
2573
|
-
let urlPath = req.url ?? "/";
|
|
2574
|
-
if (urlPath.includes("?")) urlPath = urlPath.split("?")[0];
|
|
2575
|
-
if (urlPath === "/__elur-js/actions" && req.method === "POST") {
|
|
2576
|
-
try {
|
|
2577
|
-
const response = await handleActionRequest(incomingMessageToRequest(req, await readRequestBody$1(req)), resolveAction, options.actionSecurity);
|
|
2578
|
-
res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
|
|
2579
|
-
res.end(await response.text());
|
|
2580
|
-
} catch (err) {
|
|
2581
|
-
console.error("[action] error handling", err);
|
|
2582
|
-
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
2583
|
-
res.end(toPublicErrorInfo(err).message);
|
|
2584
|
-
}
|
|
2585
|
-
return;
|
|
2586
|
-
}
|
|
2587
|
-
if (urlPath === "/__elur-js/render") {
|
|
2588
|
-
const renderUrl = new URL(req.url ?? "/", "http://localhost");
|
|
2589
|
-
const page = renderUrl.searchParams.get("page") ?? "/";
|
|
2590
|
-
const search = renderUrl.searchParams.get("search") ?? "";
|
|
2591
|
-
const wantsJson = (req.headers["accept"] ?? "").includes("application/json");
|
|
2592
|
-
try {
|
|
2593
|
-
const request = incomingMessageToRequest(req);
|
|
2594
|
-
let body;
|
|
2595
|
-
let title;
|
|
2596
|
-
let lastRenderedCookie;
|
|
2597
|
-
let lastRenderedHead;
|
|
2598
|
-
const ttl = await resolveTtl(options, page, routes);
|
|
2599
|
-
const cacheKey = `/__elur-js/render${page}?${search}`;
|
|
2600
|
-
if (options.cacheDir && typeof ttl === "number" && canUsePublicCache(request)) {
|
|
2601
|
-
const cached = await getCachedHtml(options.cacheDir, cacheKey);
|
|
2602
|
-
if (cached) {
|
|
2603
|
-
body = extractBody(cached.html);
|
|
2604
|
-
title = extractTitle(cached.html);
|
|
2605
|
-
} else {
|
|
2606
|
-
const rendered = await renderPageBody({
|
|
2607
|
-
routes,
|
|
2608
|
-
pathname: page,
|
|
2609
|
-
searchParams: new URLSearchParams(search),
|
|
2610
|
-
config: {
|
|
2611
|
-
lang: options.lang ?? "es",
|
|
2612
|
-
clientEntry: options.clientEntry
|
|
2613
|
-
},
|
|
2614
|
-
actions: publicActions,
|
|
2615
|
-
request
|
|
2616
|
-
});
|
|
2617
|
-
body = rendered.body;
|
|
2618
|
-
title = rendered.title;
|
|
2619
|
-
lastRenderedCookie = rendered.clearActionErrorCookie;
|
|
2620
|
-
lastRenderedHead = rendered.head;
|
|
2621
|
-
await setCachedHtml(options.cacheDir, cacheKey, rendered.fullHtml ?? "", ttl);
|
|
2622
|
-
}
|
|
2623
|
-
} else {
|
|
2624
|
-
const rendered = await renderPageBody({
|
|
2625
|
-
routes,
|
|
2626
|
-
pathname: page,
|
|
2627
|
-
searchParams: new URLSearchParams(search),
|
|
2628
|
-
config: {
|
|
2629
|
-
lang: options.lang ?? "es",
|
|
2630
|
-
clientEntry: options.clientEntry
|
|
2631
|
-
},
|
|
2632
|
-
actions: publicActions,
|
|
2633
|
-
request
|
|
2634
|
-
});
|
|
2635
|
-
body = rendered.body;
|
|
2636
|
-
title = rendered.title;
|
|
2637
|
-
lastRenderedCookie = rendered.clearActionErrorCookie;
|
|
2638
|
-
lastRenderedHead = rendered.head;
|
|
2639
|
-
}
|
|
2640
|
-
if (wantsJson) {
|
|
2641
|
-
const headers = { "Content-Type": "application/json; charset=utf-8" };
|
|
2642
|
-
const setCookie = lastRenderedCookie;
|
|
2643
|
-
if (setCookie) headers["X-Elur-Action-Clear-Cookie"] = setCookie;
|
|
2644
|
-
res.writeHead(200, headers);
|
|
2645
|
-
res.end(JSON.stringify({
|
|
2646
|
-
title,
|
|
2647
|
-
body,
|
|
2648
|
-
head: lastRenderedHead,
|
|
2649
|
-
clearActionErrorCookie: setCookie
|
|
2650
|
-
}));
|
|
2651
|
-
} else {
|
|
2652
|
-
const headers = { "Content-Type": "text/html; charset=utf-8" };
|
|
2653
|
-
if (lastRenderedCookie) headers["Set-Cookie"] = lastRenderedCookie;
|
|
2654
|
-
res.writeHead(200, headers);
|
|
2655
|
-
res.end(body);
|
|
2656
|
-
}
|
|
2657
|
-
} catch (err) {
|
|
2658
|
-
if (err instanceof RouteNotFoundError) {
|
|
2659
|
-
console.log(`[ssr] render endpoint: no route for ${page}`);
|
|
2660
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
2661
|
-
res.end("Not Found");
|
|
2662
|
-
return;
|
|
2663
|
-
}
|
|
2664
|
-
console.error("[ssr] streaming render error", err);
|
|
2665
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2666
|
-
res.end("Internal Server Error");
|
|
2667
|
-
}
|
|
2668
|
-
return;
|
|
2669
|
-
}
|
|
2670
|
-
let middlewareHeaders;
|
|
2671
|
-
if (middleware && matchesMiddleware(urlPath, middleware.config)) {
|
|
2672
|
-
const mwResult = await runMiddleware(middleware, incomingMessageToRequest(req));
|
|
2673
|
-
if (mwResult.kind === "response") {
|
|
2674
|
-
res.writeHead(mwResult.response.status, Object.fromEntries(mwResult.response.headers.entries()));
|
|
2675
|
-
res.end(Buffer.from(await mwResult.response.arrayBuffer()));
|
|
2676
|
-
return;
|
|
2677
|
-
}
|
|
2678
|
-
middlewareHeaders = mwResult.headers;
|
|
2679
|
-
}
|
|
2680
|
-
const apiMatch = matchApiRoute(urlPath, routes.api);
|
|
2681
|
-
if (apiMatch) {
|
|
2682
|
-
try {
|
|
2683
|
-
const handler = (await import(apiMatch.route.routePath))[req.method ?? "GET"];
|
|
2684
|
-
if (typeof handler !== "function") {
|
|
2685
|
-
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
2686
|
-
res.end(`Method not allowed: ${req.method}`);
|
|
2687
|
-
return;
|
|
2688
|
-
}
|
|
2689
|
-
const request = incomingMessageToRequest(req, req.method && req.method !== "GET" && req.method !== "HEAD" ? await readRequestBody$1(req) : void 0);
|
|
2690
|
-
applyHeaders(request.headers, middlewareHeaders);
|
|
2691
|
-
const response = await handler(request, { params: apiMatch.params });
|
|
2692
|
-
res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
|
|
2693
|
-
res.end(Buffer.from(await response.arrayBuffer()));
|
|
2694
|
-
} catch (err) {
|
|
2695
|
-
console.error("[api] error handling", urlPath, err);
|
|
2696
|
-
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
2697
|
-
res.end(toPublicErrorInfo(err).message);
|
|
2698
|
-
}
|
|
2699
|
-
return;
|
|
2700
|
-
}
|
|
2701
|
-
if (options.publicDir) try {
|
|
2702
|
-
if (await tryServeStatic(res, options.publicDir, urlPath)) return;
|
|
2703
|
-
} catch (err) {
|
|
2704
|
-
console.error("[static] error serving", urlPath, err);
|
|
2705
|
-
}
|
|
2706
|
-
const match = matchRoute(urlPath, routes.pages);
|
|
2707
|
-
const config = {
|
|
2708
|
-
lang: options.lang ?? "es",
|
|
2709
|
-
clientEntry: options.clientEntry
|
|
2710
|
-
};
|
|
2711
|
-
if (match) try {
|
|
2712
|
-
const request = incomingMessageToRequest(req);
|
|
2713
|
-
applyHeaders(request.headers, middlewareHeaders);
|
|
2714
|
-
let html;
|
|
2715
|
-
let clearActionErrorCookie;
|
|
2716
|
-
const ttl = (match.route.dataPath ? (await import(match.route.dataPath)).revalidate : void 0) ?? options.defaultRevalidate;
|
|
2717
|
-
if (options.streaming !== false && match.route.loadingPath) html = await renderStreamingPage({
|
|
2718
|
-
route: match.route,
|
|
2719
|
-
params: match.params,
|
|
2720
|
-
searchParams: new URLSearchParams(req.url?.split("?")[1] ?? ""),
|
|
2721
|
-
config,
|
|
2722
|
-
actions: publicActions,
|
|
2723
|
-
request
|
|
2724
|
-
});
|
|
2725
|
-
else if (options.cacheDir && typeof ttl === "number" && canUsePublicCache(request)) {
|
|
2726
|
-
const cacheKey = new URL(request.url).pathname + new URL(request.url).search;
|
|
2727
|
-
const cached = await getCachedHtml(options.cacheDir, cacheKey);
|
|
2728
|
-
if (cached) html = cached.html;
|
|
2729
|
-
else {
|
|
2730
|
-
const result = await renderPage({
|
|
2731
|
-
route: match.route,
|
|
2732
|
-
params: match.params,
|
|
2733
|
-
searchParams: new URLSearchParams(req.url?.split("?")[1] ?? ""),
|
|
2734
|
-
config,
|
|
2735
|
-
actions: publicActions,
|
|
2736
|
-
request
|
|
2737
|
-
});
|
|
2738
|
-
html = result.html;
|
|
2739
|
-
clearActionErrorCookie = result.clearActionErrorCookie;
|
|
2740
|
-
await setCachedHtml(options.cacheDir, cacheKey, html, ttl);
|
|
2741
|
-
}
|
|
2742
|
-
} else {
|
|
2743
|
-
const result = await renderPage({
|
|
2744
|
-
route: match.route,
|
|
2745
|
-
params: match.params,
|
|
2746
|
-
searchParams: new URLSearchParams(req.url?.split("?")[1] ?? ""),
|
|
2747
|
-
config,
|
|
2748
|
-
actions: publicActions,
|
|
2749
|
-
request
|
|
2750
|
-
});
|
|
2751
|
-
html = result.html;
|
|
2752
|
-
clearActionErrorCookie = result.clearActionErrorCookie;
|
|
2753
|
-
}
|
|
2754
|
-
const responseHeaders = { "Content-Type": "text/html; charset=utf-8" };
|
|
2755
|
-
if (clearActionErrorCookie) responseHeaders["Set-Cookie"] = clearActionErrorCookie;
|
|
2756
|
-
res.writeHead(200, responseHeaders);
|
|
2757
|
-
res.end(html);
|
|
2758
|
-
return;
|
|
2759
|
-
} catch (err) {
|
|
2760
|
-
console.error("[ssr] error rendering", urlPath, err);
|
|
2761
|
-
const errorResult = await renderErrorPage({
|
|
2762
|
-
routes,
|
|
2763
|
-
status: 500,
|
|
2764
|
-
error: err,
|
|
2765
|
-
config,
|
|
2766
|
-
actions: publicActions
|
|
2767
|
-
});
|
|
2768
|
-
if (errorResult) {
|
|
2769
|
-
res.writeHead(errorResult.status, { "Content-Type": "text/html; charset=utf-8" });
|
|
2770
|
-
res.end(errorResult.html);
|
|
2771
|
-
} else {
|
|
2772
|
-
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
2773
|
-
res.end(toPublicErrorInfo(err).message);
|
|
2774
|
-
}
|
|
2775
|
-
return;
|
|
2776
|
-
}
|
|
2777
|
-
const errorResult = await renderErrorPage({
|
|
2778
|
-
routes,
|
|
2779
|
-
status: 404,
|
|
2780
|
-
config,
|
|
2781
|
-
actions: publicActions
|
|
2782
|
-
});
|
|
2783
|
-
if (errorResult) {
|
|
2784
|
-
res.writeHead(errorResult.status, { "Content-Type": "text/html; charset=utf-8" });
|
|
2785
|
-
res.end(errorResult.html);
|
|
2786
|
-
return;
|
|
2787
|
-
}
|
|
2788
|
-
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
2789
|
-
res.end(`Not found: ${req.url}`);
|
|
2790
|
-
});
|
|
2791
|
-
return {
|
|
2792
|
-
server,
|
|
2793
|
-
listen() {
|
|
2794
|
-
return new Promise((resolve) => {
|
|
2795
|
-
server.listen(options.port ?? 3e3, options.host ?? "127.0.0.1", () => {
|
|
2796
|
-
console.log(`\n → SSR server http://${options.host ?? "127.0.0.1"}:${options.port ?? 3e3}`);
|
|
2797
|
-
resolve();
|
|
2798
|
-
});
|
|
2799
|
-
});
|
|
2800
|
-
},
|
|
2801
|
-
close() {
|
|
2802
|
-
return new Promise((resolve, reject) => {
|
|
2803
|
-
server.close((err) => err ? reject(err) : resolve());
|
|
2804
|
-
});
|
|
2805
|
-
}
|
|
2806
|
-
};
|
|
2807
|
-
}
|
|
2808
|
-
async function tryServeStatic(res, publicDir, urlPath) {
|
|
2809
|
-
const filePath = await resolveStaticFile(publicDir, urlPath);
|
|
2810
|
-
if (!filePath) return false;
|
|
2811
|
-
const contentType = guessContentType$1(filePath);
|
|
2812
|
-
let data = await readFile(filePath);
|
|
2813
|
-
if (contentType.includes("text/html")) data = data.toString("utf8").replace("<meta name=\"elur:render-endpoint\" content=\"off\" />", "<meta name=\"elur:render-endpoint\" content=\"on\" />");
|
|
2814
|
-
res.writeHead(200, {
|
|
2815
|
-
"Content-Type": contentType,
|
|
2816
|
-
"Content-Length": Buffer.byteLength(data)
|
|
2817
|
-
});
|
|
2818
|
-
res.end(data);
|
|
2819
|
-
return true;
|
|
2820
|
-
}
|
|
2821
|
-
function canUsePublicCache(request) {
|
|
2822
|
-
return !request.headers.has("cookie") && !request.headers.has("authorization");
|
|
2823
|
-
}
|
|
2824
|
-
function applyHeaders(headers, values) {
|
|
2825
|
-
if (!values) return;
|
|
2826
|
-
for (const [name, value] of Object.entries(values)) headers.set(name, value);
|
|
2827
|
-
}
|
|
2828
|
-
function readRequestBody$1(req) {
|
|
2829
|
-
return new Promise((resolve, reject) => {
|
|
2830
|
-
let body = "";
|
|
2831
|
-
req.setEncoding("utf8");
|
|
2832
|
-
req.on("data", (chunk) => {
|
|
2833
|
-
body += chunk;
|
|
2834
|
-
});
|
|
2835
|
-
req.on("end", () => resolve(body));
|
|
2836
|
-
req.on("error", reject);
|
|
2837
|
-
});
|
|
2838
|
-
}
|
|
2839
|
-
function guessContentType$1(filePath) {
|
|
2840
|
-
switch (extname(filePath)) {
|
|
2841
|
-
case ".html": return "text/html; charset=utf-8";
|
|
2842
|
-
case ".js":
|
|
2843
|
-
case ".mjs": return "application/javascript; charset=utf-8";
|
|
2844
|
-
case ".css": return "text/css; charset=utf-8";
|
|
2845
|
-
case ".json": return "application/json; charset=utf-8";
|
|
2846
|
-
case ".svg": return "image/svg+xml";
|
|
2847
|
-
case ".png": return "image/png";
|
|
2848
|
-
case ".jpg":
|
|
2849
|
-
case ".jpeg": return "image/jpeg";
|
|
2850
|
-
case ".webp": return "image/webp";
|
|
2851
|
-
case ".avif": return "image/avif";
|
|
2852
|
-
case ".ico": return "image/x-icon";
|
|
2853
|
-
case ".woff": return "font/woff";
|
|
2854
|
-
case ".woff2": return "font/woff2";
|
|
2855
|
-
case ".wasm": return "application/wasm";
|
|
2856
|
-
default: return "application/octet-stream";
|
|
2857
|
-
}
|
|
3099
|
+
function isActionFailure(value) {
|
|
3100
|
+
return typeof value === "object" && value !== null && value.__elur_js_action_failure === true;
|
|
2858
3101
|
}
|
|
2859
3102
|
/**
|
|
2860
|
-
*
|
|
2861
|
-
*
|
|
2862
|
-
* path itself when it matches an exact registry key.
|
|
3103
|
+
* Type guard for redirects. Uses the marker field so it works across bundling
|
|
3104
|
+
* boundaries where `instanceof` fails.
|
|
2863
3105
|
*/
|
|
2864
|
-
function
|
|
2865
|
-
|
|
2866
|
-
if (routes.pages.some((route) => route.path === page)) return page;
|
|
2867
|
-
const match = matchRoute(page, routes.pages);
|
|
2868
|
-
return match ? match.route.path : page;
|
|
2869
|
-
}
|
|
2870
|
-
/** Resolves the ISR TTL for a page: route `revalidate` or the default. */
|
|
2871
|
-
async function resolveTtl(options, pathname, routes) {
|
|
2872
|
-
const match = matchRoute(pathname, routes.pages);
|
|
2873
|
-
if (!match) return void 0;
|
|
2874
|
-
return (match.route.dataPath ? (await import(match.route.dataPath)).revalidate : void 0) ?? options.defaultRevalidate;
|
|
2875
|
-
}
|
|
2876
|
-
function extractBody(fullHtml) {
|
|
2877
|
-
const match = fullHtml.match(/<div id="app">([\s\S]*)<\/div>\s*(<script|$)/);
|
|
2878
|
-
return match ? match[1].trim() : fullHtml;
|
|
2879
|
-
}
|
|
2880
|
-
function extractTitle(fullHtml) {
|
|
2881
|
-
const match = fullHtml.match(/<title>([^<]*)<\/title>/);
|
|
2882
|
-
return match ? match[1] : "";
|
|
2883
|
-
}
|
|
2884
|
-
//#endregion
|
|
2885
|
-
//#region src/config/index.ts
|
|
2886
|
-
async function loadElurConfig(options = {}) {
|
|
2887
|
-
const initialRoot = resolve(options.root ?? process.cwd());
|
|
2888
|
-
const configFile = options.configFile ? resolve(initialRoot, options.configFile) : await findConfigFile(initialRoot);
|
|
2889
|
-
let loaded = {};
|
|
2890
|
-
if (configFile) {
|
|
2891
|
-
const result = await loadConfigFromFile({
|
|
2892
|
-
command: options.command === "build" ? "build" : "serve",
|
|
2893
|
-
mode: options.mode ?? "development"
|
|
2894
|
-
}, configFile, initialRoot);
|
|
2895
|
-
if (!result) throw new Error(`[elur-kit] Could not load config: ${configFile}`);
|
|
2896
|
-
loaded = result.config;
|
|
2897
|
-
}
|
|
2898
|
-
const merged = mergeConfig(loaded, options.overrides ?? {});
|
|
2899
|
-
const root = resolve(initialRoot, merged.root ?? ".");
|
|
2900
|
-
const resolved = resolveConfig(root, merged, configFile);
|
|
2901
|
-
await runIntegrationHook(resolved.integrations, "config", [resolved, {
|
|
2902
|
-
root,
|
|
2903
|
-
command: options.command ?? "dev"
|
|
2904
|
-
}]);
|
|
2905
|
-
return resolved;
|
|
3106
|
+
function isRedirectResponse(value) {
|
|
3107
|
+
return typeof value === "object" && value !== null && value.__elur_js_action_redirect === true;
|
|
2906
3108
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
3109
|
+
/**
|
|
3110
|
+
* Maps an arbitrary thrown value to a public-safe error info. By default the
|
|
3111
|
+
* public message is generic; `includeDetail` (dev/verbose mode) appends the
|
|
3112
|
+
* original `Error.message` for local debugging.
|
|
3113
|
+
*/
|
|
3114
|
+
function toPublicErrorInfo(error, options = {}) {
|
|
3115
|
+
const code = "INTERNAL_SERVER_ERROR";
|
|
3116
|
+
const status = 500;
|
|
3117
|
+
if (options.includeDetail && error instanceof Error && error.message) return {
|
|
3118
|
+
code,
|
|
3119
|
+
status,
|
|
3120
|
+
message: error.message
|
|
3121
|
+
};
|
|
2912
3122
|
return {
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
contentDir: resolveInside(root, config.contentDir ?? "src/content", "contentDir"),
|
|
2917
|
-
publicDir: resolveInside(root, config.publicDir ?? "public", "publicDir"),
|
|
2918
|
-
outDir: resolveInside(root, config.outDir ?? "dist", "outDir"),
|
|
2919
|
-
site: config.site,
|
|
2920
|
-
base,
|
|
2921
|
-
trailingSlash: config.trailingSlash ?? "ignore",
|
|
2922
|
-
output: config.output ?? "static",
|
|
2923
|
-
adapter: config.adapter,
|
|
2924
|
-
images: {
|
|
2925
|
-
formats: config.images?.formats ?? ["webp", "avif"],
|
|
2926
|
-
quality: imageQuality,
|
|
2927
|
-
strict: config.images?.strict ?? false
|
|
2928
|
-
},
|
|
2929
|
-
cache: {
|
|
2930
|
-
dir: resolveInside(root, config.cache?.dir ?? ".elur/cache", "cache.dir"),
|
|
2931
|
-
defaultRevalidate: config.cache?.defaultRevalidate
|
|
2932
|
-
},
|
|
2933
|
-
security: {
|
|
2934
|
-
allowedOrigins: config.security?.allowedOrigins ?? [],
|
|
2935
|
-
strictOrigin: config.security?.strictOrigin ?? false,
|
|
2936
|
-
bodyLimit: config.security?.bodyLimit ?? 1048576,
|
|
2937
|
-
headers: config.security?.headers === false ? false : config.security?.headers ?? {}
|
|
2938
|
-
},
|
|
2939
|
-
router: {
|
|
2940
|
-
enabled: config.router?.enabled ?? true,
|
|
2941
|
-
prefetch: config.router?.prefetch ?? true
|
|
2942
|
-
},
|
|
2943
|
-
integrations: config.integrations ?? [],
|
|
2944
|
-
configFile
|
|
3123
|
+
code,
|
|
3124
|
+
status,
|
|
3125
|
+
message: "Internal Server Error"
|
|
2945
3126
|
};
|
|
2946
3127
|
}
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
...base.cache,
|
|
2957
|
-
...override.cache
|
|
2958
|
-
},
|
|
2959
|
-
security: {
|
|
2960
|
-
...base.security,
|
|
2961
|
-
...override.security
|
|
2962
|
-
},
|
|
2963
|
-
router: {
|
|
2964
|
-
...base.router,
|
|
2965
|
-
...override.router
|
|
2966
|
-
},
|
|
2967
|
-
integrations: override.integrations ?? base.integrations
|
|
3128
|
+
/**
|
|
3129
|
+
* Builds a production-safe JSON error Response. Logs the raw error separately
|
|
3130
|
+
* (never reflected in the response body) and keeps the request id header.
|
|
3131
|
+
*/
|
|
3132
|
+
function publicErrorResponse(error, options = {}) {
|
|
3133
|
+
const info = toPublicErrorInfo(error, options);
|
|
3134
|
+
const headers = {
|
|
3135
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
3136
|
+
"Cache-Control": "no-store"
|
|
2968
3137
|
};
|
|
3138
|
+
if (options.requestId) headers["X-Request-Id"] = options.requestId;
|
|
3139
|
+
return new Response(JSON.stringify({ error: info }), {
|
|
3140
|
+
status: info.status,
|
|
3141
|
+
headers
|
|
3142
|
+
});
|
|
2969
3143
|
}
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
3144
|
+
var init_errors = __esmMin((() => {}));
|
|
3145
|
+
//#endregion
|
|
3146
|
+
//#region src/action/origin.ts
|
|
3147
|
+
/**
|
|
3148
|
+
* Returns the host:port of a URL string, or undefined if it cannot be parsed.
|
|
3149
|
+
*/
|
|
3150
|
+
function originOf(urlString) {
|
|
3151
|
+
if (!urlString) return void 0;
|
|
3152
|
+
try {
|
|
3153
|
+
const url = new URL(urlString);
|
|
3154
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
|
|
3155
|
+
return url.origin;
|
|
3156
|
+
} catch {
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
2975
3159
|
}
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
3160
|
+
/**
|
|
3161
|
+
* Verifies that a request originates from the same host (or an allow-listed
|
|
3162
|
+
* origin). Returns an error message when the request must be rejected, or
|
|
3163
|
+
* undefined when it is allowed.
|
|
3164
|
+
*
|
|
3165
|
+
* @param request The incoming Request to actions.
|
|
3166
|
+
* @param options Origin check configuration.
|
|
3167
|
+
*/
|
|
3168
|
+
function verifyOrigin(request, options = {}) {
|
|
3169
|
+
const targetOrigin = originOf(request.url);
|
|
3170
|
+
if (!targetOrigin) return "Invalid target URL";
|
|
3171
|
+
const origin = request.headers.get("Origin");
|
|
3172
|
+
const referer = request.headers.get("Referer");
|
|
3173
|
+
if (!origin && !referer) return options.strictOrigin ? "Missing Origin and Referer headers" : void 0;
|
|
3174
|
+
const sourceOrigin = origin ? originOf(origin) : originOf(referer);
|
|
3175
|
+
if (!sourceOrigin) return origin ? "Invalid Origin header" : "Invalid Referer header";
|
|
3176
|
+
if (sourceOrigin === targetOrigin) return void 0;
|
|
3177
|
+
if (options.allowedOrigins?.some((allowed) => originOf(allowed) === sourceOrigin)) return void 0;
|
|
3178
|
+
return `Cross-origin request blocked: source "${sourceOrigin}" != target "${targetOrigin}"`;
|
|
2979
3179
|
}
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3180
|
+
/** Builds a 403 Response for a rejected origin. */
|
|
3181
|
+
function originForbidden(message) {
|
|
3182
|
+
return new Response(message, {
|
|
3183
|
+
status: 403,
|
|
3184
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
3187
|
+
var init_origin = __esmMin((() => {}));
|
|
3188
|
+
//#endregion
|
|
3189
|
+
//#region src/cache/adapter.ts
|
|
3190
|
+
var adapter_exports = /* @__PURE__ */ __exportAll({
|
|
3191
|
+
cacheKey: () => cacheKey,
|
|
3192
|
+
createFsCacheAdapter: () => createFsCacheAdapter,
|
|
3193
|
+
getWithSWR: () => getWithSWR
|
|
3194
|
+
});
|
|
3195
|
+
/** Computes a SHA-256 key from a normalized identity string. */
|
|
3196
|
+
function cacheKey(...parts) {
|
|
3197
|
+
return createHash("sha256").update(parts.join("\n")).digest("hex");
|
|
3198
|
+
}
|
|
3199
|
+
function createFsCacheAdapter(options) {
|
|
3200
|
+
const { cacheDir } = options;
|
|
3201
|
+
const maxEntries = options.maxEntries ?? 1e3;
|
|
3202
|
+
const maxAgeMs = options.maxAgeMs ?? 864e5;
|
|
3203
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
3204
|
+
const tagIndexPath = join(cacheDir, "_tag-index.json");
|
|
3205
|
+
async function loadTagIndex() {
|
|
2993
3206
|
try {
|
|
2994
|
-
await
|
|
2995
|
-
return
|
|
2996
|
-
} catch {
|
|
3207
|
+
const raw = await readFile(tagIndexPath, "utf8");
|
|
3208
|
+
return JSON.parse(raw);
|
|
3209
|
+
} catch {
|
|
3210
|
+
return {};
|
|
3211
|
+
}
|
|
2997
3212
|
}
|
|
2998
|
-
|
|
2999
|
-
|
|
3213
|
+
async function saveTagIndex(index) {
|
|
3214
|
+
await mkdir(dirname(tagIndexPath), { recursive: true });
|
|
3215
|
+
const tmp = `${tagIndexPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
3000
3216
|
try {
|
|
3001
|
-
await
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
}
|
|
3007
|
-
//#endregion
|
|
3008
|
-
//#region src/manifest/index.ts
|
|
3009
|
-
init_scan();
|
|
3010
|
-
init_route_scanner();
|
|
3011
|
-
async function createAppManifest(config) {
|
|
3012
|
-
const [routes, actions, islands] = await Promise.all([
|
|
3013
|
-
scanRoutes(config.appDir),
|
|
3014
|
-
scanActions(config.appDir),
|
|
3015
|
-
scanIslands(config.islandsDir)
|
|
3016
|
-
]);
|
|
3017
|
-
validateManifestRoutes(routes);
|
|
3018
|
-
validateIslands(islands);
|
|
3019
|
-
const manifest = {
|
|
3020
|
-
version: 1,
|
|
3021
|
-
root: config.root,
|
|
3022
|
-
routes,
|
|
3023
|
-
actions,
|
|
3024
|
-
islands,
|
|
3025
|
-
base: config.base,
|
|
3026
|
-
output: config.output
|
|
3027
|
-
};
|
|
3028
|
-
await runIntegrationHook(config.integrations, "routes", [manifest, {
|
|
3029
|
-
root: config.root,
|
|
3030
|
-
command: "build"
|
|
3031
|
-
}]);
|
|
3032
|
-
return manifest;
|
|
3033
|
-
}
|
|
3034
|
-
async function writeAppManifest(manifest, path) {
|
|
3035
|
-
await mkdir(dirname(path), { recursive: true });
|
|
3036
|
-
await writeFile(path, JSON.stringify(toPortableManifest(manifest), null, 2), "utf8");
|
|
3037
|
-
}
|
|
3038
|
-
async function writeRouteTypes(manifest, path) {
|
|
3039
|
-
const routePaths = manifest.routes.pages.map((route) => JSON.stringify(route.path));
|
|
3040
|
-
const actionNames = Object.values(manifest.actions).flatMap((actions) => Object.keys(actions)).filter((name, index, names) => names.indexOf(name) === index).map((name) => JSON.stringify(name));
|
|
3041
|
-
const source = [
|
|
3042
|
-
`export type ElurRoutePath = ${routePaths.length ? routePaths.join(" | ") : "never"};`,
|
|
3043
|
-
`export type ElurActionName = ${actionNames.length ? actionNames.join(" | ") : "never"};`,
|
|
3044
|
-
"export interface ElurRouteParams { [name: string]: string | string[] | undefined }",
|
|
3045
|
-
""
|
|
3046
|
-
].join("\n");
|
|
3047
|
-
await mkdir(dirname(path), { recursive: true });
|
|
3048
|
-
await writeFile(path, source, "utf8");
|
|
3049
|
-
}
|
|
3050
|
-
function validateManifestRoutes(routes) {
|
|
3051
|
-
const seen = /* @__PURE__ */ new Map();
|
|
3052
|
-
for (const route of routes.pages) {
|
|
3053
|
-
registerRoute(seen, route.path, route.pagePath, "page");
|
|
3054
|
-
assertNotReserved(route.path, route.pagePath);
|
|
3217
|
+
await writeFile(tmp, JSON.stringify(index), "utf8");
|
|
3218
|
+
await rename(tmp, tagIndexPath);
|
|
3219
|
+
} finally {
|
|
3220
|
+
await rm(tmp, { force: true });
|
|
3221
|
+
}
|
|
3055
3222
|
}
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
assertNotReserved(route.path, route.routePath);
|
|
3223
|
+
function entryPath(key) {
|
|
3224
|
+
return join(cacheDir, `${key}.html.json`);
|
|
3059
3225
|
}
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3226
|
+
async function get(key) {
|
|
3227
|
+
const existing = inFlight.get(key);
|
|
3228
|
+
if (existing) return existing;
|
|
3229
|
+
const promise = (async () => {
|
|
3230
|
+
try {
|
|
3231
|
+
const raw = await readFile(entryPath(key), "utf8");
|
|
3232
|
+
return JSON.parse(raw);
|
|
3233
|
+
} catch {
|
|
3234
|
+
return null;
|
|
3235
|
+
}
|
|
3236
|
+
})();
|
|
3237
|
+
inFlight.set(key, promise);
|
|
3238
|
+
try {
|
|
3239
|
+
return await promise;
|
|
3240
|
+
} finally {
|
|
3241
|
+
inFlight.delete(key);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
async function set(key, value, opts) {
|
|
3245
|
+
const path = entryPath(key);
|
|
3246
|
+
await mkdir(dirname(path), { recursive: true });
|
|
3247
|
+
const entry = {
|
|
3248
|
+
html: value.html,
|
|
3249
|
+
generatedAt: value.generatedAt ?? Date.now(),
|
|
3250
|
+
revalidate: opts.revalidate,
|
|
3251
|
+
tags: opts.tags,
|
|
3252
|
+
version: opts.version
|
|
3253
|
+
};
|
|
3254
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
3255
|
+
try {
|
|
3256
|
+
await writeFile(tmp, JSON.stringify(entry), "utf8");
|
|
3257
|
+
await rename(tmp, path);
|
|
3258
|
+
} finally {
|
|
3259
|
+
await rm(tmp, { force: true });
|
|
3260
|
+
}
|
|
3261
|
+
if (opts.tags && opts.tags.length > 0) {
|
|
3262
|
+
const index = await loadTagIndex();
|
|
3263
|
+
for (const tag of opts.tags) {
|
|
3264
|
+
if (!index[tag]) index[tag] = [];
|
|
3265
|
+
if (!index[tag].includes(key)) index[tag].push(key);
|
|
3266
|
+
}
|
|
3267
|
+
await saveTagIndex(index);
|
|
3268
|
+
}
|
|
3269
|
+
await maybeCleanup();
|
|
3270
|
+
}
|
|
3271
|
+
async function del(key) {
|
|
3272
|
+
await rm(entryPath(key), { force: true });
|
|
3273
|
+
}
|
|
3274
|
+
async function invalidateTags(tags) {
|
|
3275
|
+
if (tags.length === 0) return;
|
|
3276
|
+
const index = await loadTagIndex();
|
|
3277
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
3278
|
+
for (const tag of tags) {
|
|
3279
|
+
const keys = index[tag];
|
|
3280
|
+
if (keys) {
|
|
3281
|
+
for (const key of keys) keysToDelete.add(key);
|
|
3282
|
+
delete index[tag];
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
await Promise.all([...keysToDelete].map((key) => del(key)));
|
|
3286
|
+
await saveTagIndex(index);
|
|
3287
|
+
}
|
|
3288
|
+
let lastCleanup = 0;
|
|
3289
|
+
async function maybeCleanup() {
|
|
3290
|
+
const now = Date.now();
|
|
3291
|
+
if (now - lastCleanup < 6e4) return;
|
|
3292
|
+
lastCleanup = now;
|
|
3293
|
+
try {
|
|
3294
|
+
const files = await readdir(cacheDir);
|
|
3295
|
+
let entryCount = 0;
|
|
3296
|
+
const toDelete = [];
|
|
3297
|
+
for (const file of files) {
|
|
3298
|
+
if (!file.endsWith(".html.json")) continue;
|
|
3299
|
+
entryCount++;
|
|
3300
|
+
const filePath = join(cacheDir, file);
|
|
3301
|
+
try {
|
|
3302
|
+
if (now - (await stat(filePath)).mtimeMs > maxAgeMs) toDelete.push(filePath);
|
|
3303
|
+
} catch {}
|
|
3304
|
+
}
|
|
3305
|
+
if (entryCount - toDelete.length > maxEntries) {
|
|
3306
|
+
const candidates = [];
|
|
3307
|
+
for (const file of files) {
|
|
3308
|
+
if (!file.endsWith(".html.json")) continue;
|
|
3309
|
+
const filePath = join(cacheDir, file);
|
|
3310
|
+
if (toDelete.includes(filePath)) continue;
|
|
3311
|
+
try {
|
|
3312
|
+
const stats = await stat(filePath);
|
|
3313
|
+
candidates.push({
|
|
3314
|
+
path: filePath,
|
|
3315
|
+
mtime: stats.mtimeMs
|
|
3316
|
+
});
|
|
3317
|
+
} catch {}
|
|
3318
|
+
}
|
|
3319
|
+
candidates.sort((a, b) => a.mtime - b.mtime);
|
|
3320
|
+
const excess = entryCount - toDelete.length - maxEntries;
|
|
3321
|
+
for (let i = 0; i < excess && i < candidates.length; i++) toDelete.push(candidates[i].path);
|
|
3322
|
+
}
|
|
3323
|
+
await Promise.all(toDelete.map((p) => rm(p, { force: true })));
|
|
3324
|
+
} catch {}
|
|
3074
3325
|
}
|
|
3075
|
-
}
|
|
3076
|
-
function toPortableManifest(manifest) {
|
|
3077
|
-
const relativePath = (path) => path ? relative(manifest.root, path).split("\\").join("/") : void 0;
|
|
3078
|
-
const routes = {
|
|
3079
|
-
pages: manifest.routes.pages.map((route) => ({
|
|
3080
|
-
...route,
|
|
3081
|
-
pagePath: relativePath(route.pagePath),
|
|
3082
|
-
dataPath: relativePath(route.dataPath),
|
|
3083
|
-
actionPath: relativePath(route.actionPath),
|
|
3084
|
-
loadingPath: relativePath(route.loadingPath),
|
|
3085
|
-
layouts: route.layouts.map((layout) => relativePath(layout))
|
|
3086
|
-
})),
|
|
3087
|
-
api: manifest.routes.api.map((route) => ({
|
|
3088
|
-
...route,
|
|
3089
|
-
routePath: relativePath(route.routePath)
|
|
3090
|
-
})),
|
|
3091
|
-
error404: manifest.routes.error404 ? {
|
|
3092
|
-
...manifest.routes.error404,
|
|
3093
|
-
pagePath: relativePath(manifest.routes.error404.pagePath),
|
|
3094
|
-
dataPath: relativePath(manifest.routes.error404.dataPath),
|
|
3095
|
-
actionPath: relativePath(manifest.routes.error404.actionPath),
|
|
3096
|
-
loadingPath: relativePath(manifest.routes.error404.loadingPath),
|
|
3097
|
-
layouts: manifest.routes.error404.layouts.map((layout) => relativePath(layout))
|
|
3098
|
-
} : void 0,
|
|
3099
|
-
error500: manifest.routes.error500 ? {
|
|
3100
|
-
...manifest.routes.error500,
|
|
3101
|
-
pagePath: relativePath(manifest.routes.error500.pagePath),
|
|
3102
|
-
dataPath: relativePath(manifest.routes.error500.dataPath),
|
|
3103
|
-
actionPath: relativePath(manifest.routes.error500.actionPath),
|
|
3104
|
-
loadingPath: relativePath(manifest.routes.error500.loadingPath),
|
|
3105
|
-
layouts: manifest.routes.error500.layouts.map((layout) => relativePath(layout))
|
|
3106
|
-
} : void 0
|
|
3107
|
-
};
|
|
3108
|
-
const actions = {};
|
|
3109
|
-
for (const [page, pageActions] of Object.entries(manifest.actions)) actions[page] = Object.fromEntries(Object.entries(pageActions).map(([name, path]) => [name, relativePath(path)]));
|
|
3110
3326
|
return {
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
islands: manifest.islands.map((island) => ({
|
|
3116
|
-
...island,
|
|
3117
|
-
filePath: relativePath(island.filePath)
|
|
3118
|
-
}))
|
|
3327
|
+
get,
|
|
3328
|
+
set,
|
|
3329
|
+
delete: del,
|
|
3330
|
+
invalidateTags
|
|
3119
3331
|
};
|
|
3120
3332
|
}
|
|
3121
|
-
//#endregion
|
|
3122
|
-
//#region src/runtime/capabilities.ts
|
|
3123
|
-
/** True when the host can write to persistent storage (for ISR/cache/image writes). */
|
|
3124
|
-
function supportsPersistentStorage(capabilities) {
|
|
3125
|
-
return capabilities.filesystem === "persistent";
|
|
3126
|
-
}
|
|
3127
3333
|
/**
|
|
3128
|
-
*
|
|
3129
|
-
*
|
|
3130
|
-
*
|
|
3334
|
+
* Gets a cached entry. If the entry is stale (past revalidate), serves it
|
|
3335
|
+
* immediately and triggers a background revalidation.
|
|
3336
|
+
*
|
|
3337
|
+
* @param adapter The cache adapter.
|
|
3338
|
+
* @param key The cache key.
|
|
3339
|
+
* @param revalidate The revalidation function (called if stale or missing).
|
|
3340
|
+
* @returns The cache entry (fresh or stale), or null if missing.
|
|
3131
3341
|
*/
|
|
3132
|
-
function
|
|
3133
|
-
const
|
|
3134
|
-
if (
|
|
3135
|
-
|
|
3136
|
-
|
|
3342
|
+
async function getWithSWR(adapter, key, revalidate) {
|
|
3343
|
+
const entry = await adapter.get(key);
|
|
3344
|
+
if (!entry) return {
|
|
3345
|
+
entry: await revalidate(),
|
|
3346
|
+
stale: false
|
|
3347
|
+
};
|
|
3348
|
+
if (Date.now() - entry.generatedAt >= entry.revalidate * 1e3) {
|
|
3349
|
+
revalidate().then((fresh) => {
|
|
3350
|
+
if (fresh) adapter.set(key, fresh, {
|
|
3351
|
+
revalidate: entry.revalidate,
|
|
3352
|
+
tags: entry.tags,
|
|
3353
|
+
version: entry.version
|
|
3354
|
+
}).catch((err) => {
|
|
3355
|
+
console.error("[elur-kit] background cache write failed:", err);
|
|
3356
|
+
});
|
|
3357
|
+
}, (err) => {
|
|
3358
|
+
console.error("[elur-kit] background revalidation failed:", err);
|
|
3359
|
+
});
|
|
3360
|
+
return {
|
|
3361
|
+
entry,
|
|
3362
|
+
stale: true
|
|
3363
|
+
};
|
|
3364
|
+
}
|
|
3137
3365
|
return {
|
|
3138
|
-
|
|
3139
|
-
|
|
3366
|
+
entry,
|
|
3367
|
+
stale: false
|
|
3140
3368
|
};
|
|
3141
3369
|
}
|
|
3142
|
-
var
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3370
|
+
var init_adapter = __esmMin((() => {}));
|
|
3371
|
+
//#endregion
|
|
3372
|
+
//#region src/cache/invalidation.ts
|
|
3373
|
+
/**
|
|
3374
|
+
* Connects a CacheAdapter to the default invalidator so that tag/path
|
|
3375
|
+
* invalidation events from actions are dispatched to the cache.
|
|
3376
|
+
*
|
|
3377
|
+
* Returns an unsubscribe function.
|
|
3378
|
+
*/
|
|
3379
|
+
function connectCacheAdapter(adapter, invalidator = defaultInvalidator) {
|
|
3380
|
+
return invalidator.on(async (event) => {
|
|
3381
|
+
if (event.tags && event.tags.length > 0) await adapter.invalidateTags(event.tags);
|
|
3382
|
+
if (event.paths && event.paths.length > 0 && adapter.delete) {
|
|
3383
|
+
const { cacheKey } = await Promise.resolve().then(() => (init_adapter(), adapter_exports));
|
|
3384
|
+
await Promise.all(event.paths.map((p) => adapter.delete(cacheKey(p))));
|
|
3385
|
+
}
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3388
|
+
var CacheInvalidator, defaultInvalidator;
|
|
3389
|
+
var init_invalidation = __esmMin((() => {
|
|
3390
|
+
CacheInvalidator = class {
|
|
3391
|
+
listeners = /* @__PURE__ */ new Set();
|
|
3392
|
+
/** Registers a listener for invalidation events. Returns an unsubscribe function. */
|
|
3393
|
+
on(listener) {
|
|
3394
|
+
this.listeners.add(listener);
|
|
3395
|
+
return () => this.listeners.delete(listener);
|
|
3396
|
+
}
|
|
3397
|
+
/** Emits an invalidation event to all listeners. */
|
|
3398
|
+
async emit(event) {
|
|
3399
|
+
const promises = [];
|
|
3400
|
+
for (const listener of this.listeners) try {
|
|
3401
|
+
const result = listener(event);
|
|
3402
|
+
if (result instanceof Promise) promises.push(result.catch((err) => {
|
|
3403
|
+
console.error("[elur-kit] invalidation listener error:", err);
|
|
3404
|
+
}));
|
|
3405
|
+
} catch (err) {
|
|
3406
|
+
console.error("[elur-kit] invalidation listener error:", err);
|
|
3407
|
+
}
|
|
3408
|
+
await Promise.all(promises);
|
|
3409
|
+
}
|
|
3410
|
+
/** Convenience: invalidate by tags. */
|
|
3411
|
+
async invalidateTags(tags, source) {
|
|
3412
|
+
if (tags.length === 0) return;
|
|
3413
|
+
await this.emit({
|
|
3414
|
+
tags,
|
|
3415
|
+
source
|
|
3416
|
+
});
|
|
3417
|
+
}
|
|
3418
|
+
/** Convenience: invalidate by paths. */
|
|
3419
|
+
async invalidatePaths(paths, source) {
|
|
3420
|
+
if (paths.length === 0) return;
|
|
3421
|
+
await this.emit({
|
|
3422
|
+
paths,
|
|
3423
|
+
source
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
/** Removes all listeners. */
|
|
3427
|
+
clear() {
|
|
3428
|
+
this.listeners.clear();
|
|
3429
|
+
}
|
|
3156
3430
|
};
|
|
3431
|
+
defaultInvalidator = new CacheInvalidator();
|
|
3157
3432
|
}));
|
|
3158
3433
|
//#endregion
|
|
3159
|
-
//#region src/
|
|
3160
|
-
var vite_build_exports = /* @__PURE__ */ __exportAll({
|
|
3161
|
-
beginAtomicStage: () => beginAtomicStage,
|
|
3162
|
-
buildClientBundle: () => buildClientBundle,
|
|
3163
|
-
copyPublicAssets: () => copyPublicAssets
|
|
3164
|
-
});
|
|
3434
|
+
//#region src/action/server.ts
|
|
3165
3435
|
/**
|
|
3166
|
-
*
|
|
3167
|
-
*
|
|
3168
|
-
* The user's config is loaded programmatically and the elur interpolation
|
|
3169
|
-
* plugin is injected so partial attribute interpolations inside islands are
|
|
3170
|
-
* transformed before reaching the browser.
|
|
3436
|
+
* Reads the request body as text, enforcing a maximum size.
|
|
3437
|
+
* Returns a 413 response if the body exceeds the limit.
|
|
3171
3438
|
*/
|
|
3172
|
-
async function
|
|
3173
|
-
const
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3439
|
+
async function readBodyWithLimit(request, limit) {
|
|
3440
|
+
const contentLength = request.headers.get("Content-Length");
|
|
3441
|
+
if (contentLength && parseInt(contentLength, 10) > limit) return {
|
|
3442
|
+
ok: false,
|
|
3443
|
+
response: new Response("Request body too large", {
|
|
3444
|
+
status: 413,
|
|
3445
|
+
headers: { "Content-Type": "text/plain" }
|
|
3446
|
+
})
|
|
3447
|
+
};
|
|
3448
|
+
const reader = request.body?.getReader();
|
|
3449
|
+
if (!reader) return {
|
|
3450
|
+
ok: true,
|
|
3451
|
+
text: ""
|
|
3452
|
+
};
|
|
3453
|
+
const chunks = [];
|
|
3454
|
+
let totalSize = 0;
|
|
3455
|
+
try {
|
|
3456
|
+
for (;;) {
|
|
3457
|
+
const { done, value } = await reader.read();
|
|
3458
|
+
if (done) break;
|
|
3459
|
+
totalSize += value.byteLength;
|
|
3460
|
+
if (totalSize > limit) {
|
|
3461
|
+
try {
|
|
3462
|
+
reader.cancel();
|
|
3463
|
+
} catch {}
|
|
3464
|
+
return {
|
|
3465
|
+
ok: false,
|
|
3466
|
+
response: new Response("Request body too large", {
|
|
3467
|
+
status: 413,
|
|
3468
|
+
headers: { "Content-Type": "text/plain" }
|
|
3469
|
+
})
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
chunks.push(value);
|
|
3473
|
+
}
|
|
3474
|
+
} finally {
|
|
3475
|
+
try {
|
|
3476
|
+
reader.releaseLock();
|
|
3477
|
+
} catch {}
|
|
3478
|
+
}
|
|
3479
|
+
const total = new Uint8Array(totalSize);
|
|
3480
|
+
let offset = 0;
|
|
3481
|
+
for (const chunk of chunks) {
|
|
3482
|
+
total.set(chunk, offset);
|
|
3483
|
+
offset += chunk.byteLength;
|
|
3484
|
+
}
|
|
3485
|
+
return {
|
|
3486
|
+
ok: true,
|
|
3487
|
+
text: new TextDecoder().decode(total)
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
function parseFormBody(body) {
|
|
3491
|
+
const params = new URLSearchParams(body);
|
|
3492
|
+
const result = {};
|
|
3493
|
+
for (const [key, value] of params) if (result[key] === void 0) result[key] = value;
|
|
3494
|
+
else if (Array.isArray(result[key])) result[key].push(value);
|
|
3495
|
+
else result[key] = [result[key], value];
|
|
3496
|
+
return result;
|
|
3497
|
+
}
|
|
3498
|
+
async function parseActionRequest(request, bodyLimit = DEFAULT_BODY_LIMIT) {
|
|
3499
|
+
if (request.method !== "POST") return {
|
|
3500
|
+
ok: false,
|
|
3501
|
+
response: new Response("Method not allowed", {
|
|
3502
|
+
status: 405,
|
|
3503
|
+
headers: { "Content-Type": "text/plain" }
|
|
3504
|
+
})
|
|
3505
|
+
};
|
|
3506
|
+
const contentType = request.headers.get("Content-Type") ?? "";
|
|
3507
|
+
const wantsJson = (request.headers.get("Accept") ?? "").includes("application/json");
|
|
3508
|
+
let name;
|
|
3509
|
+
let page;
|
|
3510
|
+
let args = [];
|
|
3511
|
+
if (contentType.includes("application/json")) {
|
|
3512
|
+
const bodyResult = await readBodyWithLimit(request, bodyLimit);
|
|
3513
|
+
if (!bodyResult.ok) return {
|
|
3514
|
+
ok: false,
|
|
3515
|
+
response: bodyResult.response
|
|
3516
|
+
};
|
|
3517
|
+
let body;
|
|
3518
|
+
try {
|
|
3519
|
+
body = JSON.parse(bodyResult.text);
|
|
3520
|
+
} catch {
|
|
3521
|
+
return {
|
|
3522
|
+
ok: false,
|
|
3523
|
+
response: new Response("Invalid JSON body", {
|
|
3524
|
+
status: 400,
|
|
3525
|
+
headers: { "Content-Type": "text/plain" }
|
|
3526
|
+
})
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
name = body.name;
|
|
3530
|
+
page = body.page;
|
|
3531
|
+
args = Array.isArray(body.args) ? body.args : [];
|
|
3532
|
+
} else if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
3533
|
+
if (contentType.includes("multipart/form-data")) {
|
|
3534
|
+
const contentLength = request.headers.get("Content-Length");
|
|
3535
|
+
if (contentLength && parseInt(contentLength, 10) > bodyLimit) return {
|
|
3536
|
+
ok: false,
|
|
3537
|
+
response: new Response("Request body too large", {
|
|
3538
|
+
status: 413,
|
|
3539
|
+
headers: { "Content-Type": "text/plain" }
|
|
3540
|
+
})
|
|
3541
|
+
};
|
|
3542
|
+
let form;
|
|
3543
|
+
try {
|
|
3544
|
+
form = await request.formData();
|
|
3545
|
+
} catch {
|
|
3546
|
+
return {
|
|
3547
|
+
ok: false,
|
|
3548
|
+
response: new Response("Invalid form body", {
|
|
3549
|
+
status: 400,
|
|
3550
|
+
headers: { "Content-Type": "text/plain" }
|
|
3551
|
+
})
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3554
|
+
name = form.get("__elur_js_action_name") ?? void 0;
|
|
3555
|
+
page = form.get("__elur_js_action_page") ?? void 0;
|
|
3556
|
+
const input = {};
|
|
3557
|
+
for (const [key, value] of form) {
|
|
3558
|
+
if (key === "__elur_js_action_name" || key === "__elur_js_action_page") continue;
|
|
3559
|
+
input[key] = value;
|
|
3560
|
+
}
|
|
3561
|
+
args = [input];
|
|
3562
|
+
} else {
|
|
3563
|
+
const bodyResult = await readBodyWithLimit(request, bodyLimit);
|
|
3564
|
+
if (!bodyResult.ok) return {
|
|
3565
|
+
ok: false,
|
|
3566
|
+
response: bodyResult.response
|
|
3567
|
+
};
|
|
3568
|
+
const form = parseFormBody(bodyResult.text);
|
|
3569
|
+
name = form.__elur_js_action_name;
|
|
3570
|
+
page = form.__elur_js_action_page;
|
|
3571
|
+
const input = {};
|
|
3572
|
+
for (const [key, value] of Object.entries(form)) {
|
|
3573
|
+
if (key === "__elur_js_action_name" || key === "__elur_js_action_page") continue;
|
|
3574
|
+
input[key] = value;
|
|
3575
|
+
}
|
|
3576
|
+
args = [input];
|
|
3577
|
+
}
|
|
3578
|
+
} else {
|
|
3579
|
+
const bodyResult = await readBodyWithLimit(request, bodyLimit);
|
|
3580
|
+
if (!bodyResult.ok) return {
|
|
3581
|
+
ok: false,
|
|
3582
|
+
response: bodyResult.response
|
|
3583
|
+
};
|
|
3584
|
+
const form = parseFormBody(bodyResult.text);
|
|
3585
|
+
name = form.__elur_js_action_name;
|
|
3586
|
+
page = form.__elur_js_action_page;
|
|
3587
|
+
const input = {};
|
|
3588
|
+
for (const [key, value] of Object.entries(form)) {
|
|
3589
|
+
if (key === "__elur_js_action_name" || key === "__elur_js_action_page") continue;
|
|
3590
|
+
input[key] = value;
|
|
3591
|
+
}
|
|
3592
|
+
args = [input];
|
|
3593
|
+
}
|
|
3594
|
+
if (!name || typeof name !== "string") return {
|
|
3595
|
+
ok: false,
|
|
3596
|
+
response: new Response("Missing action name", {
|
|
3597
|
+
status: 400,
|
|
3598
|
+
headers: { "Content-Type": "text/plain" }
|
|
3599
|
+
})
|
|
3191
3600
|
};
|
|
3192
|
-
const result = await build(config);
|
|
3193
|
-
const outputCount = (Array.isArray(result) ? result : [result]).reduce((n, r) => n + ("output" in r ? r.output?.length ?? 0 : 0), 0);
|
|
3194
|
-
console.log(`${log} ✓ ${outputCount} asset(s) emitted → ${relative(options.root, options.outDir)}`);
|
|
3195
3601
|
return {
|
|
3196
|
-
|
|
3197
|
-
|
|
3602
|
+
ok: true,
|
|
3603
|
+
name,
|
|
3604
|
+
page,
|
|
3605
|
+
args,
|
|
3606
|
+
wantsJson
|
|
3198
3607
|
};
|
|
3199
3608
|
}
|
|
3200
|
-
async function loadUserConfig(path, _root) {
|
|
3201
|
-
const mod = await import(path);
|
|
3202
|
-
const raw = mod.default ?? mod;
|
|
3203
|
-
const resolved = typeof raw === "function" ? await raw({
|
|
3204
|
-
command: "build",
|
|
3205
|
-
mode: "production"
|
|
3206
|
-
}) : raw;
|
|
3207
|
-
return (resolved && typeof resolved.then === "function" ? await resolved : resolved) ?? {};
|
|
3208
|
-
}
|
|
3209
3609
|
/**
|
|
3210
|
-
*
|
|
3610
|
+
* Handles a POST request to the server action endpoint.
|
|
3211
3611
|
*
|
|
3212
|
-
*
|
|
3213
|
-
*
|
|
3214
|
-
*
|
|
3215
|
-
*
|
|
3216
|
-
*
|
|
3217
|
-
*
|
|
3218
|
-
*
|
|
3219
|
-
*
|
|
3220
|
-
*
|
|
3612
|
+
* Accepts both JSON requests (`{ name, page?, args }`) and HTML form submissions
|
|
3613
|
+
* for progressive enhancement. The provided resolver looks up the action
|
|
3614
|
+
* implementation, invokes it with the supplied arguments and returns the result
|
|
3615
|
+
* as JSON or redirects back to the request origin for form submissions.
|
|
3616
|
+
*
|
|
3617
|
+
* Origin verification (CSRF protection) runs before parsing the body: any
|
|
3618
|
+
* cross-origin POST is rejected with 403 unless its origin is allow-listed via
|
|
3619
|
+
* `security.allowedOrigins`.
|
|
3620
|
+
*
|
|
3621
|
+
* For progressive-enhancement form submissions that fail, the failure payload
|
|
3622
|
+
* is relayed back via a short-lived `__elur_js_action_error` cookie (SameSite=Lax,
|
|
3623
|
+
* Max-Age=15s) instead of a query param, so errors do not leak into browser
|
|
3624
|
+
* history, server logs or third-party Referer headers.
|
|
3221
3625
|
*/
|
|
3222
|
-
async function
|
|
3223
|
-
const
|
|
3224
|
-
|
|
3225
|
-
await
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
force: true
|
|
3626
|
+
async function handleActionRequest(request, resolveAction, security = {}) {
|
|
3627
|
+
const originError = verifyOrigin(request, security);
|
|
3628
|
+
if (originError) return originForbidden(originError);
|
|
3629
|
+
const parsed = await parseActionRequest(request, security.bodyLimit ?? DEFAULT_BODY_LIMIT);
|
|
3630
|
+
if (!parsed.ok) return parsed.response;
|
|
3631
|
+
const { name, page, args, wantsJson } = parsed;
|
|
3632
|
+
try {
|
|
3633
|
+
const action = await resolveAction(name, page);
|
|
3634
|
+
if (!action) {
|
|
3635
|
+
const message = page ? `Action not found: ${name} (page: ${page})` : `Action not found: ${name}`;
|
|
3636
|
+
return new Response(message, {
|
|
3637
|
+
status: 404,
|
|
3638
|
+
headers: { "Content-Type": "text/plain" }
|
|
3236
3639
|
});
|
|
3237
|
-
await safeRename(outDir, backup);
|
|
3238
3640
|
}
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
}
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3641
|
+
const actionMeta = action.__elurAction;
|
|
3642
|
+
let result;
|
|
3643
|
+
if (actionMeta) {
|
|
3644
|
+
const ctx = {
|
|
3645
|
+
request,
|
|
3646
|
+
signal: request.signal,
|
|
3647
|
+
idempotencyKey: request.headers.get("Idempotency-Key") ?? void 0,
|
|
3648
|
+
params: {},
|
|
3649
|
+
locals: {}
|
|
3650
|
+
};
|
|
3651
|
+
result = await action(args[0], ctx);
|
|
3652
|
+
} else result = await action(...args);
|
|
3653
|
+
if (!isActionFailure(result) && actionMeta) {
|
|
3654
|
+
const tags = actionMeta.invalidateTags ?? [];
|
|
3655
|
+
const paths = actionMeta.invalidatePaths ?? [];
|
|
3656
|
+
if (tags.length > 0 || paths.length > 0) await defaultInvalidator.emit({
|
|
3657
|
+
tags,
|
|
3658
|
+
paths,
|
|
3659
|
+
source: name
|
|
3660
|
+
});
|
|
3255
3661
|
}
|
|
3256
|
-
if (
|
|
3257
|
-
|
|
3258
|
-
|
|
3662
|
+
if (isActionFailure(result)) {
|
|
3663
|
+
if (wantsJson) return new Response(JSON.stringify({
|
|
3664
|
+
__elur_js_action_failure: true,
|
|
3665
|
+
status: result.status,
|
|
3666
|
+
data: result.data
|
|
3667
|
+
}), {
|
|
3668
|
+
status: result.status,
|
|
3669
|
+
headers: { "Content-Type": "application/json" }
|
|
3670
|
+
});
|
|
3671
|
+
const referer = request.headers.get("Referer") ?? "/";
|
|
3672
|
+
const url = new URL(referer, "http://localhost");
|
|
3673
|
+
const { value } = encodeActionErrorCookie(result.data, result.status);
|
|
3674
|
+
return new Response(null, {
|
|
3675
|
+
status: 303,
|
|
3676
|
+
headers: {
|
|
3677
|
+
Location: url.pathname + url.search,
|
|
3678
|
+
"Content-Type": "text/plain",
|
|
3679
|
+
"Set-Cookie": setActionErrorCookieHeader(value)
|
|
3680
|
+
}
|
|
3681
|
+
});
|
|
3682
|
+
}
|
|
3683
|
+
if (isRedirectResponse(result)) {
|
|
3684
|
+
if (wantsJson) return new Response(JSON.stringify({
|
|
3685
|
+
__elur_js_action_redirect: true,
|
|
3686
|
+
status: result.status,
|
|
3687
|
+
location: result.location
|
|
3688
|
+
}), {
|
|
3689
|
+
status: 200,
|
|
3690
|
+
headers: { "Content-Type": "application/json" }
|
|
3691
|
+
});
|
|
3692
|
+
return new Response(null, {
|
|
3693
|
+
status: result.status,
|
|
3694
|
+
headers: {
|
|
3695
|
+
Location: result.location,
|
|
3696
|
+
"Content-Type": "text/plain"
|
|
3697
|
+
}
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3700
|
+
if (wantsJson) return new Response(JSON.stringify(result ?? null), {
|
|
3701
|
+
status: 200,
|
|
3702
|
+
headers: { "Content-Type": "application/json" }
|
|
3259
3703
|
});
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3704
|
+
const referer = request.headers.get("Referer") ?? "/";
|
|
3705
|
+
return new Response(null, {
|
|
3706
|
+
status: 303,
|
|
3707
|
+
headers: {
|
|
3708
|
+
Location: typeof result === "string" ? result : referer,
|
|
3709
|
+
"Content-Type": "text/plain"
|
|
3710
|
+
}
|
|
3265
3711
|
});
|
|
3266
|
-
};
|
|
3267
|
-
return {
|
|
3268
|
-
tempDir,
|
|
3269
|
-
commit,
|
|
3270
|
-
rollback
|
|
3271
|
-
};
|
|
3272
|
-
}
|
|
3273
|
-
function basename$1(path) {
|
|
3274
|
-
const parts = path.split(/[\\/]+/).filter(Boolean);
|
|
3275
|
-
return parts[parts.length - 1] ?? "output";
|
|
3276
|
-
}
|
|
3277
|
-
async function safeRename(src, dest) {
|
|
3278
|
-
await rm(dest, {
|
|
3279
|
-
recursive: true,
|
|
3280
|
-
force: true
|
|
3281
|
-
});
|
|
3282
|
-
try {
|
|
3283
|
-
await rename(src, dest);
|
|
3284
3712
|
} catch (err) {
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
recursive: true,
|
|
3288
|
-
force: true
|
|
3289
|
-
});
|
|
3290
|
-
await rm(src, {
|
|
3291
|
-
recursive: true,
|
|
3292
|
-
force: true
|
|
3293
|
-
});
|
|
3294
|
-
} else throw err;
|
|
3713
|
+
console.error("[elur-kit] Action error:", err);
|
|
3714
|
+
return publicErrorResponse(err, { includeDetail: false });
|
|
3295
3715
|
}
|
|
3296
3716
|
}
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3717
|
+
var DEFAULT_BODY_LIMIT;
|
|
3718
|
+
var init_server = __esmMin((() => {
|
|
3719
|
+
init_errors();
|
|
3720
|
+
init_origin();
|
|
3721
|
+
init_invalidation();
|
|
3722
|
+
init_error_store();
|
|
3723
|
+
DEFAULT_BODY_LIMIT = 1048576;
|
|
3724
|
+
}));
|
|
3725
|
+
//#endregion
|
|
3726
|
+
//#region src/ssr/stream.ts
|
|
3300
3727
|
/**
|
|
3301
|
-
*
|
|
3302
|
-
*
|
|
3728
|
+
* Render only the inner HTML body for a page. Used by the streaming endpoint
|
|
3729
|
+
* to inject the real content into the shell.
|
|
3303
3730
|
*/
|
|
3304
|
-
async function
|
|
3731
|
+
async function renderPageBody(options) {
|
|
3732
|
+
const { routes, pathname, searchParams, config, actions, importer = defaultImport$1, request } = options;
|
|
3733
|
+
const match = matchRoute(pathname, routes.pages);
|
|
3734
|
+
if (!match) throw new RouteNotFoundError(pathname);
|
|
3735
|
+
const result = await renderPage({
|
|
3736
|
+
route: match.route,
|
|
3737
|
+
params: match.params,
|
|
3738
|
+
searchParams,
|
|
3739
|
+
config,
|
|
3740
|
+
actions,
|
|
3741
|
+
importer,
|
|
3742
|
+
request
|
|
3743
|
+
});
|
|
3744
|
+
if (result.response) return {
|
|
3745
|
+
body: "",
|
|
3746
|
+
title: "",
|
|
3747
|
+
response: result.response
|
|
3748
|
+
};
|
|
3749
|
+
const body = extractAppBody(result.html)?.trim() ?? result.html.match(/<div id="app">([\s\S]*)<\/div>\s*(<script|$)/)?.[1]?.trim() ?? result.html;
|
|
3750
|
+
const titleMatch = result.html.match(/<title[^>]*>([^<]*)<\/title>/);
|
|
3751
|
+
return {
|
|
3752
|
+
body,
|
|
3753
|
+
title: titleMatch ? titleMatch[1] : result.resolvedTitle ?? "",
|
|
3754
|
+
fullHtml: result.html,
|
|
3755
|
+
clearActionErrorCookie: result.clearActionErrorCookie,
|
|
3756
|
+
head: result.head,
|
|
3757
|
+
data: result.data !== void 0 ? serializeData(result.data) : void 0,
|
|
3758
|
+
actions: actions && Object.keys(actions).length > 0 ? serializeData(actions) : void 0
|
|
3759
|
+
};
|
|
3760
|
+
}
|
|
3761
|
+
var defaultImport$1, RouteNotFoundError;
|
|
3762
|
+
var init_stream = __esmMin((() => {
|
|
3763
|
+
init_document_shell();
|
|
3764
|
+
init_match();
|
|
3765
|
+
init_render();
|
|
3766
|
+
defaultImport$1 = (path) => import(path);
|
|
3767
|
+
RouteNotFoundError = class extends Error {
|
|
3768
|
+
constructor(pathname) {
|
|
3769
|
+
super(`No route found for ${pathname}`);
|
|
3770
|
+
this.name = "RouteNotFoundError";
|
|
3771
|
+
}
|
|
3772
|
+
};
|
|
3773
|
+
}));
|
|
3774
|
+
//#endregion
|
|
3775
|
+
//#region src/runtime/static.ts
|
|
3776
|
+
function isInside(root, candidate) {
|
|
3777
|
+
return candidate === root || candidate.startsWith(`${root}${sep}`);
|
|
3778
|
+
}
|
|
3779
|
+
function decodePathname(pathname) {
|
|
3305
3780
|
try {
|
|
3306
|
-
|
|
3307
|
-
if (
|
|
3781
|
+
const decoded = decodeURIComponent(pathname);
|
|
3782
|
+
if (decoded.includes("\0") || decoded.includes("\\") || /%(?:00|2e|2f|5c)/i.test(decoded)) return null;
|
|
3783
|
+
if (decoded.split("/").some((segment) => segment === "..")) return null;
|
|
3784
|
+
return decoded;
|
|
3308
3785
|
} catch {
|
|
3309
|
-
return
|
|
3786
|
+
return null;
|
|
3310
3787
|
}
|
|
3311
|
-
await mkdir(options.outDir, { recursive: true });
|
|
3312
|
-
await cp(options.publicDir, options.outDir, {
|
|
3313
|
-
recursive: true,
|
|
3314
|
-
force: true
|
|
3315
|
-
});
|
|
3316
|
-
return countFiles(options.outDir);
|
|
3317
3788
|
}
|
|
3318
|
-
async function
|
|
3319
|
-
const
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3789
|
+
async function resolveStaticFile(root, pathname) {
|
|
3790
|
+
const decoded = decodePathname(pathname);
|
|
3791
|
+
if (decoded === null) return null;
|
|
3792
|
+
const resolvedRoot = resolve(root);
|
|
3793
|
+
const relativePath = decoded.replace(/^\/+/, "");
|
|
3794
|
+
let candidate = resolve(resolvedRoot, relativePath);
|
|
3795
|
+
if (!isInside(resolvedRoot, candidate)) return null;
|
|
3796
|
+
try {
|
|
3797
|
+
if ((await stat(candidate)).isDirectory()) candidate = resolve(candidate, "index.html");
|
|
3798
|
+
} catch {
|
|
3799
|
+
if (decoded.endsWith("/") || extname(decoded) === "") candidate = resolve(candidate, "index.html");
|
|
3800
|
+
}
|
|
3801
|
+
if (!isInside(resolvedRoot, candidate)) return null;
|
|
3802
|
+
try {
|
|
3803
|
+
const [canonicalRoot, canonicalCandidate, candidateStat] = await Promise.all([
|
|
3804
|
+
realpath(resolvedRoot),
|
|
3805
|
+
realpath(candidate),
|
|
3806
|
+
stat(candidate)
|
|
3807
|
+
]);
|
|
3808
|
+
if (!candidateStat.isFile() || !isInside(canonicalRoot, canonicalCandidate)) return null;
|
|
3809
|
+
return canonicalCandidate;
|
|
3810
|
+
} catch {
|
|
3811
|
+
return null;
|
|
3328
3812
|
}
|
|
3329
|
-
await walk(dir);
|
|
3330
|
-
return count;
|
|
3331
3813
|
}
|
|
3332
|
-
var
|
|
3333
|
-
init_interpolation_plugin();
|
|
3334
|
-
}));
|
|
3814
|
+
var init_static = __esmMin((() => {}));
|
|
3335
3815
|
//#endregion
|
|
3336
3816
|
//#region src/runtime/context.ts
|
|
3337
3817
|
function htmlResponse(body, status = 200, headers) {
|
|
@@ -3567,6 +4047,213 @@ var init_security_headers = __esmMin((() => {
|
|
|
3567
4047
|
};
|
|
3568
4048
|
}));
|
|
3569
4049
|
//#endregion
|
|
4050
|
+
//#region src/router/redirects.ts
|
|
4051
|
+
/**
|
|
4052
|
+
* Checks if a pathname matches a redirect rule and returns the redirect
|
|
4053
|
+
* Response if so.
|
|
4054
|
+
*/
|
|
4055
|
+
function matchRedirect(pathname, rules) {
|
|
4056
|
+
for (const rule of rules) {
|
|
4057
|
+
const params = matchPattern(pathname, rule.from);
|
|
4058
|
+
if (params) {
|
|
4059
|
+
const location = interpolatePath(rule.to, params);
|
|
4060
|
+
const status = rule.status ?? 308;
|
|
4061
|
+
return new Response(null, {
|
|
4062
|
+
status,
|
|
4063
|
+
headers: { Location: location }
|
|
4064
|
+
});
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
/**
|
|
4069
|
+
* Checks if a pathname matches a rewrite rule and returns the rewritten
|
|
4070
|
+
* pathname if so.
|
|
4071
|
+
*/
|
|
4072
|
+
function matchRewrite(pathname, rules) {
|
|
4073
|
+
for (const rule of rules) {
|
|
4074
|
+
const params = matchPattern(pathname, rule.from);
|
|
4075
|
+
if (params) return interpolatePath(rule.to, params);
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
/**
|
|
4079
|
+
* Returns headers that should be applied to a response for the given pathname.
|
|
4080
|
+
*/
|
|
4081
|
+
function matchRouteHeaders(pathname, rules) {
|
|
4082
|
+
for (const rule of rules) if (matchPattern(pathname, rule.path)) return rule.headers;
|
|
4083
|
+
}
|
|
4084
|
+
/**
|
|
4085
|
+
* Matches a pathname against a pattern with :param and * wildcards.
|
|
4086
|
+
* Returns the extracted params, or undefined if no match.
|
|
4087
|
+
*/
|
|
4088
|
+
function matchPattern(pathname, pattern) {
|
|
4089
|
+
const requestSegments = pathname.split("?")[0].split("/").filter(Boolean);
|
|
4090
|
+
const patternSegments = pattern.split("/").filter(Boolean);
|
|
4091
|
+
const params = {};
|
|
4092
|
+
let i = 0;
|
|
4093
|
+
for (let r = 0; r < patternSegments.length; r++) {
|
|
4094
|
+
const seg = patternSegments[r];
|
|
4095
|
+
if (seg === "*") return params;
|
|
4096
|
+
if (seg.endsWith("*")) {
|
|
4097
|
+
const name = seg.slice(1, -1);
|
|
4098
|
+
params[name] = requestSegments.slice(i).join("/");
|
|
4099
|
+
return params;
|
|
4100
|
+
}
|
|
4101
|
+
if (seg.startsWith(":")) {
|
|
4102
|
+
const name = seg.slice(1);
|
|
4103
|
+
if (requestSegments[i] === void 0) return void 0;
|
|
4104
|
+
params[name] = requestSegments[i];
|
|
4105
|
+
i++;
|
|
4106
|
+
continue;
|
|
4107
|
+
}
|
|
4108
|
+
if (seg !== requestSegments[i]) return void 0;
|
|
4109
|
+
i++;
|
|
4110
|
+
}
|
|
4111
|
+
if (i !== requestSegments.length) return void 0;
|
|
4112
|
+
return params;
|
|
4113
|
+
}
|
|
4114
|
+
/**
|
|
4115
|
+
* Interpolates :param placeholders in a path with actual values.
|
|
4116
|
+
*/
|
|
4117
|
+
function interpolatePath(template, params) {
|
|
4118
|
+
return template.replace(/:(\w+)\*?/g, (_match, name) => {
|
|
4119
|
+
return params[name] ?? "";
|
|
4120
|
+
});
|
|
4121
|
+
}
|
|
4122
|
+
var init_redirects = __esmMin((() => {}));
|
|
4123
|
+
//#endregion
|
|
4124
|
+
//#region src/middleware/stream-boundary.ts
|
|
4125
|
+
/**
|
|
4126
|
+
* Builds the resolved content chunk for a boundary ID.
|
|
4127
|
+
* Emits a `<template>` element with the resolved content, followed by a
|
|
4128
|
+
* `<script>` that replaces the fallback div with the template content
|
|
4129
|
+
* in-place. This is real Suspense streaming — the browser swaps the DOM
|
|
4130
|
+
* node without a full re-render.
|
|
4131
|
+
*
|
|
4132
|
+
* (v2.1 — Fix #4: real Suspense streaming with `<template>` replacement)
|
|
4133
|
+
*/
|
|
4134
|
+
function buildResolvedChunk(boundaryId, resolvedHtml) {
|
|
4135
|
+
return `<template id="${boundaryId}-tpl">${resolvedHtml}</template><script>(function(){var t=document.getElementById(${JSON.stringify(boundaryId + "-tpl")});var f=document.getElementById(${JSON.stringify(boundaryId)});if(t&&f){f.replaceWith(t.content.cloneNode(true));}document.dispatchEvent(new CustomEvent("elur:rendered"));})();<\/script>`;
|
|
4136
|
+
}
|
|
4137
|
+
var init_stream_boundary = __esmMin((() => {
|
|
4138
|
+
new AsyncLocalStorage();
|
|
4139
|
+
}));
|
|
4140
|
+
//#endregion
|
|
4141
|
+
//#region src/ssr/stream-response.ts
|
|
4142
|
+
/**
|
|
4143
|
+
* Mid-stream error notice swapped into the loading boundary when the
|
|
4144
|
+
* background render fails after the shell was already sent. Inline styles
|
|
4145
|
+
* keep it self-contained (the page's CSS may assume the final layout).
|
|
4146
|
+
*/
|
|
4147
|
+
function buildErrorNotice() {
|
|
4148
|
+
return "<div role=\"alert\" style=\"margin:2rem auto;max-width:32rem;padding:1rem 1.25rem;border:1px solid #e5484d;border-radius:8px;color:#b3373c;font-family:system-ui,sans-serif\"><strong style=\"display:block;margin-bottom:.25rem\">No se pudo cargar el contenido.</strong><span>Recarga la página para intentarlo de nuevo.</span></div>";
|
|
4149
|
+
}
|
|
4150
|
+
/**
|
|
4151
|
+
* Creates a streaming Response that sends the shell + loading fallback first,
|
|
4152
|
+
* then appends the resolved content.
|
|
4153
|
+
*
|
|
4154
|
+
* If the route has no loading boundary, falls back to a normal renderPage.
|
|
4155
|
+
*/
|
|
4156
|
+
async function createStreamingResponse(options) {
|
|
4157
|
+
const { route, params, searchParams, config, actions, importer = defaultImport, request, signal } = options;
|
|
4158
|
+
if (!route.loadingPath) {
|
|
4159
|
+
const result = await renderPage({
|
|
4160
|
+
route,
|
|
4161
|
+
params,
|
|
4162
|
+
searchParams,
|
|
4163
|
+
config,
|
|
4164
|
+
actions,
|
|
4165
|
+
importer,
|
|
4166
|
+
request
|
|
4167
|
+
});
|
|
4168
|
+
if (result.response) return result.response;
|
|
4169
|
+
return new Response(result.html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
4170
|
+
}
|
|
4171
|
+
if (signal?.aborted) return new Response("Client Closed Request", { status: 499 });
|
|
4172
|
+
const loadingHtml = await renderToString$1((await importer(route.loadingPath)).default);
|
|
4173
|
+
const boundaryId = `elur-stream-${randomUUID().slice(0, 8)}`;
|
|
4174
|
+
const routerCfg = config.router;
|
|
4175
|
+
const routerEnabled = routerCfg?.enabled !== false;
|
|
4176
|
+
const routerEntry = routerCfg?.entry && routerEnabled && config.js !== "legacy" ? routerCfg.entry : void 0;
|
|
4177
|
+
const shellHtml = documentShell({
|
|
4178
|
+
title: "Loading...",
|
|
4179
|
+
lang: config.lang,
|
|
4180
|
+
body: `<div id="${boundaryId}">${loadingHtml}</div>`,
|
|
4181
|
+
data: {
|
|
4182
|
+
__elur_js_streaming: true,
|
|
4183
|
+
page: route.path
|
|
4184
|
+
},
|
|
4185
|
+
actions,
|
|
4186
|
+
clientEntry: config.clientEntry,
|
|
4187
|
+
routerEntry,
|
|
4188
|
+
routerEnabled: routerCfg ? routerEnabled : void 0,
|
|
4189
|
+
renderEndpoint: config.renderEndpoint
|
|
4190
|
+
});
|
|
4191
|
+
let aborted = false;
|
|
4192
|
+
const stream = new ReadableStream({
|
|
4193
|
+
async start(controller) {
|
|
4194
|
+
const encoder = new TextEncoder();
|
|
4195
|
+
const onAbort = () => {
|
|
4196
|
+
aborted = true;
|
|
4197
|
+
try {
|
|
4198
|
+
controller.close();
|
|
4199
|
+
} catch {}
|
|
4200
|
+
};
|
|
4201
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4202
|
+
/** Enqueue unless the client went away mid-render. */
|
|
4203
|
+
const send = (html) => {
|
|
4204
|
+
if (aborted) return;
|
|
4205
|
+
controller.enqueue(encoder.encode(html));
|
|
4206
|
+
};
|
|
4207
|
+
try {
|
|
4208
|
+
send(shellHtml);
|
|
4209
|
+
const result = await renderPage({
|
|
4210
|
+
route,
|
|
4211
|
+
params,
|
|
4212
|
+
searchParams,
|
|
4213
|
+
config,
|
|
4214
|
+
actions,
|
|
4215
|
+
importer,
|
|
4216
|
+
request
|
|
4217
|
+
});
|
|
4218
|
+
if (aborted) return;
|
|
4219
|
+
if (result.response) {
|
|
4220
|
+
const status = result.response.status;
|
|
4221
|
+
const location = result.response.headers.get("Location");
|
|
4222
|
+
if (location && (status === 301 || status === 302 || status === 307 || status === 308)) send(`<script>window.location.href=${JSON.stringify(location)};<\/script>`);
|
|
4223
|
+
else send(buildResolvedChunk(boundaryId, buildErrorNotice()) + `<script>console.error(${JSON.stringify(`Loader responded with status ${status}`)});<\/script>`);
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
const innerBody = extractAppBody(result.html) ?? result.html.match(/<div id="app">([\s\S]*)<\/div>\s*(<script|$)/)?.[1]?.trim() ?? result.html;
|
|
4227
|
+
send(buildResolvedChunk(boundaryId, innerBody));
|
|
4228
|
+
} catch (err) {
|
|
4229
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
4230
|
+
send(buildResolvedChunk(boundaryId, buildErrorNotice()) + `<script>console.error(${JSON.stringify(errorMsg)});<\/script>`);
|
|
4231
|
+
} finally {
|
|
4232
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4233
|
+
if (!aborted) controller.close();
|
|
4234
|
+
}
|
|
4235
|
+
},
|
|
4236
|
+
cancel() {
|
|
4237
|
+
aborted = true;
|
|
4238
|
+
}
|
|
4239
|
+
});
|
|
4240
|
+
return new Response(stream, { headers: STREAM_HEADERS });
|
|
4241
|
+
}
|
|
4242
|
+
var STREAM_HEADERS, defaultImport;
|
|
4243
|
+
var init_stream_response = __esmMin((() => {
|
|
4244
|
+
init_render_to_string();
|
|
4245
|
+
init_document_shell();
|
|
4246
|
+
init_render();
|
|
4247
|
+
init_stream_boundary();
|
|
4248
|
+
init_capabilities();
|
|
4249
|
+
STREAM_HEADERS = {
|
|
4250
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
4251
|
+
"X-Accel-Buffering": "no",
|
|
4252
|
+
"Cache-Control": "no-store"
|
|
4253
|
+
};
|
|
4254
|
+
defaultImport = (path) => import(path);
|
|
4255
|
+
}));
|
|
4256
|
+
//#endregion
|
|
3570
4257
|
//#region src/runtime/handler.ts
|
|
3571
4258
|
var handler_exports = /* @__PURE__ */ __exportAll({ createWebHandler: () => createWebHandler });
|
|
3572
4259
|
/**
|
|
@@ -3580,14 +4267,28 @@ function createWebHandler(routes, actions, options) {
|
|
|
3580
4267
|
const clientEntry = options.clientEntry;
|
|
3581
4268
|
const renderEndpoint = options.renderEndpoint ?? true;
|
|
3582
4269
|
const noCache = options.noCache ?? false;
|
|
3583
|
-
const cacheDir = options.cacheDir;
|
|
3584
4270
|
const defaultRevalidate = options.defaultRevalidate;
|
|
3585
4271
|
const renderConfig = {
|
|
3586
4272
|
lang,
|
|
3587
4273
|
clientEntry,
|
|
3588
|
-
renderEndpoint
|
|
4274
|
+
renderEndpoint,
|
|
4275
|
+
router: options.router ? {
|
|
4276
|
+
enabled: options.router.enabled !== false,
|
|
4277
|
+
entry: options.router.entry
|
|
4278
|
+
} : void 0,
|
|
4279
|
+
js: options.js
|
|
3589
4280
|
};
|
|
3590
4281
|
const securityHeadersConfig = options.securityHeaders ?? {};
|
|
4282
|
+
const redirectRules = options.redirects ?? [];
|
|
4283
|
+
const rewriteRules = options.rewrites ?? [];
|
|
4284
|
+
const routeHeaderRules = options.routeHeaders ?? [];
|
|
4285
|
+
const capabilities = options.capabilities ?? DEFAULT_CAPABILITIES;
|
|
4286
|
+
const streamingEnabled = options.streaming === true && supportsStreaming(capabilities);
|
|
4287
|
+
const cacheAdapter = resolveCacheAdapter(options);
|
|
4288
|
+
if (cacheAdapter && !invalidatorConnectedAdapters.has(cacheAdapter)) {
|
|
4289
|
+
invalidatorConnectedAdapters.add(cacheAdapter);
|
|
4290
|
+
connectCacheAdapter(cacheAdapter);
|
|
4291
|
+
}
|
|
3591
4292
|
function createActionResolver() {
|
|
3592
4293
|
return async (name, page) => {
|
|
3593
4294
|
const pageKey = page ? routes.pages.some((route) => route.path === page) ? page : matchRoute(page, routes.pages)?.route.path ?? page : void 0;
|
|
@@ -3604,20 +4305,29 @@ function createWebHandler(routes, actions, options) {
|
|
|
3604
4305
|
};
|
|
3605
4306
|
}
|
|
3606
4307
|
const actionResolver = createActionResolver();
|
|
3607
|
-
async function handleActions(request) {
|
|
4308
|
+
async function handleActions(request, logger) {
|
|
4309
|
+
const stopTimer = logger.startTimer("action", "Server action");
|
|
3608
4310
|
try {
|
|
3609
4311
|
return await handleActionRequest(request, actionResolver);
|
|
3610
4312
|
} catch (err) {
|
|
3611
|
-
|
|
4313
|
+
logger.error("[elur-kit] action error", {
|
|
4314
|
+
path: new URL(request.url).pathname,
|
|
4315
|
+
method: request.method,
|
|
4316
|
+
error: errorMessage(err),
|
|
4317
|
+
stack: errorStack(err)
|
|
4318
|
+
});
|
|
3612
4319
|
return publicErrorResponse(err, { includeDetail: noCache });
|
|
4320
|
+
} finally {
|
|
4321
|
+
stopTimer();
|
|
3613
4322
|
}
|
|
3614
4323
|
}
|
|
3615
|
-
async function handleRenderEndpoint(request, url) {
|
|
4324
|
+
async function handleRenderEndpoint(request, url, logger) {
|
|
3616
4325
|
const page = url.searchParams.get("page") ?? "/";
|
|
3617
4326
|
const search = url.searchParams.get("search") ?? "";
|
|
3618
4327
|
const wantsJson = (request.headers.get("Accept") ?? "").includes("application/json");
|
|
4328
|
+
const stopTimer = logger.startTimer("render-endpoint", "SPA render endpoint");
|
|
3619
4329
|
try {
|
|
3620
|
-
const
|
|
4330
|
+
const result = await renderPageBody({
|
|
3621
4331
|
routes,
|
|
3622
4332
|
pathname: page,
|
|
3623
4333
|
searchParams: new URLSearchParams(search),
|
|
@@ -3626,21 +4336,39 @@ function createWebHandler(routes, actions, options) {
|
|
|
3626
4336
|
request,
|
|
3627
4337
|
importer: options.importer
|
|
3628
4338
|
});
|
|
3629
|
-
if (
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
4339
|
+
if (result.response) return result.response;
|
|
4340
|
+
const { body, title, head, clearActionErrorCookie, data, actions } = result;
|
|
4341
|
+
if (wantsJson) {
|
|
4342
|
+
const headers = {};
|
|
4343
|
+
if (clearActionErrorCookie) headers["X-Elur-Action-Clear-Cookie"] = clearActionErrorCookie;
|
|
4344
|
+
return jsonResponse({
|
|
4345
|
+
title,
|
|
4346
|
+
body,
|
|
4347
|
+
head: head ?? null,
|
|
4348
|
+
data: data ?? null,
|
|
4349
|
+
actions: actions ?? null,
|
|
4350
|
+
clearActionErrorCookie: clearActionErrorCookie ?? null
|
|
4351
|
+
}, 200, headers);
|
|
4352
|
+
}
|
|
4353
|
+
return htmlResponse(body, 200, clearActionErrorCookie ? { "Set-Cookie": clearActionErrorCookie } : void 0);
|
|
3634
4354
|
} catch (err) {
|
|
3635
4355
|
if (err instanceof RouteNotFoundError) return notFound("Not Found");
|
|
3636
4356
|
if (err instanceof Response) return err;
|
|
3637
|
-
|
|
4357
|
+
logger.error("[elur-kit] render endpoint error", {
|
|
4358
|
+
path: url.pathname,
|
|
4359
|
+
page,
|
|
4360
|
+
error: errorMessage(err),
|
|
4361
|
+
stack: errorStack(err)
|
|
4362
|
+
});
|
|
3638
4363
|
return publicErrorResponse(err, { includeDetail: noCache });
|
|
4364
|
+
} finally {
|
|
4365
|
+
stopTimer();
|
|
3639
4366
|
}
|
|
3640
4367
|
}
|
|
3641
|
-
async function handleApiRoute(request, pathname) {
|
|
4368
|
+
async function handleApiRoute(request, pathname, logger, middlewareLocals) {
|
|
3642
4369
|
const apiMatch = matchApiRoute(pathname, routes.api);
|
|
3643
4370
|
if (!apiMatch) return null;
|
|
4371
|
+
const stopTimer = logger.startTimer("api", "API route");
|
|
3644
4372
|
try {
|
|
3645
4373
|
let mod;
|
|
3646
4374
|
if (options.importer) mod = await options.importer(apiMatch.route.routePath);
|
|
@@ -3649,11 +4377,19 @@ function createWebHandler(routes, actions, options) {
|
|
|
3649
4377
|
if (typeof handler !== "function") return methodNotAllowed(request.method ?? "GET");
|
|
3650
4378
|
return await handler(request, {
|
|
3651
4379
|
params: apiMatch.params,
|
|
3652
|
-
locals: {}
|
|
4380
|
+
locals: middlewareLocals ?? {}
|
|
3653
4381
|
});
|
|
3654
4382
|
} catch (err) {
|
|
3655
|
-
|
|
4383
|
+
logger.error("[elur-kit] API route error", {
|
|
4384
|
+
path: pathname,
|
|
4385
|
+
method: request.method,
|
|
4386
|
+
route: apiMatch.route.path,
|
|
4387
|
+
error: errorMessage(err),
|
|
4388
|
+
stack: errorStack(err)
|
|
4389
|
+
});
|
|
3656
4390
|
return publicErrorResponse(err, { includeDetail: noCache });
|
|
4391
|
+
} finally {
|
|
4392
|
+
stopTimer();
|
|
3657
4393
|
}
|
|
3658
4394
|
}
|
|
3659
4395
|
async function handleStatic(pathname, request) {
|
|
@@ -3698,7 +4434,7 @@ function createWebHandler(routes, actions, options) {
|
|
|
3698
4434
|
}
|
|
3699
4435
|
return response;
|
|
3700
4436
|
}
|
|
3701
|
-
async function handleDynamicRender(request, pathname) {
|
|
4437
|
+
async function handleDynamicRender(request, pathname, logger) {
|
|
3702
4438
|
const match = matchRoute(pathname, routes.pages);
|
|
3703
4439
|
if (!match) {
|
|
3704
4440
|
const errorResult = await renderErrorPage({
|
|
@@ -3711,12 +4447,44 @@ function createWebHandler(routes, actions, options) {
|
|
|
3711
4447
|
if (errorResult) return htmlResponse(errorResult.html, errorResult.status);
|
|
3712
4448
|
return notFound(`Not found: ${pathname}`);
|
|
3713
4449
|
}
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
4450
|
+
if (streamingEnabled && match.route.loadingPath) {
|
|
4451
|
+
const stopStreamTimer = logger.startTimer("ssr", "SSR stream shell");
|
|
4452
|
+
try {
|
|
4453
|
+
return await createStreamingResponse({
|
|
4454
|
+
route: match.route,
|
|
4455
|
+
params: match.params,
|
|
4456
|
+
searchParams: new URLSearchParams(request.url.split("?")[1] ?? ""),
|
|
4457
|
+
config: renderConfig,
|
|
4458
|
+
actions: publicActions,
|
|
4459
|
+
importer: options.importer,
|
|
4460
|
+
request,
|
|
4461
|
+
signal: request.signal
|
|
4462
|
+
});
|
|
4463
|
+
} catch (err) {
|
|
4464
|
+
if (err instanceof Response) return err;
|
|
4465
|
+
logger.error("[elur-kit] SSR stream error", {
|
|
4466
|
+
path: pathname,
|
|
4467
|
+
route: match.route.path,
|
|
4468
|
+
error: errorMessage(err),
|
|
4469
|
+
stack: errorStack(err)
|
|
4470
|
+
});
|
|
4471
|
+
const errorResult = await renderErrorPage({
|
|
4472
|
+
routes,
|
|
4473
|
+
status: 500,
|
|
4474
|
+
error: err,
|
|
4475
|
+
config: renderConfig,
|
|
4476
|
+
actions: publicActions,
|
|
4477
|
+
importer: options.importer
|
|
4478
|
+
}).catch(() => void 0);
|
|
4479
|
+
if (errorResult) return htmlResponse(errorResult.html, errorResult.status);
|
|
4480
|
+
return publicErrorResponse(err, { includeDetail: noCache });
|
|
4481
|
+
} finally {
|
|
4482
|
+
stopStreamTimer();
|
|
4483
|
+
}
|
|
3718
4484
|
}
|
|
3719
|
-
|
|
4485
|
+
const cacheable = !noCache && cacheAdapter && isCacheable(request);
|
|
4486
|
+
const pageCacheKey = cacheable ? cacheKey(pathname) : void 0;
|
|
4487
|
+
const renderAndStore = async () => {
|
|
3720
4488
|
const result = await renderPage({
|
|
3721
4489
|
route: match.route,
|
|
3722
4490
|
params: match.params,
|
|
@@ -3727,14 +4495,43 @@ function createWebHandler(routes, actions, options) {
|
|
|
3727
4495
|
importer: options.importer
|
|
3728
4496
|
});
|
|
3729
4497
|
if (result.response) return result.response;
|
|
3730
|
-
if (cacheable &&
|
|
4498
|
+
if (cacheable && cacheAdapter && pageCacheKey && isResultCacheable(result, request)) {
|
|
3731
4499
|
const revalidateSeconds = result.revalidate ?? defaultRevalidate ?? 0;
|
|
3732
|
-
if (revalidateSeconds > 0) await
|
|
4500
|
+
if (revalidateSeconds > 0) await cacheAdapter.set(pageCacheKey, {
|
|
4501
|
+
html: result.html,
|
|
4502
|
+
generatedAt: Date.now(),
|
|
4503
|
+
revalidate: revalidateSeconds
|
|
4504
|
+
}, {
|
|
4505
|
+
revalidate: revalidateSeconds,
|
|
4506
|
+
tags: result.cachePolicy?.tags
|
|
4507
|
+
});
|
|
3733
4508
|
}
|
|
3734
4509
|
return htmlResponse(result.html);
|
|
4510
|
+
};
|
|
4511
|
+
if (cacheable && cacheAdapter && pageCacheKey) {
|
|
4512
|
+
const cached = await cacheAdapter.get(pageCacheKey);
|
|
4513
|
+
if (cached) {
|
|
4514
|
+
if (Date.now() - cached.generatedAt >= cached.revalidate * 1e3) renderAndStore().catch((err) => {
|
|
4515
|
+
logger.error("[elur-kit] background cache revalidation failed", {
|
|
4516
|
+
path: pathname,
|
|
4517
|
+
error: errorMessage(err),
|
|
4518
|
+
stack: errorStack(err)
|
|
4519
|
+
});
|
|
4520
|
+
});
|
|
4521
|
+
return htmlResponse(cached.html);
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
const stopTimer = logger.startTimer("ssr", "SSR render");
|
|
4525
|
+
try {
|
|
4526
|
+
return await renderAndStore();
|
|
3735
4527
|
} catch (err) {
|
|
3736
4528
|
if (err instanceof Response) return err;
|
|
3737
|
-
|
|
4529
|
+
logger.error("[elur-kit] SSR render error", {
|
|
4530
|
+
path: pathname,
|
|
4531
|
+
route: match.route.path,
|
|
4532
|
+
error: errorMessage(err),
|
|
4533
|
+
stack: errorStack(err)
|
|
4534
|
+
});
|
|
3738
4535
|
const errorResult = await renderErrorPage({
|
|
3739
4536
|
routes,
|
|
3740
4537
|
status: 500,
|
|
@@ -3745,22 +4542,83 @@ function createWebHandler(routes, actions, options) {
|
|
|
3745
4542
|
}).catch(() => void 0);
|
|
3746
4543
|
if (errorResult) return htmlResponse(errorResult.html, errorResult.status);
|
|
3747
4544
|
return publicErrorResponse(err, { includeDetail: noCache });
|
|
4545
|
+
} finally {
|
|
4546
|
+
stopTimer();
|
|
3748
4547
|
}
|
|
3749
4548
|
}
|
|
4549
|
+
function finalizeResponse(response, logger, secHeaders, routeHeaders) {
|
|
4550
|
+
const secured = applySecurityHeaders(response, secHeaders);
|
|
4551
|
+
const headers = new Headers(secured.headers);
|
|
4552
|
+
if (routeHeaders) for (const [key, value] of Object.entries(routeHeaders)) headers.set(key, value);
|
|
4553
|
+
const timing = logger.getServerTimingHeader();
|
|
4554
|
+
if (timing) headers.set("Server-Timing", timing);
|
|
4555
|
+
headers.set("X-Request-ID", logger.getRequestId());
|
|
4556
|
+
return new Response(secured.body, {
|
|
4557
|
+
status: secured.status,
|
|
4558
|
+
statusText: secured.statusText,
|
|
4559
|
+
headers
|
|
4560
|
+
});
|
|
4561
|
+
}
|
|
3750
4562
|
return async function handler(request) {
|
|
4563
|
+
const logger = createRequestLogger(request, options.logLevel);
|
|
3751
4564
|
const url = new URL(request.url);
|
|
3752
|
-
const
|
|
4565
|
+
const originalPathname = url.pathname;
|
|
3753
4566
|
const isHttps = url.protocol === "https:";
|
|
3754
4567
|
const secHeaders = securityHeadersConfig === false ? {} : buildSecurityHeaders(securityHeadersConfig, isHttps);
|
|
3755
|
-
if (
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
4568
|
+
if (redirectRules.length > 0) {
|
|
4569
|
+
const redirect = matchRedirect(originalPathname, redirectRules);
|
|
4570
|
+
if (redirect) return finalizeResponse(redirect, logger, secHeaders, matchRouteHeaders(originalPathname, routeHeaderRules));
|
|
4571
|
+
}
|
|
4572
|
+
let pathname = originalPathname;
|
|
4573
|
+
if (rewriteRules.length > 0) pathname = matchRewrite(originalPathname, rewriteRules) ?? originalPathname;
|
|
4574
|
+
const routeHeaders = matchRouteHeaders(originalPathname, routeHeaderRules);
|
|
4575
|
+
if (pathname === "/__elur-js/actions" && request.method === "POST") return finalizeResponse(await handleActions(request, logger), logger, secHeaders, routeHeaders);
|
|
4576
|
+
if (pathname === "/__elur-js/render" && renderEndpoint) return finalizeResponse(await handleRenderEndpoint(request, url, logger), logger, secHeaders, routeHeaders);
|
|
4577
|
+
let middlewareLocals;
|
|
4578
|
+
const middleware = options.middleware;
|
|
4579
|
+
if (middleware && matchesMiddleware(pathname, middleware.config)) {
|
|
4580
|
+
let mwResult;
|
|
4581
|
+
try {
|
|
4582
|
+
mwResult = await runMiddleware(middleware, request);
|
|
4583
|
+
} catch (err) {
|
|
4584
|
+
logger.error("[elur-kit] middleware error", {
|
|
4585
|
+
path: pathname,
|
|
4586
|
+
error: errorMessage(err),
|
|
4587
|
+
stack: errorStack(err)
|
|
4588
|
+
});
|
|
4589
|
+
return finalizeResponse(publicErrorResponse(err, { includeDetail: noCache }), logger, secHeaders, routeHeaders);
|
|
4590
|
+
}
|
|
4591
|
+
if (mwResult.kind === "response") return finalizeResponse(mwResult.response, logger, secHeaders, routeHeaders);
|
|
4592
|
+
middlewareLocals = mwResult.locals;
|
|
4593
|
+
if (mwResult.headers) {
|
|
4594
|
+
const merged = new Headers(request.headers);
|
|
4595
|
+
for (const [key, value] of Object.entries(mwResult.headers)) merged.set(key, value);
|
|
4596
|
+
request = new Request(request, { headers: merged });
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
const apiResponse = await handleApiRoute(request, pathname, logger, middlewareLocals);
|
|
4600
|
+
if (apiResponse) return finalizeResponse(apiResponse, logger, secHeaders, routeHeaders);
|
|
3759
4601
|
const staticResponse = await handleStatic(pathname, request);
|
|
3760
|
-
if (staticResponse) return
|
|
3761
|
-
return
|
|
4602
|
+
if (staticResponse) return finalizeResponse(staticResponse, logger, secHeaders, routeHeaders);
|
|
4603
|
+
return finalizeResponse(await handleDynamicRender(request, pathname, logger), logger, secHeaders, routeHeaders);
|
|
3762
4604
|
};
|
|
3763
4605
|
}
|
|
4606
|
+
function resolveCacheAdapter(options) {
|
|
4607
|
+
if (options.cacheAdapter) return options.cacheAdapter;
|
|
4608
|
+
if (!options.cacheDir) return void 0;
|
|
4609
|
+
let adapter = defaultCacheAdapters.get(options.cacheDir);
|
|
4610
|
+
if (!adapter) {
|
|
4611
|
+
adapter = createFsCacheAdapter({ cacheDir: options.cacheDir });
|
|
4612
|
+
defaultCacheAdapters.set(options.cacheDir, adapter);
|
|
4613
|
+
}
|
|
4614
|
+
return adapter;
|
|
4615
|
+
}
|
|
4616
|
+
function errorMessage(err) {
|
|
4617
|
+
return err instanceof Error ? err.message : String(err);
|
|
4618
|
+
}
|
|
4619
|
+
function errorStack(err) {
|
|
4620
|
+
return err instanceof Error ? err.stack : void 0;
|
|
4621
|
+
}
|
|
3764
4622
|
function isCacheable(request) {
|
|
3765
4623
|
if (request.method !== "GET" && request.method !== "HEAD") return false;
|
|
3766
4624
|
if (request.headers.get("Cookie")) return false;
|
|
@@ -3780,6 +4638,7 @@ function isResultCacheable(result, request) {
|
|
|
3780
4638
|
if (request.headers.get("Authorization")) return false;
|
|
3781
4639
|
return true;
|
|
3782
4640
|
}
|
|
4641
|
+
var defaultCacheAdapters, invalidatorConnectedAdapters;
|
|
3783
4642
|
var init_handler = __esmMin((() => {
|
|
3784
4643
|
init_match();
|
|
3785
4644
|
init_server();
|
|
@@ -3788,9 +4647,16 @@ var init_handler = __esmMin((() => {
|
|
|
3788
4647
|
init_scan();
|
|
3789
4648
|
init_context();
|
|
3790
4649
|
init_errors();
|
|
3791
|
-
|
|
4650
|
+
init_adapter();
|
|
4651
|
+
init_invalidation();
|
|
3792
4652
|
init_policy();
|
|
3793
4653
|
init_security_headers();
|
|
4654
|
+
init_redirects();
|
|
4655
|
+
init_middleware();
|
|
4656
|
+
init_stream_response();
|
|
4657
|
+
init_capabilities();
|
|
4658
|
+
defaultCacheAdapters = /* @__PURE__ */ new Map();
|
|
4659
|
+
invalidatorConnectedAdapters = /* @__PURE__ */ new WeakSet();
|
|
3794
4660
|
}));
|
|
3795
4661
|
//#endregion
|
|
3796
4662
|
//#region src/adapters/shared.ts
|
|
@@ -3878,8 +4744,10 @@ async function buildSsrEntry(routes, options, entryDir) {
|
|
|
3878
4744
|
}
|
|
3879
4745
|
if (names.length > 0) actionsRegistry[page.path] = names;
|
|
3880
4746
|
}
|
|
4747
|
+
const routerEnabled = options.router?.enabled !== false;
|
|
4748
|
+
const routerEntry = routerEnabled && options.js !== "legacy" && existsSync(resolve(options.root, options.outDir, "_elur", "router.js")) ? "/_elur/router.js" : null;
|
|
3881
4749
|
return `// AUTO-GENERATED by @elurjs/kit. Do not edit.
|
|
3882
|
-
import { handleActionRequest, matchApiRoute, matchRoute, renderPage, renderErrorPage } from "@elurjs/kit";
|
|
4750
|
+
import { handleActionRequest, matchApiRoute, matchRoute, renderPage, renderPageBody, renderErrorPage, createStreamingResponse } from "@elurjs/kit";
|
|
3883
4751
|
${imports}
|
|
3884
4752
|
|
|
3885
4753
|
const registry = new Map<string, unknown>([
|
|
@@ -3909,6 +4777,15 @@ const routes = {
|
|
|
3909
4777
|
|
|
3910
4778
|
const clientEntry = ${JSON.stringify(options.clientEntry)};
|
|
3911
4779
|
const lang = ${JSON.stringify(options.lang)};
|
|
4780
|
+
// Client router/JS emission rules baked at adapter build time.
|
|
4781
|
+
const router = ${JSON.stringify({
|
|
4782
|
+
enabled: routerEnabled,
|
|
4783
|
+
entry: routerEntry
|
|
4784
|
+
})};
|
|
4785
|
+
const jsMode = ${JSON.stringify(options.js === "legacy" ? "legacy" : "modern")};
|
|
4786
|
+
// Opt-in streaming SSR (experimental): routes with a loading boundary stream
|
|
4787
|
+
// the shell first and swap in the resolved content as a follow-up chunk.
|
|
4788
|
+
const streaming = ${options.streaming === true};
|
|
3912
4789
|
|
|
3913
4790
|
function loadModule(path: string) {
|
|
3914
4791
|
const mod = registry.get(path);
|
|
@@ -3957,37 +4834,38 @@ export default async function handler(request: Request): Promise<Response> {
|
|
|
3957
4834
|
const search = url.searchParams.get("search") ?? "";
|
|
3958
4835
|
const wantsJson = (request.headers.get("Accept") ?? "").includes("application/json");
|
|
3959
4836
|
try {
|
|
3960
|
-
const
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
route: match.route,
|
|
3964
|
-
params: match.params,
|
|
4837
|
+
const result = await renderPageBody({
|
|
4838
|
+
routes,
|
|
4839
|
+
pathname: page,
|
|
3965
4840
|
searchParams: new URLSearchParams(search),
|
|
3966
|
-
config: { lang, clientEntry },
|
|
4841
|
+
config: { lang, clientEntry, router, js: jsMode },
|
|
3967
4842
|
importer: loadModule,
|
|
3968
4843
|
actions,
|
|
3969
4844
|
request,
|
|
3970
4845
|
});
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
const body
|
|
3974
|
-
? html.slice(bodyStart + 13, bodyEnd).trim()
|
|
3975
|
-
: html;
|
|
3976
|
-
const titleStart = html.indexOf("<title>");
|
|
3977
|
-
const titleEnd = html.indexOf("</title>");
|
|
3978
|
-
const title = titleStart >= 0 && titleEnd > titleStart
|
|
3979
|
-
? html.slice(titleStart + 7, titleEnd)
|
|
3980
|
-
: "";
|
|
4846
|
+
// A thrown Response from a loader is a first-class response (A-22).
|
|
4847
|
+
if (result.response) return result.response;
|
|
4848
|
+
const { body, title, head, clearActionErrorCookie, data, actions: actionsPayload } = result;
|
|
3981
4849
|
if (wantsJson) {
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
4850
|
+
const headers = { "Content-Type": "application/json; charset=utf-8" };
|
|
4851
|
+
if (clearActionErrorCookie) headers["X-Elur-Action-Clear-Cookie"] = clearActionErrorCookie;
|
|
4852
|
+
// The ?? null fallbacks keep every key present — JSON.stringify drops
|
|
4853
|
+
// undefined and the SPA payload shape must be stable across runtimes.
|
|
4854
|
+
return new Response(
|
|
4855
|
+
JSON.stringify({
|
|
4856
|
+
title,
|
|
4857
|
+
body,
|
|
4858
|
+
head: head ?? null,
|
|
4859
|
+
data: data ?? null,
|
|
4860
|
+
actions: actionsPayload ?? null,
|
|
4861
|
+
clearActionErrorCookie: clearActionErrorCookie ?? null,
|
|
4862
|
+
}),
|
|
4863
|
+
{ status: 200, headers },
|
|
4864
|
+
);
|
|
3986
4865
|
}
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
});
|
|
4866
|
+
const headers = { "Content-Type": "text/html; charset=utf-8" };
|
|
4867
|
+
if (clearActionErrorCookie) headers["Set-Cookie"] = clearActionErrorCookie;
|
|
4868
|
+
return new Response(body, { status: 200, headers });
|
|
3991
4869
|
} catch (err) {
|
|
3992
4870
|
if ((err as { name?: string }).name === "RouteNotFoundError") {
|
|
3993
4871
|
return new Response("Not Found", {
|
|
@@ -4018,7 +4896,7 @@ export default async function handler(request: Request): Promise<Response> {
|
|
|
4018
4896
|
|
|
4019
4897
|
const match = matchRoute(url.pathname, routes.pages);
|
|
4020
4898
|
if (!match) {
|
|
4021
|
-
const errorResult = await renderErrorPage({ routes, status: 404, config: { lang, clientEntry }, actions, importer: loadModule });
|
|
4899
|
+
const errorResult = await renderErrorPage({ routes, status: 404, config: { lang, clientEntry, router, js: jsMode }, actions, importer: loadModule });
|
|
4022
4900
|
if (errorResult) {
|
|
4023
4901
|
return new Response(errorResult.html, { status: errorResult.status, headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
4024
4902
|
}
|
|
@@ -4026,19 +4904,37 @@ export default async function handler(request: Request): Promise<Response> {
|
|
|
4026
4904
|
}
|
|
4027
4905
|
|
|
4028
4906
|
try {
|
|
4029
|
-
|
|
4907
|
+
if (streaming && match.route.loadingPath) {
|
|
4908
|
+
// Streaming SSR: shell + loading boundary first, resolved content as a
|
|
4909
|
+
// follow-up chunk. Streamed pages are rendered live (no cache).
|
|
4910
|
+
return createStreamingResponse({
|
|
4911
|
+
route: match.route,
|
|
4912
|
+
params: match.params,
|
|
4913
|
+
searchParams: new URLSearchParams(url.search),
|
|
4914
|
+
config: { lang, clientEntry, router, js: jsMode },
|
|
4915
|
+
importer: loadModule,
|
|
4916
|
+
actions,
|
|
4917
|
+
request,
|
|
4918
|
+
signal: request.signal,
|
|
4919
|
+
});
|
|
4920
|
+
}
|
|
4921
|
+
const result = await renderPage({
|
|
4030
4922
|
route: match.route,
|
|
4031
4923
|
params: match.params,
|
|
4032
4924
|
searchParams: new URLSearchParams(url.search),
|
|
4033
|
-
config: { lang, clientEntry },
|
|
4925
|
+
config: { lang, clientEntry, router, js: jsMode },
|
|
4034
4926
|
importer: loadModule,
|
|
4035
4927
|
actions,
|
|
4036
4928
|
request,
|
|
4037
4929
|
});
|
|
4038
|
-
|
|
4930
|
+
// A thrown Response from a loader is a first-class response (A-22).
|
|
4931
|
+
if (result.response) return result.response;
|
|
4932
|
+
const headers = { "Content-Type": "text/html; charset=utf-8" };
|
|
4933
|
+
if (result.clearActionErrorCookie) headers["Set-Cookie"] = result.clearActionErrorCookie;
|
|
4934
|
+
return new Response(result.html, { status: 200, headers });
|
|
4039
4935
|
} catch (err) {
|
|
4040
4936
|
console.error("[elur-kit] SSR render error:", err);
|
|
4041
|
-
const errorResult = await renderErrorPage({ routes, status: 500, error: err, config: { lang, clientEntry }, actions, importer: loadModule });
|
|
4937
|
+
const errorResult = await renderErrorPage({ routes, status: 500, error: err, config: { lang, clientEntry, router, js: jsMode }, actions, importer: loadModule });
|
|
4042
4938
|
if (errorResult) {
|
|
4043
4939
|
return new Response(errorResult.html, { status: errorResult.status, headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
4044
4940
|
}
|
|
@@ -4201,6 +5097,15 @@ var init_netlify = __esmMin((() => {
|
|
|
4201
5097
|
//#region src/adapters/bun.ts
|
|
4202
5098
|
var bun_exports = /* @__PURE__ */ __exportAll({ bunAdapter: () => bunAdapter });
|
|
4203
5099
|
function buildBunServerSource(outDirUrl, options) {
|
|
5100
|
+
const logLevelOption = options.logLevel ? `, logLevel: ${JSON.stringify(options.logLevel)}` : "";
|
|
5101
|
+
const routingOptions = [
|
|
5102
|
+
"redirects",
|
|
5103
|
+
"rewrites",
|
|
5104
|
+
"routeHeaders"
|
|
5105
|
+
].map((key) => {
|
|
5106
|
+
const rules = options[key];
|
|
5107
|
+
return rules && rules.length > 0 ? `, ${key}: ${JSON.stringify(rules)}` : "";
|
|
5108
|
+
}).join("");
|
|
4204
5109
|
return `// AUTO-GENERATED by @elurjs/kit. Do not edit.
|
|
4205
5110
|
import { fileURLToPath } from "node:url";
|
|
4206
5111
|
import { createWebHandler } from "@elurjs/kit/runtime";
|
|
@@ -4212,7 +5117,7 @@ const port = Number(process.env.PORT) || ${options.port ?? 3e3};
|
|
|
4212
5117
|
const webHandler = createWebHandler(
|
|
4213
5118
|
{ pages: [], api: [], error404: undefined, error500: undefined },
|
|
4214
5119
|
{},
|
|
4215
|
-
{ staticRoot: outDir, lang: ${JSON.stringify(options.lang)}, clientEntry: ${JSON.stringify(options.clientEntry)} },
|
|
5120
|
+
{ staticRoot: outDir, lang: ${JSON.stringify(options.lang)}, clientEntry: ${JSON.stringify(options.clientEntry)}${logLevelOption}${routingOptions} },
|
|
4216
5121
|
);
|
|
4217
5122
|
|
|
4218
5123
|
Bun.serve({
|
|
@@ -4268,11 +5173,20 @@ var init_bun = __esmMin((() => {
|
|
|
4268
5173
|
//#region src/adapters/node.ts
|
|
4269
5174
|
var node_exports = /* @__PURE__ */ __exportAll({ nodeAdapter: () => nodeAdapter });
|
|
4270
5175
|
function buildNodeServerSource(outDirUrl, options) {
|
|
5176
|
+
const logLevelOption = options.logLevel ? `, logLevel: ${JSON.stringify(options.logLevel)}` : "";
|
|
5177
|
+
const routingOptions = [
|
|
5178
|
+
"redirects",
|
|
5179
|
+
"rewrites",
|
|
5180
|
+
"routeHeaders"
|
|
5181
|
+
].map((key) => {
|
|
5182
|
+
const rules = options[key];
|
|
5183
|
+
return rules && rules.length > 0 ? `, ${key}: ${JSON.stringify(rules)}` : "";
|
|
5184
|
+
}).join("");
|
|
4271
5185
|
return `// AUTO-GENERATED by @elurjs/kit. Do not edit.
|
|
4272
5186
|
import { createServer } from "node:http";
|
|
4273
5187
|
import { fileURLToPath } from "node:url";
|
|
4274
5188
|
import { createWebHandler } from "@elurjs/kit/runtime";
|
|
4275
|
-
import { incomingMessageToRequest } from "@elurjs/kit/runtime";
|
|
5189
|
+
import { incomingMessageToRequest, sendWebResponse } from "@elurjs/kit/runtime";
|
|
4276
5190
|
import handler from "./node-index.ts";
|
|
4277
5191
|
|
|
4278
5192
|
const outDir = fileURLToPath(new URL(${JSON.stringify(outDirUrl)}, import.meta.url));
|
|
@@ -4284,7 +5198,7 @@ const port = Number(process.env.PORT) || ${options.port ?? 3e3};
|
|
|
4284
5198
|
const webHandler = createWebHandler(
|
|
4285
5199
|
{ pages: [], api: [], error404: undefined, error500: undefined },
|
|
4286
5200
|
{},
|
|
4287
|
-
{ staticRoot: outDir, lang: ${JSON.stringify(options.lang)}, clientEntry: ${JSON.stringify(options.clientEntry)} },
|
|
5201
|
+
{ staticRoot: outDir, lang: ${JSON.stringify(options.lang)}, clientEntry: ${JSON.stringify(options.clientEntry)}${logLevelOption}${routingOptions} },
|
|
4288
5202
|
);
|
|
4289
5203
|
|
|
4290
5204
|
createServer(async (req, res) => {
|
|
@@ -4298,8 +5212,8 @@ createServer(async (req, res) => {
|
|
|
4298
5212
|
if (response.status === 404) {
|
|
4299
5213
|
response = await handler(request);
|
|
4300
5214
|
}
|
|
4301
|
-
|
|
4302
|
-
|
|
5215
|
+
// Streams the body: streaming SSR chunks flush as they are produced.
|
|
5216
|
+
await sendWebResponse(res, response);
|
|
4303
5217
|
}).listen(port, () => {
|
|
4304
5218
|
console.log(\`Node server running at http://localhost:\${port}\`);
|
|
4305
5219
|
});
|
|
@@ -4621,6 +5535,7 @@ var init_commands = __esmMin((() => {
|
|
|
4621
5535
|
//#region src/cli.ts
|
|
4622
5536
|
init_scan();
|
|
4623
5537
|
init_route_scanner();
|
|
5538
|
+
init_logger();
|
|
4624
5539
|
init_capabilities();
|
|
4625
5540
|
function parseArgs(argv) {
|
|
4626
5541
|
const args = argv.slice(2);
|
|
@@ -4649,6 +5564,7 @@ function parseArgs(argv) {
|
|
|
4649
5564
|
let cacheDir;
|
|
4650
5565
|
let defaultRevalidate;
|
|
4651
5566
|
let configFile;
|
|
5567
|
+
let logLevel;
|
|
4652
5568
|
for (let i = optionStart; i < args.length; i++) {
|
|
4653
5569
|
const arg = args[i];
|
|
4654
5570
|
const next = args[i + 1];
|
|
@@ -4716,6 +5632,12 @@ function parseArgs(argv) {
|
|
|
4716
5632
|
defaultRevalidate = Number(next);
|
|
4717
5633
|
i++;
|
|
4718
5634
|
break;
|
|
5635
|
+
case "--verbose":
|
|
5636
|
+
if (logLevel !== "error") logLevel = "debug";
|
|
5637
|
+
break;
|
|
5638
|
+
case "--quiet":
|
|
5639
|
+
logLevel = "error";
|
|
5640
|
+
break;
|
|
4719
5641
|
case "--help":
|
|
4720
5642
|
case "-?":
|
|
4721
5643
|
printHelp();
|
|
@@ -4741,7 +5663,8 @@ function parseArgs(argv) {
|
|
|
4741
5663
|
clientConfig: clientConfig ? resolve(root, clientConfig) : void 0,
|
|
4742
5664
|
cacheDir: cacheDir ? resolve(root, cacheDir) : void 0,
|
|
4743
5665
|
defaultRevalidate,
|
|
4744
|
-
configFile: configFile ? resolve(root, configFile) : void 0
|
|
5666
|
+
configFile: configFile ? resolve(root, configFile) : void 0,
|
|
5667
|
+
logLevel
|
|
4745
5668
|
};
|
|
4746
5669
|
}
|
|
4747
5670
|
function printHelp() {
|
|
@@ -4773,8 +5696,23 @@ Options:
|
|
|
4773
5696
|
--config <path> Elur config file (default: elur.config.ts/js/mjs)
|
|
4774
5697
|
--cache-dir <dir> Directory for ISR cache (only used by start)
|
|
4775
5698
|
--default-revalidate <s> Default ISR revalidate interval in seconds
|
|
5699
|
+
--verbose Debug logging (overrides logger.level)
|
|
5700
|
+
--quiet Only errors are printed (overrides logger.level)
|
|
4776
5701
|
`);
|
|
4777
5702
|
}
|
|
5703
|
+
/**
|
|
5704
|
+
* Reads the kit's own version from package.json. The CLI runs from two
|
|
5705
|
+
* layouts: src/cli.ts in the repo (../package.json) and dist/lib/cli.js when
|
|
5706
|
+
* installed (../../package.json).
|
|
5707
|
+
*/
|
|
5708
|
+
function getKitVersion() {
|
|
5709
|
+
const require = createRequire(import.meta.url);
|
|
5710
|
+
for (const rel of ["../package.json", "../../package.json"]) try {
|
|
5711
|
+
const pkg = require(rel);
|
|
5712
|
+
if (typeof pkg.version === "string") return pkg.version;
|
|
5713
|
+
} catch {}
|
|
5714
|
+
return "unknown";
|
|
5715
|
+
}
|
|
4778
5716
|
function toBuildConfig(options) {
|
|
4779
5717
|
return {
|
|
4780
5718
|
root: options.root,
|
|
@@ -4788,58 +5726,96 @@ function toBuildConfig(options) {
|
|
|
4788
5726
|
hydrateImport: options.hydrateImport,
|
|
4789
5727
|
routerImport: options.routerImport,
|
|
4790
5728
|
imageFormats: options.resolvedConfig?.images.formats,
|
|
4791
|
-
integrations: options.resolvedConfig?.integrations
|
|
5729
|
+
integrations: options.resolvedConfig?.integrations,
|
|
5730
|
+
site: options.resolvedConfig?.site,
|
|
5731
|
+
js: options.resolvedConfig?.js,
|
|
5732
|
+
router: options.resolvedConfig ? {
|
|
5733
|
+
enabled: options.resolvedConfig.router.enabled,
|
|
5734
|
+
prefetch: options.resolvedConfig.router.prefetch,
|
|
5735
|
+
morph: options.resolvedConfig.router.morph,
|
|
5736
|
+
loadingIndicator: options.resolvedConfig.router.loadingIndicator,
|
|
5737
|
+
speculation: options.resolvedConfig.router.speculation,
|
|
5738
|
+
separate: options.routerSeparate,
|
|
5739
|
+
entry: "/_elur/router.js",
|
|
5740
|
+
outFile: join(dirname(options.generatedEntry), "router.ts")
|
|
5741
|
+
} : void 0
|
|
4792
5742
|
};
|
|
4793
5743
|
}
|
|
4794
5744
|
async function doBuild(options) {
|
|
5745
|
+
const buildStart = Date.now();
|
|
4795
5746
|
const transformedRoot = join(options.root, ".elur", "transformed");
|
|
4796
5747
|
const transformedAppDir$1 = transformedAppDir(options.root, options.appDir, options.islandsDir, transformedRoot);
|
|
5748
|
+
let phaseStart = performance.now();
|
|
4797
5749
|
await transformProjectFiles({
|
|
4798
5750
|
root: options.root,
|
|
4799
5751
|
appDir: options.appDir,
|
|
4800
5752
|
islandsDir: options.islandsDir,
|
|
4801
5753
|
outDir: transformedRoot
|
|
4802
5754
|
});
|
|
5755
|
+
phase("transform", performance.now() - phaseStart);
|
|
4803
5756
|
const { beginAtomicStage } = await Promise.resolve().then(() => (init_vite_build(), vite_build_exports));
|
|
4804
5757
|
const stage = await beginAtomicStage({ outDir: options.outDir });
|
|
4805
5758
|
const tempOutDir = stage.tempDir;
|
|
5759
|
+
if (options.islandsDir && !options.clientConfig) {
|
|
5760
|
+
const autoConfig = await findClientConfig(options.root);
|
|
5761
|
+
if (autoConfig) options.clientConfig = autoConfig;
|
|
5762
|
+
}
|
|
5763
|
+
options.routerSeparate = await resolveRouterSeparate(options);
|
|
4806
5764
|
try {
|
|
4807
5765
|
const buildConfig = toBuildConfig(options);
|
|
4808
5766
|
buildConfig.appDir = transformedAppDir$1;
|
|
4809
5767
|
buildConfig.outDir = tempOutDir;
|
|
5768
|
+
buildConfig.onPhase = (name, ms) => phase(name, ms);
|
|
4810
5769
|
const result = await build$1(buildConfig);
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
5770
|
+
options.hasIslands = result.islands.length > 0;
|
|
5771
|
+
if (options.resolvedConfig) {
|
|
5772
|
+
phaseStart = performance.now();
|
|
5773
|
+
try {
|
|
5774
|
+
const manifest = await createAppManifest(options.resolvedConfig);
|
|
5775
|
+
await writeAppManifest(manifest, join(tempOutDir, ".elur", "manifest.json"));
|
|
5776
|
+
await writeRouteTypes(manifest, join(options.root, ".elur", "routes.d.ts"));
|
|
5777
|
+
phase("manifest", performance.now() - phaseStart);
|
|
5778
|
+
} catch (err) {
|
|
5779
|
+
warn(`manifest generation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5780
|
+
}
|
|
4822
5781
|
}
|
|
4823
|
-
|
|
5782
|
+
const routerEnabled = options.resolvedConfig?.router.enabled !== false;
|
|
5783
|
+
if (options.clientConfig || options.hasIslands || routerEnabled) {
|
|
4824
5784
|
const originalOutDir = options.outDir;
|
|
4825
5785
|
options.outDir = tempOutDir;
|
|
4826
5786
|
try {
|
|
5787
|
+
phaseStart = performance.now();
|
|
4827
5788
|
await buildClient(options);
|
|
5789
|
+
phase("client bundle", performance.now() - phaseStart);
|
|
4828
5790
|
} finally {
|
|
4829
5791
|
options.outDir = originalOutDir;
|
|
4830
5792
|
}
|
|
4831
5793
|
}
|
|
4832
5794
|
await stage.commit();
|
|
4833
|
-
|
|
4834
|
-
|
|
5795
|
+
const elapsed = ((Date.now() - buildStart) / 1e3).toFixed(2);
|
|
5796
|
+
success(`${bold("Build completo")} ${dim(`en ${elapsed}s`)}`);
|
|
5797
|
+
info(`${result.pages} página(s), ${result.islands.length} island(s), ${result.files.length} archivo(s)`);
|
|
5798
|
+
const fileEntries = [];
|
|
5799
|
+
for (const file of result.files) {
|
|
5800
|
+
const finalPath = join(options.outDir, relative(tempOutDir, file));
|
|
5801
|
+
let bytes = 0;
|
|
5802
|
+
try {
|
|
5803
|
+
bytes = (await stat(finalPath)).size;
|
|
5804
|
+
} catch {}
|
|
5805
|
+
fileEntries.push({
|
|
5806
|
+
path: relative(options.root, finalPath),
|
|
5807
|
+
bytes
|
|
5808
|
+
});
|
|
5809
|
+
}
|
|
5810
|
+
fileList(fileEntries);
|
|
4835
5811
|
if (result.islands.length > 0) {
|
|
4836
|
-
|
|
4837
|
-
for (const island of result.islands)
|
|
4838
|
-
if (result.generatedEntry)
|
|
5812
|
+
success(`${result.islands.length} island(s) detectada(s):`);
|
|
5813
|
+
for (const island of result.islands) detail(island.name);
|
|
5814
|
+
if (result.generatedEntry) detail(`entry: ${relative(options.root, result.generatedEntry)}`);
|
|
4839
5815
|
}
|
|
4840
5816
|
if (result.skipped.length > 0) {
|
|
4841
|
-
|
|
4842
|
-
for (const path of result.skipped)
|
|
5817
|
+
warn("Rutas dinámicas omitidas (necesitan generateStaticParams):");
|
|
5818
|
+
for (const path of result.skipped) detail(path);
|
|
4843
5819
|
}
|
|
4844
5820
|
} catch (err) {
|
|
4845
5821
|
await stage.rollback();
|
|
@@ -4859,16 +5835,32 @@ async function doDev(options) {
|
|
|
4859
5835
|
});
|
|
4860
5836
|
const actions = await scanActions(transformedAppDir$2);
|
|
4861
5837
|
const routes = await scanRoutes(transformedAppDir$2);
|
|
4862
|
-
const
|
|
5838
|
+
const middleware = await loadUserMiddleware(options.root);
|
|
5839
|
+
options.routerEntry = detectRouterEntry(options);
|
|
5840
|
+
const server = createServer((req, res) => handleRequest(req, res, options, actions, routes, true, middleware));
|
|
4863
5841
|
const shutdown = () => {
|
|
4864
5842
|
server.close(() => process.exit(0));
|
|
4865
5843
|
setTimeout(() => process.exit(0), 2e3).unref();
|
|
4866
5844
|
};
|
|
4867
5845
|
process.on("SIGTERM", shutdown);
|
|
4868
5846
|
process.on("SIGINT", shutdown);
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
5847
|
+
try {
|
|
5848
|
+
const usedPort = await listenWithFallback(server, options.host, options.port, { onFallback: (busyPort, nextPort) => warn(`Puerto ${busyPort} ocupado, usando ${nextPort}`) });
|
|
5849
|
+
const network = getNetworkAddress();
|
|
5850
|
+
serverBanner({
|
|
5851
|
+
name: "elur-kit",
|
|
5852
|
+
version: `v${getKitVersion()}`,
|
|
5853
|
+
command: "dev",
|
|
5854
|
+
localUrl: `http://${options.host}:${usedPort}/`,
|
|
5855
|
+
networkUrl: network ? `http://${network}:${usedPort}/` : void 0
|
|
5856
|
+
});
|
|
5857
|
+
} catch (err) {
|
|
5858
|
+
if (err.code === "EADDRINUSE") {
|
|
5859
|
+
error(`No hay puerto disponible entre ${options.port} y ${options.port + 20}.`);
|
|
5860
|
+
process.exit(78);
|
|
5861
|
+
}
|
|
5862
|
+
throw err;
|
|
5863
|
+
}
|
|
4872
5864
|
}
|
|
4873
5865
|
/**
|
|
4874
5866
|
* Dev supervisor: runs the actual dev server in a child process and restarts
|
|
@@ -4886,7 +5878,8 @@ async function doDevSupervisor(options) {
|
|
|
4886
5878
|
let respawnTimer = null;
|
|
4887
5879
|
const startWorker = () => {
|
|
4888
5880
|
intentional = false;
|
|
4889
|
-
console.log(
|
|
5881
|
+
console.log();
|
|
5882
|
+
event("dev", "Starting dev server...");
|
|
4890
5883
|
child = spawn(process.execPath, [spawnPath, ...args], {
|
|
4891
5884
|
env: {
|
|
4892
5885
|
...process.env,
|
|
@@ -4902,7 +5895,11 @@ async function doDevSupervisor(options) {
|
|
|
4902
5895
|
return;
|
|
4903
5896
|
}
|
|
4904
5897
|
if (code !== 0) {
|
|
4905
|
-
|
|
5898
|
+
if (code === 78) {
|
|
5899
|
+
error("[dev] Stopping: no available port.");
|
|
5900
|
+
process.exit(code);
|
|
5901
|
+
}
|
|
5902
|
+
error(`[dev] Dev server exited with code ${code}; restarting...`);
|
|
4906
5903
|
respawnTimer = setTimeout(startWorker, 600);
|
|
4907
5904
|
}
|
|
4908
5905
|
});
|
|
@@ -4916,7 +5913,8 @@ async function doDevSupervisor(options) {
|
|
|
4916
5913
|
if (watchedDirs.length > 0) {
|
|
4917
5914
|
let timer = null;
|
|
4918
5915
|
const scheduleRestart = () => {
|
|
4919
|
-
console.log(
|
|
5916
|
+
console.log();
|
|
5917
|
+
event("change", "Restarting dev server...");
|
|
4920
5918
|
if (timer) clearTimeout(timer);
|
|
4921
5919
|
timer = setTimeout(() => restart(), 150);
|
|
4922
5920
|
};
|
|
@@ -4926,7 +5924,7 @@ async function doDevSupervisor(options) {
|
|
|
4926
5924
|
else if (filename && /\.ts$/.test(filename)) scheduleRestart();
|
|
4927
5925
|
});
|
|
4928
5926
|
} catch (err) {
|
|
4929
|
-
|
|
5927
|
+
error(`[dev] failed to watch ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4930
5928
|
}
|
|
4931
5929
|
}
|
|
4932
5930
|
const cleanup = () => {
|
|
@@ -4940,14 +5938,20 @@ async function doDevSupervisor(options) {
|
|
|
4940
5938
|
process.on("SIGTERM", cleanup);
|
|
4941
5939
|
startWorker();
|
|
4942
5940
|
}
|
|
4943
|
-
|
|
5941
|
+
/**
|
|
5942
|
+
* Shared production-serving path for `preview` and `start`: serves the build
|
|
5943
|
+
* output plus dynamic SSR through the unified Web handler, with middleware,
|
|
5944
|
+
* streaming, port fallback and the startup banner. Both commands behave
|
|
5945
|
+
* identically; the label only differs in the banner.
|
|
5946
|
+
*/
|
|
5947
|
+
async function startProductionServer(options, command) {
|
|
4944
5948
|
try {
|
|
4945
5949
|
if (!(await stat(options.outDir)).isDirectory()) throw new Error(`Output path is not a directory: ${options.outDir}`);
|
|
4946
5950
|
} catch (err) {
|
|
4947
5951
|
if (err.code === "ENOENT") throw new Error(`No build output found at ${options.outDir}. Run \`elur-kit build\` first.`);
|
|
4948
5952
|
throw err;
|
|
4949
5953
|
}
|
|
4950
|
-
const transformedRoot = join(options.root, ".elur", "preview-transformed");
|
|
5954
|
+
const transformedRoot = join(options.root, ".elur", command === "preview" ? "preview-transformed" : "transformed");
|
|
4951
5955
|
const transformedAppDir$3 = transformedAppDir(options.root, options.appDir, options.islandsDir, transformedRoot);
|
|
4952
5956
|
await transformProjectFiles({
|
|
4953
5957
|
root: options.root,
|
|
@@ -4957,32 +5961,25 @@ async function doPreview(options) {
|
|
|
4957
5961
|
});
|
|
4958
5962
|
const actions = await scanActions(transformedAppDir$3);
|
|
4959
5963
|
const routes = await scanRoutes(transformedAppDir$3);
|
|
4960
|
-
const
|
|
4961
|
-
|
|
4962
|
-
|
|
5964
|
+
const middleware = await loadUserMiddleware(options.root);
|
|
5965
|
+
options.routerEntry = detectRouterEntry(options);
|
|
5966
|
+
const server = createServer((req, res) => handleRequest(req, res, options, actions, routes, false, middleware));
|
|
5967
|
+
const usedPort = await listenWithFallback(server, options.host, options.port, { onFallback: (busyPort, nextPort) => warn(`Puerto ${busyPort} ocupado, usando ${nextPort}`) });
|
|
5968
|
+
const network = getNetworkAddress();
|
|
5969
|
+
serverBanner({
|
|
5970
|
+
name: "elur-kit",
|
|
5971
|
+
version: `v${getKitVersion()}`,
|
|
5972
|
+
command,
|
|
5973
|
+
localUrl: `http://${options.host}:${usedPort}/`,
|
|
5974
|
+
networkUrl: network ? `http://${network}:${usedPort}/` : void 0
|
|
4963
5975
|
});
|
|
4964
5976
|
return server;
|
|
4965
5977
|
}
|
|
5978
|
+
async function doPreview(options) {
|
|
5979
|
+
return startProductionServer(options, "preview");
|
|
5980
|
+
}
|
|
4966
5981
|
async function doStart(options) {
|
|
4967
|
-
|
|
4968
|
-
const transformedAppDir$4 = transformedAppDir(options.root, options.appDir, options.islandsDir, transformedRoot);
|
|
4969
|
-
await transformProjectFiles({
|
|
4970
|
-
root: options.root,
|
|
4971
|
-
appDir: options.appDir,
|
|
4972
|
-
islandsDir: options.islandsDir,
|
|
4973
|
-
outDir: transformedRoot
|
|
4974
|
-
});
|
|
4975
|
-
await (await createSsrServer({
|
|
4976
|
-
root: options.root,
|
|
4977
|
-
appDir: transformedAppDir$4,
|
|
4978
|
-
publicDir: options.outDir,
|
|
4979
|
-
clientEntry: options.clientEntry,
|
|
4980
|
-
lang: options.lang,
|
|
4981
|
-
port: options.port,
|
|
4982
|
-
host: options.host,
|
|
4983
|
-
cacheDir: options.cacheDir,
|
|
4984
|
-
defaultRevalidate: options.defaultRevalidate
|
|
4985
|
-
})).listen();
|
|
5982
|
+
await startProductionServer(options, "start");
|
|
4986
5983
|
}
|
|
4987
5984
|
async function findClientConfig(root) {
|
|
4988
5985
|
for (const name of [
|
|
@@ -4996,21 +5993,71 @@ async function findClientConfig(root) {
|
|
|
4996
5993
|
} catch {}
|
|
4997
5994
|
}
|
|
4998
5995
|
}
|
|
5996
|
+
/**
|
|
5997
|
+
* Decides whether the client bundle emits the router as its own chunk.
|
|
5998
|
+
*
|
|
5999
|
+
* - `js: "legacy"` or `router.enabled: false` → never split (the entry is
|
|
6000
|
+
* hydrate-only when the router is off; legacy embeds the router).
|
|
6001
|
+
* - A user-provided client config splits only when it declares the generated
|
|
6002
|
+
* router module (`.elur/router.ts`) as a bundle input — a single input is
|
|
6003
|
+
* "legacy de facto": the router stays embedded in `entry-client.js`.
|
|
6004
|
+
* - Without a user config, the kit synthesizes a default two-input config →
|
|
6005
|
+
* split.
|
|
6006
|
+
*/
|
|
6007
|
+
async function resolveRouterSeparate(options) {
|
|
6008
|
+
const rc = options.resolvedConfig;
|
|
6009
|
+
if (!rc || rc.js === "legacy" || rc.router.enabled === false) return false;
|
|
6010
|
+
const routerFile = join(dirname(options.generatedEntry), "router.ts");
|
|
6011
|
+
if (!options.clientConfig) return true;
|
|
6012
|
+
try {
|
|
6013
|
+
const { resolveClientInputs } = await Promise.resolve().then(() => (init_vite_build(), vite_build_exports));
|
|
6014
|
+
return (await resolveClientInputs(options.clientConfig, options.root)).includes(routerFile);
|
|
6015
|
+
} catch {
|
|
6016
|
+
return false;
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6019
|
+
/**
|
|
6020
|
+
* Public URL of the router chunk when the built bundle actually contains it.
|
|
6021
|
+
* The file check keeps `preview`/`start` consistent with whatever layout the
|
|
6022
|
+
* last build produced (split or legacy single-entry).
|
|
6023
|
+
*/
|
|
6024
|
+
function detectRouterEntry(options) {
|
|
6025
|
+
const rc = options.resolvedConfig;
|
|
6026
|
+
if (!rc || rc.js === "legacy" || rc.router.enabled === false) return void 0;
|
|
6027
|
+
return existsSync(join(options.outDir, "_elur", "router.js")) ? "/_elur/router.js" : void 0;
|
|
6028
|
+
}
|
|
4999
6029
|
async function buildClient(options) {
|
|
5000
|
-
if (!options.clientConfig) return;
|
|
5001
6030
|
const { buildClientBundle } = await Promise.resolve().then(() => (init_vite_build(), vite_build_exports));
|
|
5002
6031
|
const clientOutDir = join(options.outDir, "_elur");
|
|
6032
|
+
const clientBase = "/_elur/";
|
|
6033
|
+
const defaultInputs = { "entry-client": options.generatedEntry };
|
|
6034
|
+
if (options.routerSeparate) defaultInputs.router = join(dirname(options.generatedEntry), "router.ts");
|
|
5003
6035
|
await buildClientBundle({
|
|
5004
6036
|
root: options.root,
|
|
5005
|
-
userConfigPath: resolve(options.clientConfig),
|
|
6037
|
+
userConfigPath: options.clientConfig ? resolve(options.clientConfig) : void 0,
|
|
6038
|
+
defaultInputs,
|
|
5006
6039
|
appDir: join(options.root, "src", "app"),
|
|
5007
6040
|
islandsDir: join(options.root, "src", "islands"),
|
|
5008
6041
|
outDir: clientOutDir,
|
|
5009
|
-
base:
|
|
5010
|
-
logPrefix: "[client]"
|
|
6042
|
+
base: clientBase,
|
|
6043
|
+
logPrefix: "[client]",
|
|
6044
|
+
quiet: options.logLevel === "error"
|
|
5011
6045
|
});
|
|
5012
6046
|
}
|
|
5013
|
-
|
|
6047
|
+
/**
|
|
6048
|
+
* Loads the project's `src/middleware.ts` for dev/preview/start. A missing
|
|
6049
|
+
* file is fine; a broken one warns but does not stop the server.
|
|
6050
|
+
*/
|
|
6051
|
+
async function loadUserMiddleware(root) {
|
|
6052
|
+
const { loadMiddleware } = await Promise.resolve().then(() => (init_middleware(), middleware_exports));
|
|
6053
|
+
try {
|
|
6054
|
+
return await loadMiddleware(root);
|
|
6055
|
+
} catch (err) {
|
|
6056
|
+
warn(err instanceof Error ? err.message : String(err));
|
|
6057
|
+
return null;
|
|
6058
|
+
}
|
|
6059
|
+
}
|
|
6060
|
+
async function handleRequest(req, res, options, actions, routes, noCache = false, middleware) {
|
|
5014
6061
|
const { createWebHandler } = await Promise.resolve().then(() => (init_handler(), handler_exports));
|
|
5015
6062
|
const securityHeaders = options.resolvedConfig?.security?.headers;
|
|
5016
6063
|
const webHandler = createWebHandler(routes, actions, {
|
|
@@ -5021,20 +6068,36 @@ async function handleRequest(req, res, options, actions, routes, noCache = false
|
|
|
5021
6068
|
lang: options.lang,
|
|
5022
6069
|
clientEntry: options.clientEntry,
|
|
5023
6070
|
renderEndpoint: true,
|
|
5024
|
-
|
|
6071
|
+
router: {
|
|
6072
|
+
enabled: options.resolvedConfig?.router.enabled ?? true,
|
|
6073
|
+
entry: options.routerEntry
|
|
6074
|
+
},
|
|
6075
|
+
js: options.resolvedConfig?.js,
|
|
6076
|
+
securityHeaders: securityHeaders === void 0 ? false : securityHeaders,
|
|
6077
|
+
logLevel: options.resolvedConfig?.logger?.level,
|
|
6078
|
+
cacheAdapter: options.resolvedConfig?.cache?.adapter,
|
|
6079
|
+
redirects: options.resolvedConfig?.redirects,
|
|
6080
|
+
rewrites: options.resolvedConfig?.rewrites,
|
|
6081
|
+
routeHeaders: options.resolvedConfig?.headers,
|
|
6082
|
+
streaming: options.resolvedConfig?.streaming,
|
|
6083
|
+
middleware: middleware ?? void 0
|
|
5025
6084
|
});
|
|
5026
6085
|
const request = incomingMessageToRequest(req, req.method && req.method !== "GET" && req.method !== "HEAD" ? await readRequestBody(req) : void 0);
|
|
5027
6086
|
let response;
|
|
5028
6087
|
try {
|
|
5029
6088
|
response = await webHandler(request);
|
|
5030
6089
|
} catch (err) {
|
|
5031
|
-
|
|
6090
|
+
createRequestLogger(request, options.resolvedConfig?.logger?.level).error("[elur-kit] request error", {
|
|
6091
|
+
path: new URL(request.url).pathname,
|
|
6092
|
+
method: request.method,
|
|
6093
|
+
error: err instanceof Error ? err.message : String(err),
|
|
6094
|
+
stack: err instanceof Error ? err.stack : void 0
|
|
6095
|
+
});
|
|
5032
6096
|
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
5033
6097
|
res.end("Internal Server Error");
|
|
5034
6098
|
return;
|
|
5035
6099
|
}
|
|
5036
|
-
res
|
|
5037
|
-
res.end(Buffer.from(await response.arrayBuffer()));
|
|
6100
|
+
await sendWebResponse(res, response);
|
|
5038
6101
|
}
|
|
5039
6102
|
function readRequestBody(req) {
|
|
5040
6103
|
return new Promise((resolve, reject) => {
|
|
@@ -5056,34 +6119,47 @@ async function doAdapter(options) {
|
|
|
5056
6119
|
publicDir: options.publicDir,
|
|
5057
6120
|
clientEntry: options.clientEntry,
|
|
5058
6121
|
lang: options.lang,
|
|
5059
|
-
hydrateImport: options.hydrateImport
|
|
6122
|
+
hydrateImport: options.hydrateImport,
|
|
6123
|
+
logLevel: options.resolvedConfig?.logger?.level,
|
|
6124
|
+
cacheAdapter: options.resolvedConfig?.cache?.adapter,
|
|
6125
|
+
redirects: options.resolvedConfig?.redirects,
|
|
6126
|
+
rewrites: options.resolvedConfig?.rewrites,
|
|
6127
|
+
routeHeaders: options.resolvedConfig?.headers,
|
|
6128
|
+
streaming: options.resolvedConfig?.streaming,
|
|
6129
|
+
router: { enabled: options.resolvedConfig?.router.enabled ?? true },
|
|
6130
|
+
js: options.resolvedConfig?.js
|
|
5060
6131
|
};
|
|
5061
6132
|
const resolvedConfig = options.resolvedConfig;
|
|
5062
6133
|
const features = {
|
|
5063
6134
|
isr: typeof resolvedConfig?.cache?.defaultRevalidate === "number" && resolvedConfig.cache.defaultRevalidate > 0,
|
|
5064
|
-
images: resolvedConfig?.images?.strict === true
|
|
6135
|
+
images: resolvedConfig?.images?.strict === true,
|
|
6136
|
+
streaming: resolvedConfig?.streaming === true
|
|
5065
6137
|
};
|
|
5066
6138
|
let adapterName = options.adapterName;
|
|
5067
6139
|
if (adapterName === "vercel") {
|
|
5068
6140
|
const { vercelAdapter } = await Promise.resolve().then(() => (init_vercel(), vercel_exports));
|
|
5069
6141
|
assertCapabilities(vercelAdapter, features, adapterName);
|
|
5070
6142
|
await vercelAdapter.build(adapterOptions);
|
|
5071
|
-
console.log(
|
|
6143
|
+
console.log();
|
|
6144
|
+
info("Vercel output generated at .vercel/output");
|
|
5072
6145
|
} else if (adapterName === "netlify") {
|
|
5073
6146
|
const { netlifyAdapter } = await Promise.resolve().then(() => (init_netlify(), netlify_exports));
|
|
5074
6147
|
assertCapabilities(netlifyAdapter, features, adapterName);
|
|
5075
6148
|
await netlifyAdapter.build(adapterOptions);
|
|
5076
|
-
console.log(
|
|
6149
|
+
console.log();
|
|
6150
|
+
info("Netlify output generated at netlify/functions/__elur-js-kit.mjs");
|
|
5077
6151
|
} else if (adapterName === "bun") {
|
|
5078
6152
|
const { bunAdapter } = await Promise.resolve().then(() => (init_bun(), bun_exports));
|
|
5079
6153
|
assertCapabilities(bunAdapter, features, adapterName);
|
|
5080
6154
|
await bunAdapter.build(adapterOptions);
|
|
5081
|
-
console.log(
|
|
6155
|
+
console.log();
|
|
6156
|
+
info("Bun server generated at .elur/bun-server.ts");
|
|
5082
6157
|
} else if (adapterName === "node") {
|
|
5083
6158
|
const { nodeAdapter } = await Promise.resolve().then(() => (init_node(), node_exports));
|
|
5084
6159
|
assertCapabilities(nodeAdapter, features, adapterName);
|
|
5085
6160
|
await nodeAdapter.build(adapterOptions);
|
|
5086
|
-
console.log(
|
|
6161
|
+
console.log();
|
|
6162
|
+
info("Node server generated at .elur/node-server.mjs");
|
|
5087
6163
|
}
|
|
5088
6164
|
}
|
|
5089
6165
|
function assertCapabilities(adapter, features, adapterName) {
|
|
@@ -5107,11 +6183,13 @@ async function applyProjectConfig(options, argv) {
|
|
|
5107
6183
|
if (!has("--public")) options.publicDir = config.publicDir;
|
|
5108
6184
|
if (!has("--cache-dir")) options.cacheDir = config.cache.dir;
|
|
5109
6185
|
if (!has("--default-revalidate")) options.defaultRevalidate = config.cache.defaultRevalidate;
|
|
6186
|
+
if (options.logLevel) config.logger.level = options.logLevel;
|
|
5110
6187
|
options.generatedEntry = resolve(config.root, ".elur/entry-client.ts");
|
|
5111
6188
|
options.resolvedConfig = config;
|
|
5112
6189
|
}
|
|
5113
6190
|
async function run(argv) {
|
|
5114
6191
|
const options = parseArgs(argv);
|
|
6192
|
+
if (options.logLevel === "error") setQuiet(true);
|
|
5115
6193
|
if (options.command === "doctor") {
|
|
5116
6194
|
const { doDoctor } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5117
6195
|
const code = await doDoctor(options);
|