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.
- package/README.md +35 -20
- package/README.template.md +74 -17
- package/bin/primate.js +1 -1
- package/package.json +4 -4
- 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/modules/configure.js +3 -0
- package/readme/modules/domains/configure.js +5 -0
- package/readme/routing/accessing-the-request-body.js +3 -0
- package/readme/routing/aliasing.js +14 -0
- package/readme/routing/basic.js +4 -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 +7 -8
- package/src/conf.js +14 -9
- package/src/handlers/http404.js +3 -6
- package/src/handlers/json.js +3 -6
- package/src/handlers/stream.js +3 -6
- package/src/handlers/text.js +2 -5
- package/src/log.js +5 -1
- package/src/preset/primate.conf.js +22 -0
- package/src/route.js +15 -6
- package/src/run.js +6 -11
- package/src/serve.js +71 -70
- package/TODO +0 -4
- package/src/preset/primate.json +0 -25
- /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
|
-
|
|
98
|
-
(same pathname and same HTTP verb) throws an error.
|
|
116
|
+
### Basic
|
|
99
117
|
|
|
100
|
-
|
|
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
|
|
107
|
-
|
|
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
|
-
###
|
|
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
|
|
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
|
-
##
|
|
206
|
+
## Data persistance
|
|
190
207
|
|
|
191
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/README.template.md
CHANGED
|
@@ -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
|
-
|
|
56
|
-
(same pathname and same HTTP verb) throws an error.
|
|
75
|
+
### Basic
|
|
57
76
|
|
|
58
|
-
|
|
77
|
+
```js
|
|
78
|
+
// routing/basic.js
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### The request object
|
|
59
82
|
|
|
60
83
|
```js
|
|
61
|
-
// routing/
|
|
84
|
+
// routing/the-request-object.js
|
|
62
85
|
```
|
|
63
86
|
|
|
64
|
-
###
|
|
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/
|
|
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
|
|
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
|
-
//
|
|
142
|
+
// modules/domains/configure.js
|
|
92
143
|
```
|
|
93
144
|
|
|
94
|
-
|
|
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
|
-
|
|
151
|
+
#### Databases
|
|
100
152
|
|
|
101
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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.36.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
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import {File} from "runtime-compat/filesystem";
|
|
2
2
|
|
|
3
|
-
export default async
|
|
4
|
-
const {paths} =
|
|
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 {
|
|
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.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.
|
|
18
|
+
export default async (filename = "primate.conf.js") => {
|
|
17
19
|
const root = Path.resolve();
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
.
|
|
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
|
-
|
|
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
|
+
};
|
package/src/handlers/http404.js
CHANGED
package/src/handlers/json.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
+
}];
|
package/src/handlers/stream.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
+
}];
|
package/src/handlers/text.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =>
|
|
11
|
-
|
|
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
|
|
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: () =>
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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(
|
|
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
|
|
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
|
|
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]};`, "");
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
28
|
+
result = await (await env.router.process(request))(env, headers);
|
|
47
29
|
} catch (error) {
|
|
48
|
-
|
|
49
|
-
|
|
30
|
+
env.error(error.message);
|
|
31
|
+
result = http404(env, headers)``;
|
|
50
32
|
}
|
|
51
|
-
|
|
33
|
+
return new Response(...result);
|
|
34
|
+
};
|
|
52
35
|
|
|
53
|
-
async
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
:
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
71
|
-
const req = {pathname, method: request.method.toLowerCase(), payload};
|
|
72
|
-
let result;
|
|
49
|
+
const handle = async request => {
|
|
73
50
|
try {
|
|
74
|
-
|
|
51
|
+
return await _serve(request);
|
|
75
52
|
} catch (error) {
|
|
76
|
-
|
|
77
|
-
|
|
53
|
+
env.error(error.message);
|
|
54
|
+
return new Response(null, {status: statuses.InternalServerError});
|
|
78
55
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
package/src/preset/primate.json
DELETED
|
@@ -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
|