@astroscope/node 1.3.0 → 2.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 (65) hide show
  1. package/README.md +33 -1
  2. package/dist/boot.d.ts +0 -1
  3. package/dist/boot.d.ts.map +1 -1
  4. package/dist/{construct-DgB-jR0a.d.ts → construct-BGlPfWMF.d.ts} +1 -2
  5. package/dist/construct-BGlPfWMF.d.ts.map +1 -0
  6. package/dist/csrf-middleware-entrypoint.d.ts.map +1 -1
  7. package/dist/dev-middleware-entrypoint.d.ts +0 -1
  8. package/dist/dev-middleware-entrypoint.d.ts.map +1 -1
  9. package/dist/emitters-C20tjKLp.js +43 -0
  10. package/dist/emitters-C20tjKLp.js.map +1 -0
  11. package/dist/{events-CUzQ2_cp.d.ts → events-u7J3ezJR.d.ts} +1 -2
  12. package/dist/events-u7J3ezJR.d.ts.map +1 -0
  13. package/dist/{excludes-DLF3A_Cf.d.ts → excludes-BDiE3eyp.d.ts} +1 -2
  14. package/dist/excludes-BDiE3eyp.d.ts.map +1 -0
  15. package/dist/excludes.d.ts +1 -1
  16. package/dist/health.d.ts.map +1 -1
  17. package/dist/image-endpoint.d.ts +11 -0
  18. package/dist/image-endpoint.d.ts.map +1 -0
  19. package/dist/image-endpoint.js +11 -0
  20. package/dist/image-endpoint.js.map +1 -0
  21. package/dist/image-service.d.ts +15 -0
  22. package/dist/image-service.d.ts.map +1 -0
  23. package/dist/image-service.js +28 -0
  24. package/dist/image-service.js.map +1 -0
  25. package/dist/index.d.ts +27 -4
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +280 -7
  28. package/dist/index.js.map +1 -1
  29. package/dist/islands-middleware-entrypoint.d.ts +5 -0
  30. package/dist/islands-middleware-entrypoint.d.ts.map +1 -0
  31. package/dist/islands-middleware-entrypoint.js +58 -0
  32. package/dist/islands-middleware-entrypoint.js.map +1 -0
  33. package/dist/islands-runtime.d.ts +1 -0
  34. package/dist/islands-runtime.js +113 -0
  35. package/dist/islands-runtime.js.map +1 -0
  36. package/dist/islands.d.ts +2 -0
  37. package/dist/islands.js +3 -0
  38. package/dist/lifecycle/events.d.ts +1 -1
  39. package/dist/log/index.d.ts +1 -2
  40. package/dist/log/index.d.ts.map +1 -1
  41. package/dist/log-B69HEBvg.js.map +1 -1
  42. package/dist/native-mount-DjYEnO4X.js.map +1 -1
  43. package/dist/native.d.ts +0 -1
  44. package/dist/native.d.ts.map +1 -1
  45. package/dist/prepare-CXZsyAVk.js.map +1 -1
  46. package/dist/prerendered-CpEAJN_q.js +34 -0
  47. package/dist/prerendered-CpEAJN_q.js.map +1 -0
  48. package/dist/preview.d.ts +0 -1
  49. package/dist/preview.d.ts.map +1 -1
  50. package/dist/route-middleware-entrypoint.d.ts +0 -1
  51. package/dist/route-middleware-entrypoint.d.ts.map +1 -1
  52. package/dist/route-store-DdxGePj2.js +27 -0
  53. package/dist/route-store-DdxGePj2.js.map +1 -0
  54. package/dist/route-store-DtY3uvLf.d.ts +69 -0
  55. package/dist/route-store-DtY3uvLf.d.ts.map +1 -0
  56. package/dist/server.d.ts.map +1 -1
  57. package/dist/server.js +32 -15
  58. package/dist/server.js.map +1 -1
  59. package/dist/transform-D8dIBGEr.js +191 -0
  60. package/dist/transform-D8dIBGEr.js.map +1 -0
  61. package/dist/types-D0uMBi2M.d.ts.map +1 -1
  62. package/package.json +31 -11
  63. package/dist/construct-DgB-jR0a.d.ts.map +0 -1
  64. package/dist/events-CUzQ2_cp.d.ts.map +0 -1
  65. package/dist/excludes-DLF3A_Cf.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"native-mount-DjYEnO4X.js","names":[],"sources":["../src/server/native-mount.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { log } from '../observability/log/index.js';\nimport { overrideRequestRoute } from '../observability/request-route.js';\n\n/**\n * Native mounts: raw `(req, res)` handlers dispatched before static/astro,\n * for Node libraries that need the real request and response (e.g.\n * `oidc-provider`'s `callback()`). Mounted requests bypass astro middleware\n * entirely but stay inside request logging and tracing.\n *\n * Keyed on `globalThis` because registration (boot file, vite runner in dev)\n * and dispatch (server runtime) may live in different module instances.\n */\n\nconst STORE_KEY = Symbol.for('@astroscope/node/native-mounts');\n\nexport type NativeHandler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;\n\nexport interface NativeMountMatcher {\n /** match requests whose pathname starts with this prefix */\n prefix?: string | undefined;\n\n /** predicate over the native request; evaluated when no prefix mount matches */\n match?: ((req: IncomingMessage) => boolean) | undefined;\n\n /** route label for logs, metrics and span names; defaults to the prefix */\n name?: string | undefined;\n}\n\ninterface Mount {\n prefix: string | undefined;\n match: ((req: IncomingMessage) => boolean) | undefined;\n name: string | undefined;\n handler: NativeHandler;\n}\n\ninterface Store {\n mounts: Mount[];\n}\n\nfunction getStore(): Store {\n const g = globalThis as Record<symbol, unknown>;\n let store = g[STORE_KEY] as Store | undefined;\n\n if (!store) {\n store = { mounts: [] };\n g[STORE_KEY] = store;\n }\n\n return store;\n}\n\nfunction matchesPrefix(pathname: string, prefix: string): boolean {\n if (!pathname.startsWith(prefix)) return false;\n\n const rest = pathname.slice(prefix.length);\n\n return rest === '' || rest.startsWith('/') || rest.startsWith('?');\n}\n\n/**\n * Mount a native `(req, res)` handler on the adapter's server. The handler\n * owns the response completely — matched requests never reach astro\n * middleware or rendering. Dispatch happens before static file serving, in\n * production and dev alike.\n *\n * Call from `onStartup`. Returns an unregister function; mounts still\n * registered after `onShutdown` are removed automatically.\n *\n * When several prefix mounts match, the longest prefix wins; predicate\n * mounts are consulted afterwards in registration order.\n *\n * @example\n * ```ts\n * // src/boot.ts\n * import { mountNativeHandler } from '@astroscope/node/native';\n *\n * export function onStartup() {\n * mountNativeHandler({ prefix: '/oidc', name: 'oidc' }, getOidcProvider().callback());\n * }\n * ```\n */\nexport function mountNativeHandler(matcher: NativeMountMatcher, handler: NativeHandler): () => void {\n if (!matcher.prefix && !matcher.match) {\n throw new Error('[@astroscope/node] mountNativeHandler requires a prefix or a match predicate');\n }\n\n const mount: Mount = {\n prefix: matcher.prefix,\n match: matcher.match,\n name: matcher.name ?? matcher.prefix,\n handler,\n };\n\n const store = getStore();\n\n store.mounts.push(mount);\n\n return () => {\n const index = store.mounts.indexOf(mount);\n\n if (index !== -1) store.mounts.splice(index, 1);\n };\n}\n\n/**\n * Remove every registered mount. Runs after `onShutdown` (prod) and between\n * dev generations, so re-running `onStartup` never stacks duplicates.\n */\nexport function clearNativeMounts(): void {\n getStore().mounts.length = 0;\n}\n\nfunction findMount(req: IncomingMessage): Mount | undefined {\n const url = req.url ?? '';\n const queryIndex = url.indexOf('?');\n const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);\n\n let best: Mount | undefined;\n\n for (const mount of getStore().mounts) {\n if (mount.prefix && matchesPrefix(pathname, mount.prefix)) {\n if (!best?.prefix || mount.prefix.length > best.prefix.length) {\n best = mount;\n }\n }\n }\n\n if (best) return best;\n\n return getStore().mounts.find((mount) => mount.match?.(req));\n}\n\nfunction failResponse(res: ServerResponse): void {\n if (res.writableEnded) return;\n\n if (!res.headersSent) {\n res.writeHead(500, { 'content-type': 'text/plain' });\n }\n\n res.end('Internal Server Error');\n}\n\n/**\n * Dispatch a request to a matching mount. Returns `false` when no mount\n * matches — the caller continues with static/astro handling.\n */\nexport function dispatchNativeMount(req: IncomingMessage, res: ServerResponse): boolean {\n const mount = findMount(req);\n\n if (!mount) return false;\n\n // the mount, not astro's routing, is what serves this request\n if (mount.name) {\n overrideRequestRoute(mount.name);\n }\n\n try {\n const result = mount.handler(req, res);\n\n if (result instanceof Promise) {\n result.catch((err: unknown) => {\n log.error(err instanceof Error ? { err } : { reason: err }, 'native mount handler failed');\n failResponse(res);\n });\n }\n } catch (err) {\n log.error(err instanceof Error ? { err } : { reason: err }, 'native mount handler failed');\n failResponse(res);\n }\n\n return true;\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAM,YAAY,OAAO,IAAI,gCAAgC;AA0B7D,SAAS,WAAkB;CACzB,MAAM,IAAI;CACV,IAAI,QAAQ,EAAE;CAEd,IAAI,CAAC,OAAO;EACV,QAAQ,EAAE,QAAQ,CAAC,EAAE;EACrB,EAAE,aAAa;CACjB;CAEA,OAAO;AACT;AAEA,SAAS,cAAc,UAAkB,QAAyB;CAChE,IAAI,CAAC,SAAS,WAAW,MAAM,GAAG,OAAO;CAEzC,MAAM,OAAO,SAAS,MAAM,OAAO,MAAM;CAEzC,OAAO,SAAS,MAAM,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,GAAG;AACnE;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,mBAAmB,SAA6B,SAAoC;CAClG,IAAI,CAAC,QAAQ,UAAU,CAAC,QAAQ,OAC9B,MAAM,IAAI,MAAM,8EAA8E;CAGhG,MAAM,QAAe;EACnB,QAAQ,QAAQ;EAChB,OAAO,QAAQ;EACf,MAAM,QAAQ,QAAQ,QAAQ;EAC9B;CACF;CAEA,MAAM,QAAQ,SAAS;CAEvB,MAAM,OAAO,KAAK,KAAK;CAEvB,aAAa;EACX,MAAM,QAAQ,MAAM,OAAO,QAAQ,KAAK;EAExC,IAAI,UAAU,IAAI,MAAM,OAAO,OAAO,OAAO,CAAC;CAChD;AACF;;;;;AAMA,SAAgB,oBAA0B;CACxC,SAAS,CAAC,CAAC,OAAO,SAAS;AAC7B;AAEA,SAAS,UAAU,KAAyC;CAC1D,MAAM,MAAM,IAAI,OAAO;CACvB,MAAM,aAAa,IAAI,QAAQ,GAAG;CAClC,MAAM,WAAW,eAAe,KAAK,MAAM,IAAI,MAAM,GAAG,UAAU;CAElE,IAAI;CAEJ,KAAK,MAAM,SAAS,SAAS,CAAC,CAAC,QAC7B,IAAI,MAAM,UAAU,cAAc,UAAU,MAAM,MAAM;MAClD,CAAC,MAAM,UAAU,MAAM,OAAO,SAAS,KAAK,OAAO,QACrD,OAAO;CAAA;CAKb,IAAI,MAAM,OAAO;CAEjB,OAAO,SAAS,CAAC,CAAC,OAAO,MAAM,UAAU,MAAM,QAAQ,GAAG,CAAC;AAC7D;AAEA,SAAS,aAAa,KAA2B;CAC/C,IAAI,IAAI,eAAe;CAEvB,IAAI,CAAC,IAAI,aACP,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;CAGrD,IAAI,IAAI,uBAAuB;AACjC;;;;;AAMA,SAAgB,oBAAoB,KAAsB,KAA8B;CACtF,MAAM,QAAQ,UAAU,GAAG;CAE3B,IAAI,CAAC,OAAO,OAAO;CAGnB,IAAI,MAAM,MACR,qBAAqB,MAAM,IAAI;CAGjC,IAAI;EACF,MAAM,SAAS,MAAM,QAAQ,KAAK,GAAG;EAErC,IAAI,kBAAkB,SACpB,OAAO,OAAO,QAAiB;GAC7B,IAAI,MAAM,eAAe,QAAQ,EAAE,IAAI,IAAI,EAAE,QAAQ,IAAI,GAAG,6BAA6B;GACzF,aAAa,GAAG;EAClB,CAAC;CAEL,SAAS,KAAK;EACZ,IAAI,MAAM,eAAe,QAAQ,EAAE,IAAI,IAAI,EAAE,QAAQ,IAAI,GAAG,6BAA6B;EACzF,aAAa,GAAG;CAClB;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"native-mount-DjYEnO4X.js","names":[],"sources":["../src/server/native-mount.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { log } from '../observability/log/index.js';\nimport { overrideRequestRoute } from '../observability/request-route.js';\n\n/**\n * Native mounts: raw `(req, res)` handlers dispatched before static/astro,\n * for Node libraries that need the real request and response (e.g.\n * `oidc-provider`'s `callback()`). Mounted requests bypass astro middleware\n * entirely but stay inside request logging and tracing.\n *\n * Keyed on `globalThis` because registration (boot file, vite runner in dev)\n * and dispatch (server runtime) may live in different module instances.\n */\n\nconst STORE_KEY = Symbol.for('@astroscope/node/native-mounts');\n\nexport type NativeHandler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;\n\nexport interface NativeMountMatcher {\n /** match requests whose pathname starts with this prefix */\n prefix?: string | undefined;\n\n /** predicate over the native request; evaluated when no prefix mount matches */\n match?: ((req: IncomingMessage) => boolean) | undefined;\n\n /** route label for logs, metrics and span names; defaults to the prefix */\n name?: string | undefined;\n}\n\ninterface Mount {\n prefix: string | undefined;\n match: ((req: IncomingMessage) => boolean) | undefined;\n name: string | undefined;\n handler: NativeHandler;\n}\n\ninterface Store {\n mounts: Mount[];\n}\n\nfunction getStore(): Store {\n const g = globalThis as Record<symbol, unknown>;\n let store = g[STORE_KEY] as Store | undefined;\n\n if (!store) {\n store = { mounts: [] };\n g[STORE_KEY] = store;\n }\n\n return store;\n}\n\nfunction matchesPrefix(pathname: string, prefix: string): boolean {\n if (!pathname.startsWith(prefix)) return false;\n\n const rest = pathname.slice(prefix.length);\n\n return rest === '' || rest.startsWith('/') || rest.startsWith('?');\n}\n\n/**\n * Mount a native `(req, res)` handler on the adapter's server. The handler\n * owns the response completely — matched requests never reach astro\n * middleware or rendering. Dispatch happens before static file serving, in\n * production and dev alike.\n *\n * Call from `onStartup`. Returns an unregister function; mounts still\n * registered after `onShutdown` are removed automatically.\n *\n * When several prefix mounts match, the longest prefix wins; predicate\n * mounts are consulted afterwards in registration order.\n *\n * @example\n * ```ts\n * // src/boot.ts\n * import { mountNativeHandler } from '@astroscope/node/native';\n *\n * export function onStartup() {\n * mountNativeHandler({ prefix: '/oidc', name: 'oidc' }, getOidcProvider().callback());\n * }\n * ```\n */\nexport function mountNativeHandler(matcher: NativeMountMatcher, handler: NativeHandler): () => void {\n if (!matcher.prefix && !matcher.match) {\n throw new Error('[@astroscope/node] mountNativeHandler requires a prefix or a match predicate');\n }\n\n const mount: Mount = {\n prefix: matcher.prefix,\n match: matcher.match,\n name: matcher.name ?? matcher.prefix,\n handler,\n };\n\n const store = getStore();\n\n store.mounts.push(mount);\n\n return () => {\n const index = store.mounts.indexOf(mount);\n\n if (index !== -1) store.mounts.splice(index, 1);\n };\n}\n\n/**\n * Remove every registered mount. Runs after `onShutdown` (prod) and between\n * dev generations, so re-running `onStartup` never stacks duplicates.\n */\nexport function clearNativeMounts(): void {\n getStore().mounts.length = 0;\n}\n\nfunction findMount(req: IncomingMessage): Mount | undefined {\n const url = req.url ?? '';\n const queryIndex = url.indexOf('?');\n const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);\n\n let best: Mount | undefined;\n\n for (const mount of getStore().mounts) {\n if (mount.prefix && matchesPrefix(pathname, mount.prefix)) {\n if (!best?.prefix || mount.prefix.length > best.prefix.length) {\n best = mount;\n }\n }\n }\n\n if (best) return best;\n\n return getStore().mounts.find((mount) => mount.match?.(req));\n}\n\nfunction failResponse(res: ServerResponse): void {\n if (res.writableEnded) return;\n\n if (!res.headersSent) {\n res.writeHead(500, { 'content-type': 'text/plain' });\n }\n\n res.end('Internal Server Error');\n}\n\n/**\n * Dispatch a request to a matching mount. Returns `false` when no mount\n * matches — the caller continues with static/astro handling.\n */\nexport function dispatchNativeMount(req: IncomingMessage, res: ServerResponse): boolean {\n const mount = findMount(req);\n\n if (!mount) return false;\n\n // the mount, not astro's routing, is what serves this request\n if (mount.name) {\n overrideRequestRoute(mount.name);\n }\n\n try {\n const result = mount.handler(req, res);\n\n if (result instanceof Promise) {\n result.catch((err: unknown) => {\n log.error(err instanceof Error ? { err } : { reason: err }, 'native mount handler failed');\n failResponse(res);\n });\n }\n } catch (err) {\n log.error(err instanceof Error ? { err } : { reason: err }, 'native mount handler failed');\n failResponse(res);\n }\n\n return true;\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAM,YAAY,OAAO,IAAI,gCAAgC;AA0B7D,SAAS,WAAkB;CACzB,MAAM,IAAI;CACV,IAAI,QAAQ,EAAE;CAEd,IAAI,CAAC,OAAO;EACV,QAAQ,EAAE,QAAQ,CAAC,EAAE;EACrB,EAAE,aAAa;CACjB;CAEA,OAAO;AACT;AAEA,SAAS,cAAc,UAAkB,QAAyB;CAChE,IAAI,CAAC,SAAS,WAAW,MAAM,GAAG,OAAO;CAEzC,MAAM,OAAO,SAAS,MAAM,OAAO,MAAM;CAEzC,OAAO,SAAS,MAAM,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,GAAG;AACnE;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,mBAAmB,SAA6B,SAAoC;CAClG,IAAI,CAAC,QAAQ,UAAU,CAAC,QAAQ,OAC9B,MAAM,IAAI,MAAM,8EAA8E;CAGhG,MAAM,QAAe;EACnB,QAAQ,QAAQ;EAChB,OAAO,QAAQ;EACf,MAAM,QAAQ,QAAQ,QAAQ;EAC9B;CACF;CAEA,MAAM,QAAQ,SAAS;CAEvB,MAAM,OAAO,KAAK,KAAK;CAEvB,aAAa;EACX,MAAM,QAAQ,MAAM,OAAO,QAAQ,KAAK;EAExC,IAAI,UAAU,IAAI,MAAM,OAAO,OAAO,OAAO,CAAC;CAChD;AACF;;;;;AAMA,SAAgB,oBAA0B;CACxC,SAAS,CAAC,CAAC,OAAO,SAAS;AAC7B;AAEA,SAAS,UAAU,KAAyC;CAC1D,MAAM,MAAM,IAAI,OAAO;CACvB,MAAM,aAAa,IAAI,QAAQ,GAAG;CAClC,MAAM,WAAW,eAAe,KAAK,MAAM,IAAI,MAAM,GAAG,UAAU;CAElE,IAAI;CAEJ,KAAK,MAAM,SAAS,SAAS,CAAC,CAAC,QAC7B,IAAI,MAAM,UAAU,cAAc,UAAU,MAAM,MAAM,GAClD;MAAA,CAAC,MAAM,UAAU,MAAM,OAAO,SAAS,KAAK,OAAO,QACrD,OAAO;CAAA;CAKb,IAAI,MAAM,OAAO;CAEjB,OAAO,SAAS,CAAC,CAAC,OAAO,MAAM,UAAU,MAAM,QAAQ,GAAG,CAAC;AAC7D;AAEA,SAAS,aAAa,KAA2B;CAC/C,IAAI,IAAI,eAAe;CAEvB,IAAI,CAAC,IAAI,aACP,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;CAGrD,IAAI,IAAI,uBAAuB;AACjC;;;;;AAMA,SAAgB,oBAAoB,KAAsB,KAA8B;CACtF,MAAM,QAAQ,UAAU,GAAG;CAE3B,IAAI,CAAC,OAAO,OAAO;CAGnB,IAAI,MAAM,MACR,qBAAqB,MAAM,IAAI;CAGjC,IAAI;EACF,MAAM,SAAS,MAAM,QAAQ,KAAK,GAAG;EAErC,IAAI,kBAAkB,SACpB,OAAO,OAAO,QAAiB;GAC7B,IAAI,MAAM,eAAe,QAAQ,EAAE,IAAI,IAAI,EAAE,QAAQ,IAAI,GAAG,6BAA6B;GACzF,aAAa,GAAG;EAClB,CAAC;CAEL,SAAS,KAAK;EACZ,IAAI,MAAM,eAAe,QAAQ,EAAE,IAAI,IAAI,EAAE,QAAQ,IAAI,GAAG,6BAA6B;EACzF,aAAa,GAAG;CAClB;CAEA,OAAO;AACT"}
package/dist/native.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
-
3
2
  //#region src/server/native-mount.d.ts
4
3
  type NativeHandler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
5
4
  interface NativeMountMatcher {
@@ -1 +1 @@
1
- {"version":3,"file":"native.d.ts","names":[],"sources":["../src/server/native-mount.ts"],"mappings":";;;KAgBY,aAAA,IAAiB,GAAA,EAAK,eAAA,EAAiB,GAAA,EAAK,cAAA,YAA0B,OAAA;AAAA,UAEjE,kBAAA;EAFL;EAIV,MAAA;;EAGA,KAAA,KAAU,GAAA,EAAK,eAAe;EAPwB;EAUtD,IAAA;AAAA;;;;;;;;AAVuF;AAEzF;;;;;;;;;;AAQM;AAwDN;;;iBAAgB,kBAAA,CAAmB,OAAA,EAAS,kBAAA,EAAoB,OAAA,EAAS,aAAa;;;;;iBA2BtE,iBAAA;AA3BsE;AA2BtF;;;AA3BsF,iBAiEtE,mBAAA,CAAoB,GAAA,EAAK,eAAA,EAAiB,GAAA,EAAK,cAAc"}
1
+ {"version":3,"file":"native.d.ts","names":[],"sources":["../src/server/native-mount.ts"],"mappings":";;KAgBY,iBAAiB,KAAK,iBAAiB,KAAK,0BAA0B;UAEjE;;EAEf;;EAGA,UAAU,KAAK;;EAGf;;;;;;;;;;;;;;;;;;;;;;;;iBAwDc,mBAAmB,SAAS,oBAAoB,SAAS;;;;;iBA2BzD;;;;;iBAsCA,oBAAoB,KAAK,iBAAiB,KAAK"}
@@ -1 +1 @@
1
- {"version":3,"file":"prepare-CXZsyAVk.js","names":["LIB_NAME"],"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"],"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 { type Histogram, type UpDownCounter, ValueType, metrics } from '@opentelemetry/api';\n\nconst LIB_NAME = '@astroscope/node';\n\n// lazy initialization so instruments bind to the SDK meter provider\nlet httpRequestDuration: Histogram | null = null;\nlet httpActiveRequests: UpDownCounter | null = null;\nlet actionDuration: Histogram | null = null;\n\nfunction getHttpRequestDuration(): Histogram {\n return (httpRequestDuration ??= metrics.getMeter(LIB_NAME).createHistogram('http.server.request.duration', {\n description: 'Duration of HTTP server requests',\n unit: 's',\n valueType: ValueType.DOUBLE,\n }));\n}\n\nfunction getHttpActiveRequests(): UpDownCounter {\n return (httpActiveRequests ??= metrics.getMeter(LIB_NAME).createUpDownCounter('http.server.active_requests', {\n description: 'Number of active HTTP server requests',\n unit: '{request}',\n valueType: ValueType.INT,\n }));\n}\n\nfunction getActionDuration(): Histogram {\n return (actionDuration ??= metrics.getMeter(LIB_NAME).createHistogram('astro.action.duration', {\n description: 'Duration of Astro action executions',\n unit: 's',\n valueType: ValueType.DOUBLE,\n }));\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 getHttpActiveRequests().add(1, { 'http.request.method': method });\n\n return () => {\n getHttpActiveRequests().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 getHttpRequestDuration().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 getActionDuration().record(durationMs / 1000, {\n 'astro.action.name': attributes.name,\n 'http.response.status_code': attributes.status,\n });\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 { recordActionDuration, recordHttpRequestDuration, recordHttpRequestStart } 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 };\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 responseTime = performance.now() - startTime;\n const ttfb = roundTime((firstByteTime ?? performance.now()) - startTime);\n\n if (requestLogger) {\n const level = 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 },\n 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 (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 };\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';\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\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"],"mappings":"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"}
1
+ {"version":3,"file":"prepare-CXZsyAVk.js","names":["LIB_NAME"],"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"],"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 { type Histogram, type UpDownCounter, ValueType, metrics } from '@opentelemetry/api';\n\nconst LIB_NAME = '@astroscope/node';\n\n// lazy initialization so instruments bind to the SDK meter provider\nlet httpRequestDuration: Histogram | null = null;\nlet httpActiveRequests: UpDownCounter | null = null;\nlet actionDuration: Histogram | null = null;\n\nfunction getHttpRequestDuration(): Histogram {\n return (httpRequestDuration ??= metrics.getMeter(LIB_NAME).createHistogram('http.server.request.duration', {\n description: 'Duration of HTTP server requests',\n unit: 's',\n valueType: ValueType.DOUBLE,\n }));\n}\n\nfunction getHttpActiveRequests(): UpDownCounter {\n return (httpActiveRequests ??= metrics.getMeter(LIB_NAME).createUpDownCounter('http.server.active_requests', {\n description: 'Number of active HTTP server requests',\n unit: '{request}',\n valueType: ValueType.INT,\n }));\n}\n\nfunction getActionDuration(): Histogram {\n return (actionDuration ??= metrics.getMeter(LIB_NAME).createHistogram('astro.action.duration', {\n description: 'Duration of Astro action executions',\n unit: 's',\n valueType: ValueType.DOUBLE,\n }));\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 getHttpActiveRequests().add(1, { 'http.request.method': method });\n\n return () => {\n getHttpActiveRequests().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 getHttpRequestDuration().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 getActionDuration().record(durationMs / 1000, {\n 'astro.action.name': attributes.name,\n 'http.response.status_code': attributes.status,\n });\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 { recordActionDuration, recordHttpRequestDuration, recordHttpRequestStart } 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 };\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 responseTime = performance.now() - startTime;\n const ttfb = roundTime((firstByteTime ?? performance.now()) - startTime);\n\n if (requestLogger) {\n const level = 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 },\n 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 (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 };\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';\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\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"],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import { t as createIslandsTransformer } from "./transform-D8dIBGEr.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ //#region src/islands/prerendered.ts
5
+ function* walkHtmlFiles(dir) {
6
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
7
+ const full = path.join(dir, entry.name);
8
+ if (entry.isDirectory()) yield* walkHtmlFiles(full);
9
+ else if (entry.isFile() && entry.name.endsWith(".html")) yield full;
10
+ }
11
+ }
12
+ /**
13
+ * Prerendered pages never pass through the middleware — they get the same rewrite
14
+ * once, at build time, before the client dir is compressed. Runs without a request
15
+ * context, so registered emitters that need one contribute nothing here.
16
+ */
17
+ async function transformPrerenderedHtml(clientDir, manifest) {
18
+ const transformer = createIslandsTransformer(manifest);
19
+ let transformed = 0;
20
+ for (const file of walkHtmlFiles(clientDir)) {
21
+ const html = fs.readFileSync(file, "utf-8");
22
+ const rewriter = transformer.createDocumentRewriter();
23
+ const result = rewriter.write(html) + await rewriter.end();
24
+ if (result !== html) {
25
+ fs.writeFileSync(file, result);
26
+ transformed++;
27
+ }
28
+ }
29
+ return transformed;
30
+ }
31
+ //#endregion
32
+ export { transformPrerenderedHtml };
33
+
34
+ //# sourceMappingURL=prerendered-CpEAJN_q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prerendered-CpEAJN_q.js","names":[],"sources":["../src/islands/prerendered.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { createIslandsTransformer } from './transform.js';\nimport type { IslandsManifest } from './types.js';\n\nfunction* walkHtmlFiles(dir: string): Generator<string> {\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n yield* walkHtmlFiles(full);\n } else if (entry.isFile() && entry.name.endsWith('.html')) {\n yield full;\n }\n }\n}\n\n/**\n * Prerendered pages never pass through the middleware — they get the same rewrite\n * once, at build time, before the client dir is compressed. Runs without a request\n * context, so registered emitters that need one contribute nothing here.\n */\nexport async function transformPrerenderedHtml(clientDir: string, manifest: IslandsManifest): Promise<number> {\n const transformer = createIslandsTransformer(manifest);\n let transformed = 0;\n\n for (const file of walkHtmlFiles(clientDir)) {\n const html = fs.readFileSync(file, 'utf-8');\n const rewriter = transformer.createDocumentRewriter();\n const result = rewriter.write(html) + (await rewriter.end());\n\n if (result !== html) {\n fs.writeFileSync(file, result);\n transformed++;\n }\n }\n\n return transformed;\n}\n"],"mappings":";;;;AAKA,UAAU,cAAc,KAAgC;CACtD,KAAK,MAAM,SAAS,GAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;EAChE,MAAM,OAAO,KAAK,KAAK,KAAK,MAAM,IAAI;EAEtC,IAAI,MAAM,YAAY,GACpB,OAAO,cAAc,IAAI;OACpB,IAAI,MAAM,OAAO,KAAK,MAAM,KAAK,SAAS,OAAO,GACtD,MAAM;CAEV;AACF;;;;;;AAOA,eAAsB,yBAAyB,WAAmB,UAA4C;CAC5G,MAAM,cAAc,yBAAyB,QAAQ;CACrD,IAAI,cAAc;CAElB,KAAK,MAAM,QAAQ,cAAc,SAAS,GAAG;EAC3C,MAAM,OAAO,GAAG,aAAa,MAAM,OAAO;EAC1C,MAAM,WAAW,YAAY,uBAAuB;EACpD,MAAM,SAAS,SAAS,MAAM,IAAI,IAAK,MAAM,SAAS,IAAI;EAE1D,IAAI,WAAW,MAAM;GACnB,GAAG,cAAc,MAAM,MAAM;GAC7B;EACF;CACF;CAEA,OAAO;AACT"}
package/dist/preview.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { CreatePreviewServer } from "astro";
2
-
3
2
  //#region src/server/preview.d.ts
