@astroscope/node 2.1.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +8 -5
  2. package/dist/boot.d.ts +2 -2
  3. package/dist/boot.d.ts.map +1 -1
  4. package/dist/csrf-middleware-entrypoint.d.ts +1 -2
  5. package/dist/csrf-middleware-entrypoint.d.ts.map +1 -1
  6. package/dist/csrf-middleware-entrypoint.js +1 -1
  7. package/dist/dev-middleware-entrypoint.d.ts +1 -2
  8. package/dist/dev-middleware-entrypoint.d.ts.map +1 -1
  9. package/dist/{prepare-CXZsyAVk.js → duplicate-slashes-C6hmOLl9.js} +94 -38
  10. package/dist/duplicate-slashes-C6hmOLl9.js.map +1 -0
  11. package/dist/{excludes-pE23EbmQ.js → excludes-10eDopVB.js} +18 -4
  12. package/dist/excludes-10eDopVB.js.map +1 -0
  13. package/dist/{excludes-BDiE3eyp.d.ts → excludes-rvkVGg6B.d.ts} +11 -3
  14. package/dist/excludes-rvkVGg6B.d.ts.map +1 -0
  15. package/dist/excludes.d.ts +2 -2
  16. package/dist/excludes.js +2 -2
  17. package/dist/graph-DdAxiaxa.js +35 -0
  18. package/dist/graph-DdAxiaxa.js.map +1 -0
  19. package/dist/health.d.ts +3 -4
  20. package/dist/health.d.ts.map +1 -1
  21. package/dist/image-endpoint.d.ts +1 -2
  22. package/dist/image-endpoint.d.ts.map +1 -1
  23. package/dist/index.d.ts +4 -4
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +39 -118
  26. package/dist/index.js.map +1 -1
  27. package/dist/islands-middleware-entrypoint.d.ts +1 -2
  28. package/dist/islands-middleware-entrypoint.d.ts.map +1 -1
  29. package/dist/islands-middleware-entrypoint.js +5 -3
  30. package/dist/islands-middleware-entrypoint.js.map +1 -1
  31. package/dist/islands.d.ts +57 -2
  32. package/dist/islands.d.ts.map +1 -0
  33. package/dist/islands.js +4 -2
  34. package/dist/log/index.d.ts +5 -5
  35. package/dist/log/index.d.ts.map +1 -1
  36. package/dist/native.d.ts +5 -6
  37. package/dist/native.d.ts.map +1 -1
  38. package/dist/{prerendered-DVjzWNbW.js → prerendered-Z-Qi-FFK.js} +4 -4
  39. package/dist/prerendered-Z-Qi-FFK.js.map +1 -0
  40. package/dist/request-route-DcnZOOM4.js.map +1 -1
  41. package/dist/route-islands-B53rE5MH.js +73 -0
  42. package/dist/route-islands-B53rE5MH.js.map +1 -0
  43. package/dist/route-middleware-entrypoint.d.ts +1 -2
  44. package/dist/route-middleware-entrypoint.d.ts.map +1 -1
  45. package/dist/{route-store-neUA2nBb.d.ts → route-store-Cl3OT0-q.d.ts} +7 -3
  46. package/dist/route-store-Cl3OT0-q.d.ts.map +1 -0
  47. package/dist/{route-store-DdxGePj2.js → route-store-DVSmOa34.js} +14 -4
  48. package/dist/route-store-DVSmOa34.js.map +1 -0
  49. package/dist/routes-D1o_WrJ5.js +39 -0
  50. package/dist/routes-D1o_WrJ5.js.map +1 -0
  51. package/dist/server.d.ts +2 -3
  52. package/dist/server.d.ts.map +1 -1
  53. package/dist/server.js +73 -47
  54. package/dist/server.js.map +1 -1
  55. package/dist/telemetry-B9aziFyQ.js +202 -0
  56. package/dist/telemetry-B9aziFyQ.js.map +1 -0
  57. package/dist/telemetry.d.ts +68 -0
  58. package/dist/telemetry.d.ts.map +1 -0
  59. package/dist/telemetry.js +2 -0
  60. package/dist/transform-Bz5z5w2Y.js +594 -0
  61. package/dist/transform-Bz5z5w2Y.js.map +1 -0
  62. package/package.json +13 -11
  63. package/dist/excludes-BDiE3eyp.d.ts.map +0 -1
  64. package/dist/excludes-pE23EbmQ.js.map +0 -1
  65. package/dist/prepare-CXZsyAVk.js.map +0 -1
  66. package/dist/prerendered-DVjzWNbW.js.map +0 -1
  67. package/dist/route-store-DdxGePj2.js.map +0 -1
  68. package/dist/route-store-neUA2nBb.d.ts.map +0 -1
  69. package/dist/transform-BqV8SOEm.js +0 -195
  70. package/dist/transform-BqV8SOEm.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { t as createIslandsTransformer } from "./transform-BqV8SOEm.js";
