primate 0.6.2 → 0.7.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 +276 -12
- package/package.json +5 -2
- package/source/App.js +6 -8
- package/source/Bundler.js +4 -6
- package/source/EagerPromise.js +1 -1
- package/source/Server.js +15 -27
- package/source/conf.js +4 -5
- package/source/domain/Domain.js +2 -2
- package/source/exports.js +0 -3
- package/source/handlers/DOM/Node.js +2 -3
- package/source/handlers/DOM/Parser.js +11 -2
- package/source/handlers/html.js +2 -3
- package/source/invariants.js +9 -5
- package/source/map_entries.js +6 -0
- package/source/sanitize.js +8 -10
- package/source/store/Store.js +2 -2
- package/source/Directory.js +0 -35
- package/source/File.js +0 -121
- package/source/crypto.js +0 -8
- package/source/log.js +0 -22
package/README.md
CHANGED
|
@@ -4,12 +4,6 @@ A full-stack Javascript framework, Primate relieves you of dealing with
|
|
|
4
4
|
repetitive, error-prone tasks and lets you concentrate on writing effective,
|
|
5
5
|
expressive code.
|
|
6
6
|
|
|
7
|
-
## Installing
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
npm install primate
|
|
11
|
-
```
|
|
12
|
-
|
|
13
7
|
## Highlights
|
|
14
8
|
|
|
15
9
|
* Flexible HTTP routing, returning HTML, JSON or a custom handler
|
|
@@ -17,15 +11,285 @@ npm install primate
|
|
|
17
11
|
* Built-in support for sessions with secure cookies
|
|
18
12
|
* Input verification using data domains
|
|
19
13
|
* Many different data store modules: In-Memory (built-in),
|
|
20
|
-
[File][primate-store
|
|
21
|
-
[MongoDB][primate-store
|
|
14
|
+
[File][primate-file-store], [JSON][primate-json-store],
|
|
15
|
+
[MongoDB][primate-mongodb-store]
|
|
22
16
|
* Easy modelling of`1:1`, `1:n` and `n:m` relationships
|
|
23
17
|
* Minimally opinionated with sane, overrideable defaults
|
|
24
18
|
* No dependencies
|
|
25
19
|
|
|
26
20
|
## Getting started
|
|
27
21
|
|
|
28
|
-
|
|
22
|
+
Lay out your app
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
$ mkdir -p primate-app/{routes,components,ssl} && cd primate-app
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Create a route for `/`
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
// routes/site.js
|
|
32
|
+
|
|
33
|
+
import {router, html} from "primate";
|
|
34
|
+
|
|
35
|
+
router.get("/", () => html`<site-index date=${new Date()} />`);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Create a component for your route (in `components/site-index.html`)
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
Today's date is <span data-value="date"></span>.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Generate SSL key/certificate
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
openssl req -x509 -out ssl/default.crt -keyout ssl/default.key -newkey rsa:2048 -nodes -sha256 -batch
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Add an entry file
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// app.js
|
|
54
|
+
|
|
55
|
+
import {app} from "primate";
|
|
56
|
+
app.run();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Create a start script and enable ES modules (in `package.json`)
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"scripts": {
|
|
64
|
+
"start": "node --experimental-json-modules app.js"
|
|
65
|
+
},
|
|
66
|
+
"type": "module"
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Install Primate
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
$ npm install primate
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Run app
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
$ npm start
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Visit `https://localhost:9999`
|
|
83
|
+
|
|
84
|
+
## Table of Contents
|
|
85
|
+
|
|
86
|
+
* [Routes](#routes)
|
|
87
|
+
* [Handlers](#handlers)
|
|
88
|
+
* [Components](#components)
|
|
89
|
+
* [Domains](#domains)
|
|
90
|
+
* [Verification](#verification)
|
|
91
|
+
|
|
92
|
+
## Routes
|
|
93
|
+
|
|
94
|
+
Create routes in the `routes` directory by importing and using the `router`
|
|
95
|
+
singleton. You can group your routes across several files or keep them
|
|
96
|
+
in one file.
|
|
97
|
+
|
|
98
|
+
### `router[get|post](pathname, request => ...)`
|
|
99
|
+
|
|
100
|
+
Routes are tied to a pathname and execute their callback when the pathname is
|
|
101
|
+
encountered.
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
// in routes/some-file.js
|
|
105
|
+
import {router, json} from "primate";
|
|
106
|
+
|
|
107
|
+
// on matching the exact pathname /, returns {"foo": "bar"} as JSON
|
|
108
|
+
router.get("/", () => json`${{"foo": "bar"}}`);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
All routes must return a template function handler. See the
|
|
112
|
+
[section on handlers for common handlers](#handlers).
|
|
113
|
+
|
|
114
|
+
The callback has one parameter, the request data.
|
|
115
|
+
|
|
116
|
+
### The `request` object
|
|
117
|
+
|
|
118
|
+
The request contains the `path`, a `/` separated array of the pathname.
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
import {router, html} from "primate";
|
|
122
|
+
|
|
123
|
+
router.get("/site/login", request => json`${{"path": request.path}}`);
|
|
124
|
+
// accessing /site/login -> {"path":["site","login"]}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The HTTP request's body is available under `request.payload`.
|
|
128
|
+
|
|
129
|
+
### Regular expressions in routes
|
|
130
|
+
|
|
131
|
+
All routes are treated as regular expressions.
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
import {router, json} from "primate";
|
|
135
|
+
|
|
136
|
+
router.get("/user/view/([0-9])+", request => json`${{"path": request.path}}`);
|
|
137
|
+
// accessing /user/view/1234 -> {"path":["site","login","1234"]}
|
|
138
|
+
// accessing /user/view/abcd -> error 404
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `router.alias(from, to)`
|
|
142
|
+
|
|
143
|
+
To reuse certain parts of a pathname, you can define aliases which will be
|
|
144
|
+
applied before matching.
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
import {router, json} from "primate";
|
|
148
|
+
|
|
149
|
+
router.alias("_id", "([0-9])+");
|
|
150
|
+
|
|
151
|
+
router.get("/user/view/_id", request => json`${{"path": request.path}}`);
|
|
152
|
+
|
|
153
|
+
router.get("/user/edit/_id", request => ...);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `router.map(pathname, request => ...)`
|
|
157
|
+
|
|
158
|
+
You can reuse functionality across the same path but different HTTP verbs. This
|
|
159
|
+
function has the same signature as `router[get|post]`.
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
import {router, html, redirect} from "primate";
|
|
163
|
+
|
|
164
|
+
router.alias("_id", "([0-9])+");
|
|
165
|
+
|
|
166
|
+
router.map("/user/edit/_id", request => {
|
|
167
|
+
const user = {"name": "Donald"};
|
|
168
|
+
// return original request and user
|
|
169
|
+
return {...request, user};
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
router.get("/user/edit/_id", request => {
|
|
173
|
+
// show user edit form
|
|
174
|
+
return html`<user-edit user=${request.user} />`
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
router.post("/user/edit/_id", request => {
|
|
178
|
+
const {user} = request;
|
|
179
|
+
// verify form and save / show errors
|
|
180
|
+
return await user.save() ? redirect`/users` : html`<user-edit user=${user} />`
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Handlers
|
|
185
|
+
|
|
186
|
+
Handlers are tagged template functions usually associated with data.
|
|
187
|
+
|
|
188
|
+
### ``html`<component-name attribute=${value} />` ``
|
|
189
|
+
|
|
190
|
+
Compiles and serves a component from the `components` directory and with the
|
|
191
|
+
specified attributes and their values. Returns an HTTP 200 response with the
|
|
192
|
+
`text/html` content type.
|
|
193
|
+
|
|
194
|
+
### ``json`${{data}}` ``
|
|
195
|
+
|
|
196
|
+
Serves JSON `data`. Returns an HTTP 200 response with the `application/json`
|
|
197
|
+
content type.
|
|
198
|
+
|
|
199
|
+
### ``redirect`${url}` ``
|
|
200
|
+
|
|
201
|
+
Redirects to `url`. Returns an HTTP 302 response.
|
|
202
|
+
|
|
203
|
+
## Components
|
|
204
|
+
|
|
205
|
+
Create HTML components in the `components` directory. Use `data`-attributes to
|
|
206
|
+
show data within your component.
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
// in routes/user.js
|
|
210
|
+
import {router, html, redirect} from "primate";
|
|
211
|
+
|
|
212
|
+
router.alias("_id", "([0-9])+");
|
|
213
|
+
|
|
214
|
+
router.map("/user/edit/_id", request => {
|
|
215
|
+
const user = {"name": "Donald", "email": "donald@was.here"};
|
|
216
|
+
// return original request and user
|
|
217
|
+
return {...request, user};
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
router.get("/user/edit/_id", request => {
|
|
221
|
+
// show user edit form
|
|
222
|
+
return html`<user-edit user=${request.user} />`
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
router.post("/user/edit/_id", request => {
|
|
226
|
+
const {user, payload} = request;
|
|
227
|
+
// verify form and save / show errors
|
|
228
|
+
// this assumes `user` has a method `save` to verify data
|
|
229
|
+
return await user.save(payload) ? redirect`/users` : html`<user-edit user=${user} />`
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```html
|
|
234
|
+
<!-- components/edit-user.html -->
|
|
235
|
+
<form method="post">
|
|
236
|
+
<h1>Edit user</h1>
|
|
237
|
+
<p>
|
|
238
|
+
<input name="user.name" data-value="user.name"></textarea>
|
|
239
|
+
</p>
|
|
240
|
+
<p>
|
|
241
|
+
<input name="user.email" data-value="user.email"></textarea>
|
|
242
|
+
</p>
|
|
243
|
+
<input type="submit" value="Save user" />
|
|
244
|
+
</form>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Grouping objects with `data-for`
|
|
248
|
+
|
|
249
|
+
You can use the special attribute `data-for` to group objects.
|
|
250
|
+
|
|
251
|
+
```html
|
|
252
|
+
<!-- components/edit-user.html -->
|
|
253
|
+
<form data-for="user" method="post">
|
|
254
|
+
<h1>Edit user</h1>
|
|
255
|
+
<p>
|
|
256
|
+
<input name="name" data-value="name" />
|
|
257
|
+
</p>
|
|
258
|
+
<p>
|
|
259
|
+
<input name="email" data-value="email" />
|
|
260
|
+
</p>
|
|
261
|
+
<input type="submit" value="Save user" />
|
|
262
|
+
</form>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Expanding arrays
|
|
266
|
+
|
|
267
|
+
`data-for` can also be used to expand arrays.
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
// in routes/user.js
|
|
271
|
+
import {router, html} from "primate";
|
|
272
|
+
|
|
273
|
+
router.get("/users", request => {
|
|
274
|
+
const users = [
|
|
275
|
+
{"name": "Donald", "email": "donald@was.here"},
|
|
276
|
+
{"name": "Ryan", "email": "ryan@was.here"},
|
|
277
|
+
];
|
|
278
|
+
return html`<user-index users=${users} />`;
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```html
|
|
283
|
+
<!-- in components/user-index.html -->
|
|
284
|
+
<div data-for="users">
|
|
285
|
+
User <span data-value="name"></span>
|
|
286
|
+
Email <span data-value="email"></span>
|
|
287
|
+
</div>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Resources
|
|
291
|
+
|
|
292
|
+
* [Getting started guide][getting-started]
|
|
29
293
|
|
|
30
294
|
## License
|
|
31
295
|
|
|
@@ -34,6 +298,6 @@ BSD-3-Clause
|
|
|
34
298
|
[getting-started]: https://primatejs.com/getting-started
|
|
35
299
|
[source-code]: https://github.com/primatejs/primate
|
|
36
300
|
[issues]: https://github.com/primatejs/primate/issues
|
|
37
|
-
[primate-store
|
|
38
|
-
[primate-store
|
|
39
|
-
[primate-store
|
|
301
|
+
[primate-file-store]: https://npmjs.com/primate-file-store
|
|
302
|
+
[primate-json-store]: https://npmjs.com/primate-json-store
|
|
303
|
+
[primate-mongodb-store]: https://npmjs.com/primate-mongodb-store
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"author": "Primate core team <core@primatejs.com>",
|
|
5
5
|
"homepage": "https://primatejs.com",
|
|
6
6
|
"bugs": "https://github.com/primatejs/primate/issues",
|
|
7
7
|
"repository": "https://github.com/primatejs/primate",
|
|
8
|
-
"description": "
|
|
8
|
+
"description": "Full-stack JavaScript framework",
|
|
9
9
|
"license": "BSD-3-Clause",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"copy": "rm -rf output && mkdir output && cp source/* output -a",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"coverage": "npm run instrument && nyc npm run debris",
|
|
15
15
|
"test": "npm run copy && npm run debris"
|
|
16
16
|
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"runtime-compat": "^0.1.0"
|
|
19
|
+
},
|
|
17
20
|
"devDependencies": {
|
|
18
21
|
"debris": "^0.2.2",
|
|
19
22
|
"eslint": "^8.13.0",
|
package/source/App.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Path, File, log} from "runtime-compat";
|
|
2
2
|
import Bundler from "./Bundler.js";
|
|
3
|
-
import File from "./File.js";
|
|
4
|
-
import Directory from "./Directory.js";
|
|
5
3
|
import Router from "./Router.js";
|
|
6
4
|
import Server from "./Server.js";
|
|
7
|
-
import log from "./log.js";
|
|
8
5
|
import package_json from "../package.json" assert {"type": "json"};
|
|
9
6
|
|
|
10
7
|
export default class App {
|
|
@@ -14,8 +11,7 @@ export default class App {
|
|
|
14
11
|
|
|
15
12
|
async run() {
|
|
16
13
|
log.reset("Primate").yellow(package_json.version);
|
|
17
|
-
|
|
18
|
-
const routes = await Directory.list(this.conf.paths.routes);
|
|
14
|
+
const routes = await File.list(this.conf.paths.routes);
|
|
19
15
|
for (const route of routes) {
|
|
20
16
|
await import(`${this.conf.paths.routes}/${route}`);
|
|
21
17
|
}
|
|
@@ -25,8 +21,10 @@ export default class App {
|
|
|
25
21
|
"serve_from": this.conf.paths.public,
|
|
26
22
|
"http": {
|
|
27
23
|
...this.conf.http,
|
|
28
|
-
"key": File.read_sync(resolve(this.conf.http.ssl.key)),
|
|
29
|
-
"cert": File.read_sync(resolve(this.conf.http.ssl.cert)),
|
|
24
|
+
"key": File.read_sync(Path.resolve(this.conf.http.ssl.key)),
|
|
25
|
+
"cert": File.read_sync(Path.resolve(this.conf.http.ssl.cert)),
|
|
26
|
+
"keyFile": Path.resolve(this.conf.http.ssl.key),
|
|
27
|
+
"certFile": Path.resolve(this.conf.http.ssl.cert),
|
|
30
28
|
},
|
|
31
29
|
};
|
|
32
30
|
this.server = new Server(conf);
|
package/source/Bundler.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {fileURLToPath} from "url";
|
|
3
|
-
import File from "./File.js";
|
|
1
|
+
import {Path, File} from "runtime-compat";
|
|
4
2
|
|
|
5
|
-
const meta_url =
|
|
6
|
-
const directory = dirname(meta_url);
|
|
3
|
+
const meta_url = new Path(import.meta.url).path;
|
|
4
|
+
const directory = Path.dirname(meta_url);
|
|
7
5
|
const preset = `${directory}/preset`;
|
|
8
6
|
|
|
9
7
|
export default class Bundler {
|
|
@@ -22,7 +20,7 @@ export default class Bundler {
|
|
|
22
20
|
|
|
23
21
|
// copy any user code over it, not recreating the folder
|
|
24
22
|
try {
|
|
25
|
-
await File.copy(paths[subdirectory], to
|
|
23
|
+
await File.copy(paths[subdirectory], to);
|
|
26
24
|
} catch(error) {
|
|
27
25
|
// directory doesn't exist
|
|
28
26
|
}
|
package/source/EagerPromise.js
CHANGED
|
@@ -44,6 +44,6 @@ const last = -1;
|
|
|
44
44
|
const eager = async (strings, ...keys) =>
|
|
45
45
|
(await Promise.all(strings.slice(0, last).map(async (string, i) =>
|
|
46
46
|
strings[i] + await keys[i]
|
|
47
|
-
))).join("") + strings
|
|
47
|
+
))).join("") + strings.at(last);
|
|
48
48
|
|
|
49
49
|
export {eager};
|
package/source/Server.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {createServer} from "https";
|
|
3
|
-
import {join} from "path";
|
|
4
|
-
import {parse} from "url";
|
|
1
|
+
import {Path, File, WebServer, log} from "runtime-compat";
|
|
5
2
|
import Session from "./Session.js";
|
|
6
|
-
import File from "./File.js";
|
|
7
|
-
import log from "./log.js";
|
|
8
3
|
import codes from "./http-codes.json" assert {"type": "json"};
|
|
9
4
|
import mimes from "./mimes.json" assert {"type": "json"};
|
|
10
5
|
import {http404} from "./handlers/http.js";
|
|
@@ -13,11 +8,8 @@ const regex = /\.([a-z1-9]*)$/u;
|
|
|
13
8
|
const mime = filename => mimes[filename.match(regex)[1]] ?? mimes.binary;
|
|
14
9
|
|
|
15
10
|
const stream = (from, response) => {
|
|
16
|
-
response.
|
|
17
|
-
response.
|
|
18
|
-
return from.pipe(zlib.createBrotliCompress())
|
|
19
|
-
.pipe(response)
|
|
20
|
-
.on("close", () => response.end());
|
|
11
|
+
response.setStatus(codes.OK);
|
|
12
|
+
return from.pipe(response).on("close", () => response.end());
|
|
21
13
|
};
|
|
22
14
|
|
|
23
15
|
export default class Server {
|
|
@@ -31,25 +23,20 @@ export default class Server {
|
|
|
31
23
|
this.csp = Object.keys(csp).reduce((policy_string, key) =>
|
|
32
24
|
policy_string + `${key} ${csp[key]};`, "");
|
|
33
25
|
|
|
34
|
-
this.server =
|
|
26
|
+
this.server = new WebServer(http, async (request, response) => {
|
|
35
27
|
const session = await Session.get(request.headers.cookie);
|
|
36
28
|
if (!session.has_cookie) {
|
|
37
29
|
const {cookie} = session;
|
|
38
30
|
response.setHeader("Set-Cookie", `${cookie}; SameSite=${same_site}`);
|
|
39
31
|
}
|
|
40
32
|
response.session = session;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
for await (const chunk of request) {
|
|
44
|
-
buffers.push(chunk);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const data = Buffer.concat(buffers).toString();
|
|
48
|
-
const payload = Object.fromEntries(decodeURI(data).replaceAll("+", " ")
|
|
33
|
+
const body = await request.body;
|
|
34
|
+
const payload = Object.fromEntries(decodeURI(body).replaceAll("+", " ")
|
|
49
35
|
.split("&")
|
|
50
36
|
.map(part => part.split("="))
|
|
51
37
|
.filter(([, value]) => value !== ""));
|
|
52
|
-
|
|
38
|
+
const {pathname, search} = new URL(`https://1${request.url}`);
|
|
39
|
+
return this.try(pathname + search, request, response, payload);
|
|
53
40
|
});
|
|
54
41
|
}
|
|
55
42
|
|
|
@@ -58,20 +45,20 @@ export default class Server {
|
|
|
58
45
|
await this.serve(url, request, response, payload);
|
|
59
46
|
} catch (error) {
|
|
60
47
|
console.log(error);
|
|
61
|
-
response.
|
|
48
|
+
response.setStatus(codes.InternalServerError);
|
|
62
49
|
response.end();
|
|
63
50
|
}
|
|
64
51
|
}
|
|
65
52
|
|
|
66
53
|
async serve_file(url, filename, file, response) {
|
|
67
54
|
response.setHeader("Content-Type", mime(filename));
|
|
68
|
-
response.setHeader("Etag", file.modified);
|
|
69
|
-
await response.session.log("green", url);
|
|
55
|
+
response.setHeader("Etag", await file.modified);
|
|
56
|
+
//await response.session.log("green", url);
|
|
70
57
|
return stream(file.read_stream, response);
|
|
71
58
|
}
|
|
72
59
|
|
|
73
60
|
async serve(url, request, response, payload) {
|
|
74
|
-
const filename = join(this.conf.serve_from, url);
|
|
61
|
+
const filename = Path.join(this.conf.serve_from, url);
|
|
75
62
|
const file = await new File(filename);
|
|
76
63
|
return await file.is_file
|
|
77
64
|
? this.serve_file(url, filename, file, response, payload)
|
|
@@ -93,8 +80,9 @@ export default class Server {
|
|
|
93
80
|
const {body, code} = result;
|
|
94
81
|
response.setHeader("Content-Security-Policy", this.csp);
|
|
95
82
|
response.setHeader("Referrer-Policy", "same-origin");
|
|
96
|
-
response.
|
|
97
|
-
response.
|
|
83
|
+
response.setStatus(code);
|
|
84
|
+
response.setBody(body);
|
|
85
|
+
response.end();
|
|
98
86
|
}
|
|
99
87
|
|
|
100
88
|
listen() {
|
package/source/conf.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Path, File} from "runtime-compat";
|
|
2
2
|
import cache from "./cache.js";
|
|
3
|
-
import File from "./File.js";
|
|
4
3
|
import extend_object from "./extend_object.js";
|
|
5
4
|
import primate_json from "./preset/primate.json" assert {"type": "json" };
|
|
6
5
|
|
|
@@ -8,16 +7,16 @@ const qualify = (root, paths) =>
|
|
|
8
7
|
Object.keys(paths).reduce((sofar, key) => {
|
|
9
8
|
const value = paths[key];
|
|
10
9
|
sofar[key] = typeof value === "string"
|
|
11
|
-
? join(root, value)
|
|
10
|
+
? Path.join(root, value)
|
|
12
11
|
: qualify(`${root}/${key}`, value);
|
|
13
12
|
return sofar;
|
|
14
13
|
}, {});
|
|
15
14
|
|
|
16
15
|
export default (file = "primate.json") => cache("conf", file, () => {
|
|
17
16
|
let conf = primate_json;
|
|
18
|
-
const root = resolve();
|
|
17
|
+
const root = Path.resolve();
|
|
19
18
|
try {
|
|
20
|
-
conf = extend_object(conf, JSON.parse(File.read_sync(join(root, file))));
|
|
19
|
+
conf = extend_object(conf, JSON.parse(File.read_sync(Path.join(root, file))));
|
|
21
20
|
} catch (error) {
|
|
22
21
|
// local primate.json not required
|
|
23
22
|
}
|
package/source/domain/Domain.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import {Crypto} from "runtime-compat";
|
|
1
2
|
import Field from "./Field.js";
|
|
2
3
|
import {PredicateError} from "../errors.js";
|
|
3
4
|
import EagerPromise from "../EagerPromise.js";
|
|
4
5
|
import Store from "../store/Store.js";
|
|
5
6
|
import cache from "../cache.js";
|
|
6
7
|
import DomainType from "../types/Domain.js";
|
|
7
|
-
import {random} from "../crypto.js";
|
|
8
8
|
|
|
9
9
|
const length = 12;
|
|
10
10
|
|
|
@@ -23,7 +23,7 @@ export default class Domain {
|
|
|
23
23
|
this.define("_id", {
|
|
24
24
|
"type": String,
|
|
25
25
|
"predicates": ["unique"],
|
|
26
|
-
"in": value => value ?? random(length).toString("hex"),
|
|
26
|
+
"in": value => value ?? Crypto.random(length).toString("hex"),
|
|
27
27
|
});
|
|
28
28
|
return new Proxy(this, {"get": (target, property, receiver) =>
|
|
29
29
|
Reflect.get(target, property, receiver) ?? target.#proxy(property),
|
package/source/exports.js
CHANGED
|
@@ -3,8 +3,6 @@ import App from "./App.js";
|
|
|
3
3
|
|
|
4
4
|
export {App};
|
|
5
5
|
export {default as Bundler} from "./Bundler.js";
|
|
6
|
-
export {default as Directory} from "./Directory.js";
|
|
7
|
-
export {default as File} from "./File.js";
|
|
8
6
|
export {default as EagerPromise, eager} from "./EagerPromise.js" ;
|
|
9
7
|
|
|
10
8
|
export {default as Domain} from "./domain/Domain.js";
|
|
@@ -16,7 +14,6 @@ export * from "./invariants.js";
|
|
|
16
14
|
export {default as MemoryStore} from "./store/Memory.js";
|
|
17
15
|
export {default as Store} from "./store/Store.js";
|
|
18
16
|
|
|
19
|
-
export {default as log} from "./log.js";
|
|
20
17
|
export {default as extend_object} from "./extend_object.js";
|
|
21
18
|
export {default as sanitize} from "./sanitize.js";
|
|
22
19
|
|
|
@@ -164,9 +164,8 @@ export default class Node {
|
|
|
164
164
|
this.text = fulfilled;
|
|
165
165
|
}
|
|
166
166
|
break;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
this.attributes.href = fulfilled;
|
|
167
|
+
default:
|
|
168
|
+
this.attributes[attribute.slice(5)] = fulfilled;
|
|
170
169
|
break;
|
|
171
170
|
}
|
|
172
171
|
delete this.attributes[attribute];
|
|
@@ -74,12 +74,19 @@ export default class Parser {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
try_create_text_node() {
|
|
78
|
+
if (this.buffer.length > 0) {
|
|
79
|
+
const child = new Node(this.node, "span");
|
|
80
|
+
child.text = this.buffer;
|
|
81
|
+
this.buffer = "";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
// currently outside of a tag
|
|
78
86
|
process_not_reading_tag() {
|
|
79
87
|
// encountered '<'
|
|
80
88
|
if (this.current === "<") {
|
|
81
|
-
this.
|
|
82
|
-
this.buffer = "";
|
|
89
|
+
this.try_create_text_node();
|
|
83
90
|
// mark as inside tag
|
|
84
91
|
this.reading_tag = true;
|
|
85
92
|
if (this.next === "/") {
|
|
@@ -111,6 +118,8 @@ export default class Parser {
|
|
|
111
118
|
if (this.balance !== 0) {
|
|
112
119
|
throw Error(`unbalanced DOM tree: ${this.balance}`);
|
|
113
120
|
}
|
|
121
|
+
// anything left at the end could be potentially a text node
|
|
122
|
+
this.try_create_text_node();
|
|
114
123
|
return this.tree;
|
|
115
124
|
}
|
|
116
125
|
|
package/source/handlers/html.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
import {File} from "runtime-compat";
|
|
1
2
|
import Parser from "./DOM/Parser.js";
|
|
2
|
-
import Directory from "../Directory.js";
|
|
3
|
-
import File from "../File.js";
|
|
4
3
|
import {index} from "../Bundler.js";
|
|
5
4
|
import _conf from "../conf.js";
|
|
6
5
|
const conf = _conf();
|
|
@@ -9,7 +8,7 @@ const last = -1;
|
|
|
9
8
|
const {"paths": {"components": path}} = conf;
|
|
10
9
|
const components = {};
|
|
11
10
|
if (await File.exists(path)) {
|
|
12
|
-
const names = await
|
|
11
|
+
const names = await File.list(path);
|
|
13
12
|
for (const name of names) {
|
|
14
13
|
components[name.slice(0, -5)] = await File.read(`${path}/${name}`);
|
|
15
14
|
}
|
package/source/invariants.js
CHANGED
|
@@ -14,6 +14,9 @@ const assert = (predicate, error) => Boolean(predicate) || errored(error);
|
|
|
14
14
|
const is = {
|
|
15
15
|
"array": value => assert(Array.isArray(value), "must be array"),
|
|
16
16
|
"string": value => assert(typeof value === "string", "must be string"),
|
|
17
|
+
"object": value => assert(typeof value === "object" && value !== null,
|
|
18
|
+
"must be object"),
|
|
19
|
+
"function": value => assert(typeof value === "function", "must be function"),
|
|
17
20
|
"defined": (value, error) => assert(value !== undefined, error),
|
|
18
21
|
"undefined": value => assert(value === undefined, "must be undefined"),
|
|
19
22
|
"constructible": (value, error) => assert(constructible(value), error),
|
|
@@ -24,9 +27,10 @@ const is = {
|
|
|
24
27
|
};
|
|
25
28
|
const {defined} = is;
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}, {});
|
|
30
|
+
// too early to use map_entries here, as it relies on invariants
|
|
31
|
+
const maybe = Object.fromEntries(Object.entries(is).map(([key, value]) =>
|
|
32
|
+
[key, (...args) => nullish(args[0]) || value(...args)]));
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
const invariant = predicate => predicate();
|
|
35
|
+
|
|
36
|
+
export {assert, defined, is, maybe, invariant};
|
package/source/sanitize.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return data;
|
|
10
|
-
}, {});
|
|
1
|
+
import {is} from "./invariants.js";
|
|
2
|
+
import map_entries from "./map_entries.js";
|
|
3
|
+
|
|
4
|
+
export default (dirty = {}) => is.object(dirty)
|
|
5
|
+
&& map_entries(dirty, (key, value) => {
|
|
6
|
+
const trimmed = value.toString().trim();
|
|
7
|
+
return [key, trimmed === "" ? undefined : trimmed];
|
|
8
|
+
});
|
package/source/store/Store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Path} from "runtime-compat";
|
|
2
2
|
const preset = "../preset/stores";
|
|
3
3
|
|
|
4
4
|
export default class Store {
|
|
@@ -21,7 +21,7 @@ export default class Store {
|
|
|
21
21
|
static async get(directory, file) {
|
|
22
22
|
let store;
|
|
23
23
|
try {
|
|
24
|
-
store = await import(resolve(`${directory}/${file}`));
|
|
24
|
+
store = await import(Path.resolve(`${directory}/${file}`));
|
|
25
25
|
} catch(error) {
|
|
26
26
|
store = await import(`${preset}/${file}`);
|
|
27
27
|
}
|
package/source/Directory.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import {join} from "path";
|
|
3
|
-
|
|
4
|
-
const options = {"encoding": "utf8"};
|
|
5
|
-
|
|
6
|
-
const readdir = path => new Promise((resolve, reject) =>
|
|
7
|
-
fs.readdir(path, options,
|
|
8
|
-
(error, files) => error === null ? resolve(files) : reject(error)
|
|
9
|
-
));
|
|
10
|
-
|
|
11
|
-
const rm = path => new Promise((resolve, reject) =>
|
|
12
|
-
fs.rm(path, {"recursive": true, "force": true},
|
|
13
|
-
error => error === null ? resolve(true) : reject(error)
|
|
14
|
-
));
|
|
15
|
-
|
|
16
|
-
const mkdir = path => new Promise((resolve, reject) =>
|
|
17
|
-
fs.mkdir(path, error => error === null ? resolve(true) : reject(error)));
|
|
18
|
-
|
|
19
|
-
export default class Directory {
|
|
20
|
-
static list(...args) {
|
|
21
|
-
return readdir(join(...args));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
static remove(...args) {
|
|
25
|
-
return rm(join(...args));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
static make(...args) {
|
|
29
|
-
return mkdir(join(...args));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
static async remake(...args) {
|
|
33
|
-
await this.remove(...args) && this.make(...args);
|
|
34
|
-
}
|
|
35
|
-
}
|
package/source/File.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import {join} from "path";
|
|
3
|
-
import Directory from "./Directory.js";
|
|
4
|
-
import EagerPromise from "./EagerPromise.js";
|
|
5
|
-
|
|
6
|
-
const array = maybe => Array.isArray(maybe) ? maybe : [maybe];
|
|
7
|
-
|
|
8
|
-
const filter_files = (files, filter) =>
|
|
9
|
-
files.filter(file => array(filter).some(ending => file.endsWith(ending)));
|
|
10
|
-
|
|
11
|
-
export default class File {
|
|
12
|
-
constructor(...args) {
|
|
13
|
-
this.path = join(...args);
|
|
14
|
-
return EagerPromise.resolve(new Promise(resolve => {
|
|
15
|
-
fs.lstat(this.path, (error, stats) => {
|
|
16
|
-
this.stats = stats;
|
|
17
|
-
resolve(this);
|
|
18
|
-
});
|
|
19
|
-
}));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
get modified() {
|
|
23
|
-
return Math.round(this.stats.mtimeMs);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
get exists() {
|
|
27
|
-
return this.stats !== undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
get is_file() {
|
|
31
|
-
return this.exists && !this.stats.isDirectory();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
get stream() {
|
|
35
|
-
return this.read_stream;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
get read_stream() {
|
|
39
|
-
return fs.createReadStream(this.path, {"flags": "r"});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
get write_stream() {
|
|
43
|
-
return fs.createWriteStream(this.path);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
remove() {
|
|
47
|
-
return new Promise((resolve, reject) => fs.rm(this.path,
|
|
48
|
-
{"recursive": true, "force": true},
|
|
49
|
-
error => error === null ? resolve(this) : reject(error)
|
|
50
|
-
));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
create() {
|
|
54
|
-
return new Promise((resolve, reject) => fs.mkdir(this.path, error =>
|
|
55
|
-
error === null ? resolve(this) : reject(error)
|
|
56
|
-
));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async copy(to, recreate = true) {
|
|
60
|
-
if (this.stats.isDirectory()) {
|
|
61
|
-
if (recreate) {
|
|
62
|
-
await new File(`${to}`).recreate();
|
|
63
|
-
}
|
|
64
|
-
// copy all files
|
|
65
|
-
return Promise.all((await this.list()).map(file =>
|
|
66
|
-
new File(`${this.path}/${file}`).copy(`${to}/${file}`)
|
|
67
|
-
));
|
|
68
|
-
} else {
|
|
69
|
-
return new Promise((resolve, reject) => fs.copyFile(this.path, to,
|
|
70
|
-
error => error === null ? resolve(this) : reject(error)));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async list(filter) {
|
|
75
|
-
if (!this.exists) {
|
|
76
|
-
return [];
|
|
77
|
-
}
|
|
78
|
-
const files = await Directory.list(this.path);
|
|
79
|
-
return filter !== undefined ? filter_files(files, filter) : files;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async recreate() {
|
|
83
|
-
return (await this.remove()).create();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
read(options = {"encoding": "utf8"}) {
|
|
87
|
-
return new Promise((resolve, reject) =>
|
|
88
|
-
fs.readFile(this.path, options, (error, nonerror) =>
|
|
89
|
-
error === null ? resolve(nonerror) : reject(error)));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
write(data, options = {"encoding": "utf8"}) {
|
|
93
|
-
return new Promise((resolve, reject) => fs.writeFile(this.path, data,
|
|
94
|
-
options,
|
|
95
|
-
error => error === null ? resolve(this) : reject(error)));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
static read_sync(path, options = {"encoding": "utf8"}) {
|
|
99
|
-
return fs.readFileSync(path, options);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
static exists(...args) {
|
|
103
|
-
return new File(...args).exists;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
static read(...args) {
|
|
107
|
-
return new File(...args).read();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
static write(path, data, options) {
|
|
111
|
-
return new File(path).write(data, options);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
static remove(...args) {
|
|
115
|
-
return new File(...args).remove();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
static copy(from, to, recreate) {
|
|
119
|
-
return new File(from).copy(to, recreate);
|
|
120
|
-
}
|
|
121
|
-
}
|
package/source/crypto.js
DELETED
package/source/log.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
const colors = {
|
|
2
|
-
"red": 31,
|
|
3
|
-
"green": 32,
|
|
4
|
-
"yellow": 33,
|
|
5
|
-
"blue": 34,
|
|
6
|
-
};
|
|
7
|
-
const reset = 0;
|
|
8
|
-
|
|
9
|
-
const Log = {
|
|
10
|
-
"paint": (color, message) => {
|
|
11
|
-
process.stdout.write(`\x1b[${color}m${message}\x1b[0m`);
|
|
12
|
-
return log;
|
|
13
|
-
},
|
|
14
|
-
"nl": () => log.paint(reset, "\n"),
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const log = new Proxy(Log, {
|
|
18
|
-
"get": (target, property) => target[property] ?? (message =>
|
|
19
|
-
log.paint(colors[property] ?? reset, message).paint(reset, " ")),
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
export default log;
|