4
3
  /**
5
4
  * `astro preview` support: imports the built server entry with autostart
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","names":[],"sources":["../src/server/preview.ts"],"mappings":";;;;;AAAiD;;;cAQ3C,mBAAA,EAAqB,mBAe1B"}
1
+ {"version":3,"file":"preview.d.ts","names":[],"sources":["../src/server/preview.ts"],"mappings":";;;;;;;cAQM,qBAAqB"}
@@ -1,5 +1,4 @@
1
1
  import { MiddlewareHandler } from "astro";
2
-
3
2
  //#region src/observability/route-middleware-entrypoint.d.ts
4
3
  /**
5
4
  * Route enrichment: the native handler starts spans and request logging
@@ -1 +1 @@
1
- {"version":3,"file":"route-middleware-entrypoint.d.ts","names":[],"sources":["../src/observability/route-middleware-entrypoint.ts"],"mappings":";;;;;AAWA;;;;AAMC;;cANY,SAAA,EAAW,iBAMvB"}
1
+ {"version":3,"file":"route-middleware-entrypoint.d.ts","names":[],"sources":["../src/observability/route-middleware-entrypoint.ts"],"mappings":";;;;;;;;;;cAWa,WAAW"}
@@ -0,0 +1,27 @@
1
+ //#region src/server/route-store.ts
2
+ /**
3
+ * The route matched for a request, stashed by the server handler before
4
+ * rendering so middleware can tell what kind of route produced a response —
5
+ * a `.astro` page or an endpoint that happens to return html (a proxy
6
+ * catch-all, an html-returning `.ts` route). Keyed on `globalThis` via
7
+ * `Symbol.for` so the vite-runner and native module instances share it —
8
+ * same pattern as the log store.
9
+ *
10
+ * Absent in dev (no adapter handler) — consumers fall back to their
11
+ * content-type gates.
12
+ */
13
+ const STORE = Symbol.for("@astroscope/node.requestRoutes");
14
+ function store() {
15
+ const scope = globalThis;
16
+ return scope[STORE] ??= /* @__PURE__ */ new WeakMap();
17
+ }
18
+ function setRequestRouteData(request, routeData) {
19
+ store().set(request, routeData);
20
+ }
21
+ function getRequestRouteData(request) {
22
+ return store().get(request);
23
+ }
24
+ //#endregion
25
+ export { setRequestRouteData as n, getRequestRouteData as t };
26
+
27
+ //# sourceMappingURL=route-store-DdxGePj2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-store-DdxGePj2.js","names":[],"sources":["../src/server/route-store.ts"],"sourcesContent":["import type { RouteData } from 'astro';\n\n/**\n * The route matched for a request, stashed by the server handler before\n * rendering so middleware can tell what kind of route produced a response —\n * a `.astro` page or an endpoint that happens to return html (a proxy\n * catch-all, an html-returning `.ts` route). Keyed on `globalThis` via\n * `Symbol.for` so the vite-runner and native module instances share it —\n * same pattern as the log store.\n *\n * Absent in dev (no adapter handler) — consumers fall back to their\n * content-type gates.\n */\nconst STORE = Symbol.for('@astroscope/node.requestRoutes');\n\ntype Scope = { [STORE]?: WeakMap<Request, RouteData> };\n\nfunction store(): WeakMap<Request, RouteData> {\n const scope = globalThis as Scope;\n\n return (scope[STORE] ??= new WeakMap());\n}\n\nexport function setRequestRouteData(request: Request, routeData: RouteData): void {\n store().set(request, routeData);\n}\n\nexport function getRequestRouteData(request: Request): RouteData | undefined {\n return store().get(request);\n}\n"],"mappings":";;;;;;;;;;;;AAaA,MAAM,QAAQ,OAAO,IAAI,gCAAgC;AAIzD,SAAS,QAAqC;CAC5C,MAAM,QAAQ;CAEd,OAAQ,MAAM,2BAAW,IAAI,QAAQ;AACvC;AAEA,SAAgB,oBAAoB,SAAkB,WAA4B;CAChF,MAAM,CAAC,CAAC,IAAI,SAAS,SAAS;AAChC;AAEA,SAAgB,oBAAoB,SAAyC;CAC3E,OAAO,MAAM,CAAC,CAAC,IAAI,OAAO;AAC5B"}
@@ -0,0 +1,69 @@
1
+ import { APIContext, RouteData } from "astro";
2
+ //#region src/islands/types.d.ts
3
+ /**
4
+ * One island as seen by the streaming scanner, with its chunk closures resolved.
5
+ * URLs are public (prefixed) — `staticClosure` and `fullClosure` include the
6
+ * component and renderer entry URLs themselves.
7
+ */
8
+ type IslandInfo = {
9
+ componentUrl: string;
10
+ rendererUrl: string | null;
11
+ /** raw `client` attribute value, e.g. `load`, `visible`, `idle-x` */
12
+ client: string | null;
13
+ /** transitive static imports of component + renderer — what preloading warms */
14
+ staticClosure: string[];
15
+ /** static + dynamic transitive imports — what data providers must cover */
16
+ fullClosure: string[];
17
+ };
18
+ /**
19
+ * What an emitter contributes for one island. `links` are merged across emitters and
20
+ * either emitted as `<link rel="modulepreload">` tags (immediate directives) or
21
+ * registered on the preload global for the gate runtime (deferred directives).
22
+ * `html` is emitted right before the island tag regardless of directive — an inline
23
+ * script there is parsed strictly before the island connects, so it is the place
24
+ * for data the island's chunks read at execution time.
25
+ */
26
+ type IslandEmission = {
27
+ links?: string[] | undefined;
28
+ html?: string | undefined;
29
+ };
30
+ /**
31
+ * `context` is the request's APIContext when the transform runs in the middleware,
32
+ * and undefined when it runs over prerendered HTML at build time.
33
+ */
34
+ type IslandEmitter = (island: IslandInfo, context?: APIContext | undefined) => IslandEmission | null;
35
+ /**
36
+ * What a document emitter contributes for one html page response.
37
+ */
38
+ type DocumentEmission = {
39
+ head?: string | undefined;
40
+ end?: (() => string | null) | undefined;
41
+ };
42
+ /**
43
+ * Called once per html page response by the islands middleware, before the body
44
+ * streams. Return null to contribute nothing. Unlike island emitters, document
45
+ * emitters never run over prerendered HTML — their data is per-request by nature.
46
+ */
47
+ type DocumentEmitter = (context: APIContext) => DocumentEmission | null;
48
+ //#endregion
49
+ //#region src/islands/emitters.d.ts
50
+ /**
51
+ * Register an emitter that contributes preload links and/or attributes for every
52
+ * island the islands middleware sees. Registration is process-wide — call it once
53
+ * during boot or module initialization, not per request.
54
+ */
55
+ declare function registerIslandEmitter(emitter: IslandEmitter): void;
56
+ /**
57
+ * Register an emitter that contributes document-level content (a head bootstrap
58
+ * script, a stream-end script) for every html page response the islands middleware
59
+ * streams. Registration is process-wide — call it once during boot or module
60
+ * initialization, not per request.
61
+ */
62
+ declare function registerDocumentEmitter(emitter: DocumentEmitter): void;
63
+ //#endregion
64
+ //#region src/server/route-store.d.ts
65
+ declare function setRequestRouteData(request: Request, routeData: RouteData): void;
66
+ declare function getRequestRouteData(request: Request): RouteData | undefined;
67
+ //#endregion
68
+ export { DocumentEmission as a, IslandEmitter as c, registerIslandEmitter as i, IslandInfo as l, setRequestRouteData as n, DocumentEmitter as o, registerDocumentEmitter as r, IslandEmission as s, getRequestRouteData as t };
69
+ //# sourceMappingURL=route-store-DtY3uvLf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-store-DtY3uvLf.d.ts","names":[],"sources":["../src/islands/types.ts","../src/islands/emitters.ts","../src/server/route-store.ts"],"mappings":";;;;;;;KAqBY;EACV;EACA;;EAEA;;EAEA;;EAEA;;;;;;;;;;KAWU;EACV;EACA;;;;;;KAOU,iBAAiB,QAAQ,YAAY,UAAU,2BAA2B;;;;KAK1E;EACV;EACA;;;;;;;KAQU,mBAAmB,SAAS,eAAe;;;;;;;;iBCnCvC,sBAAsB,SAAS;;;;;;;iBAc/B,wBAAwB,SAAS;;;iBCpBjC,oBAAoB,SAAS,SAAS,WAAW;iBAIjD,oBAAoB,SAAS,UAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server/server.ts"],"mappings":";UA2EiB,YAAA;EACf,IAAA;EACA,IAAA;EACA,IAAA,IAAQ,OAAA;EACR,MAAA,IAAU,OAAO;AAAA;AAAA,iBAGG,WAAA,CAAY,SAAA;EAChC,IAAA;EACA,IAAA;AAAA,IACE,OAAO,CAAC,YAAA"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server/server.ts"],"mappings":";UAoFiB;EACf;EACA;EACA,QAAQ;EACR,UAAU;;iBAGU,YAAY;EAChC;EACA;IACE,QAAQ"}
package/dist/server.js CHANGED
@@ -3,6 +3,7 @@ import { a as runShutdown, i as createRequestInstrumentation, n as shutdownTelem
3
3
  import { i as getRequestRecord } from "./request-route-DcnZOOM4.js";
4
4
  import { n as log } from "./log-B69HEBvg.js";
5
5
  import { n as dispatchNativeMount, t as clearNativeMounts } from "./native-mount-DjYEnO4X.js";
6
+ import { n as setRequestRouteData } from "./route-store-DdxGePj2.js";
6
7
  import { n as deactivateHealthChecks, t as activateHealthChecks } from "./store-8pnTxM1x.js";
7
8
  import { n as MIME_TYPES, t as COMPRESSIBLE } from "./mime-C_GwZovh.js";
8
9
  import fs, { createReadStream } from "node:fs";
@@ -135,14 +136,17 @@ function createAppHandler(app, options, client) {
135
136
  return;
136
137
  }
137
138
  const routeData = app.match(request, true);
138
- await writeResponse(routeData && !(routeData.type === "page" && routeData.prerender) ? await app.render(request, {
139
+ const matched = routeData && !(routeData.type === "page" && routeData.prerender) ? routeData : void 0;
140
+ if (matched) setRequestRouteData(request, matched);
141
+ const response = matched ? await app.render(request, {
139
142
  addCookieHeader: true,
140
- routeData,
143
+ routeData: matched,
141
144
  prerenderedErrorPageFetch
142
145
  }) : await app.render(request, {
143
146
  addCookieHeader: true,
144
147
  prerenderedErrorPageFetch
145
- }), res);
148
+ });
149
+ await writeResponse(response, res);
146
150
  };
