primate 0.12.0 → 0.13.1
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 +40 -201
- package/eslint.config.js +1 -0
- package/module.json +9 -4
- package/package.json +12 -15
- package/readme/extensions/handlers/html/user.js +1 -1
- package/readme/extensions/handlers/htmx/user.js +2 -2
- package/readme/extensions/handlers/redirect/user.js +1 -1
- package/readme/routing/explicit-handlers.js +4 -0
- package/readme/routing/sharing-logic-across-requests.js +9 -12
- package/readme/serving-content/html.js +12 -0
- package/readme/serving-content/json.js +2 -2
- package/readme/serving-content/plain-text.js +1 -1
- package/readme/serving-content/response.js +1 -1
- package/readme/template.md +14 -105
- package/src/bundle.js +15 -6
- package/src/compile.js +5 -0
- package/src/config.js +36 -8
- package/src/handlers/exports.js +5 -0
- package/src/handlers/html.js +18 -0
- package/src/handlers/http404.js +6 -4
- package/src/handlers/json.js +6 -4
- package/src/handlers/redirect.js +7 -0
- package/src/handlers/stream.js +6 -4
- package/src/handlers/text.js +6 -11
- package/src/index.html +9 -0
- package/src/log.js +3 -3
- package/src/register.js +5 -0
- package/src/respond.js +8 -8
- package/src/route.js +12 -5
- package/src/run.js +7 -6
- package/src/serve.js +12 -3
- package/readme/getting-started/generate-ssl.sh +0 -1
- package/readme/getting-started/lay-out-app.sh +0 -1
- package/readme/getting-started/site-index.html +0 -1
- package/readme/getting-started/site.js +0 -3
- /package/readme/{getting-started/hello.js → getting-started.js} +0 -0
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ Add `{"type": "module"}` to your `package.json` and run `npx -y primate@latest`.
|
|
|
22
22
|
- [JSON](#json)
|
|
23
23
|
- [Streams](#streams)
|
|
24
24
|
- [Response](#response)
|
|
25
|
+
- [HTML](#html)
|
|
25
26
|
- [Routing](#routing)
|
|
26
27
|
- [Basic](#basic)
|
|
27
28
|
- [The request object](#the-request-object)
|
|
@@ -30,13 +31,7 @@ Add `{"type": "module"}` to your `package.json` and run `npx -y primate@latest`.
|
|
|
30
31
|
- [Named groups](#named-groups)
|
|
31
32
|
- [Aliasing](#aliasing)
|
|
32
33
|
- [Sharing logic across requests](#sharing-logic-across-requests)
|
|
33
|
-
- [
|
|
34
|
-
- [Handlers](#handlers)
|
|
35
|
-
- [HTML](#html)
|
|
36
|
-
- [Redirect](#redirect)
|
|
37
|
-
- [HTMX](#htmx)
|
|
38
|
-
- [Modules](#modules)
|
|
39
|
-
- [Data persistance](#data-persistance)
|
|
34
|
+
- [Explicit handlers](#explicit-handlers)
|
|
40
35
|
|
|
41
36
|
## Serving content
|
|
42
37
|
|
|
@@ -46,7 +41,7 @@ Create a file in `routes` that exports a default function.
|
|
|
46
41
|
|
|
47
42
|
```js
|
|
48
43
|
export default router => {
|
|
49
|
-
// strings
|
|
44
|
+
// Serve strings as plain text
|
|
50
45
|
router.get("/user", () => "Donald");
|
|
51
46
|
};
|
|
52
47
|
|
|
@@ -58,13 +53,13 @@ export default router => {
|
|
|
58
53
|
import {File} from "runtime-compat/filesystem";
|
|
59
54
|
|
|
60
55
|
export default router => {
|
|
61
|
-
//
|
|
56
|
+
// Serve proper JavaScript objects as JSON
|
|
62
57
|
router.get("/users", () => [
|
|
63
58
|
{name: "Donald"},
|
|
64
59
|
{name: "Ryan"},
|
|
65
60
|
]);
|
|
66
61
|
|
|
67
|
-
//
|
|
62
|
+
// Load from file and serve as JSON
|
|
68
63
|
router.get("/users-from-file", () => File.json("users.json"));
|
|
69
64
|
};
|
|
70
65
|
|
|
@@ -88,12 +83,30 @@ export default router => {
|
|
|
88
83
|
import {Response} from "runtime-compat/http";
|
|
89
84
|
|
|
90
85
|
export default router => {
|
|
91
|
-
//
|
|
86
|
+
// Use a Response object for custom response status
|
|
92
87
|
router.get("/create", () => new Response("created!", {status: 201}));
|
|
93
88
|
};
|
|
94
89
|
|
|
95
90
|
```
|
|
96
91
|
|
|
92
|
+
### HTML
|
|
93
|
+
|
|
94
|
+
```js
|
|
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"));
|
|
100
|
+
|
|
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}));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
|
|
97
110
|
## Routing
|
|
98
111
|
|
|
99
112
|
Routes map requests to responses. They are loaded from `routes`.
|
|
@@ -177,206 +190,38 @@ export default router => {
|
|
|
177
190
|
### Sharing logic across requests
|
|
178
191
|
|
|
179
192
|
```js
|
|
180
|
-
import html from "@primate/html";
|
|
181
|
-
import redirect from "@primate/redirect";
|
|
182
|
-
|
|
183
193
|
export default router => {
|
|
184
|
-
//
|
|
194
|
+
// Declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
|
|
185
195
|
router.alias("edit-user", "/user/edit/([0-9])+");
|
|
186
196
|
|
|
187
|
-
//
|
|
188
|
-
router.map("edit-user", () =>
|
|
197
|
+
// Pass user instead of request to all verbs on this route
|
|
198
|
+
router.map("edit-user", ({body}) => body?.name ?? "Donald");
|
|
189
199
|
|
|
190
|
-
//
|
|
191
|
-
router.get("edit-user", user =>
|
|
200
|
+
// Show user as plain text
|
|
201
|
+
router.get("edit-user", user => user);
|
|
192
202
|
|
|
193
|
-
//
|
|
194
|
-
router.post("edit-user",
|
|
195
|
-
?
|
|
196
|
-
:
|
|
203
|
+
// Verify or show error
|
|
204
|
+
router.post("edit-user", user => user === "Donald"
|
|
205
|
+
? "Hi Donald!"
|
|
206
|
+
: {message: "Error saving user"});
|
|
197
207
|
};
|
|
198
208
|
|
|
199
209
|
```
|
|
200
210
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
There are two ways to extend Primate's core functionality. Handlers are used
|
|
204
|
-
per route to serve new types of content not supported by core. Modules extend
|
|
205
|
-
an app's entire scope.
|
|
206
|
-
|
|
207
|
-
Handlers and modules listed here are officially developed and supported by
|
|
208
|
-
Primate.
|
|
209
|
-
|
|
210
|
-
### Handlers
|
|
211
|
-
|
|
212
|
-
#### HTML
|
|
211
|
+
### Explicit handlers
|
|
213
212
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`components`.
|
|
218
|
-
|
|
219
|
-
Create an HTML component in `components/user-index.html`
|
|
220
|
-
|
|
221
|
-
```html
|
|
222
|
-
<div for="${users}">
|
|
223
|
-
User ${name}.
|
|
224
|
-
Email ${email}.
|
|
225
|
-
</div>
|
|
226
|
-
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Create a route in `route/user.js` and serve the component in your route
|
|
213
|
+
Most often we can figure out the content type to respond with based on the
|
|
214
|
+
return type from the handler. To handle content not automatically detected, use
|
|
215
|
+
the second argument of the exported function.
|
|
230
216
|
|
|
231
217
|
```js
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
// the HTML tagged template handler loads a component from the `components`
|
|
236
|
-
// directory and serves it as HTML, passing any given data as attributes
|
|
237
|
-
router.get("/users", () => {
|
|
238
|
-
const users = [
|
|
239
|
-
{name: "Donald", email: "donald@the.duck"},
|
|
240
|
-
{name: "Joe", email: "joe@was.absent"},
|
|
241
|
-
];
|
|
242
|
-
return html`<user-index users="${users}" />`;
|
|
243
|
-
});
|
|
218
|
+
export default (router, {redirect}) => {
|
|
219
|
+
// redirect from source to target
|
|
220
|
+
router.get("/source", () => redirect("/target"));
|
|
244
221
|
};
|
|
245
222
|
|
|
246
223
|
```
|
|
247
224
|
|
|
248
|
-
#### Redirect
|
|
249
|
-
|
|
250
|
-
*[`@primate/redirect`][primate-redirect]*
|
|
251
|
-
|
|
252
|
-
Redirect the request.
|
|
253
|
-
|
|
254
|
-
Create a route in `route/user.js`
|
|
255
|
-
|
|
256
|
-
```js
|
|
257
|
-
import redirect from "@primate/html";
|
|
258
|
-
|
|
259
|
-
export default router => {
|
|
260
|
-
// redirect the request
|
|
261
|
-
router.get("/user", () => redirect`/users`);
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
#### HTMX
|
|
267
|
-
|
|
268
|
-
*[`@primate/htmx`][primate-htmx]*
|
|
269
|
-
|
|
270
|
-
Serve HTML tagged templates with HTMX support. This handler reads HTML component
|
|
271
|
-
files from `components`.
|
|
272
|
-
|
|
273
|
-
Create an HTML component in `components/user-index.html`
|
|
274
|
-
|
|
275
|
-
```html
|
|
276
|
-
<div for="${users}" hx-get="/other-users" hx-swap="outerHTML">
|
|
277
|
-
User ${name}.
|
|
278
|
-
Email ${email}.
|
|
279
|
-
</div>
|
|
280
|
-
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
Create a route in `route/user.js` and serve the component in your route
|
|
284
|
-
|
|
285
|
-
```js
|
|
286
|
-
import {default as htmx, partial} from "@primate/htmx";
|
|
287
|
-
|
|
288
|
-
export default router => {
|
|
289
|
-
// the HTML tagged template handler loads a component from the `components`
|
|
290
|
-
// directory and serves it as HTML, passing any given data as attributes
|
|
291
|
-
router.get("/users", () => {
|
|
292
|
-
const users = [
|
|
293
|
-
{name: "Donald", email: "donald@the.duck"},
|
|
294
|
-
{name: "Joe", email: "joe@was.absent"},
|
|
295
|
-
];
|
|
296
|
-
return htmx`<user-index users="${users}" />`;
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// this is the same as above, with support for partial rendering (without
|
|
300
|
-
// index.html)
|
|
301
|
-
router.get("/other-users", () => {
|
|
302
|
-
const users = [
|
|
303
|
-
{name: "Other Donald", email: "donald@the.goose"},
|
|
304
|
-
{name: "Other Joe", email: "joe@was.around"},
|
|
305
|
-
];
|
|
306
|
-
return partial`<user-index users="${users}" />`;
|
|
307
|
-
});
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
### Modules
|
|
313
|
-
|
|
314
|
-
To add modules, create a `primate.config.js` configuration file in your
|
|
315
|
-
project's root. This file should export a default object with the property
|
|
316
|
-
`modules` used for extending your app.
|
|
317
|
-
|
|
318
|
-
```js
|
|
319
|
-
export default {
|
|
320
|
-
modules: [],
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
#### Data persistance
|
|
326
|
-
|
|
327
|
-
*[`@primate/domains`][primate-domains]*
|
|
328
|
-
|
|
329
|
-
Add data persistance in the form of ORM backed up by various drivers.
|
|
330
|
-
|
|
331
|
-
Import and initialize this module in your configuration file
|
|
332
|
-
|
|
333
|
-
```js
|
|
334
|
-
import domains from "@primate/domains";
|
|
335
|
-
|
|
336
|
-
export default {
|
|
337
|
-
modules: [domains()],
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
A domain represents a collection in a store using the static `fields` property
|
|
343
|
-
|
|
344
|
-
```js
|
|
345
|
-
import {Domain} from "@primate/domains";
|
|
346
|
-
|
|
347
|
-
// A basic domain with two properies
|
|
348
|
-
export default class User extends Domain {
|
|
349
|
-
static fields = {
|
|
350
|
-
// a user's name is a string
|
|
351
|
-
name: String,
|
|
352
|
-
// a user's age is a number
|
|
353
|
-
age: Number,
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
Field types may also be specified as an array with additional predicates
|
|
360
|
-
aside from the type
|
|
361
|
-
|
|
362
|
-
```js
|
|
363
|
-
import {Domain} from "@primate/domains";
|
|
364
|
-
import House from "./House.js";
|
|
365
|
-
|
|
366
|
-
export default class User extends Domain {
|
|
367
|
-
static fields = {
|
|
368
|
-
// a user's name is a string unique across the user collection
|
|
369
|
-
name: [String, "unique"],
|
|
370
|
-
// a user's age is a positive integer
|
|
371
|
-
age: [Number, "integer", "positive"],
|
|
372
|
-
// a user's house has the foreign id of a house record and no two
|
|
373
|
-
// users may have the same house
|
|
374
|
-
house_id: [House, "unique"],
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
```
|
|
379
|
-
|
|
380
225
|
## Resources
|
|
381
226
|
|
|
382
227
|
* Website: https://primatejs.com
|
|
@@ -385,9 +230,3 @@ export default class User extends Domain {
|
|
|
385
230
|
## License
|
|
386
231
|
|
|
387
232
|
MIT
|
|
388
|
-
|
|
389
|
-
[primate-html]: https://github.com/primatejs/primate-html
|
|
390
|
-
[primate-redirect]: https://github.com/primatejs/primate-redirect
|
|
391
|
-
[primate-htmx]: https://github.com/primatejs/primate-htmx
|
|
392
|
-
[primate-domains]: https://github.com/primatejs/primate-domains
|
|
393
|
-
[primate-sessions]: https://github.com/primatejs/primate-sessions
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {default} from "maximin";
|
package/module.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "Expressive, minimal and extensible framework for JavaScript",
|
|
4
5
|
"homepage": "https://primatejs.com",
|
|
5
6
|
"bugs": "https://github.com/primatejs/primate/issues",
|
|
6
|
-
"repository": "https://github.com/primatejs/primate",
|
|
7
|
-
"description": "Expressive, minimal and extensible framework for JavaScript",
|
|
8
7
|
"license": "MIT",
|
|
9
|
-
"bin": "src/bin.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.
|
|
3
|
+
"version": "0.13.1",
|
|
4
|
+
"description": "Expressive, minimal and extensible framework for JavaScript",
|
|
4
5
|
"homepage": "https://primatejs.com",
|
|
5
6
|
"bugs": "https://github.com/primatejs/primate/issues",
|
|
6
|
-
"repository": "https://github.com/primatejs/primate",
|
|
7
|
-
"description": "Expressive, minimal and extensible framework for JavaScript",
|
|
8
7
|
"license": "MIT",
|
|
9
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
16
|
"runtime-compat": "^0.14.1"
|
|
12
17
|
},
|
|
13
18
|
"devDependencies": {
|
|
14
|
-
"
|
|
15
|
-
"@babel/eslint-parser": "^7.21.3",
|
|
16
|
-
"@babel/plugin-syntax-import-assertions": "^7.20.0",
|
|
17
|
-
"eslint": "^8.37.0",
|
|
18
|
-
"eslint-plugin-json": "^3.1.0"
|
|
19
|
+
"maximin": "^0.1.2"
|
|
19
20
|
},
|
|
20
|
-
"scripts": {
|
|
21
|
-
"docs": "scripts/docs.sh",
|
|
22
|
-
"test": "npx debris"
|
|
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
|
}
|
|
@@ -8,7 +8,7 @@ export default router => {
|
|
|
8
8
|
{name: "Donald", email: "donald@the.duck"},
|
|
9
9
|
{name: "Joe", email: "joe@was.absent"},
|
|
10
10
|
];
|
|
11
|
-
return htmx
|
|
11
|
+
return htmx("user-index", {users});
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
// this is the same as above, with support for partial rendering (without
|
|
@@ -18,6 +18,6 @@ export default router => {
|
|
|
18
18
|
{name: "Other Donald", email: "donald@the.goose"},
|
|
19
19
|
{name: "Other Joe", email: "joe@was.around"},
|
|
20
20
|
];
|
|
21
|
-
return partial
|
|
21
|
+
return partial("user-index", {users});
|
|
22
22
|
});
|
|
23
23
|
};
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import html from "@primate/html";
|
|
2
|
-
import redirect from "@primate/redirect";
|
|
3
|
-
|
|
4
1
|
export default router => {
|
|
5
|
-
//
|
|
2
|
+
// Declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
|
|
6
3
|
router.alias("edit-user", "/user/edit/([0-9])+");
|
|
7
4
|
|
|
8
|
-
//
|
|
9
|
-
router.map("edit-user", () =>
|
|
5
|
+
// Pass user instead of request to all verbs on this route
|
|
6
|
+
router.map("edit-user", ({body}) => body?.name ?? "Donald");
|
|
10
7
|
|
|
11
|
-
//
|
|
12
|
-
router.get("edit-user", user =>
|
|
8
|
+
// Show user as plain text
|
|
9
|
+
router.get("edit-user", user => user);
|
|
13
10
|
|
|
14
|
-
//
|
|
15
|
-
router.post("edit-user",
|
|
16
|
-
?
|
|
17
|
-
:
|
|
11
|
+
// Verify or show error
|
|
12
|
+
router.post("edit-user", user => user === "Donald"
|
|
13
|
+
? "Hi Donald!"
|
|
14
|
+
: {message: "Error saving user"});
|
|
18
15
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
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"));
|
|
6
|
+
|
|
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}));
|
|
12
|
+
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {File} from "runtime-compat/filesystem";
|
|
2
2
|
|
|
3
3
|
export default router => {
|
|
4
|
-
//
|
|
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
|
-
//
|
|
10
|
+
// Load from file and serve as JSON
|
|
11
11
|
router.get("/users-from-file", () => File.json("users.json"));
|
|
12
12
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Response} from "runtime-compat/http";
|
|
2
2
|
|
|
3
3
|
export default router => {
|
|
4
|
-
//
|
|
4
|
+
// Use a Response object for custom response status
|
|
5
5
|
router.get("/create", () => new Response("created!", {status: 201}));
|
|
6
6
|
};
|
package/readme/template.md
CHANGED
|
@@ -7,7 +7,7 @@ Expressive, minimal and extensible framework for JavaScript.
|
|
|
7
7
|
Create a route in `routes/hello.js`
|
|
8
8
|
|
|
9
9
|
```js
|
|
10
|
-
// getting-started
|
|
10
|
+
// getting-started.js
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Add `{"type": "module"}` to your `package.json` and run `npx -y primate@latest`.
|
|
@@ -19,6 +19,7 @@ Add `{"type": "module"}` to your `package.json` and run `npx -y primate@latest`.
|
|
|
19
19
|
- [JSON](#json)
|
|
20
20
|
- [Streams](#streams)
|
|
21
21
|
- [Response](#response)
|
|
22
|
+
- [HTML](#html)
|
|
22
23
|
- [Routing](#routing)
|
|
23
24
|
- [Basic](#basic)
|
|
24
25
|
- [The request object](#the-request-object)
|
|
@@ -27,13 +28,7 @@ Add `{"type": "module"}` to your `package.json` and run `npx -y primate@latest`.
|
|
|
27
28
|
- [Named groups](#named-groups)
|
|
28
29
|
- [Aliasing](#aliasing)
|
|
29
30
|
- [Sharing logic across requests](#sharing-logic-across-requests)
|
|
30
|
-
- [
|
|
31
|
-
- [Handlers](#handlers)
|
|
32
|
-
- [HTML](#html)
|
|
33
|
-
- [Redirect](#redirect)
|
|
34
|
-
- [HTMX](#htmx)
|
|
35
|
-
- [Modules](#modules)
|
|
36
|
-
- [Data persistance](#data-persistance)
|
|
31
|
+
- [Explicit handlers](#explicit-handlers)
|
|
37
32
|
|
|
38
33
|
## Serving content
|
|
39
34
|
|
|
@@ -63,6 +58,12 @@ Create a file in `routes` that exports a default function.
|
|
|
63
58
|
// serving-content/response.js
|
|
64
59
|
```
|
|
65
60
|
|
|
61
|
+
### HTML
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
// serving-content/html.js
|
|
65
|
+
```
|
|
66
|
+
|
|
66
67
|
## Routing
|
|
67
68
|
|
|
68
69
|
Routes map requests to responses. They are loaded from `routes`.
|
|
@@ -114,100 +115,14 @@ to the content type sent along the request. Currently supported are
|
|
|
114
115
|
// routing/sharing-logic-across-requests.js
|
|
115
116
|
```
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
There are two ways to extend Primate's core functionality. Handlers are used
|
|
120
|
-
per route to serve new types of content not supported by core. Modules extend
|
|
121
|
-
an app's entire scope.
|
|
118
|
+
### Explicit handlers
|
|
122
119
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
### Handlers
|
|
127
|
-
|
|
128
|
-
#### HTML
|
|
129
|
-
|
|
130
|
-
*[`@primate/html`][primate-html]*
|
|
131
|
-
|
|
132
|
-
Serve HTML tagged templates. This handler reads HTML component files from
|
|
133
|
-
`components`.
|
|
134
|
-
|
|
135
|
-
Create an HTML component in `components/user-index.html`
|
|
136
|
-
|
|
137
|
-
```html
|
|
138
|
-
<!-- extensions/handlers/html/user-index.html -->
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Create a route in `route/user.js` and serve the component in your route
|
|
120
|
+
Most often we can figure out the content type to respond with based on the
|
|
121
|
+
return type from the handler. To handle content not automatically detected, use
|
|
122
|
+
the second argument of the exported function.
|
|
142
123
|
|
|
143
124
|
```js
|
|
144
|
-
//
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
#### Redirect
|
|
148
|
-
|
|
149
|
-
*[`@primate/redirect`][primate-redirect]*
|
|
150
|
-
|
|
151
|
-
Redirect the request.
|
|
152
|
-
|
|
153
|
-
Create a route in `route/user.js`
|
|
154
|
-
|
|
155
|
-
```js
|
|
156
|
-
// extensions/handlers/redirect/user.js
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
#### HTMX
|
|
160
|
-
|
|
161
|
-
*[`@primate/htmx`][primate-htmx]*
|
|
162
|
-
|
|
163
|
-
Serve HTML tagged templates with HTMX support. This handler reads HTML component
|
|
164
|
-
files from `components`.
|
|
165
|
-
|
|
166
|
-
Create an HTML component in `components/user-index.html`
|
|
167
|
-
|
|
168
|
-
```html
|
|
169
|
-
<!-- extensions/handlers/htmx/user-index.html -->
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Create a route in `route/user.js` and serve the component in your route
|
|
173
|
-
|
|
174
|
-
```js
|
|
175
|
-
// extensions/handlers/htmx/user.js
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Modules
|
|
179
|
-
|
|
180
|
-
To add modules, create a `primate.config.js` configuration file in your
|
|
181
|
-
project's root. This file should export a default object with the property
|
|
182
|
-
`modules` used for extending your app.
|
|
183
|
-
|
|
184
|
-
```js
|
|
185
|
-
// extensions/modules/configure.js
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
#### Data persistance
|
|
189
|
-
|
|
190
|
-
*[`@primate/domains`][primate-domains]*
|
|
191
|
-
|
|
192
|
-
Add data persistance in the form of ORM backed up by various drivers.
|
|
193
|
-
|
|
194
|
-
Import and initialize this module in your configuration file
|
|
195
|
-
|
|
196
|
-
```js
|
|
197
|
-
// extensions/modules/domains/configure.js
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
A domain represents a collection in a store using the static `fields` property
|
|
201
|
-
|
|
202
|
-
```js
|
|
203
|
-
// extensions/modules/domains/fields.js
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
Field types may also be specified as an array with additional predicates
|
|
207
|
-
aside from the type
|
|
208
|
-
|
|
209
|
-
```js
|
|
210
|
-
// extensions/modules/domains/predicates.js
|
|
125
|
+
// routing/explicit-handlers.js
|
|
211
126
|
```
|
|
212
127
|
|
|
213
128
|
## Resources
|
|
@@ -218,9 +133,3 @@ aside from the type
|
|
|
218
133
|
## License
|
|
219
134
|
|
|
220
135
|
MIT
|
|
221
|
-
|
|
222
|
-
[primate-html]: https://github.com/primatejs/primate-html
|
|
223
|
-
[primate-redirect]: https://github.com/primatejs/primate-redirect
|
|
224
|
-
[primate-htmx]: https://github.com/primatejs/primate-htmx
|
|
225
|
-
[primate-domains]: https://github.com/primatejs/primate-domains
|
|
226
|
-
[primate-sessions]: https://github.com/primatejs/primate-sessions
|
package/src/bundle.js
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import {File} from "runtime-compat/fs";
|
|
2
|
+
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
const makePublic = async env => {
|
|
4
5
|
const {paths} = env;
|
|
6
|
+
|
|
7
|
+
// remove public directory in case exists
|
|
8
|
+
if (await paths.public.exists) {
|
|
9
|
+
await paths.public.file.remove();
|
|
10
|
+
}
|
|
11
|
+
await paths.public.file.create();
|
|
12
|
+
|
|
5
13
|
if (await paths.static.exists) {
|
|
6
|
-
// remove public directory in case exists
|
|
7
|
-
if (await paths.public.exists) {
|
|
8
|
-
await paths.public.file.remove();
|
|
9
|
-
}
|
|
10
|
-
await paths.public.file.create();
|
|
11
14
|
// copy static files to public
|
|
12
15
|
await File.copy(paths.static, paths.public);
|
|
13
16
|
}
|
|
14
17
|
};
|
|
18
|
+
|
|
19
|
+
export default async env => {
|
|
20
|
+
await makePublic(env);
|
|
21
|
+
[...filter("bundle", env.modules), _ => _].reduceRight((acc, handler) =>
|
|
22
|
+
input => handler(input, acc))(env);
|
|
23
|
+
};
|
package/src/compile.js
ADDED
package/src/config.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {Path} from "runtime-compat/fs";
|
|
1
|
+
import {File, Path} from "runtime-compat/fs";
|
|
2
2
|
import {is} from "runtime-compat/dyndef";
|
|
3
3
|
import cache from "./cache.js";
|
|
4
4
|
import extend from "./extend.js";
|
|
5
5
|
import defaults from "./primate.config.js";
|
|
6
6
|
import * as log from "./log.js";
|
|
7
|
+
import * as handlers from "./handlers/exports.js";
|
|
7
8
|
import package_json from "../package.json" assert {type: "json"};
|
|
8
9
|
|
|
9
10
|
const qualify = (root, paths) =>
|
|
@@ -23,26 +24,53 @@ const getConfig = async (root, filename) => {
|
|
|
23
24
|
}
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
is(filename).string();
|
|
28
|
-
let root;
|
|
27
|
+
const getRoot = async () => {
|
|
29
28
|
try {
|
|
30
29
|
// use module root if possible
|
|
31
|
-
|
|
30
|
+
return await Path.root();
|
|
32
31
|
} catch (error) {
|
|
33
32
|
// fall back to current directory
|
|
34
|
-
|
|
33
|
+
return Path.resolve();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const index = async env => {
|
|
38
|
+
const name = "index.html";
|
|
39
|
+
try {
|
|
40
|
+
// user-provided file
|
|
41
|
+
return await File.read(`${env.paths.static.join(name)}`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// fallback
|
|
44
|
+
return new Path(import.meta.url).directory.join(name).file.read();
|
|
35
45
|
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default async (filename = "primate.config.js") => {
|
|
49
|
+
is(filename).string();
|
|
50
|
+
const root = await getRoot();
|
|
36
51
|
const config = await getConfig(root, filename);
|
|
37
52
|
|
|
38
53
|
const env = {
|
|
39
54
|
...config,
|
|
40
55
|
paths: qualify(root, config.paths),
|
|
41
56
|
root,
|
|
42
|
-
log: {...log, error: error => log.error(error, config),
|
|
57
|
+
log: {...log, error: error => log.error(error, config)},
|
|
58
|
+
register: (name, handler) => {
|
|
59
|
+
env.handlers[name] = handler;
|
|
60
|
+
},
|
|
61
|
+
handlers: {...handlers},
|
|
62
|
+
render: async ({body = "", head = ""} = {}) => {
|
|
63
|
+
const html = await index(env);
|
|
64
|
+
return html.replace("%body%", () => body).replace("%head%", () => head);
|
|
43
65
|
},
|
|
44
66
|
};
|
|
45
67
|
env.log.info(`${package_json.name} \x1b[34m${package_json.version}\x1b[0m`);
|
|
46
68
|
const modules = await Promise.all(config.modules.map(module => module(env)));
|
|
47
|
-
|
|
69
|
+
// modules may load other modules
|
|
70
|
+
const loads = await Promise.all(modules
|
|
71
|
+
.filter(module => module.load !== undefined)
|
|
72
|
+
.map(module => module.load()(env)));
|
|
73
|
+
|
|
74
|
+
return cache("config", filename, () => ({...env,
|
|
75
|
+
modules: modules.concat(loads)}));
|
|
48
76
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const getContent = async (env, name) => {
|
|
2
|
+
try {
|
|
3
|
+
return await env.paths.components.join(`${name}.html`).file.read();
|
|
4
|
+
} catch (error) {
|
|
5
|
+
throw new Error(`cannot load component at ${name}.html`);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default (content, {status = 200, partial = false, adhoc = false} = {}) =>
|
|
10
|
+
async (env, headers) => {
|
|
11
|
+
const body = adhoc ? content : await getContent(env, content);
|
|
12
|
+
return [
|
|
13
|
+
partial ? body : await env.render({body}), {
|
|
14
|
+
status,
|
|
15
|
+
headers: {...headers, "Content-Type": "text/html"},
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
};
|
package/src/handlers/http404.js
CHANGED
package/src/handlers/json.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export default (
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
1
|
+
export default (body, {status = 200} = {}) => (_, headers) => [
|
|
2
|
+
JSON.stringify(body), {
|
|
3
|
+
status,
|
|
4
|
+
headers: {...headers, "Content-Type": "application/json"},
|
|
5
|
+
},
|
|
6
|
+
];
|
package/src/handlers/stream.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export default (
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
1
|
+
export default (body, {status = 200} = {}) => (_, headers) => [
|
|
2
|
+
body, {
|
|
3
|
+
status,
|
|
4
|
+
headers: {...headers, "Content-Type": "application/octet-stream"},
|
|
5
|
+
},
|
|
6
|
+
];
|
package/src/handlers/text.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
.map((string, i) => string + awaitedKeys[i])
|
|
8
|
-
.join("") + strings[strings.length + last];
|
|
9
|
-
|
|
10
|
-
return [body, {status: 200, headers: {"Content-Type": "text/plain"}}];
|
|
11
|
-
};
|
|
1
|
+
export default (body, {status = 200} = {}) => (_, headers) => [
|
|
2
|
+
body, {
|
|
3
|
+
status,
|
|
4
|
+
headers: {...headers, "Content-Type": "text/plain"},
|
|
5
|
+
},
|
|
6
|
+
];
|
package/src/index.html
ADDED
package/src/log.js
CHANGED
|
@@ -23,7 +23,7 @@ export const info = (...args) => log.green("[info]").reset(...args).nl();
|
|
|
23
23
|
|
|
24
24
|
export const warn = (...args) => log.yellow("[warn]").reset(...args).nl();
|
|
25
25
|
|
|
26
|
-
export const error = (
|
|
27
|
-
log.red("[error]").reset(
|
|
28
|
-
env.debug && console.log(
|
|
26
|
+
export const error = (originalError, env) => {
|
|
27
|
+
log.red("[error]").reset(originalError.message).nl();
|
|
28
|
+
env.debug && console.log(originalError);
|
|
29
29
|
};
|
package/src/register.js
ADDED
package/src/respond.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import {Blob} from "runtime-compat/fs";
|
|
2
|
-
import text from "./handlers/
|
|
3
|
-
import json from "./handlers/json.js";
|
|
4
|
-
import stream from "./handlers/stream.js";
|
|
2
|
+
import {text, json, stream} from "./handlers/exports.js";
|
|
5
3
|
import {isResponse as isResponseDuck} from "./duck.js";
|
|
6
4
|
import RouteError from "./errors/Route.js";
|
|
7
5
|
|
|
8
6
|
const isText = value => {
|
|
9
7
|
if (typeof value === "string") {
|
|
10
|
-
return text
|
|
8
|
+
return text(value);
|
|
11
9
|
}
|
|
12
10
|
throw new RouteError(`no handler found for ${value}`);
|
|
13
11
|
};
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
|
|
13
|
+
const isNonNullObject = value => typeof value === "object" && value !== null;
|
|
14
|
+
const isObject = value => isNonNullObject(value)
|
|
15
|
+
? json(value) : isText(value);
|
|
16
16
|
const isResponse = value => isResponseDuck(value)
|
|
17
17
|
? () => value : isObject(value);
|
|
18
18
|
const isStream = value => value instanceof ReadableStream
|
|
19
|
-
? stream
|
|
19
|
+
? stream(value) : isResponse(value);
|
|
20
20
|
const isBlob = value => value instanceof Blob
|
|
21
|
-
? stream
|
|
21
|
+
? stream(value) : isStream(value);
|
|
22
22
|
const guess = value => isBlob(value);
|
|
23
23
|
|
|
24
24
|
export default result => typeof result === "function" ? result : guess(result);
|
package/src/route.js
CHANGED
|
@@ -4,8 +4,14 @@ import RouteError from "./errors/Route.js";
|
|
|
4
4
|
|
|
5
5
|
// insensitive-case equal
|
|
6
6
|
const ieq = (left, right) => left.toLowerCase() === right.toLowerCase();
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// HTTP verbs
|
|
8
|
+
const verbs = [
|
|
9
|
+
// CRUD
|
|
10
|
+
"post", "get", "put", "delete",
|
|
11
|
+
// extended
|
|
12
|
+
"delete", "connect", "options", "trace", "patch",
|
|
13
|
+
];
|
|
14
|
+
export default async (definitions, handlers) => {
|
|
9
15
|
const aliases = [];
|
|
10
16
|
const routes = [];
|
|
11
17
|
const expand = path => aliases.reduce((expanded, {key, value}) =>
|
|
@@ -25,9 +31,9 @@ export default async definitions => {
|
|
|
25
31
|
ieq(route.method, method) && route.path.test(path)) ?? fallback;
|
|
26
32
|
|
|
27
33
|
const router = {
|
|
34
|
+
...Object.fromEntries(verbs.map(verb =>
|
|
35
|
+
[verb, (path, callback) => add(verb, path, callback)])),
|
|
28
36
|
map: (path, callback) => add("map", path, callback),
|
|
29
|
-
get: (path, callback) => add("get", path, callback),
|
|
30
|
-
post: (path, callback) => add("post", path, callback),
|
|
31
37
|
alias: (key, value) => aliases.push({key, value}),
|
|
32
38
|
route: async request => {
|
|
33
39
|
const {method} = request.original;
|
|
@@ -46,7 +52,8 @@ export default async definitions => {
|
|
|
46
52
|
};
|
|
47
53
|
if (await definitions.exists) {
|
|
48
54
|
const files = (await Path.list(definitions)).map(route => import(route));
|
|
49
|
-
await Promise.all(files.map(async route =>
|
|
55
|
+
await Promise.all(files.map(async route =>
|
|
56
|
+
(await route).default(router, handlers)));
|
|
50
57
|
}
|
|
51
58
|
return router;
|
|
52
59
|
};
|
package/src/run.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import bundle from "./bundle.js";
|
|
2
1
|
import config from "./config.js";
|
|
3
|
-
import
|
|
2
|
+
import register from "./register.js";
|
|
3
|
+
import compile from "./compile.js";
|
|
4
|
+
import bundle from "./bundle.js";
|
|
4
5
|
import route from "./route.js";
|
|
6
|
+
import serve from "./serve.js";
|
|
5
7
|
|
|
6
8
|
export default async () => {
|
|
7
9
|
const env = await config();
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
await register(env);
|
|
11
|
+
await compile(env);
|
|
10
12
|
await bundle(env);
|
|
11
|
-
|
|
12
|
-
serve({router, ...env});
|
|
13
|
+
serve({router: await route(env.paths.routes, env.handlers), ...env});
|
|
13
14
|
};
|
package/src/serve.js
CHANGED
|
@@ -36,7 +36,7 @@ export default env => {
|
|
|
36
36
|
return await respond(await handlers(request))(env, headers);
|
|
37
37
|
} catch (error) {
|
|
38
38
|
env.log.error(error);
|
|
39
|
-
return http404(env, headers)
|
|
39
|
+
return http404()(env, headers);
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
42
|
|
|
@@ -67,11 +67,20 @@ export default env => {
|
|
|
67
67
|
}
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
const
|
|
71
|
-
const type = contents[
|
|
70
|
+
const parseContentType = (contentType, body) => {
|
|
71
|
+
const type = contents[contentType];
|
|
72
72
|
return type === undefined ? body : type(body);
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
const parseContent = (request, body) => {
|
|
76
|
+
try {
|
|
77
|
+
return parseContentType(request.headers.get("content-type"), body);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
env.log.warn(error);
|
|
80
|
+
return body;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
75
84
|
const {http} = env;
|
|
76
85
|
const modules = filter("serve", env.modules);
|
|
77
86
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
openssl req -x509 -out ssl/default.crt -keyout ssl/default.key -newkey rsa:2048 -nodes -sha256 -batch
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
mkdir -p app/{routes,components,ssl} && cd app
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Today's date is ${date}.
|
|
File without changes
|