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 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
- - [Extensions](#extensions)
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 will be served as plain text
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
- // any proper JavaScript object will be served as JSON
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
- // load from a file and serve as JSON
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
- // use a generic response instance for a custom response status
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
- // declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
194
+ // Declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
185
195
  router.alias("edit-user", "/user/edit/([0-9])+");
186
196
 
187
- // pass user instead of request to all verbs with this route
188
- router.map("edit-user", () => ({name: "Donald"}));
197
+ // Pass user instead of request to all verbs on this route
198
+ router.map("edit-user", ({body}) => body?.name ?? "Donald");
189
199
 
190
- // show user edit form
191
- router.get("edit-user", user => html`<user-edit user="${user}" />`);
200
+ // Show user as plain text
201
+ router.get("edit-user", user => user);
192
202
 
193
- // verify form and save, or show errors
194
- router.post("edit-user", async user => await user.save()
195
- ? redirect`/users`
196
- : html`<user-edit user="${user}" />`);
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
- ## Extensions
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
- *[`@primate/html`][primate-html]*
215
-
216
- Serve HTML tagged templates. This handler reads HTML component files from
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
- import html from "@primate/html";
233
-
234
- export default router => {
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
@@ -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.11.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.12.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
- "@babel/core": "^7.21.4",
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,6 +8,6 @@ export default router => {
8
8
  {name: "Donald", email: "donald@the.duck"},
9
9
  {name: "Joe", email: "joe@was.absent"},
10
10
  ];
11
- return html`<user-index users="${users}" />`;
11
+ return html("user-index", {users});
12
12
  });
13
13
  };
@@ -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`<user-index users="${users}" />`;
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`<user-index users="${users}" />`;
21
+ return partial("user-index", {users});
22
22
  });
23
23
  };
@@ -2,5 +2,5 @@ import redirect from "@primate/html";
2
2
 
3
3
  export default router => {
4
4
  // redirect the request
5
- router.get("/user", () => redirect`/users`);
5
+ router.get("/user", () => redirect("/users"));
6
6
  };
@@ -0,0 +1,4 @@
1
+ export default (router, {redirect}) => {
2
+ // redirect from source to target
3
+ router.get("/source", () => redirect("/target"));
4
+ };
@@ -1,18 +1,15 @@
1
- import html from "@primate/html";
2
- import redirect from "@primate/redirect";
3
-
4
1
  export default router => {
5
- // declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
2
+ // Declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
6
3
  router.alias("edit-user", "/user/edit/([0-9])+");
7
4
 
8
- // pass user instead of request to all verbs with this route
9
- router.map("edit-user", () => ({name: "Donald"}));
5
+ // Pass user instead of request to all verbs on this route
6
+ router.map("edit-user", ({body}) => body?.name ?? "Donald");
10
7
 
11
- // show user edit form
12
- router.get("edit-user", user => html`<user-edit user="${user}" />`);
8
+ // Show user as plain text
9
+ router.get("edit-user", user => user);
13
10
 
14
- // verify form and save, or show errors
15
- router.post("edit-user", async user => await user.save()
16
- ? redirect`/users`
17
- : html`<user-edit user="${user}" />`);
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
- // 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
  };
@@ -1,6 +1,6 @@
1
1
  import {Response} from "runtime-compat/http";
2
2
 
3
3
  export default router => {
4
- // use a generic response instance for a custom response status
4
+ // Use a Response object for custom response status
5
5
  router.get("/create", () => new Response("created!", {status: 201}));
6
6
  };