147
151
  }
148
152
  //#endregion
@@ -217,14 +221,12 @@ function createStaticHandler(app, client) {
217
221
  case "ignore":
218
222
  if (dir && !hasSlash) pathname = `${urlPath}/index.html`;
219
223
  break;
220
- case "always":
221
- if (!hasSlash && !hasFileExtension(urlPath) && !urlPath.startsWith("/_")) {
222
- res.statusCode = 301;
223
- res.setHeader("Location", `${urlPath}/${urlQuery ? `?${urlQuery}` : ""}`);
224
- res.end();
225
- return;
226
- }
227
- break;
224
+ case "always": if (!hasSlash && !hasFileExtension(urlPath) && !urlPath.startsWith("/_")) {
225
+ res.statusCode = 301;
226
+ res.setHeader("Location", `${urlPath}/${urlQuery ? `?${urlQuery}` : ""}`);
227
+ res.end();
228
+ return;
229
+ }
228
230
  }
229
231
  pathname = prependForwardSlash(app.removeBase(pathname));
230
232
  const normalizedPathname = path.posix.normalize(pathname);
@@ -279,8 +281,10 @@ async function warmupModules() {
279
281
  app.manifest.sessionDriver,
280
282
  app.manifest.serverIslandMappings
281
283
  ].filter((load) => load !== void 0);
