@askrjs/node 0.2.0 → 0.2.2

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 CHANGED
@@ -94,7 +94,10 @@ import { serve } from "@askrjs/node";
94
94
 
95
95
  const running = await serve(app, {
96
96
  port: 3000,
97
- assets: { root: "./dist/client" },
97
+ assets: {
98
+ root: "./dist/client",
99
+ exclude: (pathname) => pathname.startsWith("/files/"),
100
+ },
98
101
  });
99
102
 
100
103
  await running.close();
@@ -103,7 +106,9 @@ await running.close();
103
106
  `serve` handles static assets and closes both the HTTP server and the application during shutdown.
104
107
  When an asset root is configured, extension-bearing `GET` and `HEAD` paths are reserved for static
105
108
  files: missing files return `404` without falling through to application routing, source maps are not
106
- served, and resolved files must remain inside the configured root. Fingerprinted files under
109
+ served, and resolved files must remain inside the configured root. Use `assets.exclude(pathname)` to
110
+ bypass static interception for dotted application routes; the predicate receives the decoded URL
111
+ pathname. Fingerprinted files under
107
112
  `/assets/` receive immutable caching; other files receive `no-cache`.
108
113
  Both `listen` and `serve` bind to `127.0.0.1` by default. A non-loopback
109
114
  `host` also requires `allowPublicBind: true` so public exposure is explicit. Public listeners should
package/dist/index.d.ts CHANGED
@@ -58,6 +58,8 @@ interface ServeOptions extends ListenOptions {
58
58
  /** Serves static files from this directory before falling back to the application. */
59
59
  readonly assets?: {
60
60
  readonly root: string;
61
+ /** Returns true when an extension-bearing path must bypass static serving. */
62
+ readonly exclude?: (pathname: string) => boolean;
61
63
  };
62
64
  /** OS signals that trigger a graceful shutdown; pass `false` to disable automatic shutdown handling. */
63
65
  readonly signals?: false | readonly NodeJS.Signals[];
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { createReadStream } from "node:fs";
6
6
  import { realpath, stat } from "node:fs/promises";
7
7
  import { extname, resolve, sep } from "node:path";
8
8
  import { pipeline } from "node:stream";
9
+ import * as serverHttp from "@askrjs/server/http";
9
10
  //#region src/client-address.ts
10
11
  /**
11
12
  * Reserved request header containing the TCP peer address authenticated by the Node adapter.
@@ -623,6 +624,12 @@ const mimeTypes = {
623
624
  ".woff": "font/woff",
624
625
  ".woff2": "font/woff2"
625
626
  };
627
+ const parseContentType = serverHttp.contentType ?? ((value) => {
628
+ if (!value) return void 0;
629
+ const separator = value.indexOf(";");
630
+ const type = (separator === -1 ? value : value.slice(0, separator)).trim();
631
+ return type ? type.toLowerCase() : void 0;
632
+ });
626
633
  function isAssetPath(pathname) {
627
634
  return extname(pathname) !== "";
628
635
  }
@@ -658,8 +665,8 @@ async function serve(app, options = {}) {
658
665
  const preparedHandlerOptions = prepareNodeHandlerOptions(handlerOptions);
659
666
  const applicationHandler = createNodeHandler({ async fetch(request, dispatchOptions) {
660
667
  const result = await app.fetch(request, dispatchOptions);
661
- const contentType = result.headers.get("content-type");
662
- if (!result.headers.has("cache-control") && contentType !== null && /^text\/html(?:;|$)/iu.test(contentType)) {
668
+ const responseContentType = parseContentType(result.headers.get("content-type"));
669
+ if (!result.headers.has("cache-control") && responseContentType === "text/html") {
663
670
  const headers = new Headers(result.headers);
664
671
  headers.set("cache-control", "no-cache");
665
672
  return new Response(result.body, {
@@ -683,7 +690,7 @@ async function serve(app, options = {}) {
683
690
  return;
684
691
  }
685
692
  const method = request.method ?? "GET";
686
- if ((method === "GET" || method === "HEAD") && isAssetPath(pathname)) {
693
+ if ((method === "GET" || method === "HEAD") && isAssetPath(pathname) && !options.assets?.exclude?.(pathname)) {
687
694
  const extension = extname(pathname).toLowerCase();
688
695
  const unresolvedCandidate = resolve(root, `.${pathname}`);
689
696
  const inside = isWithinRoot(root, unresolvedCandidate);
@@ -700,12 +707,14 @@ async function serve(app, options = {}) {
700
707
  file = void 0;
701
708
  }
702
709
  if (!candidate || !file?.isFile()) {
710
+ if (response.destroyed || response.writableEnded) return;
703
711
  response.writeHead(404, {
704
712
  "content-type": "text/plain; charset=utf-8",
705
713
  "x-content-type-options": "nosniff"
706
714
  }).end("Not Found");
707
715
  return;
708
716
  }
717
+ if (response.destroyed || response.writableEnded) return;
709
718
  response.writeHead(200, {
710
719
  "content-type": mimeTypes[extension] ?? "application/octet-stream",
711
720
  "content-length": Number(file.size),
@@ -713,7 +722,15 @@ async function serve(app, options = {}) {
713
722
  "x-content-type-options": "nosniff"
714
723
  });
715
724
  if (method === "HEAD") response.end();
716
- else pipeline(createReadStream(candidate), response, () => void 0);
725
+ else {
726
+ const source = createReadStream(candidate);
727
+ const stopSource = () => source.destroy();
728
+ response.once("close", stopSource);
729
+ pipeline(source, response, (error) => {
730
+ response.off("close", stopSource);
731
+ if (error) source.destroy();
732
+ });
733
+ }
717
734
  return;
718
735
  }
719
736
  applicationHandler(request, response);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askrjs/node",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Node http adapter for @askrjs/server",
5
5
  "keywords": [
6
6
  "askr",
@@ -57,18 +57,18 @@
57
57
  "prepublishOnly": "npm run check"
58
58
  },
59
59
  "dependencies": {
60
- "@askrjs/auth": ">=0.2.0 <0.3.0",
61
- "@askrjs/server": ">=0.2.0 <0.3.0",
60
+ "@askrjs/auth": ">=0.2.1 <0.3.0",
61
+ "@askrjs/server": ">=0.2.1 <0.3.0",
62
62
  "@types/ws": "^8.18.1",
63
63
  "ws": "^8.21.3"
64
64
  },
65
65
  "devDependencies": {
66
- "@askrjs/schema": ">=0.2.0 <0.3.0",
67
- "@types/node": "^26.2.0",
68
- "publint": "^0.3.23",
66
+ "@askrjs/schema": ">=0.2.1 <0.3.0",
67
+ "@types/node": "^26.3.0",
68
+ "publint": "^0.3.24",
69
69
  "typescript": "^7.0.2",
70
- "vite-plus": "^0.2.8",
71
- "vitest": "^4.1.10"
70
+ "vite-plus": "^0.3.0",
71
+ "vitest": "^4.1.11"
72
72
  },
73
73
  "engines": {
74
74
  "node": ">=24.0.0"