primate 0.9.1 → 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 (42) hide show
  1. package/README.md +36 -62
  2. package/README.template.md +37 -58
  3. package/bin/primate.js +1 -1
  4. package/module.json +1 -1
  5. package/package.json +5 -5
  6. package/readme/components/edit-user-for.html +10 -0
  7. package/readme/components/edit-user.html +10 -0
  8. package/readme/components/user-index.html +4 -0
  9. package/readme/components/users.html +4 -0
  10. package/readme/domains/fields.js +12 -0
  11. package/readme/domains/predicates.js +14 -0
  12. package/readme/domains/short-field-notation.js +13 -0
  13. package/readme/getting-started/generate-ssl.sh +1 -0
  14. package/readme/getting-started/hello.js +3 -0
  15. package/readme/getting-started/lay-out-app.sh +1 -0
  16. package/readme/getting-started/site-index.html +1 -0
  17. package/readme/getting-started/site.js +3 -0
  18. package/readme/routing/aliasing.js +14 -0
  19. package/readme/routing/basic.js +7 -0
  20. package/readme/routing/named-groups.js +5 -0
  21. package/readme/routing/regular-expressions.js +5 -0
  22. package/readme/routing/sharing-logic-across-requests.js +18 -0
  23. package/readme/routing/the-request-object.js +4 -0
  24. package/readme/serving-content/html.js +13 -0
  25. package/readme/serving-content/json.js +12 -0
  26. package/readme/serving-content/plain-text.js +4 -0
  27. package/readme/serving-content/streams.js +6 -0
  28. package/readme/serving-content/user-index.html +4 -0
  29. package/src/bundle.js +15 -0
  30. package/src/conf.js +11 -9
  31. package/src/handlers/http404.js +1 -1
  32. package/src/handlers/json.js +1 -1
  33. package/src/handlers/stream.js +1 -1
  34. package/src/handlers/text.js +2 -2
  35. package/src/preset/primate.js +21 -0
  36. package/src/route.js +9 -4
  37. package/src/run.js +7 -11
  38. package/src/serve.js +71 -67
  39. package/html.js +0 -13
  40. package/src/Bundler.js +0 -40
  41. package/src/preset/primate.json +0 -29
  42. /package/src/{http-codes.json → http-statuses.json} +0 -0
package/README.md CHANGED
@@ -1,54 +1,37 @@
1
1
  # Primate
2
2
 
3
- Primal JavaScript framework.
3
+ An expressive, minimal and extensible framework for JavaScript.
4
4
 
5
5
  ## Getting started
6
6
 
7
- Lay out app
8
-
9
- ```sh
10
- mkdir -p app/{routes,components,ssl} && cd app
11
-
12
- ```
13
-
14
- Create a route for `/` in `routes/site.js`
7
+ Create a route in `routes/hello.js`
15
8
 
16
9
  ```js
17
- import html from "@primate/html";
18
-
19
10
  export default router => {
20
- router.get("/", () => html`<site-index date="${new Date()}" />`);
11
+ router.get("/", () => "Hello, world!");
21
12
  };
22
13
 
23
14
  ```
24
15
 
