primate 0.6.1 → 0.6.4
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 +76 -8
- package/package.json +2 -2
- package/source/App.js +3 -23
- package/source/Bundler.js +11 -3
- package/source/EagerPromise.js +6 -8
- package/source/Server.js +17 -15
- package/source/exports.js +2 -1
- package/source/handlers/DOM/Node.js +2 -3
- package/source/handlers/DOM/Parser.js +11 -2
- package/source/handlers/html.js +6 -3
- package/source/handlers/json.js +6 -0
- package/source/invariants.js +10 -5
- package/source/map_entries.js +6 -0
- package/source/preset/primate.json +0 -4
- package/source/sanitize.js +8 -10
- package/source/domain/domains.js +0 -31
package/README.md
CHANGED
|
@@ -7,9 +7,77 @@ expressive code.
|
|
|
7
7
|
## Installing
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
npm install primate
|
|
10
|
+
$ npm install primate
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
Lay out your app
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
$ mkdir -p primate-app/{routes,components,ssl} && cd primate-app
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Create a route for `/`
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
// routes/site.js
|
|
25
|
+
|
|
26
|
+
import {router, html} from "primate";
|
|
27
|
+
|
|
28
|
+
router.get("/", () => html`<site-index date=${new Date()} />`);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Create component for your route
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
// components/site-index.html
|
|
35
|
+
|
|
36
|
+
Today's date is <span data-value="date"></span>.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Generate SSL key/certificate
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
openssl req -x509 -out ssl/default.crt -keyout ssl/default.key -newkey rsa:2048 -nodes -sha256 -batch
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Add an entry file
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
// app.js
|
|
49
|
+
|
|
50
|
+
import {app} from "primate";
|
|
51
|
+
app.run();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Create and start script and enable ES modules
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
// package.json
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
"scripts": {
|
|
61
|
+
"start": "node --experimental-json-modules app.js"
|
|
62
|
+
},
|
|
63
|
+
"type": "module"
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Install Primate
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
$ npm install primate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Run app
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
$ npm start
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Visit `https://localhost:9999`
|
|
80
|
+
|
|
13
81
|
## Highlights
|
|
14
82
|
|
|
15
83
|
* Flexible HTTP routing, returning HTML, JSON or a custom handler
|
|
@@ -17,15 +85,15 @@ npm install primate
|
|
|
17
85
|
* Built-in support for sessions with secure cookies
|
|
18
86
|
* Input verification using data domains
|
|
19
87
|
* Many different data store modules: In-Memory (built-in),
|
|
20
|
-
[File][primate-store
|
|
21
|
-
[MongoDB][primate-store
|
|
88
|
+
[File][primate-file-store], [JSON][primate-json-store],
|
|
89
|
+
[MongoDB][primate-mongodb-store]
|
|
22
90
|
* Easy modelling of`1:1`, `1:n` and `n:m` relationships
|
|
23
91
|
* Minimally opinionated with sane, overrideable defaults
|
|
24
92
|
* No dependencies
|
|
25
93
|
|
|
26
|
-
##
|
|
94
|
+
## Resources
|
|
27
95
|
|
|
28
|
-
|
|
96
|
+
* [Getting started guide]
|
|
29
97
|
|
|
30
98
|
## License
|
|
31
99
|
|
|
@@ -34,6 +102,6 @@ BSD-3-Clause
|
|
|
34
102
|
[getting-started]: https://primatejs.com/getting-started
|
|
35
103
|
[source-code]: https://github.com/primatejs/primate
|
|
36
104
|
[issues]: https://github.com/primatejs/primate/issues
|
|
37
|
-
[primate-store
|
|
38
|
-
[primate-store
|
|
39
|
-
[primate-store
|
|
105
|
+
[primate-file-store]: https://npmjs.com/primate-file-store
|
|
106
|
+
[primate-json-store]: https://npmjs.com/primate-json-store
|
|
107
|
+
[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.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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",
|
package/source/App.js
CHANGED
|
@@ -4,26 +4,12 @@ import File from "./File.js";
|
|
|
4
4
|
import Directory from "./Directory.js";
|
|
5
5
|
import Router from "./Router.js";
|
|
6
6
|
import Server from "./Server.js";
|
|
7
|
-
import cache from "./cache.js";
|
|
8
7
|
import log from "./log.js";
|
|
9
8
|
import package_json from "../package.json" assert {"type": "json"};
|
|
10
9
|
|
|
11
10
|
export default class App {
|
|
12
11
|
constructor(conf) {
|
|
13
12
|
this.conf = conf;
|
|
14
|
-
this.Bundler = Bundler;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
get routes() {
|
|
18
|
-
return cache(this, "routes", async () => {
|
|
19
|
-
try {
|
|
20
|
-
const path = `${this.conf.root}/routes.json`;
|
|
21
|
-
return (await import(path, {"assert": {"type": "json"}})).default;
|
|
22
|
-
} catch (error) {
|
|
23
|
-
// local routes.json not required
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
13
|
}
|
|
28
14
|
|
|
29
15
|
async run() {
|
|
@@ -33,9 +19,9 @@ export default class App {
|
|
|
33
19
|
for (const route of routes) {
|
|
34
20
|
await import(`${this.conf.paths.routes}/${route}`);
|
|
35
21
|
}
|
|
36
|
-
|
|
22
|
+
await new Bundler(this.conf).bundle();
|
|
37
23
|
|
|
38
|
-
const conf = {
|
|
24
|
+
const conf = {"router": Router,
|
|
39
25
|
"serve_from": this.conf.paths.public,
|
|
40
26
|
"http": {
|
|
41
27
|
...this.conf.http,
|
|
@@ -45,12 +31,6 @@ export default class App {
|
|
|
45
31
|
};
|
|
46
32
|
this.server = new Server(conf);
|
|
47
33
|
await this.server.run();
|
|
48
|
-
|
|
49
|
-
const {port, host} = this.conf.http;
|
|
50
|
-
this.server.listen(port, host);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
stop() {
|
|
54
|
-
this.server.close();
|
|
34
|
+
this.server.listen();
|
|
55
35
|
}
|
|
56
36
|
}
|
package/source/Bundler.js
CHANGED
|
@@ -35,9 +35,17 @@ export default class Bundler {
|
|
|
35
35
|
await this.copy_with_preset("static", paths.public);
|
|
36
36
|
|
|
37
37
|
// read index.html from public, then remove it (we serve it dynamically)
|
|
38
|
-
const index_html = await File.read(`${paths.public}/${this.index}`);
|
|
39
38
|
await File.remove(`${paths.public}/${this.index}`);
|
|
40
|
-
|
|
41
|
-
return index_html;
|
|
42
39
|
}
|
|
43
40
|
}
|
|
41
|
+
|
|
42
|
+
export const index = async conf => {
|
|
43
|
+
let file;
|
|
44
|
+
const subdirectory = "static";
|
|
45
|
+
try {
|
|
46
|
+
file = await File.read(`${conf.paths[subdirectory]}/${conf.files.index}`);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
file = await File.read(`${preset}/${subdirectory}/${conf.files.index}`);
|
|
49
|
+
}
|
|
50
|
+
return file;
|
|
51
|
+
};
|
package/source/EagerPromise.js
CHANGED
|
@@ -10,14 +10,12 @@ const handler = {
|
|
|
10
10
|
return promise[property].bind(promise);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
return EagerPromise.resolve(promise.then(result =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
return inconstructible_function(result[property])
|
|
13
|
+
return EagerPromise.resolve(promise.then(result => property === "bind"
|
|
14
|
+
? result
|
|
15
|
+
: inconstructible_function(result[property])
|
|
18
16
|
? result[property].bind(result)
|
|
19
|
-
: result[property]
|
|
20
|
-
|
|
17
|
+
: result[property]
|
|
18
|
+
));
|
|
21
19
|
},
|
|
22
20
|
"apply": (target, that, args) =>
|
|
23
21
|
EagerPromise.resolve(target[$promise].then(result =>
|
|
@@ -46,6 +44,6 @@ const last = -1;
|
|
|
46
44
|
const eager = async (strings, ...keys) =>
|
|
47
45
|
(await Promise.all(strings.slice(0, last).map(async (string, i) =>
|
|
48
46
|
strings[i] + await keys[i]
|
|
49
|
-
))).join("") + strings
|
|
47
|
+
))).join("") + strings.at(last);
|
|
50
48
|
|
|
51
49
|
export {eager};
|
package/source/Server.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import zlib from "zlib";
|
|
2
|
-
import {Readable} from "stream";
|
|
3
2
|
import {createServer} from "https";
|
|
4
3
|
import {join} from "path";
|
|
5
4
|
import {parse} from "url";
|
|
6
5
|
import Session from "./Session.js";
|
|
7
6
|
import File from "./File.js";
|
|
8
|
-
import {algorithm, hash} from "./crypto.js";
|
|
9
7
|
import log from "./log.js";
|
|
10
8
|
import codes from "./http-codes.json" assert {"type": "json"};
|
|
11
9
|
import mimes from "./mimes.json" assert {"type": "json"};
|
|
10
|
+
import {http404} from "./handlers/http.js";
|
|
12
11
|
|
|
13
12
|
const regex = /\.([a-z1-9]*)$/u;
|
|
14
13
|
const mime = filename => mimes[filename.match(regex)[1]] ?? mimes.binary;
|
|
@@ -59,7 +58,6 @@ export default class Server {
|
|
|
59
58
|
await this.serve(url, request, response, payload);
|
|
60
59
|
} catch (error) {
|
|
61
60
|
console.log(error);
|
|
62
|
-
// await response.session.log("red", error.message);
|
|
63
61
|
response.writeHead(codes.InternalServerError);
|
|
64
62
|
response.end();
|
|
65
63
|
}
|
|
@@ -77,26 +75,30 @@ export default class Server {
|
|
|
77
75
|
const file = await new File(filename);
|
|
78
76
|
return await file.is_file
|
|
79
77
|
? this.serve_file(url, filename, file, response, payload)
|
|
80
|
-
: this.
|
|
78
|
+
: this.serve_route(url, request, response, payload);
|
|
81
79
|
}
|
|
82
80
|
|
|
83
|
-
async
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
81
|
+
async serve_route(pathname, request, response, payload) {
|
|
82
|
+
const req = {pathname, "method": request.method.toLowerCase(), payload};
|
|
83
|
+
let result;
|
|
84
|
+
try {
|
|
85
|
+
result = await this.conf.router.process(req);
|
|
86
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
87
|
+
response.setHeader(key, value);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.log(error);
|
|
91
|
+
result = http404``;
|
|
92
92
|
}
|
|
93
|
+
const {body, code} = result;
|
|
93
94
|
response.setHeader("Content-Security-Policy", this.csp);
|
|
94
95
|
response.setHeader("Referrer-Policy", "same-origin");
|
|
95
96
|
response.writeHead(code);
|
|
96
|
-
response.end(
|
|
97
|
+
response.end(body);
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
listen(
|
|
100
|
+
listen() {
|
|
101
|
+
const {port, host} = this.conf.http;
|
|
100
102
|
log.reset("on").yellow(`https://${host}:${port}`).nl();
|
|
101
103
|
this.server.listen(port, host);
|
|
102
104
|
}
|
package/source/exports.js
CHANGED
|
@@ -8,7 +8,6 @@ export {default as File} from "./File.js";
|
|
|
8
8
|
export {default as EagerPromise, eager} from "./EagerPromise.js" ;
|
|
9
9
|
|
|
10
10
|
export {default as Domain} from "./domain/Domain.js";
|
|
11
|
-
export {default as domains} from "./domain/domains.js";
|
|
12
11
|
export {default as Storeable} from "./types/Storeable.js";
|
|
13
12
|
|
|
14
13
|
export * from "./errors.js";
|
|
@@ -22,7 +21,9 @@ export {default as extend_object} from "./extend_object.js";
|
|
|
22
21
|
export {default as sanitize} from "./sanitize.js";
|
|
23
22
|
|
|
24
23
|
export {default as html} from "./handlers/html.js";
|
|
24
|
+
export {default as json} from "./handlers/json.js";
|
|
25
25
|
export {default as redirect} from "./handlers/redirect.js";
|
|
26
|
+
|
|
26
27
|
export {default as router} from "./Router.js";
|
|
27
28
|
|
|
28
29
|
const app = new App(conf());
|
|
@@ -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,10 +1,12 @@
|
|
|
1
1
|
import Parser from "./DOM/Parser.js";
|
|
2
|
-
import conf from "../conf.js";
|
|
3
2
|
import Directory from "../Directory.js";
|
|
4
3
|
import File from "../File.js";
|
|
4
|
+
import {index} from "../Bundler.js";
|
|
5
|
+
import _conf from "../conf.js";
|
|
6
|
+
const conf = _conf();
|
|
5
7
|
|
|
6
8
|
const last = -1;
|
|
7
|
-
const {paths: {"components": path}} = conf
|
|
9
|
+
const {"paths": {"components": path}} = conf;
|
|
8
10
|
const components = {};
|
|
9
11
|
if (await File.exists(path)) {
|
|
10
12
|
const names = await Directory.list(path);
|
|
@@ -15,12 +17,13 @@ if (await File.exists(path)) {
|
|
|
15
17
|
|
|
16
18
|
export default async (strings, ...keys) => {
|
|
17
19
|
const awaited_keys = await Promise.all(keys);
|
|
18
|
-
const
|
|
20
|
+
const rendered = await (await Parser.parse(strings
|
|
19
21
|
.slice(0, last)
|
|
20
22
|
.map((string, i) => `${string}$${i}`)
|
|
21
23
|
.join("") + strings[strings.length+last], awaited_keys)
|
|
22
24
|
.unfold(components))
|
|
23
25
|
.render();
|
|
26
|
+
const body = (await index(conf)).replace("<body>", () => `<body>${rendered}`);
|
|
24
27
|
const code = 200;
|
|
25
28
|
const headers = {"Content-Type": "text/html"};
|
|
26
29
|
return {code, body, headers};
|
package/source/invariants.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {constructible, nullish} from "./attributes.js";
|
|
2
|
+
import map_entries from "./map_entries.js";
|
|
2
3
|
|
|
3
4
|
const errored = error => {
|
|
4
5
|
if (typeof error === "function") {
|
|
@@ -14,6 +15,9 @@ const assert = (predicate, error) => Boolean(predicate) || errored(error);
|
|
|
14
15
|
const is = {
|
|
15
16
|
"array": value => assert(Array.isArray(value), "must be array"),
|
|
16
17
|
"string": value => assert(typeof value === "string", "must be string"),
|
|
18
|
+
"object": value => assert(typeof value === "object" && value !== null,
|
|
19
|
+
"must be object"),
|
|
20
|
+
"function": value => assert(typeof value === "function", "must be function"),
|
|
17
21
|
"defined": (value, error) => assert(value !== undefined, error),
|
|
18
22
|
"undefined": value => assert(value === undefined, "must be undefined"),
|
|
19
23
|
"constructible": (value, error) => assert(constructible(value), error),
|
|
@@ -24,9 +28,10 @@ const is = {
|
|
|
24
28
|
};
|
|
25
29
|
const {defined} = is;
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}, {});
|
|
31
|
+
// too early to use map_entries here, as it relies on invariants
|
|
32
|
+
const maybe = Object.fromEntries(Object.entries(is).map(([key, value]) =>
|
|
33
|
+
[key, (...args) => nullish(args[0]) || value(...args)]));
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
const invariant = predicate => predicate();
|
|
36
|
+
|
|
37
|
+
export {assert, defined, is, maybe, invariant};
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"base": "/",
|
|
3
3
|
"debug": false,
|
|
4
|
-
"defaults": {
|
|
5
|
-
"context": "guest"
|
|
6
|
-
},
|
|
7
4
|
"files": {
|
|
8
5
|
"index": "index.html"
|
|
9
6
|
},
|
|
@@ -24,7 +21,6 @@
|
|
|
24
21
|
"same-site": "Strict"
|
|
25
22
|
},
|
|
26
23
|
"paths": {
|
|
27
|
-
"domains": "domains",
|
|
28
24
|
"public": "public",
|
|
29
25
|
"static": "static",
|
|
30
26
|
"routes": "routes",
|
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/domain/domains.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import conf from "../conf.js";
|
|
2
|
-
import File from "../File.js";
|
|
3
|
-
import Field from "./Field.js";
|
|
4
|
-
import Domain from "./Domain.js";
|
|
5
|
-
|
|
6
|
-
const domains = {};
|
|
7
|
-
const base = conf().paths.domains;
|
|
8
|
-
|
|
9
|
-
for (const domain of await new File(base).list(".js")) {
|
|
10
|
-
const name = domain.slice(0, -3);
|
|
11
|
-
import(`${base}/${domain}`).then(module => {
|
|
12
|
-
domains[name] = module.default;
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const actuals = {};
|
|
17
|
-
for (const domain in domains) {
|
|
18
|
-
if (domains[domain].prototype instanceof Domain) {
|
|
19
|
-
const fields = {};
|
|
20
|
-
for (const field in domains[domain]._fields) {
|
|
21
|
-
const Type = domains[domain]._fields[field].Type;
|
|
22
|
-
if(Type.prototype instanceof Domain) {
|
|
23
|
-
fields[field.slice(0, -3)] = Type.name;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
actuals[domain] = fields;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export default domains;
|