primate 0.9.2 → 0.10.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 (40) hide show
  1. package/README.md +29 -13
  2. package/README.template.md +32 -16
  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/routing/aliasing.js +14 -0
  18. package/readme/routing/basic.js +7 -0
  19. package/readme/routing/named-groups.js +5 -0
  20. package/readme/routing/regular-expressions.js +5 -0
  21. package/readme/routing/sharing-logic-across-requests.js +18 -0
  22. package/readme/routing/the-request-object.js +4 -0
  23. package/readme/serving-content/html.js +13 -0
  24. package/readme/serving-content/json.js +12 -0
  25. package/readme/serving-content/plain-text.js +4 -0
  26. package/readme/serving-content/streams.js +6 -0
  27. package/readme/serving-content/user-index.html +4 -0
  28. package/src/bundle.js +5 -5
  29. package/src/conf.js +11 -9
  30. package/src/handlers/http404.js +1 -1
  31. package/src/handlers/json.js +1 -1
  32. package/src/handlers/stream.js +1 -1
  33. package/src/handlers/text.js +2 -2
  34. package/src/preset/primate.js +21 -0
  35. package/src/route.js +5 -2
  36. package/src/run.js +4 -0
  37. package/src/serve.js +71 -67
  38. package/TODO +0 -4
  39. package/src/preset/primate.json +0 -25
  40. /package/src/{http-codes.json → http-statuses.json} +0 -0
