@jsenv/server 17.6.9 → 17.7.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.
@@ -5899,37 +5899,36 @@ const createResourcePattern = (pattern) => {
5899
5899
  });
5900
5900
  }
5901
5901
 
5902
- const patternEndsWithSlash = pathnamePatternString.endsWith("/");
5902
+ // "/docs/*" also answers the directory itself ("/docs/" and "/docs") with an
5903
+ // empty star, so one route serves a directory and its index. Only a star
5904
+ // widens a pattern: "/docs/" is that one path and "/" the home page.
5905
+ const directoryPattern = pathnamePatternString.endsWith("/*")
5906
+ ? PATTERN.create(pathnamePatternString.slice(0, -1), {
5907
+ namedGroupDelimiter: "/",
5908
+ })
5909
+ : null;
5903
5910
  // a pattern constraining nothing: every resource matches it, so whoever uses
5904
5911
  // it cannot name the resource that matched
5905
5912
  const matchesEveryResource =
5906
5913
  !searchPattern &&
5907
5914
  !hashPattern &&
5908
- (pathnamePatternString === "*" ||
5909
- pathnamePatternString === "/" ||
5910
- pathnamePatternString === "/*");
5915
+ (pathnamePatternString === "*" || pathnamePatternString === "/*");
5911
5916
 
