@astroscope/node 1.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +286 -0
  3. package/dist/boot.d.ts +15 -0
  4. package/dist/boot.d.ts.map +1 -0
  5. package/dist/boot.js +2 -0
  6. package/dist/compress-B11aeRHQ.js +58 -0
  7. package/dist/compress-B11aeRHQ.js.map +1 -0
  8. package/dist/construct-DgB-jR0a.d.ts +14 -0
  9. package/dist/construct-DgB-jR0a.d.ts.map +1 -0
  10. package/dist/context-Bkg-FnnQ.js +20 -0
  11. package/dist/context-Bkg-FnnQ.js.map +1 -0
  12. package/dist/csrf-middleware-entrypoint.d.ts +5 -0
  13. package/dist/csrf-middleware-entrypoint.d.ts.map +1 -0
  14. package/dist/csrf-middleware-entrypoint.js +30 -0
  15. package/dist/csrf-middleware-entrypoint.js.map +1 -0
  16. package/dist/dev-middleware-entrypoint.d.ts +15 -0
  17. package/dist/dev-middleware-entrypoint.d.ts.map +1 -0
  18. package/dist/dev-middleware-entrypoint.js +26 -0
  19. package/dist/dev-middleware-entrypoint.js.map +1 -0
  20. package/dist/events-CUzQ2_cp.d.ts +20 -0
  21. package/dist/events-CUzQ2_cp.d.ts.map +1 -0
  22. package/dist/events-CgoM3Fvu.js +39 -0
  23. package/dist/events-CgoM3Fvu.js.map +1 -0
  24. package/dist/excludes-DLF3A_Cf.d.ts +63 -0
  25. package/dist/excludes-DLF3A_Cf.d.ts.map +1 -0
  26. package/dist/excludes-pE23EbmQ.js +78 -0
  27. package/dist/excludes-pE23EbmQ.js.map +1 -0
  28. package/dist/excludes.d.ts +2 -0
  29. package/dist/excludes.js +2 -0
  30. package/dist/generation-Bp2IA0jf.js +45 -0
  31. package/dist/generation-Bp2IA0jf.js.map +1 -0
  32. package/dist/health.d.ts +38 -0
  33. package/dist/health.d.ts.map +1 -0
  34. package/dist/health.js +31 -0
  35. package/dist/health.js.map +1 -0
  36. package/dist/index.d.ts +167 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +732 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/lifecycle/events.d.ts +2 -0
  41. package/dist/lifecycle/events.js +2 -0
  42. package/dist/log/index.d.ts +55 -0
  43. package/dist/log/index.d.ts.map +1 -0
  44. package/dist/log/index.js +2 -0
  45. package/dist/log-CSJlKaxY.js +112 -0
  46. package/dist/log-CSJlKaxY.js.map +1 -0
  47. package/dist/mime-C_GwZovh.js +39 -0
  48. package/dist/mime-C_GwZovh.js.map +1 -0
  49. package/dist/native-mount-hhwWdLtL.js +120 -0
  50. package/dist/native-mount-hhwWdLtL.js.map +1 -0
  51. package/dist/native.d.ts +48 -0
  52. package/dist/native.d.ts.map +1 -0
  53. package/dist/native.js +2 -0
  54. package/dist/prepare-Be2GgRyf.js +390 -0
  55. package/dist/prepare-Be2GgRyf.js.map +1 -0
  56. package/dist/preview.d.ts +12 -0
  57. package/dist/preview.d.ts.map +1 -0
  58. package/dist/preview.js +23 -0
  59. package/dist/preview.js.map +1 -0
  60. package/dist/restart-page.html +107 -0
  61. package/dist/route-middleware-entrypoint.d.ts +13 -0
  62. package/dist/route-middleware-entrypoint.d.ts.map +1 -0
  63. package/dist/route-middleware-entrypoint.js +24 -0
  64. package/dist/route-middleware-entrypoint.js.map +1 -0
  65. package/dist/server.d.ts +14 -0
  66. package/dist/server.d.ts.map +1 -0
  67. package/dist/server.js +441 -0
  68. package/dist/server.js.map +1 -0
  69. package/dist/store-8pnTxM1x.js +37 -0
  70. package/dist/store-8pnTxM1x.js.map +1 -0
  71. package/dist/store-BIUF4lqk.js +29 -0
  72. package/dist/store-BIUF4lqk.js.map +1 -0
  73. package/dist/types-D0uMBi2M.d.ts +12 -0
  74. package/dist/types-D0uMBi2M.d.ts.map +1 -0
  75. package/package.json +114 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 entwico GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,286 @@
