@jsenv/server 17.7.0 → 17.7.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.
@@ -853,6 +853,18 @@ main {
853
853
  font-weight: 700;
854
854
  }
855
855
 
856
+ .route_also_method {
857
+ color: #fff;
858
+ text-shadow: 0 1px #0000001a;
859
+ background: var(--method-color);
860
+ border-radius: 3px;
861
+ margin-left: 6px;
862
+ padding: 3px 6px;
863
+ font-family: sans-serif;
864
+ font-size: 11px;
865
+ font-weight: 700;
866
+ }
867
+
856
868
  .route_resource {
857
869
  word-break: break-word;
858
870
  margin-top: 0;
@@ -897,7 +909,7 @@ main {
897
909
  opacity: .85;
898
910
  }
899
911
 
900
- .route_details[data-forbidden] .route_method {
912
+ .route_details[data-forbidden] .route_method, .route_details[data-forbidden] .route_also_method {
901
913
  opacity: .7;
902
914
  filter: grayscale(60%);
903
915
  }
@@ -1835,6 +1847,7 @@ const routesLoadingSignal = M$1(false);
1835
1847
  const RouteDetails = ({
1836
1848
  meta,
1837
1849
  method,
1850
+ methods,
1838
1851
  resource,
1839
1852
  description,
1840
1853
  availableMediaTypes,
@@ -1858,6 +1871,10 @@ const routesLoadingSignal = M$1(false);
1858
1871
  if (selectedPackageName && packageName !== selectedPackageName) {
1859
1872
  return null;
1860
1873
  }
1874
+ // what the router answers beyond the declared method (a GET route
1875
+ // also answers HEAD); a "*" route answers everything, its chip says so
1876
+ const alsoMethods =
1877
+ method === "*" ? [] : methods.filter((m) => m !== method);
1861
1878
 
1862
1879
  return p$3`<details
1863
1880
  class="route_details"
@@ -1867,6 +1884,14 @@ const routesLoadingSignal = M$1(false);
1867
1884
  >
1868
1885
  <summary class="route_summary_container">
1869
1886
  <span class="route_method">${method}</span>
1887
+ ${alsoMethods.map(
1888
+ (alsoMethod) => p$3`<span
1889
+ class="route_also_method"
1890
+ style="--method-color: var(--method-${alsoMethod.toLowerCase()}-color)"
1891
+ title="Also answers ${alsoMethod}"
1892
+ >+${alsoMethod}</span
1893
+ >`,
1894
+ )}
1870
1895
  <p class="route_resource">${resource}</p>
1871
1896
  ${method === "GET" && !forbidden
1872
1897
  ? p$3`<a
@@ -6317,9 +6317,7 @@ const createRouter = (
6317
6317
  };
6318
6318
  };
6319
6319
  const forEachMethodAllowed = (route, onMethodAllowed) => {
6320
- const supportedMethods =
6321
- route.method === "*" ? HTTP_METHODS : [route.method];
6322
- for (const supportedMethod of supportedMethods) {
6320
+ for (const supportedMethod of route.methods) {
6323
6321
  onMethodAllowed(supportedMethod);
6324
6322
  }
6325
6323
  };
@@ -6555,7 +6553,9 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
6555
6553
  // matching every resource ("GET *", "GET /*") names no resource, so
6556
6554
  // it cannot make that assertion and an unknown address stays a 404
6557
6555
  if (!route.isFallback && !route.matchesEveryResource) {
6558
- wouldHaveMatched.methodSet.add(route.method);
6556
+ for (const routeMethod of route.methods) {
6557
+ wouldHaveMatched.methodSet.add(routeMethod);
6558
+ }
6559
6559
  }
6560
6560
  continue;
6561
6561
  }
@@ -6876,6 +6876,14 @@ const createRoute = ({
6876
6876
  ) {
6877
6877
  availableMediaTypes.push(extensionWellKnownContentType);
6878
6878
  }
6879
+ // HEAD is GET without the body (RFC 9110 §9.3.2): a GET route answers it and
6880
+ // the server drops the body when sending
6881
+ const methods =
6882
+ method === "*"
6883
+ ? HTTP_METHODS
6884
+ : method === "GET"
6885
+ ? ["GET", "HEAD"]
6886
+ : [method];
6879
6887
  const resourcePattern = createResourcePattern(resource);
6880
6888
  const headersPattern = headers ? createHeadersPattern(headers) : null;
6881
6889
 
@@ -6895,8 +6903,11 @@ const createRoute = ({
6895
6903
  availableVersions,
6896
6904
  availableEncodings,
6897
6905
  acceptedMediaTypes,
6906
+ methods,
6898
6907
  matchMethod:
6899
- method === "*" ? () => true : (requestMethod) => requestMethod === method,
6908
+ method === "*"
6909
+ ? () => true
6910
+ : (requestMethod) => methods.includes(requestMethod),
6900
6911
  matchResource:
6901
6912
  resource === "*"
6902
6913
  ? () => true
@@ -6948,6 +6959,7 @@ const createRoute = ({
6948
6959
  }
6949
6960
  return {
6950
6961
  method,
6962
+ methods,
6951
6963
  resource,
6952
6964
  description,
6953
6965
  availableMediaTypes,
@@ -31,6 +31,8 @@ await startServer({
31
31
  | `GET /*` | anything, with the path in `request.params[0]` |
32
32
  | `GET *` | anything: a catch-all, put it last |
33
33
 
34
+ A `GET` route also answers `HEAD`: the response is built the same way and sent without its body. When building that body is costly, declare a `HEAD` route for the same resource before the `GET` one: it answers `HEAD` in its place.
35
+
34
36
  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.
35
37
 
36
38
  When no route answers, the router builds the response from what almost matched:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "17.7.0",
3
+ "version": "17.7.2",
4
4
  "type": "module",
5
5
  "description": "A modern Node.js HTTP server with declarative routing, content negotiation, and WebSocket support",
6
6
  "repository": {
@@ -37,7 +37,7 @@
37
37
  "prepublishOnly": "npm run build"
38
38
  },
39
39
  "dependencies": {
40
- "launch-editor": "2.14.1"
40
+ "launch-editor": "2.14.2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@jsenv/abort": "../../tooling/abort",
@@ -443,6 +443,17 @@
443
443
  background: var(--method-color);
444
444
  border-radius: 3px;
445
445
  }
446
+ .route_also_method {
447
+ margin-left: 6px;
448
+ padding: 3px 6px;
449
+ color: #fff;
450
+ font-weight: 700;
451
+ font-size: 11px;
452
+ font-family: sans-serif;
453
+ text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
454
+ background: var(--method-color);
455
+ border-radius: 3px;
456
+ }
446
457
  .route_resource {
447
458
  margin-top: 0;
448
459
  margin-bottom: 0;
@@ -486,7 +497,8 @@
486
497
  border-color: var(--forbidden-border-color);
487
498
  opacity: 0.85;
488
499
  }
489
- .route_details[data-forbidden] .route_method {
500
+ .route_details[data-forbidden] .route_method,
501
+ .route_details[data-forbidden] .route_also_method {
490
502
  opacity: 0.7;
491
503
  filter: grayscale(60%);
492
504
  }
@@ -1428,6 +1440,7 @@
1428
1440
  const RouteDetails = ({
1429
1441
  meta,
1430
1442
  method,
1443
+ methods,
1431
1444
  resource,
1432
1445
  description,
1433
1446
  availableMediaTypes,
@@ -1451,6 +1464,10 @@
1451
1464
  if (selectedPackageName && packageName !== selectedPackageName) {
1452
1465
  return null;
1453
1466
  }
1467
+ // what the router answers beyond the declared method (a GET route
1468
+ // also answers HEAD); a "*" route answers everything, its chip says so
1469
+ const alsoMethods =
1470
+ method === "*" ? [] : methods.filter((m) => m !== method);
1454
1471
 
1455
1472
  return html`<details
1456
1473
  class="route_details"
@@ -1460,6 +1477,14 @@
1460
1477
  >
1461
1478
  <summary class="route_summary_container">
1462
1479
  <span class="route_method">${method}</span>
1480
+ ${alsoMethods.map(
1481
+ (alsoMethod) => html`<span
1482
+ class="route_also_method"
1483
+ style="--method-color: var(--method-${alsoMethod.toLowerCase()}-color)"
1484
+ title="Also answers ${alsoMethod}"
1485
+ >+${alsoMethod}</span
1486
+ >`,
1487
+ )}
1463
1488
  <p class="route_resource">${resource}</p>
1464
1489
  ${method === "GET" && !forbidden
1465
1490
  ? html`<a
@@ -121,9 +121,7 @@ export const createRouter = (
121
121
  };
122
122
  };
123
123
  const forEachMethodAllowed = (route, onMethodAllowed) => {
124
- const supportedMethods =
125
- route.method === "*" ? HTTP_METHODS : [route.method];
126
- for (const supportedMethod of supportedMethods) {
124
+ for (const supportedMethod of route.methods) {
127
125
  onMethodAllowed(supportedMethod);
128
126
  }
129
127
  };
@@ -359,7 +357,9 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
359
357
  // matching every resource ("GET *", "GET /*") names no resource, so
360
358
  // it cannot make that assertion and an unknown address stays a 404
361
359
  if (!route.isFallback && !route.matchesEveryResource) {
362
- wouldHaveMatched.methodSet.add(route.method);
360
+ for (const routeMethod of route.methods) {
361
+ wouldHaveMatched.methodSet.add(routeMethod);
362
+ }
363
363
  }
364
364
  continue;
365
365
  }
@@ -683,6 +683,14 @@ const createRoute = ({
683
683
  ) {
684
684
  availableMediaTypes.push(extensionWellKnownContentType);
685
685
  }
686
+ // HEAD is GET without the body (RFC 9110 §9.3.2): a GET route answers it and
687
+ // the server drops the body when sending
688
+ const methods =
689
+ method === "*"
690
+ ? HTTP_METHODS
691
+ : method === "GET"
692
+ ? ["GET", "HEAD"]
693
+ : [method];
686
694
  const resourcePattern = createResourcePattern(resource);
687
695
  const headersPattern = headers ? createHeadersPattern(headers) : null;
688
696
 
@@ -702,8 +710,11 @@ const createRoute = ({
702
710
  availableVersions,
703
711
  availableEncodings,
704
712
  acceptedMediaTypes,
713
+ methods,
705
714
  matchMethod:
706
- method === "*" ? () => true : (requestMethod) => requestMethod === method,
715
+ method === "*"
716
+ ? () => true
717
+ : (requestMethod) => methods.includes(requestMethod),
707
718
  matchResource:
708
719
  resource === "*"
709
720
  ? () => true
@@ -755,6 +766,7 @@ const createRoute = ({
755
766
  }
756
767
  return {
757
768
  method,
769
+ methods,
758
770
  resource,
759
771
  description,
760
772
  availableMediaTypes,