primate 0.9.2 → 0.11.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 (44) hide show
  1. package/README.md +35 -20
  2. package/README.template.md +74 -17
  3. package/bin/primate.js +1 -1
  4. package/package.json +4 -4
  5. package/readme/components/edit-user-for.html +10 -0
  6. package/readme/components/edit-user.html +10 -0
  7. package/readme/components/user-index.html +4 -0
  8. package/readme/components/users.html +4 -0
  9. package/readme/domains/fields.js +12 -0
  10. package/readme/domains/predicates.js +14 -0
  11. package/readme/domains/short-field-notation.js +13 -0
  12. package/readme/getting-started/generate-ssl.sh +1 -0
  13. package/readme/getting-started/hello.js +3 -0
  14. package/readme/getting-started/lay-out-app.sh +1 -0
  15. package/readme/getting-started/site-index.html +1 -0
  16. package/readme/getting-started/site.js +3 -0
  17. package/readme/modules/configure.js +3 -0
  18. package/readme/modules/domains/configure.js +5 -0
  19. package/readme/routing/accessing-the-request-body.js +3 -0
  20. package/readme/routing/aliasing.js +14 -0
  21. package/readme/routing/basic.js +4 -0
  22. package/readme/routing/named-groups.js +5 -0
  23. package/readme/routing/regular-expressions.js +5 -0
  24. package/readme/routing/sharing-logic-across-requests.js +18 -0
  25. package/readme/routing/the-request-object.js +4 -0
  26. package/readme/serving-content/html.js +13 -0
  27. package/readme/serving-content/json.js +12 -0
  28. package/readme/serving-content/plain-text.js +4 -0
  29. package/readme/serving-content/streams.js +6 -0
  30. package/readme/serving-content/user-index.html +4 -0
  31. package/src/bundle.js +7 -8
  32. package/src/conf.js +14 -9
  33. package/src/handlers/http404.js +3 -6
  34. package/src/handlers/json.js +3 -6
  35. package/src/handlers/stream.js +3 -6
  36. package/src/handlers/text.js +2 -5
  37. package/src/log.js +5 -1
  38. package/src/preset/primate.conf.js +22 -0
  39. package/src/route.js +15 -6
  40. package/src/run.js +6 -11
  41. package/src/serve.js +71 -70
  42. package/TODO +0 -4
  43. package/src/preset/primate.json +0 -25
  44. /package/src/{http-codes.json → http-statuses.json} +0 -0