package/README.md CHANGED
@@ -15,6 +15,24 @@ 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
+ - [Regular expressions](#regular-expressions)
29
+ - [Named groups](#named-groups)
30
+ - [Aliasing](#aliasing)
31
+ - [Sharing logic across requests](#sharing-logic-across-requests)
32
+ - [Data persistance](#data-persistance)
33
+ - [Short field notation](#short-field-notation)
34
+ - [Predicates](#predicates)
35
+
18
36
  ## Serving content
19
37
 
20
38
  Create a file in `routes` that exports a default function
@@ -97,7 +115,7 @@ Routes map requests to responses. They are loaded from `routes`.
97
115
  The order in which routes are declared is irrelevant. Redeclaring a route
98
116
  (same pathname and same HTTP verb) throws an error.
99
117
 
100
- ### Basic GET route
118
+ ### Basic
101
119
 
102
120
  ```js
103
121
  import html from "@primate/html";
@@ -110,7 +128,7 @@ export default router => {
110
128
 
111
129
  ```
112
130
 
113
- ### Working with the request path
131
+ ### The request object
114
132
 
115
133
  ```js
116
134
  export default router => {
@@ -162,7 +180,7 @@ export default router => {
162
180
 
163
181
  ```
164
182
 
165
- ### Sharing logic across HTTP verbs
183
+ ### Sharing logic across requests
166
184
 
167
185
  ```js
168
186
  import html from "@primate/html";
@@ -186,19 +204,15 @@ export default router => {
186
204
 
187
205
  ```
188
206
 
189
- ## Domains
190
-
191
- Domains represent a collection in a store, primarily with the class `fields`
192
- property.
207
+ ## Data persistance
193
208
 
194
- ### Fields
195
-
196
- Field types delimit acceptable values for a field.
209
+ Primate domains (via [`@primate/domains`][primate-domains]) represent a
210
+ collection in a store using the class `fields` property.
197
211
 
198
212
  ```js
199
213
  import {Domain} from "@primate/domains";
200
214
 
201
- // A basic domain that contains two string properies
215
+ // A basic domain that contains two properies
202
216
  export default class User extends Domain {
203
217
  static fields = {
204
218
  // a user's name must be a string
@@ -211,9 +225,9 @@ export default class User extends Domain {
211
225
 
212
226
  ```
213
227
 
214
- ### Short notation
228
+ ### Short field notation
215
229
 
216
- Field types may be any constructible JavaScript object, including other
230
+ Value types may be any constructible JavaScript object, including other
217
231
  domains. When using other domains as types, data integrity (on saving) is
218
232
  ensured.
219
233
 
@@ -265,3 +279,5 @@ export default class User extends Domain {
265
279
  ## License
266
280
 
267
281
  MIT
282
+
283
+ [primate-domains]: https://github.com/primatejs/primate-domains
@@ -12,6 +12,24 @@ Create a route in `routes/hello.js`
12
12
 
13
13
  Add `{"type": "module"}` to your `package.json` and run `npx primate`.
14
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
+ - [Regular expressions](#regular-expressions)
26
+ - [Named groups](#named-groups)
27
+ - [Aliasing](#aliasing)
28
+ - [Sharing logic across requests](#sharing-logic-across-requests)
29
+ - [Data persistance](#data-persistance)
30
+ - [Short field notation](#short-field-notation)
31
+ - [Predicates](#predicates)
32
+
15
33
  ## Serving content
16
34
 
17
35
  Create a file in `routes` that exports a default function
@@ -55,16 +73,16 @@ Routes map requests to responses. They are loaded from `routes`.
55
73
  The order in which routes are declared is irrelevant. Redeclaring a route
56
74
  (same pathname and same HTTP verb) throws an error.
57
75
 
58
- ### Basic GET route
76
+ ### Basic
59
77
 
60
78
  ```js
61
- // routing/basic-get-request.js
79
+ // routing/basic.js
62
80
  ```
63
81
 
64
- ### Working with the request path
82
+ ### The request object
65
83
 
66
84
  ```js
67
- // routing/working-with-the-request-path.js
85
+ // routing/the-request-object.js
68
86
  ```
69
87
 
70
88
  ### Regular expressions
@@ -85,33 +103,29 @@ The order in which routes are declared is irrelevant. Redeclaring a route
85
103
  // routing/aliasing.js
86
104
  ```
87
105
 
88
- ### Sharing logic across HTTP verbs
106
+ ### Sharing logic across requests
89
107
 
90
108
  ```js
91
- // routing/sharing-logic-across-http-verbs.js
109
+ // routing/sharing-logic-across-requests.js
92
110
  ```
93
111
 
94
- ## Domains
95
-
96
- Domains represent a collection in a store, primarily with the class `fields`
97
- property.
112
+ ## Data persistance
98
113
 
99
- ### Fields
100
-
101
- Field types delimit acceptable values for a field.
114
+ Primate domains (via [`@primate/domains`][primate-domains]) represent a
115
+ collection in a store using the class `fields` property.
102
116
 
103
117
  ```js
104
118
  // domains/fields.js
105
119
  ```
106
120
 
107
- ### Short notation
121
+ ### Short field notation
108
122
 
109
- Field types may be any constructible JavaScript object, including other
123
+ Value types may be any constructible JavaScript object, including other
110
124
  domains. When using other domains as types, data integrity (on saving) is
111
125
  ensured.
112
126
 
113
127
  ```js
114
- // domains/short-notation.js
128
+ // domains/short-field-notation.js
115
129
  ```
116
130
 
117
131
  ### Predicates
@@ -131,3 +145,5 @@ aside from the type.
131
145
  ## License
132
146
 
133
147
  MIT
148
+
149
+ [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.10.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.34.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,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,7 @@
1
+ import html from "@primate/html";
2
+
3
+ export default router => {
4
+ // accessing /site/login will serve the contents of
5
+ // `components/site-login.html` as HTML
6
+ router.get("/site/login", () => html`<site-login />`);
7
+ };
@@ -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
@@ -2,13 +2,13 @@ import {File} from "runtime-compat/filesystem";
2
2
 
3
3
  export default async conf => {
4
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
5
 
11
6
  if (await paths.static.exists) {
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
12
  // copy static files to public
13
13
  await File.copy(paths.static, paths.public);
14
14
  }
package/src/conf.js CHANGED
@@ -1,8 +1,8 @@
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.js";
6
6
 
7
7
  const qualify = (root, paths) =>
8
8
  Object.keys(paths).reduce((sofar, key) => {
@@ -13,13 +13,15 @@ const qualify = (root, paths) =>
13
13
  return sofar;
14
14
  }, {});
15
15
 
16
- export default (filename = "primate.json") => cache("conf", filename, () => {
16
+ export default async (filename = "primate.js") => {
17
17
  const root = Path.resolve();
18
- const conf = Either
19
- .try(() => extend(json, JSON.parse(root.join(filename).file.readSync())))
20
- .match({left: () => json})
18
+ const conffile = root.join(filename);
19
+ const conf = await EagerEither
20
+ .try(async () => extend(preset, (await import(conffile)).default))
21
+ .match({left: () => preset})
21
22
  .get();
22
23
  const paths = qualify(root, conf.paths);
23
-
24
- return {...conf, paths, root};
25
- });
24
+ return cache("conf", filename, () => {
25
+ return {...conf, paths, root};
26
+ });
27
+ };
@@ -1,6 +1,6 @@
1
1
  const response = {
2
2
  body: "Page not found",
3
- code: 404,
3
+ status: 404,
4
4
  headers: {"Content-Type": "text/html"},
5
5
  };
6
6
 
@@ -1,5 +1,5 @@
1
1
  const response = {
2
- code: 200,
2
+ status: 200,
3
3
  headers: {"Content-Type": "application/json"},
4
4
  };
5
5
 
@@ -1,5 +1,5 @@
1
1
  const response = {
2
- code: 200,
2
+ status: 200,
3
3
  headers: {"Content-Type": "application/octet-stream"},
4
4
  };
5
5
 
@@ -1,8 +1,8 @@
1
1
  const last = -1;
2
2
  const response = {
3
- code: 200,
3
+ status: 200,
4
4
  headers: {"Content-Type": "text/plain"},
5
- }
5
+ };
6
6
 
7
7
  export default (strings, ...keys) => async () => {
8
8
  const awaitedKeys = await Promise.all(keys);
@@ -0,0 +1,21 @@
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
+ };
package/src/route.js CHANGED
@@ -16,6 +16,9 @@ const isFile = value => value instanceof File
16
16
  ? stream`${value}` : isStream(value);
17
17
  const guess = value => isFile(value);
18
18
 
19
+ // insensitive-case equal
20
+ const ieq = (left, right) => left.toLowerCase() === right.toLowerCase();
21
+
19
22
  export default async definitions => {
20
23
  const aliases = [];
21
24
  const routes = [];
@@ -33,7 +36,7 @@ export default async definitions => {
33
36
  };
34
37
  const find = (method, path, fallback = {handler: r => r}) =>
35
38
  routes.find(route =>
36
- route.method === method && route.path.test(path)) ?? fallback;
39
+ ieq(route.method, method) && route.path.test(path)) ?? fallback;
37
40
 
38
41
  const router = {
39
42
  map: (path, callback) => add("map", path, callback),
@@ -41,7 +44,7 @@ export default async definitions => {
41
44
  post: (path, callback) => add("post", path, callback),
42
45
  alias: (key, value) => aliases.push({key, value}),
43
46
  process: async request => {
44
- const {method} = request;
47
+ const {method} = request.original;
45
48
  const url = new URL(`https://primatejs.com${request.pathname}`);
46
49
  const {pathname, searchParams} = url;
47
50
  const params = Object.fromEntries(searchParams);
package/src/run.js CHANGED
@@ -4,8 +4,11 @@ import bundle from "./bundle.js";
4
4
  import package_json from "../package.json" assert {type: "json"};
5
5
  import log from "./log.js";
6
6
 
7
+ const extract = (modules, key) => modules.flatMap(module => module[key] ?? []);
8
+
7
9
  export default async conf => {
8
10
  log.reset("Primate").yellow(package_json.version);
11
+
9
12
  const {paths} = conf;
10
13
  const router = await route(paths.routes);
11
14
  await bundle(conf);
@@ -14,5 +17,6 @@ export default async conf => {
14
17
  paths: conf.paths,
15
18
  from: conf.paths.public,
16
19
  http: conf.http,
20
+ modules: extract(conf.modules ?? [], "serve"),
17
21
  });
18
22
  };
package/src/serve.js CHANGED
@@ -1,6 +1,6 @@
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
6
  import log from "./log.js";
@@ -8,83 +8,87 @@ import log from "./log.js";
8
8
  const regex = /\.([a-z1-9]*)$/u;
9
9
  const mime = filename => mimes[filename.match(regex)[1]] ?? mimes.binary;
10
10
 
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]};`, "");
21
-
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
- }
11
+ const contents = {
12
+ "application/x-www-form-urlencoded": body =>
13
+ Object.fromEntries(body.split("&").map(part => part.split("=")
14
+ .map(subpart => decodeURIComponent(subpart).replaceAll("+", " ")))),
15
+ "application/json": body => JSON.parse(body),
16
+ };
43
17
 
44
- async try(url, request, payload) {
18
+ export default conf => {
19
+ const route = async request => {
20
+ let result;
45
21
  try {
46
- return await this.serve(url, request, payload);
22
+ result = await (await conf.router.process(request))(conf);
47
23
  } catch (error) {
48
24
  console.log(error);
49
- return new Response(null, {status: codes.InternalServerError});
25
+ result = http404()``;
50
26
  }
51
- }
52
-
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
- }
59
-
60
- async resource(file) {
61
- return new Response(file.readable, {
62
- status: codes.OK,
27
+ const csp = Object.keys(conf.http.csp).reduce((policy_string, key) =>
28
+ `${policy_string}${key} ${conf.http.csp[key]};`, "");
29
+ return new Response(result.body, {
30
+ status: result.status,
63
31
  headers: {
64
- "Content-Type": mime(file.name),
65
- Etag: await file.modified,
32
+ ...result.headers,
33
+ "Content-Security-Policy": csp,
34
+ "Referrer-Policy": "same-origin",
66
35
  },
67
36
  });
68
- }
37
+ };
69
38
 
70
- async route(pathname, request, payload) {
71
- const req = {pathname, method: request.method.toLowerCase(), payload};
72
- let result;
39
+ const resource = async file => new Response(file.readable, {
40
+ status: statuses.OK,
41
+ headers: {
42
+ "Content-Type": mime(file.name),
43
+ Etag: await file.modified,
44
+ },
45
+ });
46
+
47
+ const _serve = async request => {
48
+ const path = new Path(conf.from, request.pathname);
49
+ return await path.isFile ? resource(path.file) : route(request);
50
+ };
51
+
52
+ const handle = async request => {
73
53
  try {
74
- result = await (await this.conf.router.process(req))(this.conf);
54
+ return await _serve(request);
75
55
  } catch (error) {
76
56
  console.log(error);
77
- result = http404``;
57
+ return new Response(null, {status: statuses.InternalServerError});
78
58
  }
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
- };
59
+ };
60
+
61
+ const parseContent = (request, body) => {
62
+ const type = contents[request.headers.get("content-type")];
63
+ return type === undefined ? body : type(body);
64
+ };
65
+
66
+ const {http, modules} = conf;
89
67
 
90
- export default conf => new Server(conf).start();
68
+ // handle is the last module to be executed
69
+ const handlers = [...modules, handle].reduceRight((acc, handler) =>
70
+ input => handler(input, acc));
71
+
72
+ const decoder = new TextDecoder();
73
+ serve(async request => {
74
+ // preprocess request
75
+ const reader = request.body.getReader();
76
+ const chunks = [];
77
+ let result;
78
+ do {
79
+ result = await reader.read();
80
+ if (result.value !== undefined) {
81
+ chunks.push(decoder.decode(result.value));
82
+ }
83
+ } while (!result.done);
84
+
85
+ const body = chunks.length === 0 ? undefined
86
+ : parseContent(request, chunks.join());
87
+
88
+ const {pathname, search} = new URL(`https://example.com${request.url}`);
89
+
90
+ return await handlers({original: request, pathname: pathname + search, body});
91
+ }, http);
92
+
93
+ log.reset("on").yellow(`${http.host}:${http.port}`).nl();
94
+ };
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