1
+ import { t as createIslandsTransformer } from "./transform-Bz5z5w2Y.js";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  //#region src/islands/prerendered.ts
@@ -14,13 +14,13 @@ function* walkHtmlFiles(dir) {
14
14
  * once, at build time, before the client dir is compressed. Runs without a request
15
15
  * context, so registered emitters that need one contribute nothing here.
16
16
  */
17
- async function transformPrerenderedHtml(clientDir, manifest) {
17
+ function transformPrerenderedHtml(clientDir, manifest) {
18
18
  const transformer = createIslandsTransformer(manifest);
19
19
  let transformed = 0;
20
20
  for (const file of walkHtmlFiles(clientDir)) {
21
21
  const html = fs.readFileSync(file, "utf-8");
22
22
  const rewriter = transformer.createDocumentRewriter();
23
- const result = rewriter.write(html) + await rewriter.end();
23
+ const result = rewriter.write(html) + rewriter.end();
24
24
  if (result !== html) {
25
25
  fs.writeFileSync(file, result);
26
26
  transformed++;
@@ -31,4 +31,4 @@ async function transformPrerenderedHtml(clientDir, manifest) {
31
31
  //#endregion
32
32
  export { transformPrerenderedHtml };
33
33
 
34
- //# sourceMappingURL=prerendered-DVjzWNbW.js.map
34
+ //# sourceMappingURL=prerendered-Z-Qi-FFK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prerendered-Z-Qi-FFK.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 function transformPrerenderedHtml(clientDir: string, manifest: IslandsManifest): 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) + 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,SAAgB,yBAAyB,WAAmB,UAAmC;CAC7F,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,IAAI,SAAS,IAAI;EAEnD,IAAI,WAAW,MAAM;GACnB,GAAG,cAAc,MAAM,MAAM;GAC7B;EACF;CACF;CAEA,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"request-route-DcnZOOM4.js","names":[],"sources":["../src/observability/log/store.ts","../src/observability/request-route.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { Logger } from 'pino';\n\n/**\n * Shared state between the public `log` proxy and the server runtime. Keyed on\n * `globalThis` because the two sides may live in different module instances\n * (bundled app vs vite module runner in dev).\n */\n\nconst STORE_KEY = Symbol.for('@astroscope/node/log');\n\nexport const EARLY_LOG_BUFFER_CAP = 100;\n\nexport interface BufferedEntry {\n level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';\n bindings: Record<string, unknown>[];\n args: unknown[];\n time: number;\n}\n\nexport interface RequestRecord {\n logger: Logger | undefined;\n url: string;\n method: string;\n route: string | undefined;\n routeOverride: boolean;\n actionName: string | undefined;\n}\n\nexport interface LogStore {\n root: Logger | undefined;\n requestStorage: AsyncLocalStorage<RequestRecord>;\n buffer: BufferedEntry[];\n dropped: number;\n}\n\nexport function getLogStore(): LogStore {\n const g = globalThis as Record<symbol, unknown>;\n let store = g[STORE_KEY] as LogStore | undefined;\n\n if (!store) {\n store = { root: undefined, requestStorage: new AsyncLocalStorage(), buffer: [], dropped: 0 };\n g[STORE_KEY] = store;\n }\n\n return store;\n}\n\nexport function getRequestRecord(): RequestRecord | undefined {\n return getLogStore().requestStorage.getStore();\n}\n","import { trace } from '@opentelemetry/api';\nimport { getRequestRecord } from './log/store.js';\n\n/**\n * Apply a route label to the request record (final log line, request duration\n * metric) and the active server span.\n *\n * Last write wins — astro's routing stamps every pass, so a rewrite lands the\n * route that rendered. An override comes from the middleware that knows what it\n * served and outranks routing, whatever the middleware order.\n */\nfunction applyRoute(route: string, method: string, override: boolean): void {\n const record = getRequestRecord();\n\n if (record) {\n if (!override && record.routeOverride) return;\n\n record.route = route;\n\n if (override) {\n record.routeOverride = true;\n }\n }\n\n const span = trace.getActiveSpan();\n\n if (span?.isRecording()) {\n span.setAttribute('http.route', route);\n\n if (!record?.actionName) {\n span.updateName(`${method} ${route}`);\n }\n }\n}\n\n/**\n * Report the route that actually served the current request, overriding the one\n * astro's routing matched.\n *\n * A middleware that rewrites (`next(url)`) or answers with its own response\n * serves a request astro has no page for, so routing matches `/404` and that is\n * what the request is logged and measured as — every such request collapsing\n * into one `/404` bucket in the request metrics. Calling this corrects the log\n * line, the metric and the server span name together.\n *\n * Pass a templated label rather than a concrete path, so metric cardinality\n * stays bounded. No-op outside instrumented requests.\n *\n * @example\n * ```ts\n * import { overrideRequestRoute } from '@astroscope/node/log';\n *\n * export const onRequest: MiddlewareHandler = (ctx, next) => {\n * const page = lookupPage(ctx.url.pathname);\n *\n * if (!page) return next();\n *\n * overrideRequestRoute('/cms/pages/[id]');\n *\n * return next(`/cms/pages/${page.id}`);\n * };\n * ```\n */\nexport function overrideRequestRoute(route: string): void {\n applyRoute(route, getRequestRecord()?.method ?? 'GET', true);\n}\n\n/**\n * Stamp the route astro's routing matched, unless it was overridden by the\n * middleware that served the request.\n * @internal\n */\nexport function setRequestRoute(route: string, method: string): void {\n applyRoute(route, method, false);\n}\n"],"mappings":";;;;;;;;AASA,MAAM,YAAY,OAAO,IAAI,sBAAsB;AA2BnD,SAAgB,cAAwB;CACtC,MAAM,IAAI;CACV,IAAI,QAAQ,EAAE;CAEd,IAAI,CAAC,OAAO;EACV,QAAQ;GAAE,MAAM,KAAA;GAAW,gBAAgB,IAAI,kBAAkB;GAAG,QAAQ,CAAC;GAAG,SAAS;EAAE;EAC3F,EAAE,aAAa;CACjB;CAEA,OAAO;AACT;AAEA,SAAgB,mBAA8C;CAC5D,OAAO,YAAY,CAAC,CAAC,eAAe,SAAS;AAC/C;;;;;;;;;;;ACvCA,SAAS,WAAW,OAAe,QAAgB,UAAyB;CAC1E,MAAM,SAAS,iBAAiB;CAEhC,IAAI,QAAQ;EACV,IAAI,CAAC,YAAY,OAAO,eAAe;EAEvC,OAAO,QAAQ;EAEf,IAAI,UACF,OAAO,gBAAgB;CAE3B;CAEA,MAAM,OAAO,MAAM,cAAc;CAEjC,IAAI,MAAM,YAAY,GAAG;EACvB,KAAK,aAAa,cAAc,KAAK;EAErC,IAAI,CAAC,QAAQ,YACX,KAAK,WAAW,GAAG,OAAO,GAAG,OAAO;CAExC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,qBAAqB,OAAqB;CACxD,WAAW,OAAO,iBAAiB,CAAC,EAAE,UAAU,OAAO,IAAI;AAC7D;;;;;;AAOA,SAAgB,gBAAgB,OAAe,QAAsB;CACnE,WAAW,OAAO,QAAQ,KAAK;AACjC"}
1
+ {"version":3,"file":"request-route-DcnZOOM4.js","names":[],"sources":["../src/observability/log/store.ts","../src/observability/request-route.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { Logger } from 'pino';\n\n/**\n * Shared state between the public `log` proxy and the server runtime. Keyed on\n * `globalThis` because the two sides may live in different module instances\n * (bundled app vs vite module runner in dev).\n */\n\nconst STORE_KEY = Symbol.for('@astroscope/node/log');\n\nexport const EARLY_LOG_BUFFER_CAP = 100;\n\nexport interface BufferedEntry {\n level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';\n bindings: Record<string, unknown>[];\n args: unknown[];\n time: number;\n}\n\nexport interface RequestRecord {\n logger: Logger | undefined;\n url: string;\n method: string;\n route: string | undefined;\n routeOverride: boolean;\n actionName: string | undefined;\n truncated: boolean;\n}\n\nexport interface LogStore {\n root: Logger | undefined;\n requestStorage: AsyncLocalStorage<RequestRecord>;\n buffer: BufferedEntry[];\n dropped: number;\n}\n\nexport function getLogStore(): LogStore {\n const g = globalThis as Record<symbol, unknown>;\n let store = g[STORE_KEY] as LogStore | undefined;\n\n if (!store) {\n store = { root: undefined, requestStorage: new AsyncLocalStorage(), buffer: [], dropped: 0 };\n g[STORE_KEY] = store;\n }\n\n return store;\n}\n\nexport function getRequestRecord(): RequestRecord | undefined {\n return getLogStore().requestStorage.getStore();\n}\n","import { trace } from '@opentelemetry/api';\nimport { getRequestRecord } from './log/store.js';\n\n/**\n * Apply a route label to the request record (final log line, request duration\n * metric) and the active server span.\n *\n * Last write wins — astro's routing stamps every pass, so a rewrite lands the\n * route that rendered. An override comes from the middleware that knows what it\n * served and outranks routing, whatever the middleware order.\n */\nfunction applyRoute(route: string, method: string, override: boolean): void {\n const record = getRequestRecord();\n\n if (record) {\n if (!override && record.routeOverride) return;\n\n record.route = route;\n\n if (override) {\n record.routeOverride = true;\n }\n }\n\n const span = trace.getActiveSpan();\n\n if (span?.isRecording()) {\n span.setAttribute('http.route', route);\n\n if (!record?.actionName) {\n span.updateName(`${method} ${route}`);\n }\n }\n}\n\n/**\n * Report the route that actually served the current request, overriding the one\n * astro's routing matched.\n *\n * A middleware that rewrites (`next(url)`) or answers with its own response\n * serves a request astro has no page for, so routing matches `/404` and that is\n * what the request is logged and measured as — every such request collapsing\n * into one `/404` bucket in the request metrics. Calling this corrects the log\n * line, the metric and the server span name together.\n *\n * Pass a templated label rather than a concrete path, so metric cardinality\n * stays bounded. No-op outside instrumented requests.\n *\n * @example\n * ```ts\n * import { overrideRequestRoute } from '@astroscope/node/log';\n *\n * export const onRequest: MiddlewareHandler = (ctx, next) => {\n * const page = lookupPage(ctx.url.pathname);\n *\n * if (!page) return next();\n *\n * overrideRequestRoute('/cms/pages/[id]');\n *\n * return next(`/cms/pages/${page.id}`);\n * };\n * ```\n */\nexport function overrideRequestRoute(route: string): void {\n applyRoute(route, getRequestRecord()?.method ?? 'GET', true);\n}\n\n/**\n * Stamp the route astro's routing matched, unless it was overridden by the\n * middleware that served the request.\n * @internal\n */\nexport function setRequestRoute(route: string, method: string): void {\n applyRoute(route, method, false);\n}\n"],"mappings":";;;;;;;;AASA,MAAM,YAAY,OAAO,IAAI,sBAAsB;AA4BnD,SAAgB,cAAwB;CACtC,MAAM,IAAI;CACV,IAAI,QAAQ,EAAE;CAEd,IAAI,CAAC,OAAO;EACV,QAAQ;GAAE,MAAM,KAAA;GAAW,gBAAgB,IAAI,kBAAkB;GAAG,QAAQ,CAAC;GAAG,SAAS;EAAE;EAC3F,EAAE,aAAa;CACjB;CAEA,OAAO;AACT;AAEA,SAAgB,mBAA8C;CAC5D,OAAO,YAAY,CAAC,CAAC,eAAe,SAAS;AAC/C;;;;;;;;;;;ACxCA,SAAS,WAAW,OAAe,QAAgB,UAAyB;CAC1E,MAAM,SAAS,iBAAiB;CAEhC,IAAI,QAAQ;EACV,IAAI,CAAC,YAAY,OAAO,eAAe;EAEvC,OAAO,QAAQ;EAEf,IAAI,UACF,OAAO,gBAAgB;CAE3B;CAEA,MAAM,OAAO,MAAM,cAAc;CAEjC,IAAI,MAAM,YAAY,GAAG;EACvB,KAAK,aAAa,cAAc,KAAK;EAErC,IAAI,CAAC,QAAQ,YACX,KAAK,WAAW,GAAG,OAAO,GAAG,OAAO;CAExC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,qBAAqB,OAAqB;CACxD,WAAW,OAAO,iBAAiB,CAAC,EAAE,UAAU,OAAO,IAAI;AAC7D;;;;;;AAOA,SAAgB,gBAAgB,OAAe,QAAsB;CACnE,WAAW,OAAO,QAAQ,KAAK;AACjC"}
@@ -0,0 +1,73 @@
1
+ import path from "node:path";
2
+ //#region src/islands/route-islands.ts
3
+ const ACTIONS_ROUTE_PATTERN = "/_actions/[...path]";
4
+ const ACTIONS_ENTRYPOINT_MODULE_ID = "\0virtual:astro:actions/entrypoint";
5
+ const WALK_BOUNDARY_MODULE_IDS = /* @__PURE__ */ new Set(["\0virtual:astro:manifest"]);
6
+ /**
7
+ * Route modules (absolute ids) → the route patterns they serve, for the walk:
8
+ * pages and endpoints (endpoints have no islands, but consumers still want them
9
+ * as known routes), one component possibly backing several routes.
10
+ */
11
+ function routeEntrypoints(root, routes) {
12
+ const entrypoints = /* @__PURE__ */ new Map();
13
+ for (const route of routes) {
14
+ if (route.type !== "page" && route.type !== "endpoint") continue;
15
+ const id = route.pattern === ACTIONS_ROUTE_PATTERN ? ACTIONS_ENTRYPOINT_MODULE_ID : path.resolve(root, route.entrypoint);
16
+ entrypoints.set(id, [...entrypoints.get(id) ?? [], route.pattern]);
17
+ }
18
+ return entrypoints;
19
+ }
20
+ /** module ids can carry vite queries (`?astro&type=...`) — the file path is the identity */
21
+ function stripQuery(id) {
22
+ const at = id.indexOf("?");
23
+ return at === -1 ? id : id.slice(0, at);
24
+ }
25
+ /**
26
+ * `pages` maps a page component's absolute path to the patterns of the routes it
27
+ * serves (one component can back several routes).
28
+ */
29
+ async function collectRouteIslands(graph, pages) {
30
+ const patternsByModule = /* @__PURE__ */ new Map();
31
+ const routes = /* @__PURE__ */ new Map();
32
+ const islandsByModule = /* @__PURE__ */ new Map();
33
+ for (const id of graph.getModuleIds()) {
34
+ const meta = graph.getModuleInfo(id)?.meta?.["astro"];
35
+ if (!meta) continue;
36
+ const islands = [];
37
+ for (const component of [...meta.hydratedComponents ?? [], ...meta.clientOnlyComponents ?? []]) if (component.resolvedPath) islands.push(stripQuery(decodeURI(component.resolvedPath)));
38
+ else {
39
+ const resolved = await graph.resolve(component.specifier, id);
40
+ if (resolved) islands.push(stripQuery(resolved.id));
41
+ }
42
+ if (islands.length > 0) islandsByModule.set(id, islands);
43
+ }
44
+ for (const [page, patterns] of pages) {
45
+ if (!graph.getModuleInfo(page)) continue;
46
+ const islands = /* @__PURE__ */ new Set();
47
+ const visited = /* @__PURE__ */ new Set();
48
+ const stack = [page];
49
+ while (stack.length > 0) {
50
+ const id = stack.pop();
51
+ if (visited.has(id) || WALK_BOUNDARY_MODULE_IDS.has(id)) continue;
52
+ visited.add(id);
53
+ let owners = patternsByModule.get(id);
54
+ if (!owners) {
55
+ owners = /* @__PURE__ */ new Set();
56
+ patternsByModule.set(id, owners);
57
+ }
58
+ patterns.forEach((pattern) => owners.add(pattern));
59
+ islandsByModule.get(id)?.forEach((island) => islands.add(island));
60
+ const moduleInfo = graph.getModuleInfo(id);
61
+ if (moduleInfo) stack.push(...moduleInfo.importedIds, ...moduleInfo.dynamicallyImportedIds);
62
+ }
63
+ for (const pattern of patterns) routes.set(pattern, islands);
64
+ }
65
+ return {
66
+ routes,
67
+ patternsByModule
68
+ };
69
+ }
70
+ //#endregion
71
+ export { routeEntrypoints as n, stripQuery as r, collectRouteIslands as t };
72
+
73
+ //# sourceMappingURL=route-islands-B53rE5MH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-islands-B53rE5MH.js","names":[],"sources":["../src/islands/route-islands.ts"],"sourcesContent":["import path from 'node:path';\nimport type { IntegrationResolvedRoute } from 'astro';\nimport type { Rolldown } from 'vite';\n\n/**\n * Build-time attribution of islands to routes, from the server build's module\n * graph: every module reachable from a page component belongs to that page's\n * routes, and the hydrated / `client:only` components astro's compiler recorded\n * on such a module are the route's islands. Same walk astro does for its own\n * client entries, kept independent of its internals — page modules are matched by\n * the route entrypoints astro resolved, not by its virtual page wrappers.\n */\n\ntype AstroComponentMeta = { exportName: string; specifier: string; resolvedPath?: string | undefined };\n\ntype AstroModuleMeta = {\n hydratedComponents?: AstroComponentMeta[] | undefined;\n clientOnlyComponents?: AstroComponentMeta[] | undefined;\n};\n\nexport type ModuleGraph = {\n getModuleIds(): IterableIterator<string>;\n getModuleInfo(id: string): Rolldown.ModuleInfo | null;\n resolve(source: string, importer: string): Promise<{ id: string } | null>;\n};\n\nexport type RouteIslandsResult = {\n /** route pattern → island component ids (absolute module ids, query stripped); pages missing from the graph are absent */\n routes: Map<string, Set<string>>;\n /** module id → route patterns whose page reaches it, the whole server module graph */\n patternsByModule: Map<string, Set<string>>;\n};\n\n// astro's actions route module reaches the project's actions through the ssr\n// manifest, not through an import — the virtual entrypoint wrapping them is the\n// module the walk has to start from\nconst ACTIONS_ROUTE_PATTERN = '/_actions/[...path]';\nconst ACTIONS_ENTRYPOINT_MODULE_ID = '\\0virtual:astro:actions/entrypoint';\n\n// astro's application manifest imports every page and the middleware\nconst WALK_BOUNDARY_MODULE_IDS = new Set(['\\0virtual:astro:manifest']);\n\n/**\n * Route modules (absolute ids) → the route patterns they serve, for the walk:\n * pages and endpoints (endpoints have no islands, but consumers still want them\n * as known routes), one component possibly backing several routes.\n */\nexport function routeEntrypoints(root: string, routes: readonly IntegrationResolvedRoute[]): Map<string, string[]> {\n const entrypoints = new Map<string, string[]>();\n\n for (const route of routes) {\n if (route.type !== 'page' && route.type !== 'endpoint') continue;\n\n const id =\n route.pattern === ACTIONS_ROUTE_PATTERN ? ACTIONS_ENTRYPOINT_MODULE_ID : path.resolve(root, route.entrypoint);\n\n entrypoints.set(id, [...(entrypoints.get(id) ?? []), route.pattern]);\n }\n\n return entrypoints;\n}\n\n/** module ids can carry vite queries (`?astro&type=...`) — the file path is the identity */\nexport function stripQuery(id: string): string {\n const at = id.indexOf('?');\n\n return at === -1 ? id : id.slice(0, at);\n}\n\n/**\n * `pages` maps a page component's absolute path to the patterns of the routes it\n * serves (one component can back several routes).\n */\nexport async function collectRouteIslands(\n graph: ModuleGraph,\n pages: Map<string, string[]>,\n): Promise<RouteIslandsResult> {\n const patternsByModule = new Map<string, Set<string>>();\n const routes = new Map<string, Set<string>>();\n const islandsByModule = new Map<string, string[]>();\n\n for (const id of graph.getModuleIds()) {\n const meta = graph.getModuleInfo(id)?.meta?.['astro'] as AstroModuleMeta | undefined;\n\n if (!meta) {\n continue;\n }\n\n const islands: string[] = [];\n\n for (const component of [...(meta.hydratedComponents ?? []), ...(meta.clientOnlyComponents ?? [])]) {\n if (component.resolvedPath) {\n islands.push(stripQuery(decodeURI(component.resolvedPath)));\n } else {\n // `client:only` components are dropped from the server graph — resolve the specifier ourselves\n const resolved = await graph.resolve(component.specifier, id);\n\n if (resolved) {\n islands.push(stripQuery(resolved.id));\n }\n }\n }\n\n if (islands.length > 0) {\n islandsByModule.set(id, islands);\n }\n }\n\n for (const [page, patterns] of pages) {\n const info = graph.getModuleInfo(page);\n\n if (!info) {\n continue;\n }\n\n const islands = new Set<string>();\n const visited = new Set<string>();\n const stack = [page];\n\n while (stack.length > 0) {\n const id = stack.pop()!;\n\n if (visited.has(id) || WALK_BOUNDARY_MODULE_IDS.has(id)) {\n continue;\n }\n\n visited.add(id);\n\n let owners = patternsByModule.get(id);\n\n if (!owners) {\n owners = new Set();\n patternsByModule.set(id, owners);\n }\n\n patterns.forEach((pattern) => owners.add(pattern));\n islandsByModule.get(id)?.forEach((island) => islands.add(island));\n\n const moduleInfo = graph.getModuleInfo(id);\n\n if (moduleInfo) {\n stack.push(...moduleInfo.importedIds, ...moduleInfo.dynamicallyImportedIds);\n }\n }\n\n for (const pattern of patterns) {\n routes.set(pattern, islands);\n }\n }\n\n return { routes, patternsByModule };\n}\n"],"mappings":";;AAoCA,MAAM,wBAAwB;AAC9B,MAAM,+BAA+B;AAGrC,MAAM,2CAA2B,IAAI,IAAI,CAAC,0BAA0B,CAAC;;;;;;AAOrE,SAAgB,iBAAiB,MAAc,QAAoE;CACjH,MAAM,8BAAc,IAAI,IAAsB;CAE9C,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,MAAM,SAAS,UAAU,MAAM,SAAS,YAAY;EAExD,MAAM,KACJ,MAAM,YAAY,wBAAwB,+BAA+B,KAAK,QAAQ,MAAM,MAAM,UAAU;EAE9G,YAAY,IAAI,IAAI,CAAC,GAAI,YAAY,IAAI,EAAE,KAAK,CAAC,GAAI,MAAM,OAAO,CAAC;CACrE;CAEA,OAAO;AACT;;AAGA,SAAgB,WAAW,IAAoB;CAC7C,MAAM,KAAK,GAAG,QAAQ,GAAG;CAEzB,OAAO,OAAO,KAAK,KAAK,GAAG,MAAM,GAAG,EAAE;AACxC;;;;;AAMA,eAAsB,oBACpB,OACA,OAC6B;CAC7B,MAAM,mCAAmB,IAAI,IAAyB;CACtD,MAAM,yBAAS,IAAI,IAAyB;CAC5C,MAAM,kCAAkB,IAAI,IAAsB;CAElD,KAAK,MAAM,MAAM,MAAM,aAAa,GAAG;EACrC,MAAM,OAAO,MAAM,cAAc,EAAE,CAAC,EAAE,OAAO;EAE7C,IAAI,CAAC,MACH;EAGF,MAAM,UAAoB,CAAC;EAE3B,KAAK,MAAM,aAAa,CAAC,GAAI,KAAK,sBAAsB,CAAC,GAAI,GAAI,KAAK,wBAAwB,CAAC,CAAE,GAC/F,IAAI,UAAU,cACZ,QAAQ,KAAK,WAAW,UAAU,UAAU,YAAY,CAAC,CAAC;OACrD;GAEL,MAAM,WAAW,MAAM,MAAM,QAAQ,UAAU,WAAW,EAAE;GAE5D,IAAI,UACF,QAAQ,KAAK,WAAW,SAAS,EAAE,CAAC;EAExC;EAGF,IAAI,QAAQ,SAAS,GACnB,gBAAgB,IAAI,IAAI,OAAO;CAEnC;CAEA,KAAK,MAAM,CAAC,MAAM,aAAa,OAAO;EAGpC,IAAI,CAFS,MAAM,cAAc,IAEzB,GACN;EAGF,MAAM,0BAAU,IAAI,IAAY;EAChC,MAAM,0BAAU,IAAI,IAAY;EAChC,MAAM,QAAQ,CAAC,IAAI;EAEnB,OAAO,MAAM,SAAS,GAAG;GACvB,MAAM,KAAK,MAAM,IAAI;GAErB,IAAI,QAAQ,IAAI,EAAE,KAAK,yBAAyB,IAAI,EAAE,GACpD;GAGF,QAAQ,IAAI,EAAE;GAEd,IAAI,SAAS,iBAAiB,IAAI,EAAE;GAEpC,IAAI,CAAC,QAAQ;IACX,yBAAS,IAAI,IAAI;IACjB,iBAAiB,IAAI,IAAI,MAAM;GACjC;GAEA,SAAS,SAAS,YAAY,OAAO,IAAI,OAAO,CAAC;GACjD,gBAAgB,IAAI,EAAE,CAAC,EAAE,SAAS,WAAW,QAAQ,IAAI,MAAM,CAAC;GAEhE,MAAM,aAAa,MAAM,cAAc,EAAE;GAEzC,IAAI,YACF,MAAM,KAAK,GAAG,WAAW,aAAa,GAAG,WAAW,sBAAsB;EAE9E;EAEA,KAAK,MAAM,WAAW,UACpB,OAAO,IAAI,SAAS,OAAO;CAE/B;CAEA,OAAO;EAAE;EAAQ;CAAiB;AACpC"}
@@ -8,7 +8,6 @@ import { MiddlewareHandler } from "astro";
8
8
  * reports it itself via `overrideRequestRoute`, which wins over this. No-op
9
9
  * outside instrumented requests.
10
10
  */
11
- declare const onRequest: MiddlewareHandler;
11
+ export declare const onRequest: MiddlewareHandler;
12
12
  //#endregion
13
- export { onRequest };
14
13
  //# sourceMappingURL=route-middleware-entrypoint.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"route-middleware-entrypoint.d.ts","names":[],"sources":["../src/observability/route-middleware-entrypoint.ts"],"mappings":";;;;;;;;;;cAWa,WAAW"}
1
+ {"version":3,"file":"route-middleware-entrypoint.d.ts","names":[],"sources":["../src/observability/route-middleware-entrypoint.ts"],"mappings":";;;;;;;;;;qBAWa,WAAW"}
@@ -66,8 +66,12 @@ declare function registerIslandEmitter(emitter: IslandEmitter): void;
66
66
  declare function registerDocumentEmitter(emitter: DocumentEmitter): void;
67
67
  //#endregion
68
68
  //#region src/server/route-store.d.ts
69
- declare function setRequestRouteData(request: Request, routeData: RouteData): void;
70
- declare function getRequestRouteData(request: Request): RouteData | undefined;
69
+ declare function setRequestRouteData(request: Request, locals: object, routeData: RouteData): void;
70
+ /**
71
+ * Look the route up by the api context (survives `next(url)` rewrites) or by
72
+ * the request as the handler created it.
73
+ */
74
+ declare function getRequestRouteData(key: Request | Pick<APIContext, 'locals'>): RouteData | undefined;
71
75
  //#endregion
72
76
  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 };
73
- //# sourceMappingURL=route-store-neUA2nBb.d.ts.map
77
+ //# sourceMappingURL=route-store-Cl3OT0-q.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-store-Cl3OT0-q.d.ts","names":[],"sources":["../src/islands/types.ts","../src/islands/emitters.ts","../src/server/route-store.ts"],"mappings":";;;;;;;KA4BY;EACV;EACA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;;KAcU;EACV;EACA;EACA;;;;;;KAOU,iBAAiB,QAAQ,YAAY,UAAU,2BAA2B;;;;KAK1E;EACV;EACA;;;;;;;KAQU,mBAAmB,SAAS,eAAe;;;;;;;;iBC9CvC,sBAAsB,SAAS;;;;;;;iBAc/B,wBAAwB,SAAS;;;iBCfjC,oBAAoB,SAAS,SAAS,gBAAgB,WAAW;;;;;iBASjE,oBAAoB,KAAK,UAAU,KAAK,wBAAwB"}
@@ -7,6 +7,11 @@
7
7
  * `Symbol.for` so the vite-runner and native module instances share it —
8
8
  * same pattern as the log store.
9
9
  *
10
+ * Stored under the request and under its `locals` object. `locals` is the
11
+ * one object astro hands to every middleware unchanged — `context.request`
12
+ * is no identity for a request, as every `next(url)` rewrite replaces it
13
+ * with a copy (once per nesting level of `sequence`).
14
+ *
10
15
  * Absent in dev (no adapter handler) — consumers fall back to their
11
16
  * content-type gates.
12
17
  */
@@ -15,13 +20,18 @@ function store() {
15
20
  const scope = globalThis;
16
21
  return scope[STORE] ??= /* @__PURE__ */ new WeakMap();
17
22
  }
18
- function setRequestRouteData(request, routeData) {
23
+ function setRequestRouteData(request, locals, routeData) {
19
24
  store().set(request, routeData);
25
+ store().set(locals, routeData);
20
26
  }
21
- function getRequestRouteData(request) {
22
- return store().get(request);
27
+ /**
28
+ * Look the route up by the api context (survives `next(url)` rewrites) or by
29
+ * the request as the handler created it.
30
+ */
31
+ function getRequestRouteData(key) {
32
+ return store().get("locals" in key ? key.locals : key);
23
33
  }
24
34
  //#endregion
25
35
  export { setRequestRouteData as n, getRequestRouteData as t };
26
36
 
27
- //# sourceMappingURL=route-store-DdxGePj2.js.map
37
+ //# sourceMappingURL=route-store-DVSmOa34.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-store-DVSmOa34.js","names":[],"sources":["../src/server/route-store.ts"],"sourcesContent":["import type { APIContext, 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 * Stored under the request and under its `locals` object. `locals` is the\n * one object astro hands to every middleware unchanged — `context.request`\n * is no identity for a request, as every `next(url)` rewrite replaces it\n * with a copy (once per nesting level of `sequence`).\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<object, RouteData> };\n\nfunction store(): WeakMap<object, RouteData> {\n const scope = globalThis as Scope;\n\n return (scope[STORE] ??= new WeakMap());\n}\n\nexport function setRequestRouteData(request: Request, locals: object, routeData: RouteData): void {\n store().set(request, routeData);\n store().set(locals, routeData);\n}\n\n/**\n * Look the route up by the api context (survives `next(url)` rewrites) or by\n * the request as the handler created it.\n */\nexport function getRequestRouteData(key: Request | Pick<APIContext, 'locals'>): RouteData | undefined {\n return store().get('locals' in key ? key.locals : key);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,MAAM,QAAQ,OAAO,IAAI,gCAAgC;AAIzD,SAAS,QAAoC;CAC3C,MAAM,QAAQ;CAEd,OAAQ,MAAM,2BAAW,IAAI,QAAQ;AACvC;AAEA,SAAgB,oBAAoB,SAAkB,QAAgB,WAA4B;CAChG,MAAM,CAAC,CAAC,IAAI,SAAS,SAAS;CAC9B,MAAM,CAAC,CAAC,IAAI,QAAQ,SAAS;AAC/B;;;;;AAMA,SAAgB,oBAAoB,KAAkE;CACpG,OAAO,MAAM,CAAC,CAAC,IAAI,YAAY,MAAM,IAAI,SAAS,GAAG;AACvD"}
@@ -0,0 +1,39 @@
1
+ import { t as createChunkGraph } from "./graph-DdAxiaxa.js";
2
+ //#region src/islands/routes.ts
3
+ const STORE = Symbol.for("@astroscope/node.routeIslands");
4
+ function installRouteIslands(manifest) {
5
+ const routes = manifest?.routes;
6
+ if (!routes) {
7
+ globalThis[STORE] = () => null;
8
+ return;
9
+ }
10
+ let graph;
11
+ const cache = /* @__PURE__ */ new Map();
12
+ globalThis[STORE] = (pattern) => {
13
+ const cached = cache.get(pattern);
14
+ if (cached) return cached;
15
+ const fileNames = routes[pattern];
16
+ if (!fileNames) return null;
17
+ graph ??= createChunkGraph(manifest);
18
+ const islands = fileNames.map((fileName) => ({
19
+ fileName,
20
+ staticClosure: [fileName, ...graph.staticClosure(fileName)],
21
+ fullClosure: [fileName, ...graph.fullClosure(fileName)]
22
+ }));
23
+ cache.set(pattern, islands);
24
+ return islands;
25
+ };
26
+ }
27
+ /**
28
+ * The islands a route's page hydrates, from the build-time attribution — `null`
29
+ * when unknown: in dev, without a manifest, or for a route whose page the server
30
+ * build did not see (astro's own injected routes, server islands). Callers
31
+ * treat `null` as "anything", never as "nothing".
32
+ */
33
+ function getRouteIslands(pattern) {
34
+ return globalThis[STORE]?.(pattern) ?? null;
35
+ }
36
+ //#endregion
37
+ export { installRouteIslands as n, getRouteIslands as t };
38
+
39
+ //# sourceMappingURL=routes-D1o_WrJ5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-D1o_WrJ5.js","names":[],"sources":["../src/islands/routes.ts"],"sourcesContent":["import { type ChunkGraph, createChunkGraph } from './graph.js';\nimport type { IslandsManifest } from './types.js';\n\n/**\n * Route → islands at runtime, for packages that must know before rendering what a\n * page will hydrate (per-route data loading). Installed by the islands middleware\n * entry from the manifest; keyed on `globalThis` via `Symbol.for` so the\n * vite-runner and native module instances share it. Closures are dist-relative\n * chunk file names — there is no island tag to observe a public prefix on yet.\n */\nexport type RouteIsland = {\n /** the island's entry chunk, relative to the client dist (e.g. `_astro/Cart.abc123.js`) */\n fileName: string;\n /** the entry plus its transitive static imports */\n staticClosure: string[];\n /** the entry plus its transitive static and dynamic imports */\n fullClosure: string[];\n};\n\ntype Resolver = (pattern: string) => RouteIsland[] | null;\n\nconst STORE = Symbol.for('@astroscope/node.routeIslands');\n\ntype Scope = { [STORE]?: Resolver };\n\nexport function installRouteIslands(manifest: IslandsManifest | null): void {\n const routes = manifest?.routes;\n\n if (!routes) {\n (globalThis as Scope)[STORE] = () => null;\n\n return;\n }\n\n let graph: ChunkGraph | undefined;\n const cache = new Map<string, RouteIsland[]>();\n\n (globalThis as Scope)[STORE] = (pattern) => {\n const cached = cache.get(pattern);\n\n if (cached) {\n return cached;\n }\n\n const fileNames = routes[pattern];\n\n if (!fileNames) {\n return null;\n }\n\n graph ??= createChunkGraph(manifest);\n\n const islands = fileNames.map((fileName) => ({\n fileName,\n staticClosure: [fileName, ...graph!.staticClosure(fileName)],\n fullClosure: [fileName, ...graph!.fullClosure(fileName)],\n }));\n\n cache.set(pattern, islands);\n\n return islands;\n };\n}\n\n/**\n * The islands a route's page hydrates, from the build-time attribution — `null`\n * when unknown: in dev, without a manifest, or for a route whose page the server\n * build did not see (astro's own injected routes, server islands). Callers\n * treat `null` as \"anything\", never as \"nothing\".\n */\nexport function getRouteIslands(pattern: string): RouteIsland[] | null {\n return (globalThis as Scope)[STORE]?.(pattern) ?? null;\n}\n"],"mappings":";;AAqBA,MAAM,QAAQ,OAAO,IAAI,+BAA+B;AAIxD,SAAgB,oBAAoB,UAAwC;CAC1E,MAAM,SAAS,UAAU;CAEzB,IAAI,CAAC,QAAQ;EACX,WAAsB,eAAe;EAErC;CACF;CAEA,IAAI;CACJ,MAAM,wBAAQ,IAAI,IAA2B;CAE7C,WAAsB,UAAU,YAAY;EAC1C,MAAM,SAAS,MAAM,IAAI,OAAO;EAEhC,IAAI,QACF,OAAO;EAGT,MAAM,YAAY,OAAO;EAEzB,IAAI,CAAC,WACH,OAAO;EAGT,UAAU,iBAAiB,QAAQ;EAEnC,MAAM,UAAU,UAAU,KAAK,cAAc;GAC3C;GACA,eAAe,CAAC,UAAU,GAAG,MAAO,cAAc,QAAQ,CAAC;GAC3D,aAAa,CAAC,UAAU,GAAG,MAAO,YAAY,QAAQ,CAAC;EACzD,EAAE;EAEF,MAAM,IAAI,SAAS,OAAO;EAE1B,OAAO;CACT;AACF;;;;;;;AAQA,SAAgB,gBAAgB,SAAuC;CACrE,OAAQ,WAAqB,MAAM,GAAG,OAAO,KAAK;AACpD"}
package/dist/server.d.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  //#region src/server/server.d.ts
2
- interface ServerHandle {
2
+ export interface ServerHandle {
3
3
  host: string;
4
4
  port: number;
5
5
  stop(): Promise<void>;
6
6
  closed(): Promise<void>;
7
7
  }
8
- declare function startServer(overrides?: {
8
+ export declare function startServer(overrides?: {
9
9
  host?: string | undefined;
10
10
  port?: number | undefined;
11
11
  }): Promise<ServerHandle>;
12
12
  //#endregion
13
- export { ServerHandle, startServer };
14
13
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server/server.ts"],"mappings":";iBAsFiB;EACf;EACA;EACA,QAAQ;EACR,UAAU;;wBAGU,YAAY;EAChC;EACA;IACE,QAAQ"}
package/dist/server.js CHANGED
@@ -1,15 +1,17 @@
1
1
  import { n as setBootContext } from "./context-Bkg-FnnQ.js";
2
- import { a as runShutdown, i as createRequestInstrumentation, n as shutdownTelemetry, o as runStartup, r as dumpEarlyLogs, t as preparePlatform } from "./prepare-CXZsyAVk.js";
2
+ import { a as createRequestInstrumentation, i as dumpEarlyLogs, n as preparePlatform, o as runShutdown, r as shutdownTelemetry, s as runStartup, t as redirectDuplicateSlashes } from "./duplicate-slashes-C6hmOLl9.js";
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
+ import { c as withSpan, s as startSpan } from "./telemetry-B9aziFyQ.js";
7
+ import { n as setRequestRouteData } from "./route-store-DVSmOa34.js";
7
8
  import { n as deactivateHealthChecks, t as activateHealthChecks } from "./store-8pnTxM1x.js";
8
9
  import { n as MIME_TYPES, t as COMPRESSIBLE } from "./mime-C_GwZovh.js";
9
10
  import fs, { createReadStream } from "node:fs";
10
11
  import path from "node:path";
11
12
  import url from "node:url";
12
- import { SpanStatusCode, context, trace } from "@opentelemetry/api";
13
+ import { defined } from "@entwico/dash";
14
+ import { SpanStatusCode } from "@opentelemetry/api";
13
15
  import http from "node:http";
14
16
  import https from "node:https";
15
17
  import { checks, probes, server } from "@entwico/health-probes";
@@ -17,39 +19,8 @@ import { createApp } from "astro/app/entrypoint";
17
19
  import { setGetEnv } from "astro/env/setup";
18
20
  import { options } from "virtual:@astroscope/node/config";
19
21
  import { Readable } from "node:stream";
20
- import { createRequestFromNodeRequest, writeResponse } from "astro/app/node";
22
+ import { createRequestFromNodeRequest, getAbortControllerCleanup } from "astro/app/node";
21
23
  import send from "send";
22
- //#region src/observability/telemetry/lifecycle.ts
23
- const LIB_NAME = "@astroscope/node";
24
- /**
25
- * Lifecycle spans (`startup` / `shutdown` with phase children). No-op when no
26
- * SDK is registered — `trace.getTracer` returns the no-op tracer.
27
- */
28
- function startLifecycleSpan(name, parent) {
29
- const parentContext = parent ?? context.active();
30
- const span = trace.getTracer(LIB_NAME).startSpan(name, void 0, parentContext);
31
- return {
32
- span,
33
- context: trace.setSpan(parentContext, span)
34
- };
35
- }
36
- async function withLifecycleSpan(name, parent, fn) {
37
- const { span, context: spanContext } = startLifecycleSpan(name, parent);
38
- try {
39
- const result = await context.with(spanContext, fn);
40
- span.setStatus({ code: SpanStatusCode.OK });
41
- return result;
42
- } catch (err) {
43
- span.setStatus({
44
- code: SpanStatusCode.ERROR,
45
- message: err instanceof Error ? err.message : "unknown error"
46
- });
47
- throw err;
48
- } finally {
49
- span.end();
50
- }
51
- }
52
- //#endregion
53
24
  //#region src/server/client-dir.ts
54
25
  /**
55
26
  * Resolve the client directory at runtime relative to the built server entry.
@@ -93,6 +64,58 @@ async function readFSErrorPage(client, status) {
93
64
  }
94
65
  }
95
66
  }
67
+ function createOutgoingHttpHeaders(headers) {
68
+ const nodeHeaders = Object.fromEntries(headers.entries());
69
+ if (Object.keys(nodeHeaders).length === 0) return;
70
+ const cookies = headers.getSetCookie();
71
+ if (cookies.length > 1) nodeHeaders["set-cookie"] = cookies;
72
+ return nodeHeaders;
73
+ }
74
+ /**
75
+ * Streams the web response into the node response. A render failing after the
76
+ * first chunk cannot change the status anymore, so it is logged through the
77
+ * request logger and marked on the request record — the completion line, the
78
+ * span and `astro.render.failures` reflect it. On the wire it behaves like
79
+ * astro's own writer: an `Internal server error` marker, then the socket is
80
+ * destroyed.
81
+ */
82
+ async function writeResponse(response, res) {
83
+ res.statusMessage = response.statusText;
84
+ res.writeHead(response.status, createOutgoingHttpHeaders(response.headers));
85
+ const cleanupAbort = getAbortControllerCleanup(res.req);
86
+ if (cleanupAbort) {
87
+ const runCleanup = () => {
88
+ cleanupAbort();
89
+ res.off("finish", runCleanup);
90
+ res.off("close", runCleanup);
91
+ };
92
+ res.on("finish", runCleanup);
93
+ res.on("close", runCleanup);
94
+ }
95
+ if (!response.body) {
96
+ res.end();
97
+ return;
98
+ }
99
+ const reader = response.body.getReader();
100
+ res.on("close", () => {
101
+ reader.cancel().catch(() => void 0);
102
+ });
103
+ try {
104
+ for (let result = await reader.read(); !result.done; result = await reader.read()) res.write(result.value);
105
+ res.end();
106
+ } catch (err) {
107
+ const record = getRequestRecord();
108
+ if (record) record.truncated = true;
109
+ log.error({
110
+ ...err instanceof Error ? { err } : { reason: err },
111
+ ...record?.route && { route: record.route },
112
+ ...!record?.logger && { url: record?.url ?? res.req.url }
113
+ }, "render failed after the response started, response truncated");
114
+ res.write("Internal server error", () => {
115
+ res.destroy(err instanceof Error ? err : void 0);
116
+ });
117
+ }
118
+ }
96
119
  /**
97
120
  * Render on-demand routes: node req → web Request → `app.render()` → node res.
98
121
  * Prerendered pages never reach this handler (the static handler serves them);
@@ -137,16 +160,18 @@ function createAppHandler(app, options, client) {
137
160
  }
138
161
  const routeData = app.match(request, true);
139
162
  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, {
163
+ const locals = {};
164
+ if (matched) setRequestRouteData(request, locals, matched);
165
+ await writeResponse(matched ? await app.render(request, {
142
166
  addCookieHeader: true,
167
+ locals,
143
168
  routeData: matched,
144
169
  prerenderedErrorPageFetch
145
170
  }) : await app.render(request, {
146
171
  addCookieHeader: true,
172
+ locals,
147
173
  prerenderedErrorPageFetch
148
- });
149
- await writeResponse(response, res);
174
+ }), res);
150
175
  };
151
176
  }
152
177
  //#endregion
@@ -280,7 +305,7 @@ async function warmupModules() {
280
305
  app.manifest.actions,
281
306
  app.manifest.sessionDriver,
282
307
  app.manifest.serverIslandMappings
283
- ].filter((load) => load !== void 0);
308
+ ].filter(defined);
284
309
  const failures = (await Promise.allSettled(loaders.map((load) => load()))).filter((result) => result.status === "rejected");
285
310
  if (failures.length === 0) return;
286
311
  for (const failure of failures) log.error(failure.reason instanceof Error ? { err: failure.reason } : { reason: failure.reason }, "warmup import failed");
@@ -335,7 +360,7 @@ async function startServer(overrides) {
335
360
  activateHealthChecks(checks);
336
361
  log.debug("health probes listening");
337
362
  }
338
- const startup = startLifecycleSpan("startup");
363
+ const startup = startSpan("startup");
339
364
  log.info({
340
365
  host,
341
366
  port
@@ -344,7 +369,7 @@ async function startServer(overrides) {
344
369
  let bootMs = 0;
345
370
  let warmupMs = 0;
346
371
  const warmupStartedAt = performance.now();
347
- const warmupSpan = startLifecycleSpan("warmup", startup.context);
372
+ const warmupSpan = startSpan("warmup", { parent: startup.context });
348
373
  const warmup = warmupModules().then(() => {
349
374
  warmupMs = roundMs(performance.now() - warmupStartedAt);
350
375
  warmupSpan.span.end();
@@ -359,7 +384,7 @@ async function startServer(overrides) {
359
384
  });
360
385
  const shutdownLifecycle = async (shutdownContext) => {
361
386
  try {
362
- if (shutdownContext) await withLifecycleSpan("onShutdown", shutdownContext, () => runShutdown(bootModule, context));
387
+ if (shutdownContext) await withSpan("onShutdown", { parent: shutdownContext }, () => runShutdown(bootModule, context));
363
388
  else await runShutdown(bootModule, context);
364
389
  } catch (err) {
365
390
  log.error(err instanceof Error ? { err } : { reason: err }, "shutdown failed");
@@ -388,7 +413,7 @@ async function startServer(overrides) {
388
413
  try {
389
414
  const bootStartedAt = performance.now();
390
415
  bootModule = await import("virtual:@astroscope/node/boot");
391
- await withLifecycleSpan("boot", startup.context, () => runStartup(bootModule, context));
416
+ await withSpan("boot", { parent: startup.context }, () => runStartup(bootModule, context));
392
417
  bootMs = roundMs(performance.now() - bootStartedAt);
393
418
  } catch (err) {
394
419
  await failStartup(err, "startup failed");
@@ -418,13 +443,14 @@ async function startServer(overrides) {
418
443
  return;
419
444
  }
420
445
  instrument(req, res, () => {
446
+ if (redirectDuplicateSlashes(req, res)) return;
421
447
  if (dispatchNativeMount(req, res)) return;
422
448
  staticHandler(req, res, () => void appHandler(req, res));
423
449
  });
424
450
  };
425
451
  const server$1 = tls ? https.createServer(tls, listener) : http.createServer(listener);
426
452
  try {
427
- await withLifecycleSpan("listen", startup.context, () => {
453
+ await withSpan("listen", { parent: startup.context }, () => {
428
454
  return new Promise((resolve, reject) => {
429
455
  server$1.once("error", reject);
430
456
  server$1.listen(port, host, resolve);
@@ -454,8 +480,8 @@ async function startServer(overrides) {
454
480
  if (health) probes.ready.disable();
455
481
  log.info("shutdown initiated");
456
482
  const drainStartedAt = performance.now();
457
- const shutdown = startLifecycleSpan("shutdown");
458
- await withLifecycleSpan("drain", shutdown.context, async () => {
483
+ const shutdown = startSpan("shutdown");
484
+ await withSpan("drain", { parent: shutdown.context }, async () => {
459
485
  const closed = new Promise((resolve) => server$1.close(() => resolve()));
460
486
  server$1.closeIdleConnections();
461
487
  const forceTimer = setTimeout(() => server$1.closeAllConnections(), runtimeOptions.shutdownTimeout);