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.
- package/README.md +36 -62
- package/README.template.md +37 -58
- package/bin/primate.js +1 -1
- package/module.json +1 -1
- package/package.json +5 -5
- package/readme/components/edit-user-for.html +10 -0
- package/readme/components/edit-user.html +10 -0
- package/readme/components/user-index.html +4 -0
- package/readme/components/users.html +4 -0
- package/readme/domains/fields.js +12 -0
- package/readme/domains/predicates.js +14 -0
- package/readme/domains/short-field-notation.js +13 -0
- package/readme/getting-started/generate-ssl.sh +1 -0
- package/readme/getting-started/hello.js +3 -0
- package/readme/getting-started/lay-out-app.sh +1 -0
- package/readme/getting-started/site-index.html +1 -0
- package/readme/getting-started/site.js +3 -0
- package/readme/routing/aliasing.js +14 -0
- package/readme/routing/basic.js +7 -0
- package/readme/routing/named-groups.js +5 -0
- package/readme/routing/regular-expressions.js +5 -0
- package/readme/routing/sharing-logic-across-requests.js +18 -0
- package/readme/routing/the-request-object.js +4 -0
- package/readme/serving-content/html.js +13 -0
- package/readme/serving-content/json.js +12 -0
- package/readme/serving-content/plain-text.js +4 -0
- package/readme/serving-content/streams.js +6 -0
- package/readme/serving-content/user-index.html +4 -0
- package/src/bundle.js +15 -0
- package/src/conf.js +11 -9
- package/src/handlers/http404.js +1 -1
- package/src/handlers/json.js +1 -1
- package/src/handlers/stream.js +1 -1
- package/src/handlers/text.js +2 -2
- package/src/preset/primate.js +21 -0
- package/src/route.js +9 -4
- package/src/run.js +7 -11
- package/src/serve.js +71 -67
- package/html.js +0 -13
- package/src/Bundler.js +0 -40
- package/src/preset/primate.json +0 -29
- /package/src/{http-codes.json → http-statuses.json} +0 -0
package/README.md
CHANGED
|
@@ -1,54 +1,37 @@
|
|
|
1
1
|
# Primate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An expressive, minimal and extensible framework for JavaScript.
|
|
4
4
|
|
|
5
5
|
## Getting started
|
|
6
6
|
|
|
7
|
-
|
|
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("/", () =>
|
|
11
|
+
router.get("/", () => "Hello, world!");
|
|
21
12
|
};
|
|
22
13
|
|
|
23
14
|
```
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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
|
|
116
|
+
(same pathname and same HTTP verb) throws an error.
|
|
134
117
|
|
|
135
|
-
### Basic
|
|
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
|
-
###
|
|
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
|
|
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
|
-
##
|
|
207
|
+
## Data persistance
|
|
225
208
|
|
|
226
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
package/README.template.md
CHANGED
|
@@ -1,46 +1,34 @@
|
|
|
1
1
|
# Primate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An expressive, minimal and extensible framework for JavaScript.
|
|
4
4
|
|
|
5
5
|
## Getting started
|
|
6
6
|
|
|
7
|
-
|
|
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/
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
```sh
|
|
34
|
-
npx primate
|
|
35
|
-
```
|
|
13
|
+
Add `{"type": "module"}` to your `package.json` and run `npx primate`.
|
|
36
14
|
|
|
37
|
-
## Table of
|
|
15
|
+
## Table of Contents
|
|
38
16
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
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
|
|
74
|
+
(same pathname and same HTTP verb) throws an error.
|
|
87
75
|
|
|
88
|
-
### Basic
|
|
76
|
+
### Basic
|
|
89
77
|
|
|
90
78
|
```js
|
|
91
|
-
// routing/basic
|
|
79
|
+
// routing/basic.js
|
|
92
80
|
```
|
|
93
81
|
|
|
94
|
-
###
|
|
82
|
+
### The request object
|
|
95
83
|
|
|
96
84
|
```js
|
|
97
|
-
// routing/
|
|
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
|
|
106
|
+
### Sharing logic across requests
|
|
119
107
|
|
|
120
108
|
```js
|
|
121
|
-
// routing/sharing-logic-across-
|
|
109
|
+
// routing/sharing-logic-across-requests.js
|
|
122
110
|
```
|
|
123
111
|
|
|
124
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
package/module.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
11
|
+
"runtime-compat": "^0.12.3"
|
|
12
12
|
},
|
|
13
13
|
"bin": "bin/primate.js",
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@babel/core": "^7.
|
|
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.
|
|
18
|
+
"eslint": "^8.34.0",
|
|
19
19
|
"eslint-plugin-json": "^3.1.0"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
|
-
"docs": "npx embedme --source-root
|
|
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,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 @@
|
|
|
1
|
+
mkdir -p app/{routes,components,ssl} && cd app
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Today's date is ${date}.
|
|
@@ -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,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,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
|
+
};
|
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 {
|
|
2
|
+
import {EagerEither} from "runtime-compat/functional";
|
|
3
3
|
import cache from "./cache.js";
|
|
4
4
|
import extend from "./extend.js";
|
|
5
|
-
import
|
|
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.
|
|
16
|
+
export default async (filename = "primate.js") => {
|
|
17
17
|
const root = Path.resolve();
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
.
|
|
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
|
-
|
|
25
|
-
});
|
|
24
|
+
return cache("conf", filename, () => {
|
|
25
|
+
return {...conf, paths, root};
|
|
26
|
+
});
|
|
27
|
+
};
|
package/src/handlers/http404.js
CHANGED
package/src/handlers/json.js
CHANGED
package/src/handlers/stream.js
CHANGED
package/src/handlers/text.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
59
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
18
|
+
export default conf => {
|
|
19
|
+
const route = async request => {
|
|
20
|
+
let result;
|
|
45
21
|
try {
|
|
46
|
-
|
|
22
|
+
result = await (await conf.router.process(request))(conf);
|
|
47
23
|
} catch (error) {
|
|
48
24
|
console.log(error);
|
|
49
|
-
|
|
25
|
+
result = http404()``;
|
|
50
26
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
65
|
-
|
|
32
|
+
...result.headers,
|
|
33
|
+
"Content-Security-Policy": csp,
|
|
34
|
+
"Referrer-Policy": "same-origin",
|
|
66
35
|
},
|
|
67
36
|
});
|
|
68
|
-
}
|
|
37
|
+
};
|
|
69
38
|
|
|
70
|
-
async
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
54
|
+
return await _serve(request);
|
|
75
55
|
} catch (error) {
|
|
76
56
|
console.log(error);
|
|
77
|
-
|
|
57
|
+
return new Response(null, {status: statuses.InternalServerError});
|
|
78
58
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
};
|
package/src/preset/primate.json
DELETED
|
@@ -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
|