primate 0.11.0 → 0.13.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 (60) hide show
  1. package/LICENSE +0 -2
  2. package/README.md +52 -99
  3. package/eslint.config.js +1 -0
  4. package/exports.js +1 -3
  5. package/module.json +10 -5
  6. package/package.json +14 -17
  7. package/readme/extensions/handlers/html/user.js +13 -0
  8. package/readme/extensions/handlers/htmx/user-index.html +4 -0
  9. package/readme/extensions/handlers/htmx/user.js +23 -0
  10. package/readme/extensions/handlers/redirect/user.js +6 -0
  11. package/readme/{domains → extensions/modules/domains}/fields.js +3 -4
  12. package/readme/{domains → extensions/modules/domains}/predicates.js +3 -3
  13. package/readme/{domains → extensions/modules/domains}/short-field-notation.js +3 -3
  14. package/readme/routing/basic.js +1 -1
  15. package/readme/routing/explicit-handlers.js +4 -0
  16. package/readme/routing/sharing-logic-across-requests.js +2 -2
  17. package/readme/serving-content/html.js +10 -11
  18. package/readme/serving-content/json.js +2 -2
  19. package/readme/serving-content/plain-text.js +1 -1
  20. package/readme/serving-content/response.js +6 -0
  21. package/readme/serving-content/streams.js +1 -1
  22. package/readme/template.md +135 -0
  23. package/scripts/docs.sh +7 -0
  24. package/src/bin.js +2 -0
  25. package/src/bundle.js +14 -7
  26. package/src/compile.js +5 -0
  27. package/src/config.js +73 -0
  28. package/src/duck.js +4 -0
  29. package/src/extend.spec.js +19 -27
  30. package/src/handlers/exports.js +5 -0
  31. package/src/handlers/html.js +18 -0
  32. package/src/handlers/http404.js +6 -4
  33. package/src/handlers/json.js +6 -4
  34. package/src/handlers/redirect.js +7 -0
  35. package/src/handlers/stream.js +6 -4
  36. package/src/handlers/text.js +6 -11
  37. package/src/http-statuses.js +5 -0
  38. package/src/index.html +8 -0
  39. package/src/log.js +7 -4
  40. package/src/mimes.js +12 -0
  41. package/src/register.js +5 -0
  42. package/src/respond.js +24 -0
  43. package/src/route.js +15 -28
  44. package/src/run.js +10 -9
  45. package/src/serve.js +25 -12
  46. package/README.template.md +0 -190
  47. package/bin/primate.js +0 -5
  48. package/readme/getting-started/generate-ssl.sh +0 -1
  49. package/readme/getting-started/lay-out-app.sh +0 -1
  50. package/readme/getting-started/site-index.html +0 -1
  51. package/readme/getting-started/site.js +0 -3
  52. package/src/conf.js +0 -30
  53. package/src/http-statuses.json +0 -5
  54. package/src/mimes.json +0 -12
  55. package/src/preset/stores/default.js +0 -2
  56. /package/readme/{serving-content → extensions/handlers/html}/user-index.html +0 -0
  57. /package/readme/{modules → extensions/modules}/configure.js +0 -0
  58. /package/readme/{modules → extensions/modules}/domains/configure.js +0 -0
  59. /package/readme/{getting-started/hello.js → getting-started.js} +0 -0
  60. /package/src/{preset/primate.conf.js → primate.config.js} +0 -0
package/LICENSE CHANGED
@@ -1,5 +1,3 @@
1
- Primate JavaScript Framework
2
-
3
1
  Copyright (c) Terrablue <terrablue@proton.me> and contributors.
4
2
 
5
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Primate
2
2
 
3
- An expressive, minimal and extensible framework for JavaScript.
3
+ Expressive, minimal and extensible framework for JavaScript.
4
4
 
5
5
  ## Getting started
6
6
 