5912
5917
  return {
5913
5918
  matchesEveryResource,
5914
5919
  match: (resource) => {
5915
5920
  const [pathname, search, hash] = resourceToParts(resource);
5916
5921
  let decodedPathname = decodeURIComponent(pathname);
5917
- let result;
5918
- if (patternEndsWithSlash && !searchPattern && !hashPattern) {
5919
- // also match the path without trailing slash (e.g. /foo matches /foo/)
5920
- const pathnamePatternWithoutSlash = pathnamePatternString.slice(0, -1);
5921
- if (decodedPathname === pathnamePatternWithoutSlash) {
5922
- result = { named: {}, stars: [""] };
5923
- } else if (!decodedPathname.startsWith(pathnamePatternString)) {
5924
- return null;
5925
- } else {
5926
- result = {
5927
- named: {},
5928
- stars: [decodedPathname.slice(pathnamePatternString.length)],
5929
- };
5922
+ let result = pathnamePattern.match(decodedPathname);
5923
+ if (!result && directoryPattern) {
5924
+ result = directoryPattern.match(
5925
+ decodedPathname.endsWith("/")
5926
+ ? decodedPathname
5927
+ : `${decodedPathname}/`,
5928
+ );
5929
+ if (result) {
5930
+ result.stars = [...(result.stars || []), ""];
5930
5931
  }
5931
- } else {
5932
- result = pathnamePattern.match(decodedPathname);
5933
5932
  }
5934
5933
  if (!result) {
5935
5934
  return null;
@@ -6553,7 +6552,7 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
6553
6552
  }
6554
6553
  if (!route.matchMethod(request.method)) {
6555
6554
  // a 405 asserts the resource exists with other methods: a route
6556
- // matching every resource ("GET *", "GET /") names no resource, so
6555
+ // matching every resource ("GET *", "GET /*") names no resource, so
6557
6556
  // it cannot make that assertion and an unknown address stays a 404
6558
6557
  if (!route.isFallback && !route.matchesEveryResource) {
6559
6558
  wouldHaveMatched.methodSet.add(route.method);
@@ -7318,7 +7317,10 @@ const TIMING_NOOP = () => {
7318
7317
  * - `endpoint` {string} — Required. `"GET /users/:id"`: an http method (or `*` for any)
7319
7318
  * and a resource pattern. `:name` captures a segment and `*` a run of segments, both
7320
7319
  * land in `request.params`; `?page=:page` captures a search param. `"*"` alone
7321
- * matches everything. An endpoint ending in `.websocket` marks a websocket route.
7320
+ * matches everything. Only `*` widens a pattern: `GET /` is the root alone and
7321
+ * `GET /docs/` that one path, while `GET /docs/*` answers `/docs`, `/docs/` and
7322
+ * everything under it (`request.params[0]` is `""` for the first two): declare the
7323
+ * routes it covers before it. An endpoint ending in `.websocket` marks a websocket route.
7322
7324
  * - `fetch` {Function} — Required. `(request, helpers) => response`, async or not.
7323
7325
  * Returns a `Response`, a plain `{ status, statusText, statusMessage, headers, body,
7324
7326
  * timing }` object (`status` defaults to 404, `statusMessage` feeds the body of 4xx/5xx
@@ -19,16 +19,19 @@ await startServer({
19
19
 
20
20
  `endpoint` is an http method (or `*`) followed by a resource pattern:
21
21
 
22
- | endpoint | matches |
23
- | ---------------------- | ------------------------------------------------------------------- |
24
- | `GET /` | exactly `/` |
25
- | `GET /users/:id` | `/users/42`, with `request.params.id === "42"` |
26
- | `GET /assets/*` | `/assets/css/main.css`, with `request.params[0] === "css/main.css"` |
27
- | `GET /search?q=:query` | `/search?q=hello`, with `request.params.query === "hello"` |
28
- | `* /api/*` | any method |
29
- | `GET *` | anything: a catch-all, put it last |
30
-
31
- Routes are tried in order. `fetch` can be async; returning `null` or `undefined` declines the request and the next route is tried.
22
+ | endpoint | matches |
23
+ | ---------------------- | ------------------------------------------------------------------------------------------------------- |
24
+ | `GET /` | `/` alone, whatever its search (`/?page=2`): the home page |
25
+ | `GET /users/:id` | `/users/42`, with `request.params.id === "42"` |
26
+ | `GET /docs/` | `/docs/` alone |
27
+ | `GET /docs/*` | `/docs/intro`, with `request.params[0] === "intro"`, and the directory itself: `/docs`, `/docs/` (`""`) |
28
+ | `GET /assets/*` | `/assets/css/main.css`, with `request.params[0] === "css/main.css"` |
29
+ | `GET /search?q=:query` | `/search?q=hello`, with `request.params.query === "hello"` |
30
+ | `* /api/*` | any method |
31
+ | `GET /*` | anything, with the path in `request.params[0]` |
32
+ | `GET *` | anything: a catch-all, put it last |
33
+
34
+ Only `*` widens a pattern. Routes are tried in order, so a route covering others (`GET /docs/*`, `GET *`) goes after them. `fetch` can be async; returning `null` or `undefined` declines the request and the next route is tried.
32
35
 
33
36
  When no route answers, the router builds the response from what almost matched:
34
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "17.6.9",
3
+ "version": "17.7.0",
4
4
  "type": "module",
5
5
  "description": "A modern Node.js HTTP server with declarative routing, content negotiation, and WebSocket support",
6
6
  "repository": {
@@ -356,7 +356,7 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
356
356
  }
357
357
  if (!route.matchMethod(request.method)) {
358
358
  // a 405 asserts the resource exists with other methods: a route
359
- // matching every resource ("GET *", "GET /") names no resource, so
359
+ // matching every resource ("GET *", "GET /*") names no resource, so
360
360
  // it cannot make that assertion and an unknown address stays a 404
361
361
  if (!route.isFallback && !route.matchesEveryResource) {
362
362
  wouldHaveMatched.methodSet.add(route.method);
@@ -59,7 +59,10 @@ const TIMING_NOOP = () => {
59
59
  * - `endpoint` {string} — Required. `"GET /users/:id"`: an http method (or `*` for any)
60
60
  * and a resource pattern. `:name` captures a segment and `*` a run of segments, both
61
61
  * land in `request.params`; `?page=:page` captures a search param. `"*"` alone
62
- * matches everything. An endpoint ending in `.websocket` marks a websocket route.
62
+ * matches everything. Only `*` widens a pattern: `GET /` is the root alone and
63
+ * `GET /docs/` that one path, while `GET /docs/*` answers `/docs`, `/docs/` and
64
+ * everything under it (`request.params[0]` is `""` for the first two): declare the
65
+ * routes it covers before it. An endpoint ending in `.websocket` marks a websocket route.
63
66
  * - `fetch` {Function} — Required. `(request, helpers) => response`, async or not.
64
67
  * Returns a `Response`, a plain `{ status, statusText, statusMessage, headers, body,
65
68
  * timing }` object (`status` defaults to 404, `statusMessage` feeds the body of 4xx/5xx