package/README.md CHANGED
@@ -15,6 +15,25 @@ export default router => {
15
15
 
16
16
  Add `{"type": "module"}` to your `package.json` and run `npx primate`.
17
17
 
18
+ ## Table of Contents
19
+
20
+ - [Serving content](#serving-content)
21
+ - [Plain text](#plain-text)
22
+ - [JSON](#json)
23
+ - [Streams](#streams)
24
+ - [HTML](#html)
25
+ - [Routing](#routing)
26
+ - [Basic](#basic)
27
+ - [The request object](#the-request-object)
28
+ - [Accessing the request body](#accessing-the-request-body)
29
+ - [Regular expressions](#regular-expressions)
30
+ - [Named groups](#named-groups)
31
+ - [Aliasing](#aliasing)
32
+ - [Sharing logic across requests](#sharing-logic-across-requests)
33
+ - [Data persistance](#data-persistance)
34
+ - [Short field notation](#short-field-notation)
35
+ - [Predicates](#predicates)
36
+
18
37
  ## Serving content
19
38
 
20
39
  Create a file in `routes` that exports a default function
@@ -94,23 +113,21 @@ export default router => {
94
113
 
95
114
  Routes map requests to responses. They are loaded from `routes`.
96
115
 
97
- The order in which routes are declared is irrelevant. Redeclaring a route
98
- (same pathname and same HTTP verb) throws an error.
116
+ ### Basic
99
117
 
100
- ### Basic GET route
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.
101
121
 
102
122
  ```js
103
- import html from "@primate/html";
104
-
105
123
  export default router => {
106
- // accessing /site/login will serve the contents of
107
- // `components/site-login.html` as HTML
108
- router.get("/site/login", () => html`<site-login />`);
124
+ // accessing /site/login will serve the `Hello, world!` as plain text
125
+ router.get("/site/login", () => "Hello, world!");
109
126
  };
110
127
 
111
128
  ```
112
129
 
113
- ### Working with the request path
130
+ ### The request object
114
131
 
115
132
  ```js
116
133
  export default router => {
@@ -162,7 +179,7 @@ export default router => {
162
179
 
163
180
  ```
164
181
 
165
- ### Sharing logic across HTTP verbs
182
+ ### Sharing logic across requests
166
183
 
167
184
  ```js
168
185
  import html from "@primate/html";
@@ -186,19 +203,15 @@ export default router => {
186
203
 
187
204
  ```
188
205
 
189
- ## Domains
206
+ ## Data persistance
190
207
 
191
- Domains represent a collection in a store, primarily with the class `fields`
192
- property.
193
-
194
- ### Fields
195
-
196
- Field types delimit acceptable values for a field.
208
+ Primate domains (via [`@primate/domains`][primate-domains]) represent a
209
+ collection in a store using the class `fields` property.
197
210
 
198
211
  ```js
199
212
  import {Domain} from "@primate/domains";
200
213
 
201
- // A basic domain that contains two string properies
214
+ // A basic domain that contains two properies
202
215
  export default class User extends Domain {
203
216
  static fields = {
204
217
  // a user's name must be a string
@@ -211,9 +224,9 @@ export default class User extends Domain {
211
224
 
212
225
  ```
213
226
 
214
- ### Short notation
227
+ ### Short field notation
215
228
 
216
- Field types may be any constructible JavaScript object, including other
229
+ Value types may be any constructible JavaScript object, including other
217
230
  domains. When using other domains as types, data integrity (on saving) is
218
231
  ensured.
219
232
 
@@ -265,3 +278,5 @@ export default class User extends Domain {
265
278
  ## License
266
279
 
267
280
  MIT
281
+
282
+ [primate-domains]: https://github.com/primatejs/primate-domains
@@ -10,7 +10,27 @@ Create a route in `routes/hello.js`
10
10
  // getting-started/hello.js
11
11
  ```
12
12
 
13
- Add `{"type": "module"}` to your `package.json` and run `npx primate`.
13
+ Add `{"type": "module"}` to your `package.json` and run `npx primate -y`.
14
+
15
+ ## Table of Contents
16
+
17
+ - [Serving content](#serving-content)
18
+ - [Plain text](#plain-text)
19
+ - [JSON](#json)
20
+ - [Streams](#streams)
21
+ - [HTML](#html)
22
+ - [Routing](#routing)
23
+ - [Basic](#basic)
24
+ - [The request object](#the-request-object)
25
+ - [Accessing the request body](#accessing-the-request-body)
26
+ - [Regular expressions](#regular-expressions)
27
+ - [Named groups](#named-groups)
28
+ - [Aliasing](#aliasing)
29
+ - [Sharing logic across requests](#sharing-logic-across-requests)
30
+ - [Modules](#modules)
31
+ - [Data persistance](#data-persistance)
32
+ - [Short field notation](#short-field-notation)
33
+ - [Predicates](#predicates)
14
34
 
15
35
  ## Serving content
16
36
 
@@ -52,19 +72,27 @@ Serve the component in your route
52
72
 
53
73
  Routes map requests to responses. They are loaded from `routes`.
54
74
 
55
- The order in which routes are declared is irrelevant. Redeclaring a route
56
- (same pathname and same HTTP verb) throws an error.
75
+ ### Basic
57
76
 
58
- ### Basic GET route
77
+ ```js
78
+ // routing/basic.js
79
+ ```
80
+
81
+ ### The request object
59
82
 
60
83
  ```js
61
- // routing/basic-get-request.js
84
+ // routing/the-request-object.js
62
85
  ```
63
86
 
64
- ### Working with the request path
87
+ ### Accessing the request body
88
+
89
+ For requests containing a body, Primate will attempt parsing the body according
90
+ to the content type sent along the request. Currently supported are
91
+ `application/x-www-form-urlencoded` (typically for form submission) and
92
+ `application/json`.
65
93
 
66
94
  ```js
67
- // routing/working-with-the-request-path.js
95
+ // routing/accessing-the-request-body.js
68
96
  ```
69
97
 
70
98
  ### Regular expressions
@@ -85,33 +113,60 @@ The order in which routes are declared is irrelevant. Redeclaring a route
85
113
  // routing/aliasing.js
86
114
  ```
87
115
 
88
- ### Sharing logic across HTTP verbs
116
+ ### Sharing logic across requests
117
+
118
+ ```js
119
+ // routing/sharing-logic-across-requests.js
120
+ ```
121
+
122
+ ### Modules
123
+
124
+ Primate has optional additional modules published separately that enrich the
125
+ core framework with functionality for common use cases.
126
+
127
+ To add modules, create a `primate.js` configuration file in your project's
128
+ root. This file exports a default export used to extend the framework.
129
+
130
+ ```js
131
+ // modules/configure.js
132
+ ```
133
+
134
+ #### Data
135
+
136
+ Run `npm i @primate/domains` to install the data domains module, used for
137
+ data persistance.
138
+
139
+ Import and initialize this module in your configuration file
89
140
 
90
141
  ```js
91
- // routing/sharing-logic-across-http-verbs.js
142
+ // modules/domains/configure.js
92
143
  ```
93
144
 
94
- ## Domains
145
+ #### Sessions
146
+
147
+ The module `@primate/sessions` is used to maintain user sessions.
148
+
95
149
 
96
- Domains represent a collection in a store, primarily with the class `fields`
97
- property.
98
150
 
99
- ### Fields
151
+ #### Databases
100
152
 
101
- Field types delimit acceptable values for a field.
153
+ ## Data persistance
154
+
155
+ Primate domains (via [`@primate/domains`][primate-domains]) represent a
156
+ collection in a store using the class `fields` property.
102
157
 
103
158
  ```js
104
159
  // domains/fields.js
105
160
  ```
106
161
 
107
- ### Short notation
162
+ ### Short field notation
108
163
 
109
- Field types may be any constructible JavaScript object, including other
164
+ Value types may be any constructible JavaScript object, including other
110
165
  domains. When using other domains as types, data integrity (on saving) is
111
166
  ensured.
112
167
 
113
168
  ```js
114
- // domains/short-notation.js
169
+ // domains/short-field-notation.js
115
170
  ```
116
171
 
117
172
  ### Predicates
@@ -131,3 +186,5 @@ aside from the type.
131
186
  ## License
132
187
 
133
188
  MIT
189
+
190
+ [primate-domains]: https://github.com/primatejs/primate-domains
package/bin/primate.js CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  import conf from "../src/conf.js";
4
4
  import run from "../src/run.js";
5
- await run(conf());
5
+ await run(await conf());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.9.2",
3
+ "version": "0.11.0",
4
4
  "author": "Terrablue <terrablue@proton.me>",
5
5
  "homepage": "https://primatejs.com",
6
6
  "bugs": "https://github.com/primatejs/primate/issues",
@@ -12,14 +12,14 @@
12
12
  },
13
13
  "bin": "bin/primate.js",
14
14
  "devDependencies": {
15
- "@babel/core": "^7.20.12",
15
+ "@babel/core": "^7.21.0",
16
16
  "@babel/eslint-parser": "^7.19.1",
17
17
  "@babel/plugin-syntax-import-assertions": "^7.20.0",
18
- "eslint": "^8.33.0",
18
+ "eslint": "^8.36.0",
19
19
  "eslint-plugin-json": "^3.1.0"
20
20
  },
21
21
  "scripts": {
22
- "docs": "npx embedme --source-root examples --strip-embed-comment --stdout README.template.md > README.md"
22
+ "docs": "npx -y embedme --source-root readme --strip-embed-comment --stdout README.template.md > README.md"
23
23
  },
24
24
  "type": "module",
25
25
  "exports": "./exports.js",
@@ -0,0 +1,10 @@
1
+ <form for="${user}" method="post">
2
+ <h1>Edit user</h1>
3
+ <p>
4
+ <input name="name" value="${name}" />
5
+ </p>
6
+ <p>
7
+ <input name="email" value="${email}" />
8
+ </p>
9
+ <input type="submit" value="Save user" />
10
+ </form>
@@ -0,0 +1,10 @@
1
+ <form method="post">
2
+ <h1>Edit user</h1>
3
+ <p>
4
+ <input name="name" value="${user.name}"></textarea>
5
+ </p>
6
+ <p>
7
+ <input name="email" value="${user.email}"></textarea>
8
+ </p>
9
+ <input type="submit" value="Save user" />
10
+ </form>
@@ -0,0 +1,4 @@
1
+ <div for="${users}">
2
+ User ${name}.
3
+ Email ${email}.
4
+ </div>
@@ -0,0 +1,4 @@
1
+ <div for="${users}">
2
+ User ${name}.
3
+ Email ${email}.
4
+ </div>
@@ -0,0 +1,12 @@
1
+ import {Domain} from "@primate/domains";
2
+
3
+ // A basic domain that contains two properies
4
+ export default class User extends Domain {
5
+ static fields = {
6
+ // a user's name must be a string
7
+ name: String,
8
+ // a user's age must be a number
9
+ age: Number,
10
+ };
11
+ }
12
+
@@ -0,0 +1,14 @@
1
+ import {Domain} from "@primate/domains";
2
+ import House from "./House.js";
3
+
4
+ export default class User extends Domain {
5
+ static fields = {
6
+ // a user's name must be a string and unique across the user collection
7
+ name: [String, "unique"],
8
+ // a user's age must be a positive integer
9
+ age: [Number, "integer", "positive"],
10
+ // a user's house must have the foreign id of a house record and no two
11
+ // users may have the same house
12
+ house_id: [House, "unique"],
13
+ };
14
+ }
@@ -0,0 +1,13 @@
1
+ import {Domain} from "@primate/domains";
2
+ import House from "./House.js";
3
+
4
+ export default class User extends Domain {
5
+ static fields = {
6
+ // a user's name must be a string
7
+ name: String,
8
+ // a user's age must be a number
9
+ age: Number,
10
+ // a user's house must have the foreign id of a house record
11
+ house_id: House,
12
+ };
13
+ }
@@ -0,0 +1 @@
1
+ openssl req -x509 -out ssl/default.crt -keyout ssl/default.key -newkey rsa:2048 -nodes -sha256 -batch
@@ -0,0 +1,3 @@
1
+ export default router => {
2
+ router.get("/", () => "Hello, world!");
3
+ };
@@ -0,0 +1 @@
1
+ mkdir -p app/{routes,components,ssl} && cd app
@@ -0,0 +1 @@
1
+ Today's date is ${date}.
@@ -0,0 +1,3 @@
1
+ export default router => {
2
+ router.get("/", () => "Hello, world!");
3
+ };
@@ -0,0 +1,3 @@
1
+ export default {
2
+ modules: [],
3
+ };
@@ -0,0 +1,5 @@
1
+ import domains from "@primate/domains";
2
+
3
+ export default {
4
+ modules: [domains()],
5
+ };
@@ -0,0 +1,3 @@
1
+ export default router => {
2
+ router.post("/site/login", ({body}) => `submitted user: ${body.username}`);
3
+ };
@@ -0,0 +1,14 @@
1
+ export default router => {
2
+ // will replace `"_id"` in any path with `"([0-9])+"`
3
+ router.alias("_id", "([0-9])+");
4
+
5
+ // equivalent to `router.get("/user/view/([0-9])+", ...)`
6
+ // will return id if matched, 404 otherwise
7
+ router.get("/user/view/_id", request => request.path[2]);
8
+
9
+ // can be combined with named groups
10
+ router.alias("_name", "(?<name>[a-z])+");
11
+
12
+ // will return name if matched, 404 otherwise
13
+ router.get("/user/view/_name", request => request.named.name);
14
+ };
@@ -0,0 +1,4 @@
1
+ export default router => {
2
+ // accessing /site/login will serve the `Hello, world!` as plain text
3
+ router.get("/site/login", () => "Hello, world!");
4
+ };
@@ -0,0 +1,5 @@
1
+ export default router => {
2
+ // named groups are mapped to properties of `request.named`
3
+ // accessing /user/view/1234 will serve `1234` as plain text
4
+ router.get("/user/view/(?<_id>[0-9])+", ({named}) => named._id);
5
+ };
@@ -0,0 +1,5 @@
1
+ export default router => {
2
+ // accessing /user/view/1234 will serve `1234` as plain text
3
+ // accessing /user/view/abcd will show a 404 error
4
+ router.get("/user/view/([0-9])+", request => request[2]);
5
+ };
@@ -0,0 +1,18 @@
1
+ import html from "@primate/html";
2
+ import redirect from "@primate/redirect";
3
+
4
+ export default router => {
5
+ // declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
6
+ router.alias("edit-user", "/user/edit/([0-9])+");
7
+
8
+ // pass user instead of request to all verbs with this route
9
+ router.map("edit-user", () => ({name: "Donald"}));
10
+
11
+ // show user edit form
12
+ router.get("edit-user", user => html`<user-edit user="${user}" />`);
13
+
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}" />`);
18
+ };
@@ -0,0 +1,4 @@
1
+ export default router => {
2
+ // accessing /site/login will serve `["site", "login"]` as JSON
3
+ router.get("/site/login", request => request.path);
4
+ };
@@ -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="${users}" />`;
12
+ });
13
+ };
@@ -0,0 +1,12 @@
1
+ import {File} from "runtime-compat/filesystem";
2
+
3
+ export default router => {
4
+ // any proper JavaScript object will be served as JSON
5
+ router.get("/users", () => [
6
+ {name: "Donald"},
7
+ {name: "Ryan"},
8
+ ]);
9
+
10
+ // load from a file and serve as JSON
11
+ router.get("/users-from-file", () => File.json("users.json"));
12
+ };
@@ -0,0 +1,4 @@
1
+ export default router => {
2
+ // strings will be served as plain text
3
+ router.get("/user", () => "Donald");
4
+ };
@@ -0,0 +1,6 @@
1
+ import {File} from "runtime-compat/filesystem";
2
+
3
+ export default router => {
4
+ // `File` implements `readable`, which is a ReadableStream
5
+ router.get("/users", () => new File("users.json"));
6
+ };
@@ -0,0 +1,4 @@
1
+ <div for="${users}">
2
+ User ${name}.
3
+ Email ${email}.
4
+ </div>
package/src/bundle.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import {File} from "runtime-compat/filesystem";
2
2
 
3
- export default async conf => {
4
- const {paths} = conf;
5
- // remove public directory in case exists
6
- if (await paths.public.exists) {
7
- await paths.public.file.remove();
8
- }
9
- await paths.public.file.create();
10
-
3
+ export default async env => {
4
+ const {paths} = env;
11
5
  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();
12
11
  // copy static files to public
13
12
  await File.copy(paths.static, paths.public);
14
13
  }
package/src/conf.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import {Path} from "runtime-compat/filesystem";
2
- import {Either} from "runtime-compat/functional";
2
+ import {EagerEither} from "runtime-compat/functional";
3
3
  import cache from "./cache.js";
4
4
  import extend from "./extend.js";
5
- import json from "./preset/primate.json" assert {type: "json"};
5
+ import preset from "./preset/primate.conf.js";
6
+ import log from "./log.js";
7
+ import package_json from "../package.json" assert {type: "json"};
6
8
 
7
9
  const qualify = (root, paths) =>
8
10
  Object.keys(paths).reduce((sofar, key) => {
@@ -13,13 +15,16 @@ const qualify = (root, paths) =>
13
15
  return sofar;
14
16
  }, {});
15
17
 
16
- export default (filename = "primate.json") => cache("conf", filename, () => {
18
+ export default async (filename = "primate.conf.js") => {
17
19
  const root = Path.resolve();
18
- const conf = Either
19
- .try(() => extend(json, JSON.parse(root.join(filename).file.readSync())))
20
- .match({left: () => json})
20
+ const conffile = root.join(filename);
21
+ const conf = await EagerEither
22
+ .try(async () => extend(preset, (await import(conffile)).default))
23
+ .match({left: () => preset})
21
24
  .get();
22
- const paths = qualify(root, conf.paths);
23
25
 
24
- return {...conf, paths, root};
25
- });
26
+ const temp = {...conf, ...log, paths: qualify(root, conf.paths), root};
27
+ temp.info(`primate \x1b[34m${package_json.version}\x1b[0m`);
28
+ const modules = await Promise.all(conf.modules.map(module => module(temp)));
29
+ return cache("conf", filename, () => ({...temp, modules}));
30
+ };
@@ -1,7 +1,4 @@
1
- const response = {
2
- body: "Page not found",
3
- code: 404,
1
+ export default () => () => ["Page not found", {
2
+ status: 404,
4
3
  headers: {"Content-Type": "text/html"},
5
- };
6
-
7
- export default () => () => ({...response});
4
+ }];
@@ -1,7 +1,4 @@
1
- const response = {
2
- code: 200,
1
+ export default (_, ...keys) => async () => [JSON.stringify(await keys[0]), {
2
+ status: 200,
3
3
  headers: {"Content-Type": "application/json"},
4
- };
5
-
6
- export default (strings, ...keys) => async () =>
7
- ({...response, body: JSON.stringify(await keys[0])});
4
+ }];
@@ -1,7 +1,4 @@
1
- const response = {
2
- code: 200,
1
+ export default (_, ...keys) => async () => [await keys[0], {
2
+ status: 200,
3
3
  headers: {"Content-Type": "application/octet-stream"},
4
- };
5
-
6
- export default (strings, ...keys) => async () =>
7
- ({...response, body: await keys[0]});
4
+ }];
@@ -1,8 +1,4 @@
1
1
  const last = -1;
2
- const response = {
3
- code: 200,
4
- headers: {"Content-Type": "text/plain"},
5
- }
6
2
 
7
3
  export default (strings, ...keys) => async () => {
8
4
  const awaitedKeys = await Promise.all(keys);
@@ -10,5 +6,6 @@ export default (strings, ...keys) => async () => {
10
6
  .slice(0, last)
11
7
  .map((string, i) => string + awaitedKeys[i])
12
8
  .join("") + strings[strings.length + last];
13
- return {...response, body};
9
+
10
+ return [body, {status: 200, headers: {"Content-Type": "text/plain"}}];
14
11
  };
package/src/log.js CHANGED
@@ -19,4 +19,8 @@ const log = new Proxy(Log, {
19
19
  log.paint(colors[property] ?? reset, message).paint(reset, " ")),
20
20
  });
21
21
 
22
- export default log;
22
+ export default {
23
+ info: (...args) => log.green("[info]").reset(...args).nl(),
24
+ warn: (...args) => log.yellow("[warn]").reset(...args).nl(),
25
+ error: (...args) => log.red("[error]").reset(...args).nl(),
26
+ };
@@ -0,0 +1,22 @@
1
+ export default {
2
+ base: "/",
3
+ debug: false,
4
+ http: {
5
+ host: "localhost",
6
+ port: 6161,
7
+ csp: {
8
+ "default-src": "'self'",
9
+ "object-src": "'none'",
10
+ "frame-ancestors": "'none'",
11
+ "form-action": "'self'",
12
+ "base-uri": "'self'",
13
+ },
14
+ },
15
+ paths: {
16
+ public: "public",
17
+ static: "static",
18
+ routes: "routes",
19
+ components: "components",
20
+ },
21
+ modules: [],
22
+ };
package/src/route.js CHANGED
@@ -1,14 +1,18 @@
1
1
  import {ReadableStream} from "runtime-compat/streams";
2
2
  import {Path, File} from "runtime-compat/filesystem";
3
3
  import {is} from "runtime-compat/dyndef";
4
- import {http404} from "./handlers/http.js";
5
4
  import text from "./handlers/text.js";
6
5
  import json from "./handlers/json.js";
7
6
  import stream from "./handlers/stream.js";
8
7
  import RouteError from "./errors/Route.js";
9
8
 
10
- const isText = value => typeof value === "string" ? text`${value}` : http404``;
11
- const isObject = value => typeof value === "object" && value !== null
9
+ const isText = value => {
10
+ if (typeof value === "string") {
11
+ return text`${value}`;
12
+ }
13
+ throw new RouteError(`no handler found for ${value}`);
14
+ };
15
+ const isObject = value => typeof value === "object" && value !== null
12
16
  ? json`${value}` : isText(value);
13
17
  const isStream = value => value instanceof ReadableStream
14
18
  ? stream`${value}` : isObject(value);
@@ -16,6 +20,9 @@ const isFile = value => value instanceof File
16
20
  ? stream`${value}` : isStream(value);
17
21
  const guess = value => isFile(value);
18
22
 
23
+ // insensitive-case equal
24
+ const ieq = (left, right) => left.toLowerCase() === right.toLowerCase();
25
+
19
26
  export default async definitions => {
20
27
  const aliases = [];
21
28
  const routes = [];
@@ -33,7 +40,7 @@ export default async definitions => {
33
40
  };
34
41
  const find = (method, path, fallback = {handler: r => r}) =>
35
42
  routes.find(route =>
36
- route.method === method && route.path.test(path)) ?? fallback;
43
+ ieq(route.method, method) && route.path.test(path)) ?? fallback;
37
44
 
38
45
  const router = {
39
46
  map: (path, callback) => add("map", path, callback),
@@ -41,11 +48,13 @@ export default async definitions => {
41
48
  post: (path, callback) => add("post", path, callback),
42
49
  alias: (key, value) => aliases.push({key, value}),
43
50
  process: async request => {
44
- const {method} = request;
51
+ const {method} = request.original;
45
52
  const url = new URL(`https://primatejs.com${request.pathname}`);
46
53
  const {pathname, searchParams} = url;
47
54
  const params = Object.fromEntries(searchParams);
48
- const verb = find(method, pathname, {handler: () => http404``});
55
+ const verb = find(method, pathname, {handler: () => {
56
+ throw new RouteError(`no ${method.toUpperCase()} route to ${pathname}`);
57
+ }});
49
58
  const path = pathname.split("/").filter(part => part !== "");
50
59
  const named = verb.path?.exec(pathname)?.groups ?? {};
51
60
 
package/src/run.js CHANGED
@@ -1,18 +1,13 @@
1
1
  import serve from "./serve.js";
2
2
  import route from "./route.js";
3
3
  import bundle from "./bundle.js";
4
- import package_json from "../package.json" assert {type: "json"};
5
- import log from "./log.js";
6
4
 
7
- export default async conf => {
8
- log.reset("Primate").yellow(package_json.version);
9
- const {paths} = conf;
5
+ const extract = (modules, key) => modules.flatMap(module => module[key] ?? []);
6
+
7
+ export default async env => {
8
+ const {paths} = env;
10
9
  const router = await route(paths.routes);
11
- await bundle(conf);
10
+ await bundle(env);
12
11
 
13
- await serve({router,
14
- paths: conf.paths,
15
- from: conf.paths.public,
16
- http: conf.http,
17
- });
12
+ await serve({router, ...env, modules: extract(env.modules ?? [], "serve")});
18
13
  };
package/src/serve.js CHANGED
@@ -1,90 +1,91 @@
1
1
  import {Path} from "runtime-compat/filesystem";
2
2
  import {serve, Response} from "runtime-compat/http";
3
- import codes from "./http-codes.json" assert {type: "json"};
3
+ import statuses from "./http-statuses.json" assert {type: "json"};
4
4
  import mimes from "./mimes.json" assert {type: "json"};
5
5
  import {http404} from "./handlers/http.js";
6
- import log from "./log.js";
7
6
 
8
7
  const regex = /\.([a-z1-9]*)$/u;
9
8
  const mime = filename => mimes[filename.match(regex)[1]] ?? mimes.binary;
10
9
 
11
- const Server = class Server {
12
- constructor(conf) {
13
- this.conf = conf;
14
- }
15
-
16
- async start() {
17
- const {http} = this.conf;
18
- const {csp, "same-site": same_site = "Strict"} = http;
19
- this.csp = Object.keys(csp).reduce((policy_string, key) =>
20
- `${policy_string}${key} ${csp[key]};`, "");
10
+ const contents = {
11
+ "application/x-www-form-urlencoded": body =>
12
+ Object.fromEntries(body.split("&").map(part => part.split("=")
13
+ .map(subpart => decodeURIComponent(subpart).replaceAll("+", " ")))),
14
+ "application/json": body => JSON.parse(body),
15
+ };
21
16
 
22
- const decoder = new TextDecoder();
23
- serve(async request => {
24
- const reader = request.body.getReader();
25
- const chunks = [];
26
- let result;
27
- do {
28
- result = await reader.read();
29
- if (result.value !== undefined) {
30
- chunks.push(decoder.decode(result.value));
31
- }
32
- } while (!result.done);
33
- const body = chunks.join();
34
- const payload = Object.fromEntries(decodeURI(body).replaceAll("+", " ")
35
- .split("&")
36
- .map(part => part.split("=")));
37
- const {pathname, search} = new URL(`https://example.com${request.url}`);
38
- return this.try(pathname + search, request, payload);
39
- }, http);
40
- const {port, host} = this.conf.http;
41
- log.reset("on").yellow(`${host}:${port}`).nl();
42
- }
17
+ export default env => {
18
+ const route = async request => {
19
+ let result;
20
+ const csp = Object.keys(env.http.csp).reduce((policy_string, key) =>
21
+ `${policy_string}${key} ${env.http.csp[key]};`, "");
22
+ const headers = {
23
+ "Content-Security-Policy": csp,
24
+ "Referrer-Policy": "same-origin",
25
+ };
43
26
 
44
- async try(url, request, payload) {
45
27
  try {
46
- return await this.serve(url, request, payload);
28
+ result = await (await env.router.process(request))(env, headers);
47
29
  } catch (error) {
48
- console.log(error);
49
- return new Response(null, {status: codes.InternalServerError});
30
+ env.error(error.message);
31
+ result = http404(env, headers)``;
50
32
  }
51
- }
33
+ return new Response(...result);
34
+ };
52
35
 
53
- async serve(url, request, payload) {
54
- const path = new Path(this.conf.from, url);
55
- return await path.isFile
56
- ? this.resource(path.file)
57
- : this.route(url, request, payload);
58
- }
36
+ const resource = async file => new Response(file.readable, {
37
+ status: statuses.OK,
38
+ headers: {
39
+ "Content-Type": mime(file.name),
40
+ Etag: await file.modified,
41
+ },
42
+ });
59
43
 
60
- async resource(file) {
61
- return new Response(file.readable, {
62
- status: codes.OK,
63
- headers: {
64
- "Content-Type": mime(file.name),
65
- Etag: await file.modified,
66
- },
67
- });
68
- }
44
+ const _serve = async request => {
45
+ const path = new Path(env.paths.public, request.pathname);
46
+ return await path.isFile ? resource(path.file) : route(request);
47
+ };
69
48
 
70
- async route(pathname, request, payload) {
71
- const req = {pathname, method: request.method.toLowerCase(), payload};
72
- let result;
49
+ const handle = async request => {
73
50
  try {
74
- result = await (await this.conf.router.process(req))(this.conf);
51
+ return await _serve(request);
75
52
  } catch (error) {
76
- console.log(error);
77
- result = http404``;
53
+ env.error(error.message);
54
+ return new Response(null, {status: statuses.InternalServerError});
78
55
  }
79
- return new Response(result.body, {
80
- status: result.code,
81
- headers: {
82
- ...result.headers,
83
- "Content-Security-Policy": this.csp,
84
- "Referrer-Policy": "same-origin",
85
- },
86
- });
87
- }
88
- };
56
+ };
57
+
58
+ const parseContent = (request, body) => {
59
+ const type = contents[request.headers.get("content-type")];
60
+ return type === undefined ? body : type(body);
61
+ };
62
+
63
+ const {http, modules} = env;
89
64
 
90
- export default conf => new Server(conf).start();
65
+ // handle is the last module to be executed
66
+ const handlers = [...modules, handle].reduceRight((acc, handler) =>
67
+ input => handler(input, acc));
68
+
69
+ const decoder = new TextDecoder();
70
+ serve(async request => {
71
+ // preprocess request
72
+ const reader = request.body.getReader();
73
+ const chunks = [];
74
+ let result;
75
+ do {
76
+ result = await reader.read();
77
+ if (result.value !== undefined) {
78
+ chunks.push(decoder.decode(result.value));
79
+ }
80
+ } while (!result.done);
81
+
82
+ const body = chunks.length === 0 ? undefined
83
+ : parseContent(request, chunks.join());
84
+
85
+ const {pathname, search} = new URL(`https://example.com${request.url}`);
86
+
87
+ return handlers({original: request, pathname: pathname + search, body});
88
+ }, http);
89
+
90
+ env.info(`running on ${http.host}:${http.port}`);
91
+ };
package/TODO DELETED
@@ -1,4 +0,0 @@
1
- remove ssl hint from documentation -> maybe add a "running an https server"
2
- add RouteError
3
- add the possibility to pass strings to attributes (rework internally data to
4
- hang at attributes)
@@ -1,25 +0,0 @@
1
- {
2
- "base": "/",
3
- "debug": false,
4
- "files": {
5
- "index": "index.html"
6
- },
7
- "http": {
8
- "host": "localhost",
9
- "port": 9999,
10
- "csp": {
11
- "default-src": "'self'",
12
- "object-src": "'none'",
13
- "frame-ancestors": "'none'",
14
- "form-action": "'self'",
15
- "base-uri": "'self'"
16
- },
17
- "same-site": "Strict"
18
- },
19
- "paths": {
20
- "public": "public",
21
- "static": "static",
22
- "routes": "routes",
23
- "components": "components"
24
- }
25
- }
File without changes