@@ -13,7 +13,7 @@ export default router => {
13
13
 
14
14
  ```
15
15
 
16
- Add `{"type": "module"}` to your `package.json` and run `npx primate`.
16
+ Add `{"type": "module"}` to your `package.json` and run `npx -y primate@latest`.
17
17
 
18
18
  ## Table of Contents
19
19
 
@@ -21,6 +21,7 @@ Add `{"type": "module"}` to your `package.json` and run `npx primate`.
21
21
  - [Plain text](#plain-text)
22
22
  - [JSON](#json)
23
23
  - [Streams](#streams)
24
+ - [Response](#response)
24
25
  - [HTML](#html)
25
26
  - [Routing](#routing)
26
27
  - [Basic](#basic)
@@ -30,19 +31,17 @@ Add `{"type": "module"}` to your `package.json` and run `npx primate`.
30
31
  - [Named groups](#named-groups)
31
32
  - [Aliasing](#aliasing)
32
33
  - [Sharing logic across requests](#sharing-logic-across-requests)
33
- - [Data persistance](#data-persistance)
34
- - [Short field notation](#short-field-notation)
35
- - [Predicates](#predicates)
34
+ - [Explicit handlers](#explicit-handlers)
36
35
 
37
36
  ## Serving content
38
37
 
39
- Create a file in `routes` that exports a default function
38
+ Create a file in `routes` that exports a default function.
40
39
 
41
40
  ### Plain text
42
41
 
43
42
  ```js
44
43
  export default router => {
45
- // strings will be served as plain text
44
+ // Serve strings as plain text
46
45
  router.get("/user", () => "Donald");
47
46
  };
48
47
 
@@ -54,13 +53,13 @@ export default router => {
54
53
  import {File} from "runtime-compat/filesystem";
55
54
 
56
55
  export default router => {
57
- // any proper JavaScript object will be served as JSON
56
+ // Serve proper JavaScript objects as JSON
58
57
  router.get("/users", () => [
59
58
  {name: "Donald"},
60
59
  {name: "Ryan"},
61
60
  ]);
62
61
 
63
- // load from a file and serve as JSON
62
+ // Load from file and serve as JSON
64
63
  router.get("/users-from-file", () => File.json("users.json"));
65
64
  };
66
65
 
@@ -72,39 +71,38 @@ export default router => {
72
71
  import {File} from "runtime-compat/filesystem";
73
72
 
74
73
  export default router => {
75
- // `File` implements `readable`, which is a ReadableStream
74
+ // `File` implements `readable`, which is a `ReadableStream`
76
75
  router.get("/users", () => new File("users.json"));
77
76
  };
78
77
 
79
78
  ```
80
79
 
81
- ### HTML
80
+ ### Response
82
81
 
83
- Create an HTML component in `components/user-index.html`
82
+ ```js
83
+ import {Response} from "runtime-compat/http";
84
84
 
85
- ```html
86
- <div for="${users}">
87
- User ${name}.
88
- Email ${email}.
89
- </div>
85
+ export default router => {
86
+ // Use a Response object for custom response status
87
+ router.get("/create", () => new Response("created!", {status: 201}));
88
+ };
90
89
 
91
90
  ```
92
91
 
93
- Serve the component in your route
92
+ ### HTML
94
93
 
95
94
  ```js
96
- import html from "@primate/html";
95
+ // Use an explicit handler as we can't detect HTML by the return value type
96
+ export default (router, {html}) => {
97
+ // Embed components/hello-world.html into static/index.html and serve it. In
98
+ // case a user-provided index.html doesn't exist, use a fallback index.html
99
+ router.get("/hello", () => html("hello-world"));
97
100
 
98
- export default router => {
99
- // the HTML tagged template handler loads a component from the `components`
100
- // directory and serves it as HTML, passing any given data as attributes
101
- router.get("/users", () => {
102
- const users = [
103
- {name: "Donald", email: "donald@the.duck"},
104
- {name: "Joe", email: "joe@was.absent"},
105
- ];
106
- return html`<user-index users="${users}" />`;
107
- });
101
+ // Same as above, but without embedding
102
+ router.get("/hello-partial", () => html("hello-world", {partial: true}));
103
+
104
+ // Serve directly from string instead of loading a component
105
+ router.get("/hello-adhoc", () => html("<p>Hello, world!</p>", {adhoc: true}));
108
106
  };
109
107
 
110
108
  ```
@@ -115,13 +113,9 @@ Routes map requests to responses. They are loaded from `routes`.
115
113
 
116
114
  ### Basic
117
115
 
118
- To start serving content, create a file in `routes` returning a function as its
119
- default export. This function has a `router` param used to configure HTTP
120
- routes.
121
-
122
116
  ```js
123
117
  export default router => {
124
- // accessing /site/login will serve the `Hello, world!` as plain text
118
+ // accessing /site/login will serve `Hello, world!` as plain text
125
119
  router.get("/site/login", () => "Hello, world!");
126
120
  };
127
121
 
@@ -137,6 +131,20 @@ export default router => {
137
131
 
138
132
  ```
139
133
 
134
+ ### Accessing the request body
135
+
136
+ For requests containing a body, Primate will attempt to parse the body according
137
+ to the content type sent along the request. Currently supported are
138
+ `application/x-www-form-urlencoded` (typically for form submission) and
139
+ `application/json`.
140
+
141
+ ```js
142
+ export default router => {
143
+ router.post("/site/login", ({body}) => `submitted user: ${body.username}`);
144
+ };
145
+
146
+ ```
147
+
140
148
  ### Regular expressions
141
149
 
142
150
  ```js
@@ -193,80 +201,27 @@ export default router => {
193
201
  router.map("edit-user", () => ({name: "Donald"}));
194
202
 
195
203
  // show user edit form
196
- router.get("edit-user", user => html`<user-edit user="${user}" />`);
204
+ router.get("edit-user", user => html("user-edit", {user}));
197
205
 
198
206
  // verify form and save, or show errors
199
207
  router.post("edit-user", async user => await user.save()
200
- ? redirect`/users`
208
+ ? redirect("/users")
201
209
  : html`<user-edit user="${user}" />`);
202
210
  };
203
211
 
204
212
  ```
205
213
 
206
- ## Data persistance
214
+ ### Explicit handlers
207
215
 
208
- Primate domains (via [`@primate/domains`][primate-domains]) represent a
209
- collection in a store using the class `fields` property.
216
+ Most often we can figure the content type to respond with based on the return
217
+ type from the handler. To handle content not automatically detected, use the
218
+ second argument of the exported function.
210
219
 
211
220
  ```js
212
- import {Domain} from "@primate/domains";
213
-
214
- // A basic domain that contains two properies
215
- export default class User extends Domain {
216
- static fields = {
217
- // a user's name must be a string
218
- name: String,
219
- // a user's age must be a number
220
- age: Number,
221
- };
222
- }
223
-
224
-
225
- ```
226
-
227
- ### Short field notation
228
-
229
- Value types may be any constructible JavaScript object, including other
230
- domains. When using other domains as types, data integrity (on saving) is
231
- ensured.
232
-
233
- ```js
234
- import {Domain} from "@primate/domains";
235
- import House from "./House.js";
236
-
237
- export default class User extends Domain {
238
- static fields = {
239
- // a user's name must be a string
240
- name: String,
241
- // a user's age must be a number
242
- age: Number,
243
- // a user's house must have the foreign id of a house record
244
- house_id: House,
245
- };
246
- }
247
-
248
- ```
249
-
250
- ### Predicates
251
-
252
- Field types may also be specified as an array, to specify additional predicates
253
- aside from the type.
254
-
255
- ```js
256
- import {Domain} from "@primate/domains";
257
- import House from "./House.js";
258
-
259
- export default class User extends Domain {
260
- static fields = {
261
- // a user's name must be a string and unique across the user collection
262
- name: [String, "unique"],
263
- // a user's age must be a positive integer
264
- age: [Number, "integer", "positive"],
265
- // a user's house must have the foreign id of a house record and no two
266
- // users may have the same house
267
- house_id: [House, "unique"],
268
- };
269
- }
221
+ export default (router, {redirect}) => {
222
+ // redirect from source to target
223
+ router.get("/source", () => redirect("/target"));
224
+ };
270
225
 
271
226
  ```
272
227
 
@@ -278,5 +233,3 @@ export default class User extends Domain {
278
233
  ## License
279
234
 
280
235
  MIT
281
-
282
- [primate-domains]: https://github.com/primatejs/primate-domains
@@ -0,0 +1 @@
1
+ export {default} from "maximin";
package/exports.js CHANGED
@@ -1,5 +1,3 @@
1
- export {default as Bundler} from "./src/Bundler.js";
2
-
3
1
  export * from "./src/errors.js";
4
2
 
5
- export {default as extend} from "./src/extend.js";
3
+ export {default} from "./src/run.js";
package/module.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.9.2",
4
- "author": "Terrablue <terrablue@proton.me>",
3
+ "version": "0.13.0",
4
+ "description": "Expressive, minimal and extensible framework for JavaScript",
5
+ "homepage": "https://primatejs.com",
5
6
  "bugs": "https://github.com/primatejs/primate/issues",
6
- "repository": "https://github.com/primatejs/primate",
7
- "description": "Primal JavaScript framework",
8
7
  "license": "MIT",
9
- "bin": "bin/primate.js"
8
+ "bin": "src/bin.js",
9
+ "repository": "https://github.com/primatejs/primate",
10
+ "scripts": {
11
+ "docs": "scripts/docs.sh",
12
+ "test": "npx debris",
13
+ "lint": "npx eslint ."
14
+ }
10
15
  }
package/package.json CHANGED
@@ -1,29 +1,26 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.11.0",
4
- "author": "Terrablue <terrablue@proton.me>",
3
+ "version": "0.13.0",
4
+ "description": "Expressive, minimal and extensible framework for JavaScript",
5
5
  "homepage": "https://primatejs.com",
6
6
  "bugs": "https://github.com/primatejs/primate/issues",
7
- "repository": "https://github.com/primatejs/primate",
8
- "description": "Primal JavaScript framework",
9
7
  "license": "MIT",
8
+ "bin": "src/bin.js",
9
+ "repository": "https://github.com/primatejs/primate",
10
+ "scripts": {
11
+ "docs": "scripts/docs.sh",
12
+ "test": "npx debris",
13
+ "lint": "npx eslint ."
14
+ },
10
15
  "dependencies": {
11
- "runtime-compat": "^0.12.3"
16
+ "runtime-compat": "^0.14.1"
12
17
  },
13
- "bin": "bin/primate.js",
14
18
  "devDependencies": {
15
- "@babel/core": "^7.21.0",
16
- "@babel/eslint-parser": "^7.19.1",
17
- "@babel/plugin-syntax-import-assertions": "^7.20.0",
18
- "eslint": "^8.36.0",
19
- "eslint-plugin-json": "^3.1.0"
19
+ "maximin": "^0.1.2"
20
20
  },
21
- "scripts": {
22
- "docs": "npx -y embedme --source-root readme --strip-embed-comment --stdout README.template.md > README.md"
23
- },
24
- "type": "module",
25
- "exports": "./exports.js",
26
21
  "engines": {
27
22
  "node": ">=17.9.0"
28
- }
23
+ },
24
+ "type": "module",
25
+ "exports": "./exports.js"
29
26
  }
@@ -0,0 +1,13 @@
1
+ import html from "@primate/html";
2
+
3
+ export default router => {
4
+ // the HTML tagged template handler loads a component from the `components`
5
+ // directory and serves it as HTML, passing any given data as attributes
6
+ router.get("/users", () => {
7
+ const users = [
8
+ {name: "Donald", email: "donald@the.duck"},
9
+ {name: "Joe", email: "joe@was.absent"},
10
+ ];
11
+ return html("user-index", {users});
12
+ });
13
+ };
@@ -0,0 +1,4 @@
1
+ <div for="${users}" hx-get="/other-users" hx-swap="outerHTML">
2
+ User ${name}.
3
+ Email ${email}.
4
+ </div>
@@ -0,0 +1,23 @@
1
+ import {default as htmx, partial} from "@primate/htmx";
2
+
3
+ export default router => {
4
+ // the HTML tagged template handler loads a component from the `components`
5
+ // directory and serves it as HTML, passing any given data as attributes
6
+ router.get("/users", () => {
7
+ const users = [
8
+ {name: "Donald", email: "donald@the.duck"},
9
+ {name: "Joe", email: "joe@was.absent"},
10
+ ];
11
+ return htmx("user-index", {users});
12
+ });
13
+
14
+ // this is the same as above, with support for partial rendering (without
15
+ // index.html)
16
+ router.get("/other-users", () => {
17
+ const users = [
18
+ {name: "Other Donald", email: "donald@the.goose"},
19
+ {name: "Other Joe", email: "joe@was.around"},
20
+ ];
21
+ return partial("user-index", {users});
22
+ });
23
+ };
@@ -0,0 +1,6 @@
1
+ import redirect from "@primate/html";
2
+
3
+ export default router => {
4
+ // redirect the request
5
+ router.get("/user", () => redirect("/users"));
6
+ };
@@ -1,12 +1,11 @@
1
1
  import {Domain} from "@primate/domains";
2
2
 
3
- // A basic domain that contains two properies
3
+ // A basic domain with two properies
4
4
  export default class User extends Domain {
5
5
  static fields = {
6
- // a user's name must be a string
6
+ // a user's name is a string
7
7
  name: String,
8
- // a user's age must be a number
8
+ // a user's age is a number
9
9
  age: Number,
10
10
  };
11
11
  }
12
-
@@ -3,11 +3,11 @@ import House from "./House.js";
3
3
 
4
4
  export default class User extends Domain {
5
5
  static fields = {
6
- // a user's name must be a string and unique across the user collection
6
+ // a user's name is a string unique across the user collection
7
7
  name: [String, "unique"],
8
- // a user's age must be a positive integer
8
+ // a user's age is a positive integer
9
9
  age: [Number, "integer", "positive"],
10
- // a user's house must have the foreign id of a house record and no two
10
+ // a user's house has the foreign id of a house record and no two
11
11
  // users may have the same house
12
12
  house_id: [House, "unique"],
13
13
  };
@@ -3,11 +3,11 @@ import House from "./House.js";
3
3
 
4
4
  export default class User extends Domain {
5
5
  static fields = {
6
- // a user's name must be a string
6
+ // a user's name is a string
7
7
  name: String,
8
- // a user's age must be a number
8
+ // a user's age is a number
9
9
  age: Number,
10
- // a user's house must have the foreign id of a house record
10
+ // a user's house has the foreign id of a house record
11
11
  house_id: House,
12
12
  };
13
13
  }
@@ -1,4 +1,4 @@
1
1
  export default router => {
2
- // accessing /site/login will serve the `Hello, world!` as plain text
2
+ // accessing /site/login will serve `Hello, world!` as plain text
3
3
  router.get("/site/login", () => "Hello, world!");
4
4
  };
@@ -0,0 +1,4 @@
1
+ export default (router, {redirect}) => {
2
+ // redirect from source to target
3
+ router.get("/source", () => redirect("/target"));
4
+ };
@@ -9,10 +9,10 @@ export default router => {
9
9
  router.map("edit-user", () => ({name: "Donald"}));
10
10
 
11
11
  // show user edit form
12
- router.get("edit-user", user => html`<user-edit user="${user}" />`);
12
+ router.get("edit-user", user => html("user-edit", {user}));
13
13
 
14
14
  // verify form and save, or show errors
15
15
  router.post("edit-user", async user => await user.save()
16
- ? redirect`/users`
16
+ ? redirect("/users")
17
17
  : html`<user-edit user="${user}" />`);
18
18
  };
@@ -1,13 +1,12 @@
1
- import html from "@primate/html";
1
+ // Use an explicit handler as we can't detect HTML by the return value type
2
+ export default (router, {html}) => {
3
+ // Embed components/hello-world.html into static/index.html and serve it. In
4
+ // case a user-provided index.html doesn't exist, use a fallback index.html
5
+ router.get("/hello", () => html("hello-world"));
2
6
 
3
- export default router => {
4
- // the HTML tagged template handler loads a component from the `components`
5
- // directory and serves it as HTML, passing any given data as attributes
6
- router.get("/users", () => {
7
- const users = [
8
- {name: "Donald", email: "donald@the.duck"},
9
- {name: "Joe", email: "joe@was.absent"},
10
- ];
11
- return html`<user-index users="${users}" />`;
12
- });
7
+ // Same as above, but without embedding
8
+ router.get("/hello-partial", () => html("hello-world", {partial: true}));
9
+
10
+ // Serve directly from string instead of loading a component
11
+ router.get("/hello-adhoc", () => html("<p>Hello, world!</p>", {adhoc: true}));
13
12
  };
@@ -1,12 +1,12 @@
1
1
  import {File} from "runtime-compat/filesystem";
2
2
 
3
3
  export default router => {
4
- // any proper JavaScript object will be served as JSON
4
+ // Serve proper JavaScript objects as JSON
5
5
  router.get("/users", () => [
6
6
  {name: "Donald"},
7
7
  {name: "Ryan"},
8
8
  ]);
9
9
 
10
- // load from a file and serve as JSON
10
+ // Load from file and serve as JSON
11
11
  router.get("/users-from-file", () => File.json("users.json"));
12
12
  };
@@ -1,4 +1,4 @@
1
1
  export default router => {
2
- // strings will be served as plain text
2
+ // Serve strings as plain text
3
3
  router.get("/user", () => "Donald");
4
4
  };
@@ -0,0 +1,6 @@
1
+ import {Response} from "runtime-compat/http";
2
+
3
+ export default router => {
4
+ // Use a Response object for custom response status
5
+ router.get("/create", () => new Response("created!", {status: 201}));
6
+ };
@@ -1,6 +1,6 @@
1
1
  import {File} from "runtime-compat/filesystem";
2
2
 
3
3
  export default router => {
4
- // `File` implements `readable`, which is a ReadableStream
4
+ // `File` implements `readable`, which is a `ReadableStream`
5
5
  router.get("/users", () => new File("users.json"));
6
6
  };
@@ -0,0 +1,135 @@
1
+ # Primate
2
+
3
+ Expressive, minimal and extensible framework for JavaScript.
4
+
5
+ ## Getting started
6
+
7
+ Create a route in `routes/hello.js`
8
+
9
+ ```js
10
+ // getting-started.js
11
+ ```
12
+
13
+ Add `{"type": "module"}` to your `package.json` and run `npx -y primate@latest`.
14
+
15
+ ## Table of Contents
16
+
17
+ - [Serving content](#serving-content)
18
+ - [Plain text](#plain-text)
19
+ - [JSON](#json)
20
+ - [Streams](#streams)
21
+ - [Response](#response)
22
+ - [HTML](#html)
23
+ - [Routing](#routing)
24
+ - [Basic](#basic)
25
+ - [The request object](#the-request-object)
26
+ - [Accessing the request body](#accessing-the-request-body)
27
+ - [Regular expressions](#regular-expressions)
28
+ - [Named groups](#named-groups)
29
+ - [Aliasing](#aliasing)
30
+ - [Sharing logic across requests](#sharing-logic-across-requests)
31
+ - [Explicit handlers](#explicit-handlers)
32
+
33
+ ## Serving content
34
+
35
+ Create a file in `routes` that exports a default function.
36
+
37
+ ### Plain text
38
+
39
+ ```js
40
+ // serving-content/plain-text.js
41
+ ```
42
+
43
+ ### JSON
44
+
45
+ ```js
46
+ // serving-content/json.js
47
+ ```
48
+
49
+ ### Streams
50
+
51
+ ```js
52
+ // serving-content/streams.js
53
+ ```
54
+
55
+ ### Response
56
+
57
+ ```js
58
+ // serving-content/response.js
59
+ ```
60
+
61
+ ### HTML
62
+
63
+ ```js
64
+ // serving-content/html.js
65
+ ```
66
+
67
+ ## Routing
68
+
69
+ Routes map requests to responses. They are loaded from `routes`.
70
+
71
+ ### Basic
72
+
73
+ ```js
74
+ // routing/basic.js
75
+ ```
76
+
77
+ ### The request object
78
+
79
+ ```js
80
+ // routing/the-request-object.js
81
+ ```
82
+
83
+ ### Accessing the request body
84
+
85
+ For requests containing a body, Primate will attempt to parse the body according
86
+ to the content type sent along the request. Currently supported are
87
+ `application/x-www-form-urlencoded` (typically for form submission) and
88
+ `application/json`.
89
+
90
+ ```js
91
+ // routing/accessing-the-request-body.js
92
+ ```
93
+
94
+ ### Regular expressions
95
+
96
+ ```js
97
+ // routing/regular-expressions.js
98
+ ```
99
+
100
+ ### Named groups
101
+
102
+ ```js
103
+ // routing/named-groups.js
104
+ ```
105
+
106
+ ### Aliasing
107
+
108
+ ```js
109
+ // routing/aliasing.js
110
+ ```
111
+
112
+ ### Sharing logic across requests
113
+
114
+ ```js
115
+ // routing/sharing-logic-across-requests.js
116
+ ```
117
+
118
+ ### Explicit handlers
119
+
120
+ Most often we can figure the content type to respond with based on the return
121
+ type from the handler. To handle content not automatically detected, use the
122
+ second argument of the exported function.
123
+
124
+ ```js
125
+ // routing/explicit-handlers.js
126
+ ```
127
+
128
+ ## Resources
129
+
130
+ * Website: https://primatejs.com
131
+ * IRC: Join the `#primate` channel on `irc.libera.chat`.
132
+
133
+ ## License
134
+
135
+ MIT