@astroscope/node 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/boot.d.ts +2 -2
- package/dist/boot.d.ts.map +1 -1
- package/dist/csrf-middleware-entrypoint.d.ts +1 -2
- package/dist/csrf-middleware-entrypoint.d.ts.map +1 -1
- package/dist/csrf-middleware-entrypoint.js +1 -1
- package/dist/dev-middleware-entrypoint.d.ts +1 -2
- package/dist/dev-middleware-entrypoint.d.ts.map +1 -1
- package/dist/{prepare-CXZsyAVk.js → duplicate-slashes-C6hmOLl9.js} +94 -38
- package/dist/duplicate-slashes-C6hmOLl9.js.map +1 -0
- package/dist/{excludes-pE23EbmQ.js → excludes-10eDopVB.js} +18 -4
- package/dist/excludes-10eDopVB.js.map +1 -0
- package/dist/{excludes-BDiE3eyp.d.ts → excludes-rvkVGg6B.d.ts} +11 -3
- package/dist/excludes-rvkVGg6B.d.ts.map +1 -0
- package/dist/excludes.d.ts +2 -2
- package/dist/excludes.js +2 -2
- package/dist/graph-DdAxiaxa.js +35 -0
- package/dist/graph-DdAxiaxa.js.map +1 -0
- package/dist/health.d.ts +3 -4
- package/dist/health.d.ts.map +1 -1
- package/dist/image-endpoint.d.ts +1 -2
- package/dist/image-endpoint.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -122
- package/dist/index.js.map +1 -1
- package/dist/islands-middleware-entrypoint.d.ts +1 -2
- package/dist/islands-middleware-entrypoint.d.ts.map +1 -1
- package/dist/islands-middleware-entrypoint.js +3 -1
- package/dist/islands-middleware-entrypoint.js.map +1 -1
- package/dist/islands-runtime.iife.js +1 -1
- package/dist/islands.d.ts +57 -2
- package/dist/islands.d.ts.map +1 -0
- package/dist/islands.js +3 -1
- package/dist/log/index.d.ts +5 -5
- package/dist/log/index.d.ts.map +1 -1
- package/dist/native.d.ts +5 -6
- package/dist/native.d.ts.map +1 -1
- package/dist/{prerendered-DVjzWNbW.js → prerendered-Z-Qi-FFK.js} +4 -4
- package/dist/prerendered-Z-Qi-FFK.js.map +1 -0
- package/dist/request-route-DcnZOOM4.js.map +1 -1
- package/dist/route-islands-B53rE5MH.js +73 -0
- package/dist/route-islands-B53rE5MH.js.map +1 -0
- package/dist/route-middleware-entrypoint.d.ts +1 -2
- package/dist/route-middleware-entrypoint.d.ts.map +1 -1
- package/dist/{route-store-CSYR4G5e.d.ts → route-store-neUA2nBb.d.ts} +10 -14
- package/dist/route-store-neUA2nBb.d.ts.map +1 -0
- package/dist/routes-D1o_WrJ5.js +39 -0
- package/dist/routes-D1o_WrJ5.js.map +1 -0
- package/dist/server.d.ts +2 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +68 -45
- package/dist/server.js.map +1 -1
- package/dist/telemetry-B9aziFyQ.js +202 -0
- package/dist/telemetry-B9aziFyQ.js.map +1 -0
- package/dist/telemetry.d.ts +68 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +2 -0
- package/dist/transform-Bz5z5w2Y.js +594 -0
- package/dist/transform-Bz5z5w2Y.js.map +1 -0
- package/package.json +13 -11
- package/dist/excludes-BDiE3eyp.d.ts.map +0 -1
- package/dist/excludes-pE23EbmQ.js.map +0 -1
- package/dist/prepare-CXZsyAVk.js.map +0 -1
- package/dist/prerendered-DVjzWNbW.js.map +0 -1
- package/dist/route-store-CSYR4G5e.d.ts.map +0 -1
- package/dist/strip-build-paths-C6eb1QdG.js +0 -63
- package/dist/strip-build-paths-C6eb1QdG.js.map +0 -1
- package/dist/transform-BqV8SOEm.js +0 -195
- package/dist/transform-BqV8SOEm.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as createIslandsTransformer } from "./transform-
|
|
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
|
-
|
|
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) +
|
|
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-
|
|
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;
|
|
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":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"route-middleware-entrypoint.d.ts","names":[],"sources":["../src/observability/route-middleware-entrypoint.ts"],"mappings":";;;;;;;;;;qBAWa,WAAW"}
|
|
@@ -16,19 +16,15 @@ type IslandInfo = {
|
|
|
16
16
|
fullClosure: string[];
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
|
-
* What an emitter contributes for one island. `links` are merged across emitters
|
|
20
|
-
* either emitted as `<link rel="modulepreload">` tags (immediate directives)
|
|
21
|
-
* registered
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* away and pulls the data modules itself. `html` is emitted right before the island
|
|
29
|
-
* tag regardless of directive — an inline script there is parsed strictly before
|
|
30
|
-
* the island connects, so it is the place for data the island's chunks read at
|
|
31
|
-
* execution time.
|
|
19
|
+
* What an emitter contributes for one island. `links` are merged across emitters
|
|
20
|
+
* and either emitted as `<link rel="modulepreload">` tags (immediate directives)
|
|
21
|
+
* or registered for the gate runtime (deferred). `imports` are data-module urls
|
|
22
|
+
* (safe to evaluate early, e.g. translation chunks) the deferred gate eagerly
|
|
23
|
+
* `import()`s on directive fire, landing them in the module cache before the
|
|
24
|
+
* component's own awaited import — urls in both sets register as imports only,
|
|
25
|
+
* and immediate directives ignore them. `html` is emitted right before the
|
|
26
|
+
* island tag regardless of directive — parsed strictly before the island
|
|
27
|
+
* connects, the place for data its chunks read at execution time.
|
|
32
28
|
*/
|
|
33
29
|
type IslandEmission = {
|
|
34
30
|
links?: string[] | undefined;
|
|
@@ -74,4 +70,4 @@ declare function setRequestRouteData(request: Request, routeData: RouteData): vo
|
|
|
74
70
|
declare function getRequestRouteData(request: Request): RouteData | undefined;
|
|
75
71
|
//#endregion
|
|
76
72
|
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 };
|
|
77
|
-
//# sourceMappingURL=route-store-
|
|
73
|
+
//# sourceMappingURL=route-store-neUA2nBb.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-store-neUA2nBb.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;;;iBCpBjC,oBAAoB,SAAS,SAAS,WAAW;iBAIjD,oBAAoB,SAAS,UAAU"}
|
|
@@ -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
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","names":[],"sources":["../src/server/server.ts"],"mappings":";
|
|
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
|
|
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 { c as withSpan, s as startSpan } from "./telemetry-B9aziFyQ.js";
|
|
6
7
|
import { n as setRequestRouteData } from "./route-store-DdxGePj2.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 {
|
|
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,
|
|
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);
|
|
@@ -138,15 +161,14 @@ function createAppHandler(app, options, client) {
|
|
|
138
161
|
const routeData = app.match(request, true);
|
|
139
162
|
const matched = routeData && !(routeData.type === "page" && routeData.prerender) ? routeData : void 0;
|
|
140
163
|
if (matched) setRequestRouteData(request, matched);
|
|
141
|
-
|
|
164
|
+
await writeResponse(matched ? await app.render(request, {
|
|
142
165
|
addCookieHeader: true,
|
|
143
166
|
routeData: matched,
|
|
144
167
|
prerenderedErrorPageFetch
|
|
145
168
|
}) : await app.render(request, {
|
|
146
169
|
addCookieHeader: true,
|
|
147
170
|
prerenderedErrorPageFetch
|
|
148
|
-
});
|
|
149
|
-
await writeResponse(response, res);
|
|
171
|
+
}), res);
|
|
150
172
|
};
|
|
151
173
|
}
|
|
152
174
|
//#endregion
|
|
@@ -280,7 +302,7 @@ async function warmupModules() {
|
|
|
280
302
|
app.manifest.actions,
|
|
281
303
|
app.manifest.sessionDriver,
|
|
282
304
|
app.manifest.serverIslandMappings
|
|
283
|
-
].filter(
|
|
305
|
+
].filter(defined);
|
|
284
306
|
const failures = (await Promise.allSettled(loaders.map((load) => load()))).filter((result) => result.status === "rejected");
|
|
285
307
|
if (failures.length === 0) return;
|
|
286
308
|
for (const failure of failures) log.error(failure.reason instanceof Error ? { err: failure.reason } : { reason: failure.reason }, "warmup import failed");
|
|
@@ -335,7 +357,7 @@ async function startServer(overrides) {
|
|
|
335
357
|
activateHealthChecks(checks);
|
|
336
358
|
log.debug("health probes listening");
|
|
337
359
|
}
|
|
338
|
-
const startup =
|
|
360
|
+
const startup = startSpan("startup");
|
|
339
361
|
log.info({
|
|
340
362
|
host,
|
|
341
363
|
port
|
|
@@ -344,7 +366,7 @@ async function startServer(overrides) {
|
|
|
344
366
|
let bootMs = 0;
|
|
345
367
|
let warmupMs = 0;
|
|
346
368
|
const warmupStartedAt = performance.now();
|
|
347
|
-
const warmupSpan =
|
|
369
|
+
const warmupSpan = startSpan("warmup", { parent: startup.context });
|
|
348
370
|
const warmup = warmupModules().then(() => {
|
|
349
371
|
warmupMs = roundMs(performance.now() - warmupStartedAt);
|
|
350
372
|
warmupSpan.span.end();
|
|
@@ -359,7 +381,7 @@ async function startServer(overrides) {
|
|
|
359
381
|
});
|
|
360
382
|
const shutdownLifecycle = async (shutdownContext) => {
|
|
361
383
|
try {
|
|
362
|
-
if (shutdownContext) await
|
|
384
|
+
if (shutdownContext) await withSpan("onShutdown", { parent: shutdownContext }, () => runShutdown(bootModule, context));
|
|
363
385
|
else await runShutdown(bootModule, context);
|
|
364
386
|
} catch (err) {
|
|
365
387
|
log.error(err instanceof Error ? { err } : { reason: err }, "shutdown failed");
|
|
@@ -388,7 +410,7 @@ async function startServer(overrides) {
|
|
|
388
410
|
try {
|
|
389
411
|
const bootStartedAt = performance.now();
|
|
390
412
|
bootModule = await import("virtual:@astroscope/node/boot");
|
|
391
|
-
await
|
|
413
|
+
await withSpan("boot", { parent: startup.context }, () => runStartup(bootModule, context));
|
|
392
414
|
bootMs = roundMs(performance.now() - bootStartedAt);
|
|
393
415
|
} catch (err) {
|
|
394
416
|
await failStartup(err, "startup failed");
|
|
@@ -418,13 +440,14 @@ async function startServer(overrides) {
|
|
|
418
440
|
return;
|
|
419
441
|
}
|
|
420
442
|
instrument(req, res, () => {
|
|
443
|
+
if (redirectDuplicateSlashes(req, res)) return;
|
|
421
444
|
if (dispatchNativeMount(req, res)) return;
|
|
422
445
|
staticHandler(req, res, () => void appHandler(req, res));
|
|
423
446
|
});
|
|
424
447
|
};
|
|
425
448
|
const server$1 = tls ? https.createServer(tls, listener) : http.createServer(listener);
|
|
426
449
|
try {
|
|
427
|
-
await
|
|
450
|
+
await withSpan("listen", { parent: startup.context }, () => {
|
|
428
451
|
return new Promise((resolve, reject) => {
|
|
429
452
|
server$1.once("error", reject);
|
|
430
453
|
server$1.listen(port, host, resolve);
|
|
@@ -454,8 +477,8 @@ async function startServer(overrides) {
|
|
|
454
477
|
if (health) probes.ready.disable();
|
|
455
478
|
log.info("shutdown initiated");
|
|
456
479
|
const drainStartedAt = performance.now();
|
|
457
|
-
const shutdown =
|
|
458
|
-
await
|
|
480
|
+
const shutdown = startSpan("shutdown");
|
|
481
|
+
await withSpan("drain", { parent: shutdown.context }, async () => {
|
|
459
482
|
const closed = new Promise((resolve) => server$1.close(() => resolve()));
|
|
460
483
|
server$1.closeIdleConnections();
|
|
461
484
|
const forceTimer = setTimeout(() => server$1.closeAllConnections(), runtimeOptions.shutdownTimeout);
|