25
- Create a component in `components/site-index.html`
26
-
27
- ```html
28
- Today's date is ${date}.
29
-
30
- ```
31
-
32
- Generate SSL files
33
-
34
- ```sh
35
- openssl req -x509 -out ssl/default.crt -keyout ssl/default.key -newkey rsa:2048 -nodes -sha256 -batch
36
-
37
- ```
38
-
39
- Run
40
-
41
- ```sh
42
- npx primate
43
- ```
44
-
45
- ## Table of contents
46
-
47
- * [Serving content](#serving-content)
48
- * [Routing](#routing)
49
- * [Domains](#domains)
50
- * [Stores](#stores)
51
- * [Components](#components)
16
+ Add `{"type": "module"}` to your `package.json` and run `npx primate`.
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)
52
35
 
53
36
  ## Serving content
54
37
 
@@ -127,12 +110,12 @@ export default router => {
127
110
 
128
111
  ## Routing
129
112
 
130
- Routes map requests to responses. Routes are loaded from `routes`.
113
+ Routes map requests to responses. They are loaded from `routes`.
131
114
 
132
115
  The order in which routes are declared is irrelevant. Redeclaring a route
133
- (same pathname and same HTTP verb) throws a `RouteError`.
116
+ (same pathname and same HTTP verb) throws an error.
134
117
 
135
- ### Basic GET route
118
+ ### Basic
136
119
 
137
120
  ```js
138
121
  import html from "@primate/html";
@@ -145,7 +128,7 @@ export default router => {
145
128
 
146
129
  ```
147
130
 
148
- ### Working with the request path
131
+ ### The request object
149
132
 
150
133
  ```js
151
134
  export default router => {
@@ -197,7 +180,7 @@ export default router => {
197
180
 
198
181
  ```
199
182
 
200
- ### Sharing logic across HTTP verbs
183
+ ### Sharing logic across requests
201
184
 
202
185
  ```js
203
186
  import html from "@primate/html";
@@ -221,19 +204,15 @@ export default router => {
221
204
 
222
205
  ```
223
206
 
224
- ## Domains
207
+ ## Data persistance
225
208
 
226
- Domains represent a collection in a store, primarily with the class `fields`
227
- property.
228
-
229
- ### Fields
230
-
231
- 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.
232
211
 
233
212
  ```js
234
213
  import {Domain} from "@primate/domains";
235
214
 
236
- // A basic domain that contains two string properies
215
+ // A basic domain that contains two properies
237
216
  export default class User extends Domain {
238
217
  static fields = {
239
218
  // a user's name must be a string
@@ -246,9 +225,9 @@ export default class User extends Domain {
246
225
 
247
226
  ```
248
227
 
249
- ### Short notation
228
+ ### Short field notation
250
229
 
251
- Field types may be any constructible JavaScript object, including other
230
+ Value types may be any constructible JavaScript object, including other
252
231
  domains. When using other domains as types, data integrity (on saving) is
253
232
  ensured.
254
233
 
@@ -292,14 +271,7 @@ export default class User extends Domain {
292
271
 
293
272
  ```
294
273
 
295
- ## Stores
296
-
297
- Stores interface data. Primate comes with volatile in-memory store used as a
298
- default. Other stores can be imported as modules.
299
-
300
- Stores are loaded from `stores`.
301
-
302
- ### Resources
274
+ ## Resources
303
275
 
304
276
  * Website: https://primatejs.com
305
277
  * IRC: Join the `#primate` channel on `irc.libera.chat`.
@@ -307,3 +279,5 @@ Stores are loaded from `stores`.
307
279
  ## License
308
280
 
309
281
  MIT
282
+
283
+ [primate-domains]: https://github.com/primatejs/primate-domains
@@ -1,46 +1,34 @@
1
1
  # Primate
2
2
 
3
- Primal JavaScript framework.
3
+ An expressive, minimal and extensible framework for JavaScript.
4
4
 
5
5
  ## Getting started
6
6
 
7
- Lay out app
8
-
9
- ```sh
10
- # getting-started/lay-out-app.sh
11
- ```
12
-
13
- Create a route for `/` in `routes/site.js`
7
+ Create a route in `routes/hello.js`
14
8
 
15
9
  ```js
16
- // getting-started/site.js
17
- ```
18
-
19
- Create a component in `components/site-index.html`
20
-
21
- ```html
22
- <!-- getting-started/site-index.html -->
23
- ```
24
-
25
- Generate SSL files
26
-
27
- ```sh
28
- # getting-started/generate-ssl.sh
10
+ // getting-started/hello.js
29
11
  ```
30
12
 
31
- Run
32
-
33
- ```sh
34
- npx primate
35
- ```
13
+ Add `{"type": "module"}` to your `package.json` and run `npx primate`.
36
14
 
37
- ## Table of contents
15
+ ## Table of Contents
38
16
 
39
- * [Serving content](#serving-content)
40
- * [Routing](#routing)
41
- * [Domains](#domains)
42
- * [Stores](#stores)
43
- * [Components](#components)
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)
44
32
 
45
33
  ## Serving content
46
34
 
@@ -80,21 +68,21 @@ Serve the component in your route
80
68
 
81
69
  ## Routing
82
70
 
83
- Routes map requests to responses. Routes are loaded from `routes`.
71
+ Routes map requests to responses. They are loaded from `routes`.
84
72
 
85
73
  The order in which routes are declared is irrelevant. Redeclaring a route
86
- (same pathname and same HTTP verb) throws a `RouteError`.
74
+ (same pathname and same HTTP verb) throws an error.
87
75
 
88
- ### Basic GET route
76
+ ### Basic
89
77
 
90
78
  ```js
91
- // routing/basic-get-request.js
79
+ // routing/basic.js
92
80
  ```
93
81
 
94
- ### Working with the request path
82
+ ### The request object
95
83
 
96
84
  ```js
97
- // routing/working-with-the-request-path.js
85
+ // routing/the-request-object.js
98
86
  ```
99
87
 
100
88
  ### Regular expressions
@@ -115,33 +103,29 @@ The order in which routes are declared is irrelevant. Redeclaring a route
115
103
  // routing/aliasing.js
116
104
  ```
117
105
 
118
- ### Sharing logic across HTTP verbs
106
+ ### Sharing logic across requests
119
107
 
120
108
  ```js
121
- // routing/sharing-logic-across-http-verbs.js
109
+ // routing/sharing-logic-across-requests.js
122
110
  ```
123
111
 
124
- ## Domains
125
-
126
- Domains represent a collection in a store, primarily with the class `fields`
127
- property.
128
-
129
- ### Fields
112
+ ## Data persistance
130
113
 
131
- 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.
132
116
 
133
117
  ```js
134
118
  // domains/fields.js
135
119
  ```
136
120
 
137
- ### Short notation
121
+ ### Short field notation
138
122
 
139
- Field types may be any constructible JavaScript object, including other
123
+ Value types may be any constructible JavaScript object, including other
140
124
  domains. When using other domains as types, data integrity (on saving) is
141
125
  ensured.
142
126
 
143
127
  ```js
144
- // domains/short-notation.js
128
+ // domains/short-field-notation.js
145
129
  ```
146
130
 
147
131
  ### Predicates
@@ -153,14 +137,7 @@ aside from the type.
153
137
  // domains/predicates.js
154
138
  ```
155
139
 
156
- ## Stores
157
-
158
- Stores interface data. Primate comes with volatile in-memory store used as a
159
- default. Other stores can be imported as modules.
160
-
161
- Stores are loaded from `stores`.
162
-
163
- ### Resources
140
+ ## Resources
164
141
 
165
142
  * Website: https://primatejs.com
166
143
  * IRC: Join the `#primate` channel on `irc.libera.chat`.
@@ -168,3 +145,5 @@ Stores are loaded from `stores`.
168
145
  ## License
169
146
 
170
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/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "author": "Terrablue <terrablue@proton.me>",
5
5
  "bugs": "https://github.com/primatejs/primate/issues",
6
6
  "repository": "https://github.com/primatejs/primate",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.9.1",
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",
@@ -8,18 +8,18 @@
8
8
  "description": "Primal JavaScript framework",
9
9
  "license": "MIT",
10
10
  "dependencies": {
11
- "runtime-compat": "^0.12.2"
11
+ "runtime-compat": "^0.12.3"
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 ADDED
@@ -0,0 +1,15 @@
1
+ import {File} from "runtime-compat/filesystem";
2
+
3
+ export default async conf => {
4
+ const {paths} = conf;
5
+
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
+ // copy static files to public
13
+ await File.copy(paths.static, paths.public);
14
+ }
15
+ };
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);
@@ -55,7 +58,9 @@ export default async definitions => {
55
58
  return typeof result === "function" ? result : guess(result);
56
59
  },
57
60
  };
58
- const files = (await Path.list(definitions)).map(route => import(route));
59
- await Promise.all(files.map(async route => (await route).default(router)));
61
+ if (await definitions.exists) {
62
+ const files = (await Path.list(definitions)).map(route => import(route));
63
+ await Promise.all(files.map(async route => (await route).default(router)));
64
+ }
60
65
  return router;
61
66
  };
package/src/run.js CHANGED
@@ -1,26 +1,22 @@
1
- import {Path, File} from "runtime-compat/filesystem";
2
- import {default as Bundler, index} from "./Bundler.js";
3
1
  import serve from "./serve.js";
4
2
  import route from "./route.js";
3
+ import bundle from "./bundle.js";
5
4
  import package_json from "../package.json" assert {type: "json"};
6
5
  import log from "./log.js";
7
6
 
7
+ const extract = (modules, key) => modules.flatMap(module => module[key] ?? []);
8
+
8
9
  export default async conf => {
9
10
  log.reset("Primate").yellow(package_json.version);
11
+
10
12
  const {paths} = conf;
11
13
  const router = await route(paths.routes);
12
- await new Bundler(conf).bundle();
14
+ await bundle(conf);
13
15
 
14
16
  await serve({router,
15
17
  paths: conf.paths,
16
- index: await index(conf),
17
18
  from: conf.paths.public,
18
- http: {
19
- ...conf.http,
20
- key: await File.read(Path.resolve(conf.http.ssl.key)),
21
- cert: await File.read(Path.resolve(conf.http.ssl.cert)),
22
- keyFile: Path.resolve(conf.http.ssl.key),
23
- certFile: Path.resolve(conf.http.ssl.cert),
24
- },
19
+ http: conf.http,
20
+ modules: extract(conf.modules ?? [], "serve"),
25
21
  });
26
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(`https://${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/html.js DELETED
@@ -1,13 +0,0 @@
1
- import html from "../src/handlers/html.js";
2
- const components = {
3
- "custom-tag": "<ct></ct>",
4
- "custom-with-attribute": "<cwa value=\"${foo}\"></cwa>",
5
- "custom-with-object-attribute": "<cwoa value=\"${foo.bar}\"></cwoa>",
6
- "custom-with-slot": "<cws><slot/></cws>",
7
- "for-with-object": "<fwo for=\"${foo}\"><span value=\"${bar}\"></span></fwo>",
8
- "slot-before-custom": "<slot/><custom-tag></custom-tag>",
9
- "custom-before-slot": "<custom-tag></custom-tag><slot/>",
10
- };
11
- const index = "<body>";
12
- const conf = {components, index};
13
- export default () => (strings, ...keys) => html(strings, ...keys)(conf);
package/src/Bundler.js DELETED
@@ -1,40 +0,0 @@
1
- import {Path, File} from "runtime-compat/filesystem";
2
-
3
- const meta_url = new Path(import.meta.url).path;
4
- const directory = Path.directory(meta_url);
5
- const preset = `${directory}/preset`;
6
-
7
- export default class Bundler {
8
- constructor(conf) {
9
- this.conf = conf;
10
- this.debug = conf.debug;
11
- this.index = conf.files.index;
12
- this.scripts = [];
13
- }
14
-
15
- async bundle() {
16
- const {paths} = this.conf;
17
-
18
- // remove public directory in case exists
19
- await File.remove(paths.public);
20
- // create public directory
21
- await File.create(paths.public);
22
-
23
- // copy static files to public
24
- await File.copy(paths.static, paths.public);
25
-
26
- // read index.html from public, then remove it (we serve it dynamically)
27
- await File.remove(`${paths.public}/${this.index}`);
28
- }
29
- }
30
-
31
- export const index = async conf => {
32
- let file;
33
- const subdirectory = "static";
34
- try {
35
- file = await File.read(`${conf.paths[subdirectory]}/${conf.files.index}`);
36
- } catch (error) {
37
- file = await File.read(`${preset}/${subdirectory}/${conf.files.index}`);
38
- }
39
- return file;
40
- };
@@ -1,29 +0,0 @@
1
- {
2
- "base": "/",
3
- "debug": false,
4
- "files": {
5
- "index": "index.html"
6
- },
7
- "http": {
8
- "host": "localhost",
9
- "port": 9999,
10
- "ssl": {
11
- "key": "ssl/default.key",
12
- "cert": "ssl/default.crt"
13
- },
14
- "csp": {
15
- "default-src": "'self'",
16
- "object-src": "'none'",
17
- "frame-ancestors": "'none'",
18
- "form-action": "'self'",
19
- "base-uri": "'self'"
20
- },
21
- "same-site": "Strict"
22
- },
23
- "paths": {
24
- "public": "public",
25
- "static": "static",
26
- "routes": "routes",
27
- "components": "components"
28
- }
29
- }
File without changes