@astroscope/node 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +8 -5
  2. package/dist/boot.d.ts +2 -2
  3. package/dist/boot.d.ts.map +1 -1
  4. package/dist/csrf-middleware-entrypoint.d.ts +1 -2
  5. package/dist/csrf-middleware-entrypoint.d.ts.map +1 -1
  6. package/dist/csrf-middleware-entrypoint.js +1 -1
  7. package/dist/dev-middleware-entrypoint.d.ts +1 -2
  8. package/dist/dev-middleware-entrypoint.d.ts.map +1 -1
  9. package/dist/{prepare-CXZsyAVk.js → duplicate-slashes-C6hmOLl9.js} +94 -38
  10. package/dist/duplicate-slashes-C6hmOLl9.js.map +1 -0
  11. package/dist/{excludes-pE23EbmQ.js → excludes-10eDopVB.js} +18 -4
  12. package/dist/excludes-10eDopVB.js.map +1 -0
  13. package/dist/{excludes-BDiE3eyp.d.ts → excludes-rvkVGg6B.d.ts} +11 -3
  14. package/dist/excludes-rvkVGg6B.d.ts.map +1 -0
  15. package/dist/excludes.d.ts +2 -2
  16. package/dist/excludes.js +2 -2
  17. package/dist/graph-DdAxiaxa.js +35 -0
  18. package/dist/graph-DdAxiaxa.js.map +1 -0
  19. package/dist/health.d.ts +3 -4
  20. package/dist/health.d.ts.map +1 -1
  21. package/dist/image-endpoint.d.ts +1 -2
  22. package/dist/image-endpoint.d.ts.map +1 -1
  23. package/dist/index.d.ts +4 -4
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +38 -122
  26. package/dist/index.js.map +1 -1
  27. package/dist/islands-middleware-entrypoint.d.ts +1 -2
  28. package/dist/islands-middleware-entrypoint.d.ts.map +1 -1
  29. package/dist/islands-middleware-entrypoint.js +3 -1
  30. package/dist/islands-middleware-entrypoint.js.map +1 -1
  31. package/dist/islands-runtime.iife.js +1 -1
  32. package/dist/islands.d.ts +57 -2
  33. package/dist/islands.d.ts.map +1 -0
  34. package/dist/islands.js +3 -1
  35. package/dist/log/index.d.ts +5 -5
  36. package/dist/log/index.d.ts.map +1 -1
  37. package/dist/native.d.ts +5 -6
  38. package/dist/native.d.ts.map +1 -1
  39. package/dist/{prerendered-DVjzWNbW.js → prerendered-Z-Qi-FFK.js} +4 -4
  40. package/dist/prerendered-Z-Qi-FFK.js.map +1 -0
  41. package/dist/request-route-DcnZOOM4.js.map +1 -1
  42. package/dist/route-islands-B53rE5MH.js +73 -0
  43. package/dist/route-islands-B53rE5MH.js.map +1 -0
  44. package/dist/route-middleware-entrypoint.d.ts +1 -2
  45. package/dist/route-middleware-entrypoint.d.ts.map +1 -1
  46. package/dist/{route-store-CSYR4G5e.d.ts → route-store-neUA2nBb.d.ts} +10 -14
  47. package/dist/route-store-neUA2nBb.d.ts.map +1 -0
  48. package/dist/routes-D1o_WrJ5.js +39 -0
  49. package/dist/routes-D1o_WrJ5.js.map +1 -0
  50. package/dist/server.d.ts +2 -3
  51. package/dist/server.d.ts.map +1 -1
  52. package/dist/server.js +68 -45
  53. package/dist/server.js.map +1 -1
  54. package/dist/telemetry-B9aziFyQ.js +202 -0
  55. package/dist/telemetry-B9aziFyQ.js.map +1 -0
  56. package/dist/telemetry.d.ts +68 -0
  57. package/dist/telemetry.d.ts.map +1 -0
  58. package/dist/telemetry.js +2 -0
  59. package/dist/transform-Bz5z5w2Y.js +594 -0
  60. package/dist/transform-Bz5z5w2Y.js.map +1 -0
  61. package/package.json +13 -11
  62. package/dist/excludes-BDiE3eyp.d.ts.map +0 -1
  63. package/dist/excludes-pE23EbmQ.js.map +0 -1
  64. package/dist/prepare-CXZsyAVk.js.map +0 -1
  65. package/dist/prerendered-DVjzWNbW.js.map +0 -1
  66. package/dist/route-store-CSYR4G5e.d.ts.map +0 -1
  67. package/dist/strip-build-paths-C6eb1QdG.js +0 -63
  68. package/dist/strip-build-paths-C6eb1QdG.js.map +0 -1
  69. package/dist/transform-BqV8SOEm.js +0 -195
  70. package/dist/transform-BqV8SOEm.js.map +0 -1
package/README.md CHANGED
@@ -14,7 +14,7 @@ Opinionated, cloud-friendly Node adapter for Astro: boot lifecycle, health probe
14
14
  - **Island preloading** — each island's JS is preloaded in parallel instead of being discovered module by module, removing the hydration request waterfall ([Island preloading](#island-preloading))
15
15
  - **Pre-compressed static serving** — build-time brotli/gzip variants, negotiated per `Accept-Encoding`
16
16
  - **Native mounts** — http-native handlers (`oidc-provider`, ACME) mounted on the adapter's server
17
- - **Build tweaks** — SSR sourcemaps, SSR effect stripping
17
+ - **Build tweaks** — SSR sourcemaps
18
18
  - **Image processing off unless configured** — without an explicit `image.service`, any `astro:assets` use fails loudly instead of opening astro's on-demand sharp endpoint ([Image processing](#image-processing))
19
19
  - **Dev restart machinery** — changes to the boot file or entry seams restart the dev server behind a holding page
20
20
  - **Dev island warmup** — `.astro` sources are scanned for `client:*` components; their deps are pre-optimized and their module graphs warmed at server start, preventing "504 Outdated Optimize Dep" hydration failures from vite's lazy dep discovery
@@ -174,12 +174,14 @@ The adapter itself emits exactly one info line on startup — `server ready { ho
174
174
  `node({ telemetry })` ships the standard bundle: NodeSDK, fetch instrumentation via undici (diagnostics-channel based — no `--import` needed), Node runtime + host metrics, and a Prometheus reader on `0.0.0.0:9464`. **Prod: on by default. Dev: off by default** (opt in with `telemetry: { dev: true }`).
175
175
 
176
176
  - server spans start at the native handler with W3C context extraction; the route pattern and span name are enriched by the internal middleware
177
- - request metrics: `http.server.request.duration`, `http.server.active_requests`, `astro.action.duration`; fetch metrics come from the undici instrumentation
177
+ - request metrics: `http.server.request.duration`, `http.server.active_requests`, `astro.action.duration` (semconv bucket boundaries, 5 ms – 10 s); fetch metrics come from the undici instrumentation
178
+ - `astro.render.failures` {`http.route`} counts renders that failed after the status went out. Such a response is truncated on the wire (astro's behavior — the status cannot change anymore), so the counter is the alert signal; the request log line reads `request truncated` at error level next to the failure itself, and the server span carries `astro.response.truncated`
178
179
  - the boot lifecycle gets `startup` (with `boot`/`warmup`/`listen` children) and `shutdown` (with `drain`/`onShutdown`) spans
179
180
  - trace exporters are driven by the standard `OTEL_*` env vars; without a configured OTLP endpoint, trace exporting defaults to `none` (no failing localhost exports)
180
181
  - `OTEL_SDK_DISABLED=true` turns the SDK off entirely
182
+ - the other astroscope packages record through the same SDK, no-op without it: `astro.island.render.duration`, `astro.island.check.duration`, `astro.island.render.failures` (`@astroscope/react`); `astro.wormhole.handler.duration`, `astro.wormhole.handler.failures` and a `wormhole <name>` span per handler (`@astroscope/wormhole`); `astro.i18n.missing`, `astro.i18n.translations.age` (`@astroscope/i18n`)
181
183
 
182
- There is no helper for tracing server-render sections: measuring a subtree's render would require buffering it, which disables streaming. Wrap frontmatter awaits with `tracer.startActiveSpan()` instead — that's where the time lives, and it nests under the request span without touching the stream.
184
+ There is no helper for tracing server-render sections: measuring a subtree's render would require buffering it, which disables streaming. Wrap frontmatter awaits with `tracer.startActiveSpan()` from `@opentelemetry/api` instead — that's where the time lives, and it nests under the request span without touching the stream.
183
185
 
184
186
  ## Health checks
185
187
 
@@ -228,7 +230,7 @@ export function onStartup() {
228
230
  ```typescript
229
231
  import { RECOMMENDED_EXCLUDES, shouldExclude, withExcluded } from '@astroscope/node/excludes';
230
232
 
231
- // pre-defined sets: DEV_EXCLUDES (vite dev paths), ASTRO_STATIC_EXCLUDES (/_astro/, /_image),
233
+ // pre-defined sets: DEV_EXCLUDES (vite dev paths), ASTRO_STATIC_EXCLUDES (/_astro/, /_image, /_i18n/),
232
234
  // STATIC_EXCLUDES (favicon, robots.txt, ...), RECOMMENDED_EXCLUDES (dev + astro internals)
233
235
 
234
236
  // in your own middleware