1
+ # @astroscope/node
2
+
3
+ Opinionated, cloud-friendly Node adapter for Astro: boot lifecycle, health probes, request logging, telemetry, CSRF and static serving run as plain code around `server.listen()`.
4
+
5
+ ## What it does
6
+
7
+ - **Boot lifecycle** — `onStartup` runs before the port opens, `onShutdown` after in-flight requests drain (`src/boot.ts`)
8
+ - **Warmup** — pages, middleware and actions are loaded in parallel with startup and awaited before `listen()`
9
+ - **Health probes** — Kubernetes-style liveness/readiness on a separate port, with registrable checks
10
+ - **Request logging** — pino at the native handler: real status code, response size, ttfb, aborted connections, static responses included
11
+ - **Telemetry** — OpenTelemetry NodeSDK, undici fetch instrumentation, runtime + host metrics, Prometheus reader
12
+ - **CSRF protection** — origin check for unsafe methods, with path exclusions
13
+ - **Platform entry files** — env loading → `src/config.ts` → `src/instrumentation.ts` → `src/log.ts` → boot, each picked up automatically when the file exists
14
+ - **Pre-compressed static serving** — build-time brotli/gzip variants, negotiated per `Accept-Encoding`
15
+ - **Native mounts** — http-native handlers (`oidc-provider`, ACME) mounted on the adapter's server
16
+ - **Build tweaks** — SSR sourcemaps, SSR effect stripping
17
+ - **Dev restart machinery** — changes to the boot file or entry seams restart the dev server behind a holding page
18
+
19
+ ## What it does NOT do — beware
20
+
21
+ The adapter assumes a container behind a load balancer / reverse proxy (Kubernetes, Docker + ingress). Outside that setup, several defaults are wrong for you:
22
+
23
+ - **No TLS.** Plain HTTP only — terminate TLS at the ingress.
24
+ - **Opens `0.0.0.0:9090` in production** — the health probe server. Meant for the kubelet; do not expose it publicly.
25
+ - **Opens `0.0.0.0:9464` in production** — the Prometheus metrics reader. Same: cluster-internal only.
26
+ - **Trusts any `Host` / `X-Forwarded-Host`** — sets `security.allowedDomains: [{}]` (unless you set it yourself), because the reverse proxy is expected to control these headers. Without one, host header injection is possible.
27
+ - **Overrides Astro security and config defaults** — `security.checkOrigin: false` (the embedded CSRF middleware replaces it; `csrf: false` restores Astro's check), `build.redirects: false` (redirects handled at runtime), `trailingSlash: 'never'` (only when yours is at the default `'ignore'`).
28
+ - **Standalone only.** No middleware mode — the adapter always owns the server.
29
+ - **No session driver.** Astro sessions are unsupported unless you configure `session.driver` yourself.
30
+ - **No health probes in dev.** The health server only exists in production and `astro preview`.
31
+
32
+ ## Usage
33
+
34
+ ```typescript
35
+ // astro.config.ts
36
+ import node from '@astroscope/node';
37
+ import { defineConfig } from 'astro/config';
38
+
39
+ export default defineConfig({
40
+ output: 'server',
41
+ adapter: node(),
42
+ });
43
+ ```
44
+
45
+ ```typescript
46
+ // src/boot.ts — picked up automatically (or src/boot/index.ts)
47
+ import type { BootContext } from '@astroscope/node';
48
+
49
+ export async function onStartup(context: BootContext) {
50
+ // connect to databases, warm caches, schedule timers
51
+ }
52
+
53
+ export async function onShutdown() {
54
+ // close connections, clear timers
55
+ }
56
+ ```
57
+
58
+ `astro preview` runs the full production path (boot, warmup, health, static serving).
59
+
60
+ In dev, the boot file and the entry files below run through the adapter's own watch/restart machinery: changes to the boot file, its dependency graph, or the config/log files restart the dev server behind a holding page that releases when the new generation is ready.
61
+
62
+ ### Boot context access
63
+
64
+ Anywhere in server code (including libraries), `getBootContext()` returns the running server's `BootContext` — or `undefined` when no @astroscope/node server has booted in the process (unit tests, one-off scripts). Libraries can use it to adapt to dev mode without making consumers thread the flag through:
65
+
66
+ ```typescript
67
+ import { getBootContext } from '@astroscope/node/boot';
68
+
69
+ const dev = getBootContext()?.dev ?? false;
70
+ ```
71
+
72
+ It is stamped before the platform entry files and the boot lifecycle run, in both prod and dev (re-stamped per dev restart generation).
73
+
74
+ ## Platform entry files
75
+
76
+ Besides `src/boot.ts`, three more files are picked up automatically when they exist. In production they run in this order inside `startServer()`, before anything else; in dev they re-run per restart generation (instrumentation only once per process):
77
+
78
+ ```
79
+ env loading → src/config.ts → src/instrumentation.ts → src/log.ts → boot → warmup → listen
80
+ ```
81
+
82
+ **Env loading** (platform, position −1): `CONFIG_PATH` env var → `./.env` → none. Existing process env vars win; each outcome is logged.
83
+
84
+ **`src/config.ts`** — schema + load (e.g. @entwico/zod-conf), nothing else; validation runs at import. On failure: in production the buffered logs are dumped to the console with the error and the process exits 1; in dev the boot gate keeps the holding page up with the error.
85
+
86
+ ```typescript
87
+ // src/config.ts
88
+ import { conf } from '@entwico/zod-conf';
89
+ import { z } from 'zod';
90
+
91
+ export const config = conf(z.object({ MONGO_URL: z.string() }));
92
+ ```
93
+
94
+ **`src/instrumentation.ts`** — extra instrumentation only (the standard bundle is a platform default, see Telemetry). Loaded once per process — dev restarts are in-process, so changes here need a full dev-server restart.
95
+
96
+ ```typescript
97
+ // src/instrumentation.ts
98
+ import type { InstrumentationContext } from '@astroscope/node';
99
+
100
+ export function register(ctx: InstrumentationContext) {
101
+ // register additional instrumentations, processors, exporters
102
+ }
103
+ ```
104
+
105
+ **`src/log.ts`** — pino logger *options* (or a factory), never a logger instance. The platform constructs the logger itself, after instrumentation, and adds a mixin that stamps `trace_id`/`span_id`/`trace_flags` onto every entry when a span is active.
106
+
107
+ ```typescript
108
+ // src/log.ts
109
+ import type { LoggerOptionsFactory } from '@astroscope/node/log';
110
+
111
+ const factory: LoggerOptionsFactory = ({ dev }) => ({
112
+ level: dev ? 'debug' : 'info',
113
+ });
114
+
115
+ export default factory;
116
+ ```
117
+
118
+ ## Logging
119
+
120
+ The `log` proxy is request-aware: inside a request its entries carry the request bindings (`reqId`, `req`), outside they go to the root logger.
121
+
122
+ ```typescript
123
+ import { log } from '@astroscope/node/log';
124
+
125
+ log.info('handling request');
126
+ log.info({ userId: 123 }, 'user logged in');
127
+ log.error(err, 'operation failed');
128
+
129
+ const dbLog = log.child({ component: 'db' });
130
+ ```
131
+
132
+ Entries logged before the logger is constructed (env loading, config, instrumentation) are buffered (cap 100) and replayed once construction completes — the original timestamp is kept as a `bufferedTime` field. If startup dies before the logger exists, the buffer is dumped to the console together with the error.
133
+
134
+ Request logging happens at the native handler on `finish`/`close`: real status code, response size, `ttfb`, aborted-vs-completed, and the route pattern (fed back by an internal astro middleware). An incoming `x-request-id` header is passed through (and echoed on the response); otherwise a short id is generated.
135
+
136
+ The adapter itself emits exactly one info line on startup — `server ready { host, port, health, bootMs, warmupMs, totalMs }` — and `draining` / `shutdown complete { drainMs }` on the way down. Everything else is debug or error level.
137
+
138
+ ## Telemetry
139
+
140
+ `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 }`).
141
+
142
+ - server spans start at the native handler with W3C context extraction; the route pattern and span name are enriched by the internal middleware
143
+ - request metrics: `http.server.request.duration`, `http.server.active_requests`, `astro.action.duration`; fetch metrics come from the undici instrumentation
144
+ - the boot lifecycle gets `startup` (with `boot`/`warmup`/`listen` children) and `shutdown` (with `drain`/`onShutdown`) spans
145
+ - trace exporters are driven by the standard `OTEL_*` env vars; without a configured OTLP endpoint, trace exporting defaults to `none` (no failing localhost exports)
146
+ - `OTEL_SDK_DISABLED=true` turns the SDK off entirely
147
+
148
+ 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.
149
+
150
+ ## Health checks
151
+
152
+ Register checks from anywhere in server code — typically `onStartup`:
153
+
154
+ ```typescript
155
+ import { registerHealthCheck } from '@astroscope/node/health';
156
+
157
+ export async function onStartup() {
158
+ const db = await connectMongo();
159
+
160
+ registerHealthCheck({ name: 'mongo', check: () => db.ping() });
161
+ }
162
+ ```
163
+
164
+ - probes flip in order: live during startup, ready only after `listen()`, draining begins the moment a shutdown signal arrives
165
+ - a check fails by throwing or returning `{ status: 'unhealthy' }`; failing required checks turn `readyz` unhealthy (`optional: true` checks don't affect it, `timeout` defaults to 5s)
166
+ - `registerHealthCheck` returns an unregister function, but cleanup is optional: checks still registered after `onShutdown` are removed automatically
167
+ - when no health runtime is active (dev mode, `health: false`), registration is a no-op — no dev/prod branching needed in boot files
168
+
169
+ ## Native mounts
170
+
171
+ For Node libraries that need the real `(req, res)` — `oidc-provider`'s `callback()`, ACME challenge handlers — mount them on the adapter's server instead of faking Node objects inside astro middleware:
172
+
173
+ ```typescript
174
+ // src/boot.ts
175
+ import { mountNativeHandler } from '@astroscope/node/native';
176
+
177
+ export function onStartup() {
178
+ mountNativeHandler({ prefix: '/oidc', name: 'oidc' }, getOidcProvider().callback());
179
+ }
180
+ ```
181
+
182
+ - the handler owns the response completely: matched requests are dispatched before static serving and never reach astro middleware or rendering — machine endpoints (`/token`, `/introspection`) need no csrf exclusions because they never meet the csrf middleware
183
+ - the handler gets the real request and response: streaming bodies, `socket.remoteAddress`, set-cookie arrays
184
+ - requests stay inside request logging and tracing; `name` becomes the route label for logs, metrics and span names
185
+ - identical in dev — the same registry is dispatched at the connect level, so there is no fake-host-header or dev/prod drift
186
+ - matching: `prefix` (segment-aware, longest wins) or a `match(req)` predicate consulted after prefixes, in registration order
187
+ - returns an unregister function; mounts are cleared automatically after `onShutdown` (and between dev restart generations)
188
+ - handler errors are logged and answered with a 500 (unless the handler already sent headers)
189
+
190
+ ## Exclude patterns
191
+
192
+ `@astroscope/node/excludes` ships the shared exclude-pattern vocabulary for middlewares. Matching itself comes from [`@entwico/dash/match`](https://github.com/entwico/dash); `ExcludePattern` is the serializable subset of dash's `StringPattern` (`exact` / `prefix` / `suffix` / `includes` / `pattern` — no matcher functions, since adapter options cross a virtual-module boundary).
193
+
194
+ ```typescript
195
+ import { RECOMMENDED_EXCLUDES, shouldExclude, withExcluded } from '@astroscope/node/excludes';
196
+
197
+ // pre-defined sets: DEV_EXCLUDES (vite dev paths), ASTRO_STATIC_EXCLUDES (/_astro/, /_image),
198
+ // STATIC_EXCLUDES (favicon, robots.txt, ...), RECOMMENDED_EXCLUDES (dev + astro internals)
199
+
200
+ // in your own middleware
201
+ if (shouldExclude(ctx, [...RECOMMENDED_EXCLUDES, { exact: '/health' }])) return next();
202
+
203
+ // or wrap a third-party middleware without built-in exclude support
204
+ export const onRequest = withExcluded(someExternalMiddleware(), RECOMMENDED_EXCLUDES);
205
+ ```
206
+
207
+ For hot paths, compile the set once with `createMatcher` from `@entwico/dash/match` instead of scanning per request (the adapter's own request instrumentation does exactly that).
208
+
209
+ ## Pre-compressed static serving
210
+
211
+ At build time, every compressible file in `dist/client` gets max-quality `.br` (brotli 11) and `.gz` (gzip 9) variants written next to it — variants that don't shrink the file are skipped. At request time the static handler negotiates `Accept-Encoding` and serves the best variant with the original's content-type, `content-encoding`, `vary: accept-encoding` and per-variant etags (304s included). Behind a caching proxy with per-encoding cache keys, the origin serves each asset once per encoding.
212
+
213
+ ## Embedded build tweaks
214
+
215
+ Always on, no configuration:
216
+
217
+ - **SSR sourcemaps** — the server bundle gets sourcemaps for readable stack traces; client bundles stay unmapped so browsers can't fetch source
218
+ - **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
219
+
220
+ ## Options
221
+
222
+ ```typescript
223
+ node({
224
+ // boot lifecycle; false disables it. Skipped automatically when no boot file exists.
225
+ boot: {
226
+ entry: 'src/boot.ts', // default: src/boot.ts or src/boot/index.ts
227
+ watch: true, // dev: restart the dev server on boot-dependency changes
228
+ },
229
+
230
+ // Kubernetes-style probes on a separate port. Enabled by default in
231
+ // production (never active in dev); false disables.
232
+ health: {
233
+ host: '0.0.0.0', // falls back to HEALTH_HOST env, then 0.0.0.0 (kubelet probes hit the pod IP)
234
+ port: 9090, // falls back to HEALTH_PORT env, then 9090
235
+ },
236
+
237
+ // CSRF protection (origin check for POST/PUT/PATCH/DELETE with exclusions).
238
+ // Enabled by default; false keeps astro's built-in checkOrigin instead.
239
+ csrf: {
240
+ exclude: [{ exact: '/api/auth/backchannel-logout' }],
241
+ },
242
+
243
+ // request logging at the native handler. Enabled by default in production;
244
+ // false disables it (the log proxy keeps working).
245
+ logging: {
246
+ exclude: [{ prefix: '/internal/' }], // replaces RECOMMENDED_EXCLUDES + STATIC_EXCLUDES
247
+ extended: false, // query/headers/client address (may capture sensitive data)
248
+ dev: false, // also log requests in dev (astro narrates them already)
249
+ },
250
+
251
+ // platform telemetry. Enabled by default in production, off in dev; false disables.
252
+ telemetry: {
253
+ exclude: [{ prefix: '/internal/' }], // replaces RECOMMENDED_EXCLUDES + STATIC_EXCLUDES
254
+ prometheus: { host: '0.0.0.0', port: 9464 }, // false disables the reader
255
+ dev: false, // start the SDK in dev too (once per process)
256
+ },
257
+
258
+ bodySizeLimit: 1024 * 1024 * 1024, // request body limit in bytes
259
+ shutdownTimeout: 10_000, // ms to wait for in-flight requests on shutdown
260
+ });
261
+ ```
262
+
263
+ ## Environment variables
264
+
265
+ | Variable | Effect |
266
+ |----------|--------|
267
+ | `HOST` / `PORT` | Override the listen address at runtime |
268
+ | `HEALTH_HOST` / `HEALTH_PORT` | Override the health probe address (when not set in options) |
269
+ | `CONFIG_PATH` | Env file to load at startup (falls back to `./.env`) |
270
+ | `OTEL_EXPORTER_PROMETHEUS_HOST` / `OTEL_EXPORTER_PROMETHEUS_PORT` | Override the Prometheus reader address |
271
+ | `OTEL_SDK_DISABLED=true` | Disable the telemetry SDK entirely |
272
+ | standard `OTEL_*` | Exporter/resource configuration (e.g. `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME`) |
273
+ | `ASTROSCOPE_NODE_AUTOSTART=disabled` | Build the entry without starting the server (exports `startServer()`) |
274
+
275
+ ## Shutdown sequence
276
+
277
+ On SIGTERM/SIGINT:
278
+
279
+ 1. readiness probe starts failing, `draining` is logged
280
+ 2. the server stops accepting connections and waits up to `shutdownTimeout` for in-flight requests
281
+ 3. `onShutdown` runs
282
+ 4. `shutdown complete { drainMs }` is logged, the health server stops, telemetry flushes, the process exits with code 0
283
+
284
+ ## License
285
+
286
+ MIT
package/dist/boot.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { t as BootContext } from "./types-D0uMBi2M.js";
2
+
3
+ //#region src/lifecycle/context.d.ts
4
+ /**
5
+ * The boot context of the server running in this process, or `undefined` when
6
+ * no @astroscope/node server has booted (unit tests, one-off scripts).
7
+ *
8
+ * Available from the platform seams (`src/config.ts` and later) onwards;
9
+ * libraries can use it to adapt behavior to dev mode without requiring
10
+ * consumers to thread the flag through.
11
+ */
12
+ declare function getBootContext(): BootContext | undefined;
13
+ //#endregion
14
+ export { type BootContext, getBootContext };
15
+ //# sourceMappingURL=boot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot.d.ts","names":[],"sources":["../src/lifecycle/context.ts"],"mappings":";;;;AAiBA;;;;AAA6C;;;iBAA7B,cAAA,IAAkB,WAAW"}
package/dist/boot.js ADDED
@@ -0,0 +1,2 @@
1
+ import { t as getBootContext } from "./context-Bkg-FnnQ.js";
2
+ export { getBootContext };
@@ -0,0 +1,58 @@
1
+ import { t as COMPRESSIBLE } from "./mime-C_GwZovh.js";
2
+ import path from "node:path";
3
+ import { readFile, readdir, writeFile } from "node:fs/promises";
4
+ import { promisify } from "node:util";
5
+ import { brotliCompress, constants, gzip } from "node:zlib";
6
+ //#region src/compress/compress.ts
7
+ const brotli = promisify(brotliCompress);
8
+ const gzip$1 = promisify(gzip);
9
+ /**
10
+ * Pre-compress every compressible file in the client build dir at maximum
11
+ * quality, writing `.br`/`.gz` variants next to the originals. Variants that
12
+ * don't shrink the file are skipped. The static handler negotiates
13
+ * `Accept-Encoding` against these at request time.
14
+ */
15
+ async function compressClientDir(clientDir, logger) {
16
+ const files = await walkDir(clientDir);
17
+ let compressed = 0;
18
+ let savedBytes = 0;
19
+ let totalRaw = 0;
20
+ await Promise.all(files.map(async (filePath) => {
21
+ if (!COMPRESSIBLE.has(path.extname(filePath))) return;
22
+ const raw = await readFile(filePath);
23
+ if (!raw.byteLength) return;
24
+ const [brBuf, gzBuf] = await Promise.all([brotli(raw, { params: { [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY } }), gzip$1(raw, { level: 9 })]);
25
+ if (brBuf.byteLength < raw.byteLength) await writeFile(`${filePath}.br`, brBuf);
26
+ if (gzBuf.byteLength < raw.byteLength) await writeFile(`${filePath}.gz`, gzBuf);
27
+ compressed++;
28
+ totalRaw += raw.byteLength;
29
+ const best = Math.min(brBuf.byteLength < raw.byteLength ? brBuf.byteLength : raw.byteLength, gzBuf.byteLength < raw.byteLength ? gzBuf.byteLength : raw.byteLength);
30
+ savedBytes += raw.byteLength - best;
31
+ }));
32
+ const savedPercent = totalRaw > 0 ? Math.round(savedBytes / totalRaw * 100) : 0;
33
+ logger.info(`${compressed} files compressed, saved up to ${formatBytes(savedBytes)} (${savedPercent}%)`);
34
+ }
35
+ async function walkDir(dir) {
36
+ return (await readdir(dir, {
37
+ withFileTypes: true,
38
+ recursive: true
39
+ })).filter((e) => e.isFile()).map((e) => path.join(e.parentPath, e.name));
40
+ }
41
+ const UNITS = [
42
+ "B",
43
+ "KB",
44
+ "MB",
45
+ "GB"
46
+ ];
47
+ function formatBytes(bytes) {
48
+ let value = bytes;
49
+ for (const unit of UNITS) {
50
+ if (value < 1024 || unit === UNITS[UNITS.length - 1]) return unit === "B" ? `${value} ${unit}` : `${value.toFixed(1)} ${unit}`;
51
+ value /= 1024;
52
+ }
53
+ return `${value.toFixed(1)} GB`;
54
+ }
55
+ //#endregion
56
+ export { compressClientDir };
57
+
58
+ //# sourceMappingURL=compress-B11aeRHQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compress-B11aeRHQ.js","names":["gzip","_gzip"],"sources":["../src/compress/compress.ts"],"sourcesContent":["import { readFile, readdir, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\nimport { gzip as _gzip, brotliCompress, constants } from 'node:zlib';\nimport type { AstroIntegrationLogger } from 'astro';\nimport { COMPRESSIBLE } from '../server/mime.js';\n\nconst brotli = promisify(brotliCompress);\nconst gzip = promisify(_gzip);\n\n/**\n * Pre-compress every compressible file in the client build dir at maximum\n * quality, writing `.br`/`.gz` variants next to the originals. Variants that\n * don't shrink the file are skipped. The static handler negotiates\n * `Accept-Encoding` against these at request time.\n */\nexport async function compressClientDir(clientDir: string, logger: AstroIntegrationLogger): Promise<void> {\n const files = await walkDir(clientDir);\n\n let compressed = 0;\n let savedBytes = 0;\n let totalRaw = 0;\n\n await Promise.all(\n files.map(async (filePath) => {\n if (!COMPRESSIBLE.has(path.extname(filePath))) return;\n\n const raw = await readFile(filePath);\n\n if (!raw.byteLength) return;\n\n const [brBuf, gzBuf] = await Promise.all([\n brotli(raw, { params: { [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY } }),\n gzip(raw, { level: 9 }),\n ]);\n\n if (brBuf.byteLength < raw.byteLength) {\n await writeFile(`${filePath}.br`, brBuf);\n }\n\n if (gzBuf.byteLength < raw.byteLength) {\n await writeFile(`${filePath}.gz`, gzBuf);\n }\n\n compressed++;\n totalRaw += raw.byteLength;\n\n const best = Math.min(\n brBuf.byteLength < raw.byteLength ? brBuf.byteLength : raw.byteLength,\n gzBuf.byteLength < raw.byteLength ? gzBuf.byteLength : raw.byteLength,\n );\n\n savedBytes += raw.byteLength - best;\n }),\n );\n\n const savedPercent = totalRaw > 0 ? Math.round((savedBytes / totalRaw) * 100) : 0;\n\n logger.info(`${compressed} files compressed, saved up to ${formatBytes(savedBytes)} (${savedPercent}%)`);\n}\n\nasync function walkDir(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true, recursive: true });\n\n return entries.filter((e) => e.isFile()).map((e) => path.join(e.parentPath, e.name));\n}\n\nconst UNITS = ['B', 'KB', 'MB', 'GB'];\n\nfunction formatBytes(bytes: number): string {\n let value = bytes;\n\n for (const unit of UNITS) {\n if (value < 1024 || unit === UNITS[UNITS.length - 1]) {\n return unit === 'B' ? `${value} ${unit}` : `${value.toFixed(1)} ${unit}`;\n }\n\n value /= 1024;\n }\n\n return `${value.toFixed(1)} GB`;\n}\n"],"mappings":";;;;;;AAOA,MAAM,SAAS,UAAU,cAAc;AACvC,MAAMA,SAAO,UAAUC,IAAK;;;;;;;AAQ5B,eAAsB,kBAAkB,WAAmB,QAA+C;CACxG,MAAM,QAAQ,MAAM,QAAQ,SAAS;CAErC,IAAI,aAAa;CACjB,IAAI,aAAa;CACjB,IAAI,WAAW;CAEf,MAAM,QAAQ,IACZ,MAAM,IAAI,OAAO,aAAa;EAC5B,IAAI,CAAC,aAAa,IAAI,KAAK,QAAQ,QAAQ,CAAC,GAAG;EAE/C,MAAM,MAAM,MAAM,SAAS,QAAQ;EAEnC,IAAI,CAAC,IAAI,YAAY;EAErB,MAAM,CAAC,OAAO,SAAS,MAAM,QAAQ,IAAI,CACvC,OAAO,KAAK,EAAE,QAAQ,GAAG,UAAU,uBAAuB,UAAU,mBAAmB,EAAE,CAAC,GAC1FD,OAAK,KAAK,EAAE,OAAO,EAAE,CAAC,CACxB,CAAC;EAED,IAAI,MAAM,aAAa,IAAI,YACzB,MAAM,UAAU,GAAG,SAAS,MAAM,KAAK;EAGzC,IAAI,MAAM,aAAa,IAAI,YACzB,MAAM,UAAU,GAAG,SAAS,MAAM,KAAK;EAGzC;EACA,YAAY,IAAI;EAEhB,MAAM,OAAO,KAAK,IAChB,MAAM,aAAa,IAAI,aAAa,MAAM,aAAa,IAAI,YAC3D,MAAM,aAAa,IAAI,aAAa,MAAM,aAAa,IAAI,UAC7D;EAEA,cAAc,IAAI,aAAa;CACjC,CAAC,CACH;CAEA,MAAM,eAAe,WAAW,IAAI,KAAK,MAAO,aAAa,WAAY,GAAG,IAAI;CAEhF,OAAO,KAAK,GAAG,WAAW,iCAAiC,YAAY,UAAU,EAAE,IAAI,aAAa,GAAG;AACzG;AAEA,eAAe,QAAQ,KAAgC;CAGrD,QAAO,MAFe,QAAQ,KAAK;EAAE,eAAe;EAAM,WAAW;CAAK,CAAC,EAAA,CAE5D,QAAQ,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,KAAK,KAAK,EAAE,YAAY,EAAE,IAAI,CAAC;AACrF;AAEA,MAAM,QAAQ;CAAC;CAAK;CAAM;CAAM;AAAI;AAEpC,SAAS,YAAY,OAAuB;CAC1C,IAAI,QAAQ;CAEZ,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,QAAQ,QAAQ,SAAS,MAAM,MAAM,SAAS,IAChD,OAAO,SAAS,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,QAAQ,CAAC,EAAE,GAAG;EAGpE,SAAS;CACX;CAEA,OAAO,GAAG,MAAM,QAAQ,CAAC,EAAE;AAC7B"}
@@ -0,0 +1,14 @@
1
+ import { Logger, LoggerOptions } from "pino";
2
+
3
+ //#region src/observability/log/construct.d.ts
4
+ /**
5
+ * Contract of the `src/log.ts` entry seam: pino logger options, or a factory
6
+ * producing them. Never a logger instance — the platform constructs the
7
+ * logger itself (after instrumentation, so trace correlation works).
8
+ */
9
+ type LoggerOptionsFactory = LoggerOptions | ((ctx: {
10
+ dev: boolean;
11
+ }) => LoggerOptions | Promise<LoggerOptions>);
12
+ //#endregion
13
+ export { LoggerOptionsFactory as t };
14
+ //# sourceMappingURL=construct-DgB-jR0a.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"construct-DgB-jR0a.d.ts","names":[],"sources":["../src/observability/log/construct.ts"],"mappings":";;;;;AASA;;;KAAY,oBAAA,GAAuB,aAAA,KAAkB,GAAA;EAAO,GAAA;AAAA,MAAmB,aAAA,GAAgB,OAAA,CAAQ,aAAA"}
@@ -0,0 +1,20 @@
1
+ //#region src/lifecycle/context.ts
2
+ const STORE_KEY = Symbol.for("@astroscope/node/boot-context");
3
+ function setBootContext(context) {
4
+ globalThis[STORE_KEY] = context;
5
+ }
6
+ /**
7
+ * The boot context of the server running in this process, or `undefined` when
8
+ * no @astroscope/node server has booted (unit tests, one-off scripts).
9
+ *
10
+ * Available from the platform seams (`src/config.ts` and later) onwards;
11
+ * libraries can use it to adapt behavior to dev mode without requiring
12
+ * consumers to thread the flag through.
13
+ */
14
+ function getBootContext() {
15
+ return globalThis[STORE_KEY];
16
+ }
17
+ //#endregion
18
+ export { setBootContext as n, getBootContext as t };
19
+
20
+ //# sourceMappingURL=context-Bkg-FnnQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-Bkg-FnnQ.js","names":[],"sources":["../src/lifecycle/context.ts"],"sourcesContent":["import type { BootContext } from './types.js';\n\n// keyed on globalThis via Symbol.for so the vite-runner and native module instances share it\nconst STORE_KEY = Symbol.for('@astroscope/node/boot-context');\n\nexport function setBootContext(context: BootContext): void {\n (globalThis as Record<symbol, BootContext | undefined>)[STORE_KEY] = context;\n}\n\n/**\n * The boot context of the server running in this process, or `undefined` when\n * no @astroscope/node server has booted (unit tests, one-off scripts).\n *\n * Available from the platform seams (`src/config.ts` and later) onwards;\n * libraries can use it to adapt behavior to dev mode without requiring\n * consumers to thread the flag through.\n */\nexport function getBootContext(): BootContext | undefined {\n return (globalThis as Record<symbol, BootContext | undefined>)[STORE_KEY];\n}\n"],"mappings":";AAGA,MAAM,YAAY,OAAO,IAAI,+BAA+B;AAE5D,SAAgB,eAAe,SAA4B;CACzD,WAAwD,aAAa;AACvE;;;;;;;;;AAUA,SAAgB,iBAA0C;CACxD,OAAQ,WAAuD;AACjE"}
@@ -0,0 +1,5 @@
1
+ //#region src/csrf/middleware-entrypoint.d.ts
2
+ declare const onRequest: import("astro").MiddlewareHandler;
3
+ //#endregion
4
+ export { onRequest };
5
+ //# sourceMappingURL=csrf-middleware-entrypoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf-middleware-entrypoint.d.ts","names":[],"sources":["../src/csrf/middleware-entrypoint.ts"],"mappings":";cAKa,SAAA,kBAAS,iBAA4D"}
@@ -0,0 +1,30 @@
1
+ import { a as shouldExclude } from "./excludes-pE23EbmQ.js";
2
+ import { excludePatterns } from "virtual:@astroscope/node/csrf";
3
+ //#region src/csrf/middleware.ts
4
+ const FORBIDDEN_METHODS = /* @__PURE__ */ new Set([
5
+ "POST",
6
+ "PUT",
7
+ "PATCH",
8
+ "DELETE"
9
+ ]);
10
+ /**
11
+ * CSRF protection middleware: compares the request's Origin header against
12
+ * `context.url.origin` for state-changing methods. Replaces Astro's built-in
13
+ * `security.checkOrigin`, adding path exclusions.
14
+ */
15
+ function createCsrfMiddleware(exclude) {
16
+ return (context, next) => {
17
+ if (shouldExclude(context, exclude)) return next();
18
+ if (!FORBIDDEN_METHODS.has(context.request.method)) return next();
19
+ const origin = context.request.headers.get("origin");
20
+ if (!origin || origin !== context.url.origin) return new Response(`Cross-site ${context.request.method} request forbidden`, { status: 403 });
21
+ return next();
22
+ };
23
+ }
24
+ //#endregion
25
+ //#region src/csrf/middleware-entrypoint.ts
26
+ const onRequest = createCsrfMiddleware(excludePatterns);
27
+ //#endregion
28
+ export { onRequest };
29
+
30
+ //# sourceMappingURL=csrf-middleware-entrypoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf-middleware-entrypoint.js","names":[],"sources":["../src/csrf/middleware.ts","../src/csrf/middleware-entrypoint.ts"],"sourcesContent":["import type { MiddlewareHandler } from 'astro';\nimport { type ExcludePattern, shouldExclude } from '../excludes/excludes.js';\n\nconst FORBIDDEN_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);\n\n/**\n * CSRF protection middleware: compares the request's Origin header against\n * `context.url.origin` for state-changing methods. Replaces Astro's built-in\n * `security.checkOrigin`, adding path exclusions.\n */\nexport function createCsrfMiddleware(exclude: ExcludePattern[]): MiddlewareHandler {\n return (context, next) => {\n if (shouldExclude(context, exclude)) {\n return next();\n }\n\n if (!FORBIDDEN_METHODS.has(context.request.method)) {\n return next();\n }\n\n const origin = context.request.headers.get('origin');\n\n if (!origin || origin !== context.url.origin) {\n return new Response(`Cross-site ${context.request.method} request forbidden`, { status: 403 });\n }\n\n return next();\n };\n}\n","// @ts-expect-error virtual module provided by the integration\nimport { excludePatterns } from 'virtual:@astroscope/node/csrf';\nimport type { ExcludePattern } from '../excludes/excludes.js';\nimport { createCsrfMiddleware } from './middleware.js';\n\nexport const onRequest = createCsrfMiddleware(excludePatterns as ExcludePattern[]);\n"],"mappings":";;;AAGA,MAAM,oCAAoB,IAAI,IAAI;CAAC;CAAQ;CAAO;CAAS;AAAQ,CAAC;;;;;;AAOpE,SAAgB,qBAAqB,SAA8C;CACjF,QAAQ,SAAS,SAAS;EACxB,IAAI,cAAc,SAAS,OAAO,GAChC,OAAO,KAAK;EAGd,IAAI,CAAC,kBAAkB,IAAI,QAAQ,QAAQ,MAAM,GAC/C,OAAO,KAAK;EAGd,MAAM,SAAS,QAAQ,QAAQ,QAAQ,IAAI,QAAQ;EAEnD,IAAI,CAAC,UAAU,WAAW,QAAQ,IAAI,QACpC,OAAO,IAAI,SAAS,cAAc,QAAQ,QAAQ,OAAO,qBAAqB,EAAE,QAAQ,IAAI,CAAC;EAG/F,OAAO,KAAK;CACd;AACF;;;ACvBA,MAAa,YAAY,qBAAqB,eAAmC"}
@@ -0,0 +1,15 @@
1
+ import { MiddlewareHandler } from "astro";
2
+
3
+ //#region src/dev-mode/middleware-entrypoint.d.ts
4
+ /**
5
+ * Wrap every request so errors thrown by a stale render (one whose user
6
+ * singletons were torn down mid-render by a dev-server restart) are swallowed
7
+ * instead of polluting the logs. The request was stamped with the generation
8
+ * it entered under by the integration's connect middleware; if that doesn't
9
+ * match the current generation, the error is recorded for an aggregated log
10
+ * line and a 503 is returned (the client socket is usually gone too).
11
+ */
12
+ declare const onRequest: MiddlewareHandler;
13
+ //#endregion
14
+ export { onRequest };
15
+ //# sourceMappingURL=dev-middleware-entrypoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-middleware-entrypoint.d.ts","names":[],"sources":["../src/dev-mode/middleware-entrypoint.ts"],"mappings":";;;;;AAWA;;;;AAcC;;cAdY,SAAA,EAAW,iBAcvB"}
@@ -0,0 +1,26 @@
1
+ import { i as recordStaleError, n as getCurrentGeneration, t as GEN_HEADER } from "./generation-Bp2IA0jf.js";
2
+ //#region src/dev-mode/middleware-entrypoint.ts
3
+ /**
4
+ * Wrap every request so errors thrown by a stale render (one whose user
5
+ * singletons were torn down mid-render by a dev-server restart) are swallowed
6
+ * instead of polluting the logs. The request was stamped with the generation
7
+ * it entered under by the integration's connect middleware; if that doesn't
8
+ * match the current generation, the error is recorded for an aggregated log
9
+ * line and a 503 is returned (the client socket is usually gone too).
10
+ */
11
+ const onRequest = async (context, next) => {
12
+ const stamp = context.request.headers.get(GEN_HEADER);
13
+ try {
14
+ return await next();
15
+ } catch (err) {
16
+ if (stamp != null && Number(stamp) !== getCurrentGeneration()) {
17
+ recordStaleError();
18
+ return new Response(null, { status: 503 });
19
+ }
20
+ throw err;
21
+ }
22
+ };
23
+ //#endregion
24
+ export { onRequest };
25
+
26
+ //# sourceMappingURL=dev-middleware-entrypoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-middleware-entrypoint.js","names":[],"sources":["../src/dev-mode/middleware-entrypoint.ts"],"sourcesContent":["import type { MiddlewareHandler } from 'astro';\nimport { GEN_HEADER, getCurrentGeneration, recordStaleError } from './generation.js';\n\n/**\n * Wrap every request so errors thrown by a stale render (one whose user\n * singletons were torn down mid-render by a dev-server restart) are swallowed\n * instead of polluting the logs. The request was stamped with the generation\n * it entered under by the integration's connect middleware; if that doesn't\n * match the current generation, the error is recorded for an aggregated log\n * line and a 503 is returned (the client socket is usually gone too).\n */\nexport const onRequest: MiddlewareHandler = async (context, next) => {\n const stamp = context.request.headers.get(GEN_HEADER);\n\n try {\n return await next();\n } catch (err) {\n if (stamp != null && Number(stamp) !== getCurrentGeneration()) {\n recordStaleError();\n\n return new Response(null, { status: 503 });\n }\n\n throw err;\n }\n};\n"],"mappings":";;;;;;;;;;AAWA,MAAa,YAA+B,OAAO,SAAS,SAAS;CACnE,MAAM,QAAQ,QAAQ,QAAQ,QAAQ,IAAI,UAAU;CAEpD,IAAI;EACF,OAAO,MAAM,KAAK;CACpB,SAAS,KAAK;EACZ,IAAI,SAAS,QAAQ,OAAO,KAAK,MAAM,qBAAqB,GAAG;GAC7D,iBAAiB;GAEjB,OAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;EAC3C;EAEA,MAAM;CACR;AACF"}
@@ -0,0 +1,20 @@
1
+ import { t as BootContext } from "./types-D0uMBi2M.js";
2
+
3
+ //#region src/lifecycle/events.d.ts
4
+ type BootEventName = 'beforeOnStartup' | 'afterOnStartup' | 'beforeOnShutdown' | 'afterOnShutdown';
5
+ type BootEventHandler = (context: BootContext) => Promise<void> | void;
6
+ /**
7
+ * Register a handler for a boot lifecycle event.
8
+ */
9
+ declare function on(event: BootEventName, handler: BootEventHandler): void;
10
+ /**
11
+ * Remove a previously registered handler.
12
+ */
13
+ declare function off(event: BootEventName, handler: BootEventHandler): void;
14
+ /**
15
+ * Emit a boot lifecycle event, running all registered handlers sequentially.
16
+ */
17
+ declare function emit(event: BootEventName, context: BootContext): Promise<void>;
18
+ //#endregion
19
+ export { on as a, off as i, BootEventName as n, emit as r, BootEventHandler as t };
20
+ //# sourceMappingURL=events-CUzQ2_cp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-CUzQ2_cp.d.ts","names":[],"sources":["../src/lifecycle/events.ts"],"mappings":";;;KAEY,aAAA;AAAA,KAEA,gBAAA,IAAoB,OAAA,EAAS,WAAA,KAAgB,OAAO;AAFhE;;;AAAA,iBAyBgB,EAAA,CAAG,KAAA,EAAO,aAAA,EAAe,OAAA,EAAS,gBAAgB;AAzBzC;AAEzB;;AAFyB,iBAwCT,GAAA,CAAI,KAAA,EAAO,aAAA,EAAe,OAAA,EAAS,gBAAgB;;;;iBAS7C,IAAA,CAAK,KAAA,EAAO,aAAA,EAAe,OAAA,EAAS,WAAA,GAAc,OAAA"}
@@ -0,0 +1,39 @@
1
+ //#region src/lifecycle/events.ts
2
+ const STORE_KEY = Symbol.for("@astroscope/node/boot-events");
3
+ function getStore() {
4
+ const existing = globalThis[STORE_KEY];
5
+ if (existing) return existing;
6
+ const store = { listeners: /* @__PURE__ */ new Map() };
7
+ globalThis[STORE_KEY] = store;
8
+ return store;
9
+ }
10
+ /**
11
+ * Register a handler for a boot lifecycle event.
12
+ */
13
+ function on(event, handler) {
14
+ const store = getStore();
15
+ let handlers = store.listeners.get(event);
16
+ if (!handlers) {
17
+ handlers = /* @__PURE__ */ new Set();
18
+ store.listeners.set(event, handlers);
19
+ }
20
+ handlers.add(handler);
21
+ }
22
+ /**
23
+ * Remove a previously registered handler.
24
+ */
25
+ function off(event, handler) {
26
+ getStore().listeners.get(event)?.delete(handler);
27
+ }
28
+ /**
29
+ * Emit a boot lifecycle event, running all registered handlers sequentially.
30
+ */
31
+ async function emit(event, context) {
32
+ const handlers = getStore().listeners.get(event);
33
+ if (!handlers) return;
34
+ for (const handler of handlers) await handler(context);
35
+ }
36
+ //#endregion
37
+ export { off as n, on as r, emit as t };
38
+
39
+ //# sourceMappingURL=events-CgoM3Fvu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-CgoM3Fvu.js","names":[],"sources":["../src/lifecycle/events.ts"],"sourcesContent":["import type { BootContext } from './types.js';\n\nexport type BootEventName = 'beforeOnStartup' | 'afterOnStartup' | 'beforeOnShutdown' | 'afterOnShutdown';\n\nexport type BootEventHandler = (context: BootContext) => Promise<void> | void;\n\nconst STORE_KEY = Symbol.for('@astroscope/node/boot-events');\n\ninterface EventStore {\n listeners: Map<BootEventName, Set<BootEventHandler>>;\n}\n\nfunction getStore(): EventStore {\n const existing = (globalThis as Record<symbol, EventStore | undefined>)[STORE_KEY];\n\n if (existing) return existing;\n\n const store: EventStore = { listeners: new Map() };\n\n (globalThis as Record<symbol, EventStore>)[STORE_KEY] = store;\n\n return store;\n}\n\n/**\n * Register a handler for a boot lifecycle event.\n */\nexport function on(event: BootEventName, handler: BootEventHandler): void {\n const store = getStore();\n let handlers = store.listeners.get(event);\n\n if (!handlers) {\n handlers = new Set();\n store.listeners.set(event, handlers);\n }\n\n handlers.add(handler);\n}\n\n/**\n * Remove a previously registered handler.\n */\nexport function off(event: BootEventName, handler: BootEventHandler): void {\n const store = getStore();\n\n store.listeners.get(event)?.delete(handler);\n}\n\n/**\n * Emit a boot lifecycle event, running all registered handlers sequentially.\n */\nexport async function emit(event: BootEventName, context: BootContext): Promise<void> {\n const store = getStore();\n const handlers = store.listeners.get(event);\n\n if (!handlers) return;\n\n for (const handler of handlers) {\n await handler(context);\n }\n}\n"],"mappings":";AAMA,MAAM,YAAY,OAAO,IAAI,8BAA8B;AAM3D,SAAS,WAAuB;CAC9B,MAAM,WAAY,WAAsD;CAExE,IAAI,UAAU,OAAO;CAErB,MAAM,QAAoB,EAAE,2BAAW,IAAI,IAAI,EAAE;CAEjD,WAA2C,aAAa;CAExD,OAAO;AACT;;;;AAKA,SAAgB,GAAG,OAAsB,SAAiC;CACxE,MAAM,QAAQ,SAAS;CACvB,IAAI,WAAW,MAAM,UAAU,IAAI,KAAK;CAExC,IAAI,CAAC,UAAU;EACb,2BAAW,IAAI,IAAI;EACnB,MAAM,UAAU,IAAI,OAAO,QAAQ;CACrC;CAEA,SAAS,IAAI,OAAO;AACtB;;;;AAKA,SAAgB,IAAI,OAAsB,SAAiC;CAGzE,SAAI,CAAC,CAAC,UAAU,IAAI,KAAK,CAAC,EAAE,OAAO,OAAO;AAC5C;;;;AAKA,eAAsB,KAAK,OAAsB,SAAqC;CAEpF,MAAM,WADQ,SACO,CAAC,CAAC,UAAU,IAAI,KAAK;CAE1C,IAAI,CAAC,UAAU;CAEf,KAAK,MAAM,WAAW,UACpB,MAAM,QAAQ,OAAO;AAEzB"}