@@ -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/hello.js
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
- - [Extensions](#extensions)
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
- ## Extensions
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
- Handlers and modules listed here are officially developed and supported by
124
- Primate.
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
- // extensions/handlers/html/user.js
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
- export default async env => {
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
@@ -0,0 +1,5 @@
1
+ const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
2
+
3
+ export default async env =>
4
+ [...filter("compile", env.modules), _ => _].reduceRight((acc, handler) =>
5
+ input => handler(input, acc))(env);
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
- export default async (filename = "primate.config.js") => {
27
- is(filename).string();
28
- let root;
27
+ const getRoot = async () => {
29
28
  try {
30
29
  // use module root if possible
31
- root = await Path.root();
30
+ return await Path.root();
32
31
  } catch (error) {
33
32
  // fall back to current directory
34
- root = Path.resolve();
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
- return cache("config", filename, () => ({...env, modules}));
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,5 @@
1
+ export {default as text} from "./text.js";
2
+ export {default as json} from "./json.js";
3
+ export {default as stream} from "./stream.js";
4
+ export {default as redirect} from "./redirect.js";
5
+ export {default as html} from "./html.js";
@@ -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
+ };
@@ -1,4 +1,6 @@
1
- export default () => () => ["Page not found", {
2
- status: 404,
3
- headers: {"Content-Type": "text/html"},
4
- }];
1
+ export default (body = "Not found") => (_, headers) => [
2
+ body, {
3
+ status: 404,
4
+ headers: {...headers, "Content-Type": "text/html"},
5
+ },
6
+ ];
@@ -1,4 +1,6 @@
1
- export default (_, ...keys) => async () => [JSON.stringify(await keys[0]), {
2
- status: 200,
3
- headers: {"Content-Type": "application/json"},
4
- }];
1
+ export default (body, {status = 200} = {}) => (_, headers) => [
2
+ JSON.stringify(body), {
3
+ status,
4
+ headers: {...headers, "Content-Type": "application/json"},
5
+ },
6
+ ];
@@ -0,0 +1,7 @@
1
+ export default (Location, {status = 302} = {}) => (_, headers) => [
2
+ /* no body */
3
+ null, {
4
+ status,
5
+ headers: {...headers, Location},
6
+ },
7
+ ];
@@ -1,4 +1,6 @@
1
- export default (_, ...keys) => async () => [await keys[0], {
2
- status: 200,
3
- headers: {"Content-Type": "application/octet-stream"},
4
- }];
1
+ export default (body, {status = 200} = {}) => (_, headers) => [
2
+ body, {
3
+ status,
4
+ headers: {...headers, "Content-Type": "application/octet-stream"},
5
+ },
6
+ ];
@@ -1,11 +1,6 @@
1
- const last = -1;
2
-
3
- export default (strings, ...keys) => async () => {
4
- const awaitedKeys = await Promise.all(keys);
5
- const body = strings
6
- .slice(0, last)
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
@@ -0,0 +1,9 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Primate app</title>
5
+ <meta charset="utf-8" />
6
+ %head%
7
+ </head>
8
+ <body>%body%</body>
9
+ </html>
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 = (error, env) => {
27
- log.red("[error]").reset(error.message).nl();
28
- env.debug && console.log(error);
26
+ export const error = (originalError, env) => {
27
+ log.red("[error]").reset(originalError.message).nl();
28
+ env.debug && console.log(originalError);
29
29
  };
@@ -0,0 +1,5 @@
1
+ const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
2
+
3
+ export default async env =>
4
+ [...filter("register", env.modules), _ => _].reduceRight((acc, handler) =>
5
+ input => handler(input, acc))(env);
package/src/respond.js CHANGED
@@ -1,24 +1,24 @@
1
1
  import {Blob} from "runtime-compat/fs";
2
- import text from "./handlers/text.js";
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`${value}`;
8
+ return text(value);
11
9
  }
12
10
  throw new RouteError(`no handler found for ${value}`);
13
11
  };
14
- const isObject = value => typeof value === "object" && value !== null
15
- ? json`${value}` : isText(value);
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`${value}` : isResponse(value);
19
+ ? stream(value) : isResponse(value);
20
20
  const isBlob = value => value instanceof Blob
21
- ? stream`${value}` : isStream(value);
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
- export default async definitions => {
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 => (await route).default(router)));
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 serve from "./serve.js";
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
- const {paths} = env;
9
- const router = await route(paths.routes);
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 parseContent = (request, body) => {
71
- const type = contents[request.headers.get("content-type")];
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}.
@@ -1,3 +0,0 @@
1
- export default router => {
2
- router.get("/", () => "Hello, world!");
3
- };