282
- const results = await Promise.allSettled(loaders.map((load) => load()));
283
- for (const result of results) if (result.status === "rejected") log.error(result.reason instanceof Error ? { err: result.reason } : { reason: result.reason }, "warmup import failed");
284
+ const failures = (await Promise.allSettled(loaders.map((load) => load()))).filter((result) => result.status === "rejected");
285
+ if (failures.length === 0) return;
286
+ for (const failure of failures) log.error(failure.reason instanceof Error ? { err: failure.reason } : { reason: failure.reason }, "warmup import failed");
287
+ throw new AggregateError(failures.map((failure) => failure.reason), "warmup import failed");
284
288
  }
285
289
  /**
286
290
  * TLS tokens from `SERVER_CERT_PATH` / `SERVER_KEY_PATH` (same contract as
@@ -344,6 +348,14 @@ async function startServer(overrides) {
344
348
  const warmup = warmupModules().then(() => {
345
349
  warmupMs = roundMs(performance.now() - warmupStartedAt);
346
350
  warmupSpan.span.end();
351
+ }, (error) => {
352
+ warmupMs = roundMs(performance.now() - warmupStartedAt);
353
+ warmupSpan.span.setStatus({
354
+ code: SpanStatusCode.ERROR,
355
+ message: "warmup import failed"
356
+ });
357
+ warmupSpan.span.end();
358
+ return error;
347
359
  });
348
360
  const shutdownLifecycle = async (shutdownContext) => {
349
361
  try {
@@ -355,7 +367,11 @@ async function startServer(overrides) {
355
367
  clearNativeMounts();
356
368
  if (health) {
357
369
  deactivateHealthChecks();
358
- await server.stop();
370
+ try {
371
+ await server.stop();
372
+ } catch (err) {
373
+ log.debug(err instanceof Error ? { err } : { reason: err }, "health probe server stop failed");
374
+ }
359
375
  }
360
376
  };
361
377
  const failStartup = async (err, message) => {
@@ -377,7 +393,8 @@ async function startServer(overrides) {
377
393
  } catch (err) {
378
394
  await failStartup(err, "startup failed");
379
395
  }
380
- await warmup;
396
+ const warmupError = await warmup;
397
+ if (warmupError !== void 0) await failStartup(warmupError, "warmup import failed");
381
398
  if (health) probes.startup.enable();
382
399
  const client = resolveClientDir(runtimeOptions, import.meta.url);
383
400
  const appHandler = createAppHandler(app, runtimeOptions, client);