@@ -255,7 +257,8 @@ At build time, every compressible file in `dist/client` gets max-quality `.br` (
255
257
  Always on, no configuration:
256
258
 
257
259
  - **SSR sourcemaps** — the server bundle gets sourcemaps for readable stack traces; client bundles stay unmapped so browsers can't fetch source
258
- - **SSR effect stripping** — `useEffect`/`useLayoutEffect`/`useInsertionEffect` callbacks are emptied in the SSR bundle (effects never run on the server), letting the bundler drop client-only dynamic imports (maplibre-gl, hls.js, …) from the server build and the docker image
260
+
261
+ SSR effect stripping for React islands lives in [`@astroscope/react`](../react).
259
262
 
260
263
  ## Image processing
261
264
 
package/dist/boot.d.ts CHANGED
@@ -8,7 +8,7 @@ import { t as BootContext } from "./types-D0uMBi2M.js";
8
8
  * libraries can use it to adapt behavior to dev mode without requiring
9
9
  * consumers to thread the flag through.
10
10
  */
11
- declare function getBootContext(): BootContext | undefined;
11
+ export declare function getBootContext(): BootContext | undefined;
12
12
  //#endregion
13
- export { type BootContext, getBootContext };
13
+ export type { BootContext };
14
14
  //# sourceMappingURL=boot.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"boot.d.ts","names":[],"sources":["../src/lifecycle/context.ts"],"mappings":";;;;;;;;;;iBAiBgB,kBAAkB"}
1
+ {"version":3,"file":"boot.d.ts","names":[],"sources":["../src/lifecycle/context.ts"],"mappings":";;;;;;;;;;wBAiBgB,kBAAkB"}
@@ -1,5 +1,4 @@
1
1
  //#region src/csrf/middleware-entrypoint.d.ts
2
- declare const onRequest: import("astro").MiddlewareHandler;
2
+ export declare const onRequest: import("astro").MiddlewareHandler;
3
3
  //#endregion
4
- export { onRequest };
5
4
  //# sourceMappingURL=csrf-middleware-entrypoint.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"csrf-middleware-entrypoint.d.ts","names":[],"sources":["../src/csrf/middleware-entrypoint.ts"],"mappings":";cAKa,2BAAS"}
1
+ {"version":3,"file":"csrf-middleware-entrypoint.d.ts","names":[],"sources":["../src/csrf/middleware-entrypoint.ts"],"mappings":";qBAKa,2BAAS"}
@@ -1,4 +1,4 @@
1
- import { a as shouldExclude } from "./excludes-pE23EbmQ.js";
1
+ import { a as shouldExclude } from "./excludes-10eDopVB.js";
2
2
  import { excludePatterns } from "virtual:@astroscope/node/csrf";
3
3
  //#region src/csrf/middleware.ts
4
4
  const FORBIDDEN_METHODS = /* @__PURE__ */ new Set([
@@ -8,7 +8,6 @@ import { MiddlewareHandler } from "astro";
8
8
  * match the current generation, the error is recorded for an aggregated log
9
9
  * line and a 503 is returned (the client socket is usually gone too).
10
10
  */
11
- declare const onRequest: MiddlewareHandler;
11
+ export declare const onRequest: MiddlewareHandler;
12
12
  //#endregion
13
- export { onRequest };
14
13
  //# sourceMappingURL=dev-middleware-entrypoint.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dev-middleware-entrypoint.d.ts","names":[],"sources":["../src/dev-mode/middleware-entrypoint.ts"],"mappings":";;;;;;;;;;cAWa,WAAW"}
1
+ {"version":3,"file":"dev-middleware-entrypoint.d.ts","names":[],"sources":["../src/dev-mode/middleware-entrypoint.ts"],"mappings":";;;;;;;;;;qBAWa,WAAW"}
@@ -1,9 +1,10 @@
1
1
  import { t as emit } from "./events-CgoM3Fvu.js";
2
- import { r as getLogStore } from "./request-route-DcnZOOM4.js";
2
+ import { r as getLogStore, t as overrideRequestRoute } from "./request-route-DcnZOOM4.js";
3
3
  import { n as log, t as generateReqId } from "./log-B69HEBvg.js";
4
+ import { a as createUpDownCounter, l as bindGauges, n as createCounter, r as createHistogram, t as DURATION_BUCKETS } from "./telemetry-B9aziFyQ.js";
4
5
  import fs from "node:fs";
5
6
  import pino from "pino";
6
- import { ROOT_CONTEXT, SpanKind, SpanStatusCode, ValueType, context, isSpanContextValid, metrics, propagation, trace } from "@opentelemetry/api";
7
+ import { ROOT_CONTEXT, SpanKind, SpanStatusCode, context, isSpanContextValid, propagation, trace } from "@opentelemetry/api";
7
8
  import { createMatcher } from "@entwico/dash/match";
8
9
  //#region src/lifecycle/lifecycle.ts
9
10
  async function runStartup(boot, context) {
@@ -21,55 +22,51 @@ async function runShutdown(boot, context) {
21
22
  }
22
23
  //#endregion
23
24
  //#region src/observability/telemetry/metrics.ts
24
- const LIB_NAME$1 = "@astroscope/node";
25
- let httpRequestDuration = null;
26
- let httpActiveRequests = null;
27
- let actionDuration = null;
28
- function getHttpRequestDuration() {
29
- return httpRequestDuration ??= metrics.getMeter(LIB_NAME$1).createHistogram("http.server.request.duration", {
30
- description: "Duration of HTTP server requests",
31
- unit: "s",
32
- valueType: ValueType.DOUBLE
33
- });
34
- }
35
- function getHttpActiveRequests() {
36
- return httpActiveRequests ??= metrics.getMeter(LIB_NAME$1).createUpDownCounter("http.server.active_requests", {
37
- description: "Number of active HTTP server requests",
38
- unit: "{request}",
39
- valueType: ValueType.INT
40
- });
41
- }
42
- function getActionDuration() {
43
- return actionDuration ??= metrics.getMeter(LIB_NAME$1).createHistogram("astro.action.duration", {
44
- description: "Duration of Astro action executions",
45
- unit: "s",
46
- valueType: ValueType.DOUBLE
47
- });
48
- }
25
+ const httpRequestDuration = createHistogram("http.server.request.duration", {
26
+ description: "Duration of HTTP server requests",
27
+ unit: "s",
28
+ buckets: DURATION_BUCKETS.http
29
+ });
30
+ const httpActiveRequests = createUpDownCounter("http.server.active_requests", {
31
+ description: "Number of active HTTP server requests",
32
+ unit: "{request}"
33
+ });
34
+ const actionDuration = createHistogram("astro.action.duration", {
35
+ description: "Duration of Astro action executions",
36
+ unit: "s",
37
+ buckets: DURATION_BUCKETS.http
38
+ });
39
+ const renderFailures = createCounter("astro.render.failures", {
40
+ description: "Responses that failed while streaming, after the status was sent",
41
+ unit: "{response}"
42
+ });
49
43
  /**
50
44
  * Record the start of an HTTP request. Returns a function to call when the
51
45
  * request ends. Route is unknown at the native-handler level, so active
52
46
  * requests carry only the method.
53
47
  */
54
48
  function recordHttpRequestStart(method) {
55
- getHttpActiveRequests().add(1, { "http.request.method": method });
49
+ httpActiveRequests.add(1, { "http.request.method": method });
56
50
  return () => {
57
- getHttpActiveRequests().add(-1, { "http.request.method": method });
51
+ httpActiveRequests.add(-1, { "http.request.method": method });
58
52
  };
59
53
  }
60
54
  function recordHttpRequestDuration(attributes, durationMs) {
61
- getHttpRequestDuration().record(durationMs / 1e3, {
55
+ httpRequestDuration.record(durationMs / 1e3, {
62
56
  "http.request.method": attributes.method,
63
57
  "http.route": attributes.route ?? "",
64
58
  "http.response.status_code": attributes.status
65
59
  });
66
60
  }
67
61
  function recordActionDuration(attributes, durationMs) {
68
- getActionDuration().record(durationMs / 1e3, {
62
+ actionDuration.record(durationMs / 1e3, {
69
63
  "astro.action.name": attributes.name,
70
64
  "http.response.status_code": attributes.status
71
65
  });
72
66
  }
67
+ function recordRenderFailure(route) {
68
+ renderFailures.add(1, { "http.route": route ?? "" });
69
+ }
73
70
  //#endregion
74
71
  //#region src/observability/instrument.ts
75
72
  const LIB_NAME = "@astroscope/node";
@@ -140,7 +137,8 @@ function createRequestInstrumentation(config) {
140
137
  method,
141
138
  route: void 0,
142
139
  routeOverride: false,
143
- actionName: isAction ? pathname.slice(10).replace(/\/$/, "") : void 0
140
+ actionName: isAction ? pathname.slice(10).replace(/\/$/, "") : void 0,
141
+ truncated: false
144
142
  };
145
143
  let span;
146
144
  let firstByteSpan;
@@ -193,16 +191,18 @@ function createRequestInstrumentation(config) {
193
191
  if (finalized) return;
194
192
  finalized = true;
195
193
  const status = res.statusCode;
194
+ const truncated = record.truncated;
196
195
  const responseTime = performance.now() - startTime;
197
196
  const ttfb = roundTime((firstByteTime ?? performance.now()) - startTime);
198
- if (requestLogger) requestLogger[status >= 500 ? "error" : status >= 400 ? "warn" : "info"]({
197
+ if (requestLogger) requestLogger[truncated || status >= 500 ? "error" : status >= 400 ? "warn" : "info"]({
199
198
  res: { statusCode: status },
200
199
  responseTime: roundTime(responseTime),
201
200
  ttfb,
202
201
  responseSize,
203
202
  ...record.route && { route: record.route },
204
- ...aborted && { aborted: true }
205
- }, aborted ? "request aborted" : "request completed");
203
+ ...aborted && { aborted: true },
204
+ ...truncated && { truncated: true }
205
+ }, truncated ? "request truncated" : aborted ? "request aborted" : "request completed");
206
206
  if (firstByteSpan && firstByteTime === void 0) {
207
207
  firstByteSpan.setStatus({
208
208
  code: SpanStatusCode.ERROR,
@@ -214,7 +214,13 @@ function createRequestInstrumentation(config) {
214
214
  span.setAttribute("http.response.status_code", status);
215
215
  span.setAttribute("http.response.body.size", responseSize);
216
216
  span.setAttribute("ttfb", ttfb);
217
- if (aborted || status >= 400) span.setStatus({
217
+ if (truncated) {
218
+ span.setAttribute("astro.response.truncated", true);
219
+ span.setStatus({
220
+ code: SpanStatusCode.ERROR,
221
+ message: "response truncated"
222
+ });
223
+ } else if (aborted || status >= 400) span.setStatus({
218
224
  code: SpanStatusCode.ERROR,
219
225
  message: aborted ? "request aborted" : `HTTP ${status}`
220
226
  });
@@ -232,6 +238,7 @@ function createRequestInstrumentation(config) {
232
238
  name: record.actionName,
233
239
  status
234
240
  }, responseTime);
241
+ if (truncated) recordRenderFailure(record.route);
235
242
  }
236
243
  };
237
244
  res.once("finish", () => finalize(false));
@@ -347,6 +354,7 @@ async function startTelemetry(options) {
347
354
  ...prometheus && { metricReaders: [new PrometheusExporter(prometheus)] }
348
355
  });
349
356
  sdk.start();
357
+ bindGauges();
350
358
  new HostMetrics().start();
351
359
  g[TELEMETRY_KEY] = { shutdown: () => sdk.shutdown() };
352
360
  if (prometheus) log.debug({
@@ -406,6 +414,54 @@ async function preparePlatform(options) {
406
414
  await constructRootLogger((await options.seams.log?.())?.default, { dev: options.dev });
407
415
  }
408
416
  //#endregion
409
- export { runShutdown as a, createRequestInstrumentation as i, shutdownTelemetry as n, runStartup as o, dumpEarlyLogs as r, preparePlatform as t };
417
+ //#region src/server/duplicate-slashes.ts
418
+ /**
419
+ * Duplicate slashes in a request path (`//weine`, `/seminare///x`) are not
420
+ * a distinct resource: astro collapses them before routing and renders the
421
+ * page as if the path were clean, so every such spelling is duplicate
422
+ * content. Astro itself only redirects duplicate *trailing* slashes; this
423
+ * handler redirects the rest, before native mounts and static serving, so a
424
+ * page, an endpoint, a public asset and a mounted handler all answer alike.
425
+ *
426
+ * Only literal slashes in the raw request target count — `%2F` stays as
427
+ * sent. Absolute-form targets (`GET http://host/x`) and `*` are not paths
428
+ * and pass through untouched.
429
+ */
430
+ const DUPLICATE_SLASHES = /\/{2,}/g;
431
+ /**
432
+ * Decide whether a raw request target with duplicate slashes in its path
433
+ * should redirect to the collapsed path. Returns `undefined` when the path
434
+ * is already clean — and when the collapsed path would start with `/\`,
435
+ * which browsers resolve as a scheme-relative url: that request is left to
436
+ * astro, which treats such paths as internal instead of echoing them into
437
+ * a `Location` header.
438
+ */
439
+ function evaluateDuplicateSlashes(url, method) {
440
+ if (!url.startsWith("/")) return void 0;
441
+ const queryIndex = url.indexOf("?");
442
+ const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);
443
+ if (!pathname.includes("//")) return void 0;
444
+ const collapsed = pathname.replace(DUPLICATE_SLASHES, "/");
445
+ if (collapsed.startsWith("/\\")) return void 0;
446
+ return {
447
+ status: method === "GET" || method === "HEAD" ? 301 : 308,
448
+ location: queryIndex === -1 ? collapsed : collapsed + url.slice(queryIndex)
449
+ };
450
+ }
451
+ /**
452
+ * Redirect a request whose path carries duplicate slashes to the collapsed
453
+ * path. Returns `false` when the request needs no redirect — the caller
454
+ * continues with mounts, static files and astro.
455
+ */
456
+ function redirectDuplicateSlashes(req, res) {
457
+ const redirect = evaluateDuplicateSlashes(req.url ?? "", req.method);
458
+ if (!redirect) return false;
459
+ overrideRequestRoute("duplicate-slashes");
460
+ res.writeHead(redirect.status, { location: redirect.location });
461
+ res.end();
462
+ return true;
463
+ }
464
+ //#endregion
465
+ export { createRequestInstrumentation as a, dumpEarlyLogs as i, preparePlatform as n, runShutdown as o, shutdownTelemetry as r, runStartup as s, redirectDuplicateSlashes as t };
410
466
 
411
- //# sourceMappingURL=prepare-CXZsyAVk.js.map
467
+ //# sourceMappingURL=duplicate-slashes-C6hmOLl9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplicate-slashes-C6hmOLl9.js","names":[],"sources":["../src/lifecycle/lifecycle.ts","../src/observability/telemetry/metrics.ts","../src/observability/instrument.ts","../src/observability/log/construct.ts","../src/observability/telemetry/sdk.ts","../src/platform/env.ts","../src/platform/prepare.ts","../src/server/duplicate-slashes.ts"],"sourcesContent":["import { emit } from './events.js';\nimport type { BootContext } from './types.js';\n\nexport interface BootModule {\n onStartup?: ((context: BootContext) => Promise<void> | void) | undefined;\n onShutdown?: ((context: BootContext) => Promise<void> | void) | undefined;\n}\n\nexport async function runStartup(boot: BootModule, context: BootContext): Promise<void> {\n await emit('beforeOnStartup', context);\n await boot.onStartup?.(context);\n await emit('afterOnStartup', context);\n}\n\nexport async function runShutdown(boot: BootModule, context: BootContext): Promise<void> {\n try {\n await emit('beforeOnShutdown', context);\n await boot.onShutdown?.(context);\n } finally {\n await emit('afterOnShutdown', context);\n }\n}\n","import { DURATION_BUCKETS, createCounter, createHistogram, createUpDownCounter } from './telemetry.js';\n\nconst httpRequestDuration = createHistogram('http.server.request.duration', {\n description: 'Duration of HTTP server requests',\n unit: 's',\n buckets: DURATION_BUCKETS.http,\n});\n\nconst httpActiveRequests = createUpDownCounter('http.server.active_requests', {\n description: 'Number of active HTTP server requests',\n unit: '{request}',\n});\n\nconst actionDuration = createHistogram('astro.action.duration', {\n description: 'Duration of Astro action executions',\n unit: 's',\n buckets: DURATION_BUCKETS.http,\n});\n\nconst renderFailures = createCounter('astro.render.failures', {\n description: 'Responses that failed while streaming, after the status was sent',\n unit: '{response}',\n});\n\n/**\n * Record the start of an HTTP request. Returns a function to call when the\n * request ends. Route is unknown at the native-handler level, so active\n * requests carry only the method.\n */\nexport function recordHttpRequestStart(method: string): () => void {\n httpActiveRequests.add(1, { 'http.request.method': method });\n\n return () => {\n httpActiveRequests.add(-1, { 'http.request.method': method });\n };\n}\n\nexport function recordHttpRequestDuration(\n attributes: { method: string; route: string | undefined; status: number },\n durationMs: number,\n): void {\n httpRequestDuration.record(durationMs / 1000, {\n 'http.request.method': attributes.method,\n 'http.route': attributes.route ?? '',\n 'http.response.status_code': attributes.status,\n });\n}\n\nexport function recordActionDuration(attributes: { name: string; status: number }, durationMs: number): void {\n actionDuration.record(durationMs / 1000, {\n 'astro.action.name': attributes.name,\n 'http.response.status_code': attributes.status,\n });\n}\n\nexport function recordRenderFailure(route: string | undefined): void {\n renderFailures.add(1, { 'http.route': route ?? '' });\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { createMatcher } from '@entwico/dash/match';\nimport { ROOT_CONTEXT, SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api';\nimport type { Logger } from 'pino';\nimport type { ExcludePattern } from '../excludes/excludes.js';\nimport { generateReqId } from './log/index.js';\nimport { type RequestRecord, getLogStore } from './log/store.js';\nimport {\n recordActionDuration,\n recordHttpRequestDuration,\n recordHttpRequestStart,\n recordRenderFailure,\n} from './telemetry/metrics.js';\n\nconst LIB_NAME = '@astroscope/node';\nconst ACTIONS_PREFIX = '/_actions/';\nconst REQUEST_ID_PATTERN = /^[\\w.-]{1,64}$/;\n\nconst roundTime = (n: number) => Math.round(n * 100) / 100;\n\nexport interface RequestLoggingConfig {\n exclude: ExcludePattern[];\n extended: boolean;\n}\n\nexport interface RequestTelemetryConfig {\n exclude: ExcludePattern[];\n}\n\nexport interface RequestInstrumentationConfig {\n logging: RequestLoggingConfig | false;\n telemetry: RequestTelemetryConfig | false;\n}\n\nfunction getClientIp(req: IncomingMessage): string | undefined {\n const forwarded = req.headers['x-forwarded-for'];\n const first = Array.isArray(forwarded) ? forwarded[0] : forwarded;\n\n return (\n first?.split(',')[0]?.trim() ??\n (req.headers['x-real-ip'] as string | undefined) ??\n (req.headers['cf-connecting-ip'] as string | undefined)\n );\n}\n\nfunction resolveReqId(req: IncomingMessage): string {\n const incoming = req.headers['x-request-id'];\n const value = Array.isArray(incoming) ? incoming[0] : incoming;\n\n return value && REQUEST_ID_PATTERN.test(value) ? value : generateReqId();\n}\n\nfunction chunkSize(chunk: unknown): number {\n if (chunk == null) return 0;\n if (ArrayBuffer.isView(chunk)) return chunk.byteLength;\n if (typeof chunk === 'string') return Buffer.byteLength(chunk);\n\n return 0;\n}\n\n/**\n * Wraps the native request/response with logging and telemetry: a request\n * logger in async context (real status, response size, aborted-vs-completed\n * on `finish`/`close`), a SERVER span with propagation extraction, and\n * request metrics. Both concerns honor their own exclude patterns; when both\n * are excluded the request passes through untouched.\n */\nexport function createRequestInstrumentation(config: RequestInstrumentationConfig) {\n const tracer = trace.getTracer(LIB_NAME);\n const store = getLogStore();\n const loggingExcluded = config.logging ? createMatcher(config.logging.exclude) : () => true;\n const telemetryExcluded = config.telemetry ? createMatcher(config.telemetry.exclude) : () => true;\n\n return (req: IncomingMessage, res: ServerResponse, inner: () => void): void => {\n const url = req.url ?? '';\n const queryIndex = url.indexOf('?');\n const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);\n const method = req.method ?? 'GET';\n\n const logging = config.logging && !loggingExcluded(pathname) ? config.logging : false;\n const telemetry = config.telemetry && !telemetryExcluded(pathname) ? config.telemetry : false;\n\n if (!logging && !telemetry) {\n inner();\n\n return;\n }\n\n const startTime = performance.now();\n const isAction = pathname.startsWith(ACTIONS_PREFIX);\n\n let requestLogger: Logger | undefined;\n\n if (logging && store.root) {\n const reqId = resolveReqId(req);\n const reqData: Record<string, unknown> = { method, url: pathname };\n\n // extended logging includes potentially sensitive data\n if (logging.extended) {\n reqData['query'] = queryIndex === -1 ? '' : url.slice(queryIndex + 1);\n reqData['headers'] = req.headers;\n reqData['remoteAddress'] = getClientIp(req) ?? req.socket.remoteAddress;\n }\n\n requestLogger = store.root.child({ reqId, req: reqData });\n\n res.setHeader('x-request-id', reqId);\n }\n\n const record: RequestRecord = {\n logger: requestLogger,\n url,\n method,\n route: undefined,\n routeOverride: false,\n actionName: isAction ? pathname.slice(ACTIONS_PREFIX.length).replace(/\\/$/, '') : undefined,\n truncated: false,\n };\n\n let span: ReturnType<typeof tracer.startSpan> | undefined;\n let firstByteSpan: ReturnType<typeof tracer.startSpan> | undefined;\n let endActiveRequest: (() => void) | undefined;\n\n if (telemetry) {\n const parentContext = propagation.extract(ROOT_CONTEXT, req.headers);\n const contentLength = req.headers['content-length'];\n const clientIp = getClientIp(req);\n const host = req.headers['host'];\n\n span = tracer.startSpan(\n isAction ? `ACTION ${record.actionName}` : method,\n {\n kind: SpanKind.SERVER,\n attributes: {\n 'http.request.method': method,\n 'url.path': pathname,\n 'url.query': queryIndex === -1 ? '' : url.slice(queryIndex + 1),\n 'url.scheme': 'http',\n 'user_agent.original': req.headers['user-agent'] ?? '',\n ...(host && { 'server.address': host }),\n ...(contentLength && { 'http.request.body.size': parseInt(contentLength) }),\n ...(clientIp && { 'client.address': clientIp }),\n },\n },\n parentContext,\n );\n\n firstByteSpan = tracer.startSpan('response:first-byte', undefined, trace.setSpan(parentContext, span));\n\n endActiveRequest = recordHttpRequestStart(method);\n }\n\n let responseSize = 0;\n let firstByteTime: number | undefined;\n\n const originalWrite = res.write.bind(res);\n const originalEnd = res.end.bind(res);\n\n const markFirstByte = (): void => {\n if (firstByteTime !== undefined) return;\n\n firstByteTime = performance.now();\n\n if (firstByteSpan) {\n firstByteSpan.setAttribute('http.response.status_code', res.statusCode);\n firstByteSpan.end();\n }\n };\n\n res.write = ((chunk: unknown, ...rest: unknown[]) => {\n markFirstByte();\n responseSize += chunkSize(chunk);\n\n return (originalWrite as (...args: unknown[]) => boolean)(chunk, ...rest);\n }) as typeof res.write;\n\n res.end = ((chunk: unknown, ...rest: unknown[]) => {\n markFirstByte();\n responseSize += chunkSize(chunk);\n\n return (originalEnd as (...args: unknown[]) => ServerResponse)(chunk, ...rest);\n }) as typeof res.end;\n\n let finalized = false;\n\n const finalize = (aborted: boolean): void => {\n if (finalized) return;\n\n finalized = true;\n\n const status = res.statusCode;\n const truncated = record.truncated;\n const responseTime = performance.now() - startTime;\n const ttfb = roundTime((firstByteTime ?? performance.now()) - startTime);\n\n if (requestLogger) {\n const level = truncated || status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';\n\n requestLogger[level](\n {\n res: { statusCode: status },\n responseTime: roundTime(responseTime),\n ttfb,\n responseSize,\n ...(record.route && { route: record.route }),\n ...(aborted && { aborted: true }),\n ...(truncated && { truncated: true }),\n },\n truncated ? 'request truncated' : aborted ? 'request aborted' : 'request completed',\n );\n }\n\n if (firstByteSpan && firstByteTime === undefined) {\n firstByteSpan.setStatus({ code: SpanStatusCode.ERROR, message: 'request aborted' });\n firstByteSpan.end();\n }\n\n if (span) {\n span.setAttribute('http.response.status_code', status);\n span.setAttribute('http.response.body.size', responseSize);\n span.setAttribute('ttfb', ttfb);\n\n if (truncated) {\n span.setAttribute('astro.response.truncated', true);\n span.setStatus({ code: SpanStatusCode.ERROR, message: 'response truncated' });\n } else if (aborted || status >= 400) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: aborted ? 'request aborted' : `HTTP ${status}` });\n } else {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n\n span.end();\n }\n\n if (telemetry) {\n endActiveRequest?.();\n recordHttpRequestDuration({ method, route: record.route, status }, responseTime);\n\n if (record.actionName) {\n recordActionDuration({ name: record.actionName, status }, responseTime);\n }\n\n if (truncated) {\n recordRenderFailure(record.route);\n }\n }\n };\n\n res.once('finish', () => finalize(false));\n res.once('close', () => finalize(!res.writableFinished));\n\n const run = (): void => store.requestStorage.run(record, inner);\n\n if (span) {\n context.with(trace.setSpan(context.active(), span), run);\n } else {\n run();\n }\n };\n}\n","import { isSpanContextValid, trace } from '@opentelemetry/api';\nimport pino, { type Bindings, type Logger, type LoggerOptions } from 'pino';\nimport { getLogStore } from './store.js';\n\n/**\n * Contract of the `src/log.ts` entry seam: pino logger options, or a factory\n * producing them. Never a logger instance — the platform constructs the\n * logger itself (after instrumentation, so trace correlation works).\n */\nexport type LoggerOptionsFactory = LoggerOptions | ((ctx: { dev: boolean }) => LoggerOptions | Promise<LoggerOptions>);\n\n/**\n * Compose the user mixin (if any) with platform trace correlation: when a\n * span is active, every entry carries `trace_id` / `span_id` / `trace_flags`.\n */\nfunction composeMixin(userMixin: LoggerOptions['mixin']): NonNullable<LoggerOptions['mixin']> {\n return (mergeObject, level, logger) => {\n const user = userMixin ? userMixin(mergeObject, level, logger) : {};\n const spanContext = trace.getActiveSpan()?.spanContext();\n\n if (!spanContext || !isSpanContextValid(spanContext)) return user;\n\n return {\n ...user,\n trace_id: spanContext.traceId,\n span_id: spanContext.spanId,\n trace_flags: `0${spanContext.traceFlags.toString(16)}`,\n };\n };\n}\n\n/**\n * Construct the root logger from the app's options seam and replay any logs\n * buffered before construction (original timestamps kept as `bufferedTime`).\n * In dev this runs once per generation; the buffer only exists the first time.\n */\nexport async function constructRootLogger(\n factory: LoggerOptionsFactory | undefined,\n ctx: { dev: boolean },\n): Promise<Logger> {\n const store = getLogStore();\n const options = (typeof factory === 'function' ? await factory(ctx) : factory) ?? {};\n const root = pino({ level: 'info', ...options, mixin: composeMixin(options.mixin) });\n\n store.root = root;\n\n for (const entry of store.buffer.splice(0)) {\n const bindings = entry.bindings.length ? (Object.assign({}, ...entry.bindings) as Bindings) : {};\n const target = root.child({ ...bindings, bufferedTime: new Date(entry.time).toISOString() });\n\n (target[entry.level] as (...args: unknown[]) => void)(...entry.args);\n }\n\n if (store.dropped > 0) {\n root.warn({ dropped: store.dropped }, 'early log buffer overflowed, entries dropped');\n store.dropped = 0;\n }\n\n return root;\n}\n\n/**\n * Failure path for startups that die before the logger exists: dump the\n * buffered entries to the console so no phase is silent.\n */\nexport function dumpEarlyLogs(): void {\n const store = getLogStore();\n\n if (store.root) return;\n\n for (const entry of store.buffer.splice(0)) {\n const bindings = entry.bindings.length ? Object.assign({}, ...entry.bindings) : undefined;\n\n console.error(\n new Date(entry.time).toISOString(),\n entry.level.toUpperCase(),\n ...(bindings ? [bindings] : []),\n ...entry.args,\n );\n }\n\n if (store.dropped > 0) {\n console.error(`(${store.dropped} early log entries dropped)`);\n store.dropped = 0;\n }\n}\n","import { log } from '../log/index.js';\nimport { bindGauges } from './gauges.js';\n\n/**\n * Platform-owned telemetry bundle: NodeSDK with undici (fetch) and node\n * runtime instrumentation, host metrics, and a Prometheus reader. Trace\n * exporters are driven by standard `OTEL_*` env vars; without any of them\n * traces stay off (no failing localhost OTLP exports).\n *\n * Guarded per process (dev restarts are in-process; a re-created NodeSDK\n * would double-register instrumentations and leak the Prometheus port).\n */\n\nconst TELEMETRY_KEY = Symbol.for('@astroscope/node/telemetry');\n\ninterface TelemetryHandle {\n shutdown: () => Promise<void>;\n}\n\nexport interface TelemetrySdkOptions {\n prometheus: { host?: string | undefined; port?: number | undefined } | false;\n}\n\nfunction getHandle(): TelemetryHandle | undefined {\n return (globalThis as Record<symbol, unknown>)[TELEMETRY_KEY] as TelemetryHandle | undefined;\n}\n\nfunction defaultEnv(key: string, value: string): void {\n if (!process.env[key]) process.env[key] = value;\n}\n\nexport async function startTelemetry(options: TelemetrySdkOptions): Promise<void> {\n const g = globalThis as Record<symbol, unknown>;\n\n if (g[TELEMETRY_KEY]) return;\n\n if (process.env['OTEL_SDK_DISABLED'] === 'true') {\n log.debug('telemetry disabled via OTEL_SDK_DISABLED');\n\n return;\n }\n\n // without an explicitly configured exporter target, exporting traces to the\n // default localhost OTLP endpoint would fail on every flush\n if (!process.env['OTEL_EXPORTER_OTLP_ENDPOINT'] && !process.env['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT']) {\n defaultEnv('OTEL_TRACES_EXPORTER', 'none');\n }\n\n defaultEnv('OTEL_METRICS_EXPORTER', 'none');\n defaultEnv('OTEL_LOGS_EXPORTER', 'none');\n\n const [\n { NodeSDK },\n { UndiciInstrumentation },\n { RuntimeNodeInstrumentation },\n { PrometheusExporter },\n { HostMetrics },\n ] = await Promise.all([\n import('@opentelemetry/sdk-node'),\n import('@opentelemetry/instrumentation-undici'),\n import('@opentelemetry/instrumentation-runtime-node'),\n import('@opentelemetry/exporter-prometheus'),\n import('@opentelemetry/host-metrics'),\n ]);\n\n const prometheus = options.prometheus\n ? {\n host: process.env['OTEL_EXPORTER_PROMETHEUS_HOST'] ?? options.prometheus.host ?? '0.0.0.0',\n port: process.env['OTEL_EXPORTER_PROMETHEUS_PORT']\n ? Number(process.env['OTEL_EXPORTER_PROMETHEUS_PORT'])\n : (options.prometheus.port ?? 9464),\n }\n : false;\n\n const sdk = new NodeSDK({\n instrumentations: [new UndiciInstrumentation(), new RuntimeNodeInstrumentation()],\n ...(prometheus && { metricReaders: [new PrometheusExporter(prometheus)] }),\n });\n\n sdk.start();\n bindGauges();\n\n const hostMetrics = new HostMetrics();\n\n hostMetrics.start();\n\n g[TELEMETRY_KEY] = {\n shutdown: () => sdk.shutdown(),\n } satisfies TelemetryHandle;\n\n if (prometheus) {\n log.debug({ host: prometheus.host, port: prometheus.port }, 'prometheus metrics listening');\n }\n}\n\n/**\n * Flush and shut the SDK down. Prod-only (dev keeps the SDK for the process\n * lifetime across generations).\n */\nexport async function shutdownTelemetry(): Promise<void> {\n const handle = getHandle();\n\n if (!handle) return;\n\n delete (globalThis as Record<symbol, unknown>)[TELEMETRY_KEY];\n\n await handle.shutdown();\n}\n","import fs from 'node:fs';\nimport { log } from '../observability/log/index.js';\n\n/**\n * Platform env loading (position −1, before the config seam):\n * `CONFIG_PATH` → `./.env` → none. Existing process env vars win\n */\nexport function loadEnvFiles(): void {\n const configPath = process.env['CONFIG_PATH'];\n\n if (configPath) {\n process.loadEnvFile(configPath);\n\n log.debug({ path: configPath }, 'loaded env file from CONFIG_PATH');\n\n return;\n }\n\n if (fs.existsSync('.env')) {\n process.loadEnvFile('.env');\n log.debug({ path: '.env' }, 'loaded env file');\n\n return;\n }\n\n log.debug('no env file loaded');\n}\n","import { type LoggerOptionsFactory, constructRootLogger } from '../observability/log/construct.js';\nimport { type TelemetrySdkOptions, startTelemetry } from '../observability/telemetry/sdk.js';\nimport { loadEnvFiles } from './env.js';\n\nconst INSTRUMENTATION_KEY = Symbol.for('@astroscope/node/instrumentation');\n\nexport interface InstrumentationContext {\n dev: boolean;\n}\n\ninterface InstrumentationSeam {\n register?: ((ctx: InstrumentationContext) => void | Promise<void>) | undefined;\n}\n\ninterface LogSeam {\n default?: LoggerOptionsFactory | undefined;\n}\n\nexport interface PlatformSeams {\n /** `src/config.ts` — validation runs at import; a throw fails the startup */\n config?: (() => Promise<unknown>) | undefined;\n /** `src/instrumentation.ts` — extra instrumentation, once per process */\n instrumentation?: (() => Promise<InstrumentationSeam>) | undefined;\n /** `src/log.ts` — pino logger options (or a factory), never an instance */\n log?: (() => Promise<LogSeam>) | undefined;\n}\n\nexport interface PreparePlatformOptions {\n dev: boolean;\n telemetry: TelemetrySdkOptions | false;\n seams: PlatformSeams;\n}\n\n/**\n * The platform sequence in front of the boot lifecycle:\n * env → config → instrumentation (platform SDK + `register`, once per\n * process) → logger construction (after instrumentation, so entries carry\n * trace correlation). Prod runs it once in `startServer()`; dev re-runs it\n * per generation with the once-per-process parts guarded.\n */\nexport async function preparePlatform(options: PreparePlatformOptions): Promise<void> {\n loadEnvFiles();\n\n await options.seams.config?.();\n\n const g = globalThis as Record<symbol, unknown>;\n\n if (!g[INSTRUMENTATION_KEY]) {\n g[INSTRUMENTATION_KEY] = true;\n\n if (options.telemetry) {\n await startTelemetry(options.telemetry);\n }\n\n const instrumentation = await options.seams.instrumentation?.();\n\n await instrumentation?.register?.({ dev: options.dev });\n }\n\n const logSeam = await options.seams.log?.();\n\n await constructRootLogger(logSeam?.default, { dev: options.dev });\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { overrideRequestRoute } from '../observability/request-route.js';\n\n/**\n * Duplicate slashes in a request path (`//weine`, `/seminare///x`) are not\n * a distinct resource: astro collapses them before routing and renders the\n * page as if the path were clean, so every such spelling is duplicate\n * content. Astro itself only redirects duplicate *trailing* slashes; this\n * handler redirects the rest, before native mounts and static serving, so a\n * page, an endpoint, a public asset and a mounted handler all answer alike.\n *\n * Only literal slashes in the raw request target count — `%2F` stays as\n * sent. Absolute-form targets (`GET http://host/x`) and `*` are not paths\n * and pass through untouched.\n */\n\nconst DUPLICATE_SLASHES = /\\/{2,}/g;\n\nexport interface DuplicateSlashRedirect {\n status: 301 | 308;\n location: string;\n}\n\n/**\n * Decide whether a raw request target with duplicate slashes in its path\n * should redirect to the collapsed path. Returns `undefined` when the path\n * is already clean — and when the collapsed path would start with `/\\`,\n * which browsers resolve as a scheme-relative url: that request is left to\n * astro, which treats such paths as internal instead of echoing them into\n * a `Location` header.\n */\nexport function evaluateDuplicateSlashes(url: string, method: string | undefined): DuplicateSlashRedirect | undefined {\n if (!url.startsWith('/')) return undefined;\n\n const queryIndex = url.indexOf('?');\n const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);\n\n if (!pathname.includes('//')) return undefined;\n\n const collapsed = pathname.replace(DUPLICATE_SLASHES, '/');\n\n if (collapsed.startsWith('/\\\\')) return undefined;\n\n return {\n status: method === 'GET' || method === 'HEAD' ? 301 : 308,\n location: queryIndex === -1 ? collapsed : collapsed + url.slice(queryIndex),\n };\n}\n\n/**\n * Redirect a request whose path carries duplicate slashes to the collapsed\n * path. Returns `false` when the request needs no redirect — the caller\n * continues with mounts, static files and astro.\n */\nexport function redirectDuplicateSlashes(req: IncomingMessage, res: ServerResponse): boolean {\n const redirect = evaluateDuplicateSlashes(req.url ?? '', req.method);\n\n if (!redirect) return false;\n\n overrideRequestRoute('duplicate-slashes');\n\n res.writeHead(redirect.status, { location: redirect.location });\n res.end();\n\n return true;\n}\n"],"mappings":"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"}
@@ -1,4 +1,13 @@
1
1
  import { matchesAny } from "@entwico/dash/match";
2
+ //#region src/excludes/serialize.ts
3
+ /**
4
+ * Serialize exclude patterns to JavaScript code for use in virtual modules.
5
+ * Handles RegExp objects which JSON.stringify cannot serialize.
6
+ */
7
+ function serializeExcludePatterns(patterns) {
8
+ return `[${patterns.map((p) => "pattern" in p ? `{ pattern: ${p.pattern.toString()} }` : JSON.stringify(p)).join(", ")}]`;
9
+ }
10
+ //#endregion
2
11
  //#region src/excludes/excludes.ts
3
12
  /**
4
13
  * Vite/Astro dev server paths - only relevant in development.
@@ -11,9 +20,14 @@ const DEV_EXCLUDES = [
11
20
  { prefix: "/node_modules/" }
12
21
  ];
13
22
  /**
14
- * Astro internal paths for static assets and image optimization.
23
+ * Astro internal paths for static assets and image optimization, plus the
24
+ * platform's own asset routes (`/_i18n/` translation chunks).
15
25
  */
16
- const ASTRO_STATIC_EXCLUDES = [{ prefix: "/_astro/" }, { prefix: "/_image" }];
26
+ const ASTRO_STATIC_EXCLUDES = [
27
+ { prefix: "/_astro/" },
28
+ { prefix: "/_image" },
29
+ { prefix: "/_i18n/" }
30
+ ];
17
31
  /**
18
32
  * Common static asset paths.
19
33
  */
@@ -73,6 +87,6 @@ function withExcluded(middleware, exclude) {
73
87
  };
74
88
  }
75
89
  //#endregion
76
- export { shouldExclude as a, STATIC_EXCLUDES as i, DEV_EXCLUDES as n, withExcluded as o, RECOMMENDED_EXCLUDES as r, ASTRO_STATIC_EXCLUDES as t };
90
+ export { shouldExclude as a, STATIC_EXCLUDES as i, DEV_EXCLUDES as n, withExcluded as o, RECOMMENDED_EXCLUDES as r, serializeExcludePatterns as s, ASTRO_STATIC_EXCLUDES as t };
77
91
 
78
- //# sourceMappingURL=excludes-pE23EbmQ.js.map
92
+ //# sourceMappingURL=excludes-10eDopVB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"excludes-10eDopVB.js","names":[],"sources":["../src/excludes/serialize.ts","../src/excludes/excludes.ts"],"sourcesContent":["import type { ExcludePattern } from './excludes.js';\n\n/**\n * Serialize exclude patterns to JavaScript code for use in virtual modules.\n * Handles RegExp objects which JSON.stringify cannot serialize.\n */\nexport function serializeExcludePatterns(patterns: ExcludePattern[]): string {\n return `[${patterns.map((p) => ('pattern' in p ? `{ pattern: ${p.pattern.toString()} }` : JSON.stringify(p))).join(', ')}]`;\n}\n","import { type StringPattern, matchesAny } from '@entwico/dash/match';\nimport type { APIContext, MiddlewareHandler } from 'astro';\n\n/**\n * Patterns accepted by @astroscope/node exclude options. The serializable\n * subset of dash's `StringPattern` — no matcher functions, since integration\n * options cross a virtual-module boundary.\n */\nexport type ExcludePattern = Exclude<StringPattern, (value: string) => boolean>;\n\nexport { serializeExcludePatterns } from './serialize.js';\n\n/**\n * Vite/Astro dev server paths - only relevant in development.\n */\nexport const DEV_EXCLUDES: ExcludePattern[] = [\n { prefix: '/@id/' },\n { prefix: '/@fs/' },\n { prefix: '/@vite/' },\n { prefix: '/src/' },\n { prefix: '/node_modules/' },\n];\n\n/**\n * Astro internal paths for static assets and image optimization, plus the\n * platform's own asset routes (`/_i18n/` translation chunks).\n */\nexport const ASTRO_STATIC_EXCLUDES: ExcludePattern[] = [\n { prefix: '/_astro/' },\n { prefix: '/_image' },\n { prefix: '/_i18n/' },\n];\n\n/**\n * Common static asset paths.\n */\nexport const STATIC_EXCLUDES: ExcludePattern[] = [\n { exact: '/favicon.ico' },\n { exact: '/robots.txt' },\n { exact: '/sitemap.xml' },\n { exact: '/browserconfig.xml' },\n { exact: '/manifest.json' },\n { exact: '/manifest.webmanifest' },\n];\n\n/**\n * Recommended excludes for middleware.\n * Includes dev paths and Astro internals.\n *\n * @example\n * ```ts\n * createI18nMiddleware({\n * locale: (ctx) => ctx.locals.session?.locale ?? 'en',\n * exclude: [\n * ...RECOMMENDED_EXCLUDES,\n * { exact: \"/health\" }, // your health endpoint\n * ],\n * })\n * ```\n */\nexport const RECOMMENDED_EXCLUDES: ExcludePattern[] = [...DEV_EXCLUDES, ...ASTRO_STATIC_EXCLUDES];\n\n/**\n * Check if a request should be excluded based on patterns or a function.\n */\nexport function shouldExclude(\n ctx: APIContext,\n exclude: readonly StringPattern[] | ((context: APIContext) => boolean) | undefined,\n): boolean {\n if (!exclude) return false;\n\n if (typeof exclude === 'function') {\n return exclude(ctx);\n }\n\n return matchesAny(ctx.url.pathname, exclude);\n}\n\n/**\n * Wraps a middleware to skip execution for excluded paths.\n * Useful for third-party middlewares that don't have built-in exclude support.\n *\n * @example\n * ```ts\n * import { RECOMMENDED_EXCLUDES, withExcluded } from '@astroscope/node/excludes';\n * import { someExternalMiddleware } from 'some-package';\n *\n * export const onRequest = sequence(\n * withExcluded(someExternalMiddleware(), [\n * ...RECOMMENDED_EXCLUDES,\n * { prefix: '/api/webhooks/' },\n * ]),\n * );\n * ```\n */\nexport function withExcluded(\n middleware: MiddlewareHandler,\n exclude: readonly StringPattern[] | ((context: APIContext) => boolean),\n): MiddlewareHandler {\n return (ctx, next) => {\n if (shouldExclude(ctx, exclude)) {\n return next();\n }\n\n return middleware(ctx, next);\n };\n}\n"],"mappings":";;;;;;AAMA,SAAgB,yBAAyB,UAAoC;CAC3E,OAAO,IAAI,SAAS,KAAK,MAAO,aAAa,IAAI,cAAc,EAAE,QAAQ,SAAS,EAAE,MAAM,KAAK,UAAU,CAAC,CAAE,CAAC,CAAC,KAAK,IAAI,EAAE;AAC3H;;;;;;ACOA,MAAa,eAAiC;CAC5C,EAAE,QAAQ,QAAQ;CAClB,EAAE,QAAQ,QAAQ;CAClB,EAAE,QAAQ,UAAU;CACpB,EAAE,QAAQ,QAAQ;CAClB,EAAE,QAAQ,iBAAiB;AAC7B;;;;;AAMA,MAAa,wBAA0C;CACrD,EAAE,QAAQ,WAAW;CACrB,EAAE,QAAQ,UAAU;CACpB,EAAE,QAAQ,UAAU;AACtB;;;;AAKA,MAAa,kBAAoC;CAC/C,EAAE,OAAO,eAAe;CACxB,EAAE,OAAO,cAAc;CACvB,EAAE,OAAO,eAAe;CACxB,EAAE,OAAO,qBAAqB;CAC9B,EAAE,OAAO,iBAAiB;CAC1B,EAAE,OAAO,wBAAwB;AACnC;;;;;;;;;;;;;;;;AAiBA,MAAa,uBAAyC,CAAC,GAAG,cAAc,GAAG,qBAAqB;;;;AAKhG,SAAgB,cACd,KACA,SACS;CACT,IAAI,CAAC,SAAS,OAAO;CAErB,IAAI,OAAO,YAAY,YACrB,OAAO,QAAQ,GAAG;CAGpB,OAAO,WAAW,IAAI,IAAI,UAAU,OAAO;AAC7C;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,aACd,YACA,SACmB;CACnB,QAAQ,KAAK,SAAS;EACpB,IAAI,cAAc,KAAK,OAAO,GAC5B,OAAO,KAAK;EAGd,OAAO,WAAW,KAAK,IAAI;CAC7B;AACF"}
@@ -1,5 +1,12 @@
1
1
  import { StringPattern } from "@entwico/dash/match";
2
2
  import { APIContext, MiddlewareHandler } from "astro";
3
+ //#region src/excludes/serialize.d.ts
4
+ /**
5
+ * Serialize exclude patterns to JavaScript code for use in virtual modules.
6
+ * Handles RegExp objects which JSON.stringify cannot serialize.
7
+ */
8
+ declare function serializeExcludePatterns(patterns: ExcludePattern[]): string;
9
+ //#endregion
3
10
  //#region src/excludes/excludes.d.ts
4
11
  /**
5
12
  * Patterns accepted by @astroscope/node exclude options. The serializable
@@ -12,7 +19,8 @@ type ExcludePattern = Exclude<StringPattern, (value: string) => boolean>;
12
19
  */
13
20
  declare const DEV_EXCLUDES: ExcludePattern[];
14
21
  /**
15
- * Astro internal paths for static assets and image optimization.
22
+ * Astro internal paths for static assets and image optimization, plus the
23
+ * platform's own asset routes (`/_i18n/` translation chunks).
16
24
  */
17
25
  declare const ASTRO_STATIC_EXCLUDES: ExcludePattern[];
18
26
  /**
@@ -58,5 +66,5 @@ declare function shouldExclude(ctx: APIContext, exclude: readonly StringPattern[
58
66
  */
59
67
  declare function withExcluded(middleware: MiddlewareHandler, exclude: readonly StringPattern[] | ((context: APIContext) => boolean)): MiddlewareHandler;
60
68
  //#endregion
61
- export { STATIC_EXCLUDES as a, RECOMMENDED_EXCLUDES as i, DEV_EXCLUDES as n, shouldExclude as o, ExcludePattern as r, withExcluded as s, ASTRO_STATIC_EXCLUDES as t };
62
- //# sourceMappingURL=excludes-BDiE3eyp.d.ts.map
69
+ export { STATIC_EXCLUDES as a, serializeExcludePatterns as c, RECOMMENDED_EXCLUDES as i, DEV_EXCLUDES as n, shouldExclude as o, ExcludePattern as r, withExcluded as s, ASTRO_STATIC_EXCLUDES as t };
70
+ //# sourceMappingURL=excludes-rvkVGg6B.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"excludes-rvkVGg6B.d.ts","names":[],"sources":["../src/excludes/serialize.ts","../src/excludes/excludes.ts"],"mappings":";;;;;;;iBAMgB,yBAAyB,UAAU;;;;;;;;KCEvC,iBAAiB,QAAQ,gBAAgB;;;;cAOxC,cAAc;;;;;cAYd,uBAAuB;;;;cASvB,iBAAiB;;;;;;;;;;;;;;;;cAwBjB,sBAAsB;;;;iBAKnB,cACd,KAAK,YACL,kBAAkB,oBAAoB,SAAS;;;;;;;;;;;;;;;;;;iBA4BjC,aACd,YAAY,mBACZ,kBAAkB,oBAAoB,SAAS,0BAC9C"}
@@ -1,2 +1,2 @@
1
- import { a as STATIC_EXCLUDES, i as RECOMMENDED_EXCLUDES, n as DEV_EXCLUDES, o as shouldExclude, r as ExcludePattern, s as withExcluded, t as ASTRO_STATIC_EXCLUDES } from "./excludes-BDiE3eyp.js";
2
- export { ASTRO_STATIC_EXCLUDES, DEV_EXCLUDES, ExcludePattern, RECOMMENDED_EXCLUDES, STATIC_EXCLUDES, shouldExclude, withExcluded };
1
+ import { a as STATIC_EXCLUDES, c as serializeExcludePatterns, i as RECOMMENDED_EXCLUDES, n as DEV_EXCLUDES, o as shouldExclude, r as ExcludePattern, s as withExcluded, t as ASTRO_STATIC_EXCLUDES } from "./excludes-rvkVGg6B.js";
2
+ export { ASTRO_STATIC_EXCLUDES, DEV_EXCLUDES, ExcludePattern, RECOMMENDED_EXCLUDES, STATIC_EXCLUDES, serializeExcludePatterns, shouldExclude, withExcluded };
package/dist/excludes.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as shouldExclude, i as STATIC_EXCLUDES, n as DEV_EXCLUDES, o as withExcluded, r as RECOMMENDED_EXCLUDES, t as ASTRO_STATIC_EXCLUDES } from "./excludes-pE23EbmQ.js";
2
- export { ASTRO_STATIC_EXCLUDES, DEV_EXCLUDES, RECOMMENDED_EXCLUDES, STATIC_EXCLUDES, shouldExclude, withExcluded };
1
+ import { a as shouldExclude, i as STATIC_EXCLUDES, n as DEV_EXCLUDES, o as withExcluded, r as RECOMMENDED_EXCLUDES, s as serializeExcludePatterns, t as ASTRO_STATIC_EXCLUDES } from "./excludes-10eDopVB.js";
2
+ export { ASTRO_STATIC_EXCLUDES, DEV_EXCLUDES, RECOMMENDED_EXCLUDES, STATIC_EXCLUDES, serializeExcludePatterns, shouldExclude, withExcluded };
@@ -0,0 +1,35 @@
1
+ //#region src/islands/graph.ts
2
+ function createClosure(chunks, edges) {
3
+ const cache = /* @__PURE__ */ new Map();
4
+ const walk = (fileName, result, visited) => {
5
+ if (visited.has(fileName)) return;
6
+ visited.add(fileName);
7
+ const entry = chunks[fileName];
8
+ if (!entry) return;
9
+ for (const dep of edges(entry)) {
10
+ result.add(dep);
11
+ walk(dep, result, visited);
12
+ }
13
+ };
14
+ return (fileName) => {
15
+ const cached = cache.get(fileName);
16
+ if (cached) return cached;
17
+ const result = /* @__PURE__ */ new Set();
18
+ walk(fileName, result, /* @__PURE__ */ new Set());
19
+ result.delete(fileName);
20
+ const list = [...result];
21
+ cache.set(fileName, list);
22
+ return list;
23
+ };
24
+ }
25
+ function createChunkGraph(manifest) {
26
+ return {
27
+ staticClosure: createClosure(manifest.chunks, (entry) => entry.i ?? []),
28
+ fullClosure: createClosure(manifest.chunks, (entry) => [...entry.i ?? [], ...entry.d ?? []]),
29
+ has: (fileName) => fileName in manifest.chunks
30
+ };
31
+ }
32
+ //#endregion
33
+ export { createChunkGraph as t };
34
+
35
+ //# sourceMappingURL=graph-DdAxiaxa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-DdAxiaxa.js","names":[],"sources":["../src/islands/graph.ts"],"sourcesContent":["import type { IslandsManifest } from './types.js';\n\n/**\n * Closure computation over the chunk import graph, memoized per manifest. The\n * manifest stores direct edges only — flattened closures repeat heavily across\n * entries and would bloat the JSON.\n */\nexport type ChunkGraph = {\n /** transitive static imports of a chunk, the chunk itself excluded */\n staticClosure(fileName: string): string[];\n /** transitive static + dynamic imports of a chunk, the chunk itself excluded */\n fullClosure(fileName: string): string[];\n has(fileName: string): boolean;\n};\n\nfunction createClosure(\n chunks: IslandsManifest['chunks'],\n edges: (entry: { i?: string[] | undefined; d?: string[] | undefined }) => string[],\n): (fileName: string) => string[] {\n const cache = new Map<string, string[]>();\n\n const walk = (fileName: string, result: Set<string>, visited: Set<string>): void => {\n if (visited.has(fileName)) {\n return;\n }\n\n visited.add(fileName);\n\n const entry = chunks[fileName];\n\n if (!entry) {\n return;\n }\n\n for (const dep of edges(entry)) {\n result.add(dep);\n walk(dep, result, visited);\n }\n };\n\n return (fileName) => {\n const cached = cache.get(fileName);\n\n if (cached) {\n return cached;\n }\n\n const result = new Set<string>();\n\n walk(fileName, result, new Set());\n result.delete(fileName);\n\n const list = [...result];\n\n cache.set(fileName, list);\n\n return list;\n };\n}\n\nexport function createChunkGraph(manifest: IslandsManifest): ChunkGraph {\n return {\n staticClosure: createClosure(manifest.chunks, (entry) => entry.i ?? []),\n fullClosure: createClosure(manifest.chunks, (entry) => [...(entry.i ?? []), ...(entry.d ?? [])]),\n has: (fileName) => fileName in manifest.chunks,\n };\n}\n"],"mappings":";AAeA,SAAS,cACP,QACA,OACgC;CAChC,MAAM,wBAAQ,IAAI,IAAsB;CAExC,MAAM,QAAQ,UAAkB,QAAqB,YAA+B;EAClF,IAAI,QAAQ,IAAI,QAAQ,GACtB;EAGF,QAAQ,IAAI,QAAQ;EAEpB,MAAM,QAAQ,OAAO;EAErB,IAAI,CAAC,OACH;EAGF,KAAK,MAAM,OAAO,MAAM,KAAK,GAAG;GAC9B,OAAO,IAAI,GAAG;GACd,KAAK,KAAK,QAAQ,OAAO;EAC3B;CACF;CAEA,QAAQ,aAAa;EACnB,MAAM,SAAS,MAAM,IAAI,QAAQ;EAEjC,IAAI,QACF,OAAO;EAGT,MAAM,yBAAS,IAAI,IAAY;EAE/B,KAAK,UAAU,wBAAQ,IAAI,IAAI,CAAC;EAChC,OAAO,OAAO,QAAQ;EAEtB,MAAM,OAAO,CAAC,GAAG,MAAM;EAEvB,MAAM,IAAI,UAAU,IAAI;EAExB,OAAO;CACT;AACF;AAEA,SAAgB,iBAAiB,UAAuC;CACtE,OAAO;EACL,eAAe,cAAc,SAAS,SAAS,UAAU,MAAM,KAAK,CAAC,CAAC;EACtE,aAAa,cAAc,SAAS,SAAS,UAAU,CAAC,GAAI,MAAM,KAAK,CAAC,GAAI,GAAI,MAAM,KAAK,CAAC,CAAE,CAAC;EAC/F,MAAM,aAAa,YAAY,SAAS;CAC1C;AACF"}
package/dist/health.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  //#region src/health/health.d.ts
2
- interface HealthCheckResult {
2
+ export interface HealthCheckResult {
3
3
  status: 'healthy' | 'unhealthy';
4
4
  latency?: number | undefined;
5
5
  error?: string | undefined;
6
6
  }
7
- interface HealthCheckDefinition {
7
+ export interface HealthCheckDefinition {
8
8
  /**
9
9
  * Unique name for this health check.
10
10
  */
@@ -32,7 +32,6 @@ interface HealthCheckDefinition {
32
32
  * files can call it unconditionally. Returns an unregister function; checks
33
33
  * still registered after `onShutdown` are removed automatically.
34
34
  */
35
- declare function registerHealthCheck(definition: HealthCheckDefinition): () => void;
35
+ export declare function registerHealthCheck(definition: HealthCheckDefinition): () => void;
36
36
  //#endregion
37
- export { HealthCheckDefinition, HealthCheckResult, registerHealthCheck };
38
37
  //# sourceMappingURL=health.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"health.d.ts","names":[],"sources":["../src/health/health.ts"],"mappings":";UAEiB;EACf;EACA;EACA;;UAGe;;;;EAIf;;;;;EAMA,aAAa,QAAQ,4BAA4B;;;;;EAMjD;;;;;EAMA;;;;;;;;;iBAUc,oBAAoB,YAAY"}
1
+ {"version":3,"file":"health.d.ts","names":[],"sources":["../src/health/health.ts"],"mappings":";iBAEiB;EACf;EACA;EACA;;iBAGe;;;;EAIf;;;;;EAMA,aAAa,QAAQ,4BAA4B;;;;;EAMjD;;;;;EAMA;;;;;;;;;wBAUc,oBAAoB,YAAY"}
@@ -5,7 +5,6 @@ import { APIRoute } from "astro";
5
5
  * clients are concerned the endpoint does not exist, so requests answer 404 —
6
6
  * a 400 would advertise a live processing surface behind the route.
7
7
  */
8
- declare const GET: APIRoute;
8
+ export declare const GET: APIRoute;
9
9
  //#endregion
10
- export { GET };
11
10
  //# sourceMappingURL=image-endpoint.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-endpoint.d.ts","names":[],"sources":["../src/image/endpoint-disabled.ts"],"mappings":";;;;;;;cAOa,KAAK"}
1
+ {"version":3,"file":"image-endpoint.d.ts","names":[],"sources":["../src/image/endpoint-disabled.ts"],"mappings":";;;;;;;qBAOa,KAAK"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { r as ExcludePattern } from "./excludes-BDiE3eyp.js";
1
+ import { r as ExcludePattern } from "./excludes-rvkVGg6B.js";
2
2
  import "./construct-BGlPfWMF.js";
3
- import { c as IslandEmitter, i as registerIslandEmitter, l as IslandInfo, s as IslandEmission, t as getRequestRouteData } from "./route-store-CSYR4G5e.js";
3
+ import { c as IslandEmitter, i as registerIslandEmitter, l as IslandInfo, s as IslandEmission, t as getRequestRouteData } from "./route-store-neUA2nBb.js";
4
4
  import { t as BootContext } from "./types-D0uMBi2M.js";
5
5
  import { n as BootEventName, t as BootEventHandler } from "./events-u7J3ezJR.js";
6
6
  import { AstroIntegration } from "astro";
@@ -173,7 +173,7 @@ interface NodeOptions {
173
173
  * as plain code around `server.listen()` instead of being injected into the
174
174
  * build output.
175
175
  */
176
- declare function node(options?: NodeOptions): AstroIntegration;
176
+ export default function node(options?: NodeOptions): AstroIntegration;
177
177
  //#endregion
178
178
  //#region src/platform/prepare.d.ts
179
179
  interface InstrumentationContext {
@@ -186,5 +186,5 @@ interface BootModule {
186
186
  onShutdown?: ((context: BootContext) => Promise<void> | void) | undefined;
187
187
  }
188
188
  //#endregion
189
- export { type BootContext, type BootEventHandler, type BootEventName, type BootModule, type HealthProbePaths, type InstrumentationContext, type IslandEmission, type IslandEmitter, type IslandInfo, type NodeBootOptions, type NodeHealthOptions, type NodeLoggingOptions, type NodeOptions, type NodePrometheusOptions, type NodeTelemetryOptions, node as default, getRequestRouteData, registerIslandEmitter };
189
+ export { type BootContext, type BootEventHandler, type BootEventName, type BootModule, type HealthProbePaths, type InstrumentationContext, type IslandEmission, type IslandEmitter, type IslandInfo, type NodeBootOptions, type NodeHealthOptions, type NodeLoggingOptions, type NodeOptions, type NodePrometheusOptions, type NodeTelemetryOptions, getRequestRouteData, registerIslandEmitter };
190
190
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/integration/integration.ts","../src/platform/prepare.ts","../src/lifecycle/lifecycle.ts"],"mappings":";;;;;;;UAEiB;;;;;EAKf;;;;;;EAOA;;;;;UAMe;EACf;EACA;EACA;EACA;;UAGe;;;;;EAKf;;;;;EAMA;;;;;EAMA,QAAQ;;UAGO;;;;;EAKf,UAAU;;;;;;EAOV;;;;;;EAOA;;UAGe;;;;;EAKf;;;;;EAMA;;UAGe;;;;;EAKf,UAAU;;;;;EAMV,aAAa;;;;;;EAOb;;UAGe;;;;;;;;EAQf,OAAO;;;;;;;EAQP,UAAU;;;;;;;EAQV,YAAY;;;;;;EAOZ,SAAS;;;;;;;EAQT;IAAS,UAAU;;;;;;;;;;;EAWnB;;;;;;;;;;;;EAaA;;;;;EAMA;;;;;;EAOA;;;;;;;;;;iBC5GsB,KAAK,UAAS,cAAmB;;;UClExC;EACf;;;;UCJe;EACf,cAAc,SAAS,gBAAgB;EACvC,eAAe,SAAS,gBAAgB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/integration/integration.ts","../src/platform/prepare.ts","../src/lifecycle/lifecycle.ts"],"mappings":";;;;;;;UAEiB;;;;;EAKf;;;;;;EAOA;;;;;UAMe;EACf;EACA;EACA;EACA;;UAGe;;;;;EAKf;;;;;EAMA;;;;;EAMA,QAAQ;;UAGO;;;;;EAKf,UAAU;;;;;;EAOV;;;;;;EAOA;;UAGe;;;;;EAKf;;;;;EAMA;;UAGe;;;;;EAKf,UAAU;;;;;EAMV,aAAa;;;;;;EAOb;;UAGe;;;;;;;;EAQf,OAAO;;;;;;;EAQP,UAAU;;;;;;;EAQV,YAAY;;;;;;EAOZ,SAAS;;;;;;;EAQT;IAAS,UAAU;;;;;;;;;;;EAWnB;;;;;;;;;;;;EAaA;;;;;EAMA;;;;;;EAOA;;;;;;;;;;wBC1GsB,KAAK,UAAS,cAAmB;;;UCpExC;EACf;;;;UCJe;EACf,cAAc,SAAS,gBAAgB;EACvC,eAAe,SAAS,gBAAgB"}