primate 0.10.0 → 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 +7 -8
- package/README.template.md +45 -4
- package/package.json +2 -2
- 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/basic.js +2 -5
- package/src/bundle.js +2 -3
- package/src/conf.js +9 -6
- package/src/handlers/http404.js +2 -5
- package/src/handlers/json.js +2 -5
- package/src/handlers/stream.js +2 -5
- package/src/handlers/text.js +2 -5
- package/src/log.js +5 -1
- package/src/preset/{primate.js → primate.conf.js} +1 -0
- package/src/route.js +10 -4
- package/src/run.js +4 -13
- package/src/serve.js +17 -20
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ Add `{"type": "module"}` to your `package.json` and run `npx primate`.
|
|
|
25
25
|
- [Routing](#routing)
|
|
26
26
|
- [Basic](#basic)
|
|
27
27
|
- [The request object](#the-request-object)
|
|
28
|
+
- [Accessing the request body](#accessing-the-request-body)
|
|
28
29
|
- [Regular expressions](#regular-expressions)
|
|
29
30
|
- [Named groups](#named-groups)
|
|
30
31
|
- [Aliasing](#aliasing)
|
|
@@ -112,18 +113,16 @@ export default router => {
|
|
|
112
113
|
|
|
113
114
|
Routes map requests to responses. They are loaded from `routes`.
|
|
114
115
|
|
|
115
|
-
The order in which routes are declared is irrelevant. Redeclaring a route
|
|
116
|
-
(same pathname and same HTTP verb) throws an error.
|
|
117
|
-
|
|
118
116
|
### Basic
|
|
119
117
|
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
122
121
|
|
|
122
|
+
```js
|
|
123
123
|
export default router => {
|
|
124
|
-
// accessing /site/login will serve the
|
|
125
|
-
|
|
126
|
-
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!");
|
|
127
126
|
};
|
|
128
127
|
|
|
129
128
|
```
|
package/README.template.md
CHANGED
|
@@ -10,7 +10,7 @@ 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
14
|
|
|
15
15
|
## Table of Contents
|
|
16
16
|
|
|
@@ -22,10 +22,12 @@ Add `{"type": "module"}` to your `package.json` and run `npx primate`.
|
|
|
22
22
|
- [Routing](#routing)
|
|
23
23
|
- [Basic](#basic)
|
|
24
24
|
- [The request object](#the-request-object)
|
|
25
|
+
- [Accessing the request body](#accessing-the-request-body)
|
|
25
26
|
- [Regular expressions](#regular-expressions)
|
|
26
27
|
- [Named groups](#named-groups)
|
|
27
28
|
- [Aliasing](#aliasing)
|
|
28
29
|
- [Sharing logic across requests](#sharing-logic-across-requests)
|
|
30
|
+
- [Modules](#modules)
|
|
29
31
|
- [Data persistance](#data-persistance)
|
|
30
32
|
- [Short field notation](#short-field-notation)
|
|
31
33
|
- [Predicates](#predicates)
|
|
@@ -70,9 +72,6 @@ Serve the component in your route
|
|
|
70
72
|
|
|
71
73
|
Routes map requests to responses. They are loaded from `routes`.
|
|
72
74
|
|
|
73
|
-
The order in which routes are declared is irrelevant. Redeclaring a route
|
|
74
|
-
(same pathname and same HTTP verb) throws an error.
|
|
75
|
-
|
|
76
75
|
### Basic
|
|
77
76
|
|
|
78
77
|
```js
|
|
@@ -85,6 +84,17 @@ The order in which routes are declared is irrelevant. Redeclaring a route
|
|
|
85
84
|
// routing/the-request-object.js
|
|
86
85
|
```
|
|
87
86
|
|
|
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`.
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
// routing/accessing-the-request-body.js
|
|
96
|
+
```
|
|
97
|
+
|
|
88
98
|
### Regular expressions
|
|
89
99
|
|
|
90
100
|
```js
|
|
@@ -109,6 +119,37 @@ The order in which routes are declared is irrelevant. Redeclaring a route
|
|
|
109
119
|
// routing/sharing-logic-across-requests.js
|
|
110
120
|
```
|
|
111
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
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
// modules/domains/configure.js
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Sessions
|
|
146
|
+
|
|
147
|
+
The module `@primate/sessions` is used to maintain user sessions.
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
#### Databases
|
|
152
|
+
|
|
112
153
|
## Data persistance
|
|
113
154
|
|
|
114
155
|
Primate domains (via [`@primate/domains`][primate-domains]) represent a
|
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",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
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": {
|
package/readme/routing/basic.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import html from "@primate/html";
|
|
2
|
-
|
|
3
1
|
export default router => {
|
|
4
|
-
// accessing /site/login will serve the
|
|
5
|
-
|
|
6
|
-
router.get("/site/login", () => html`<site-login />`);
|
|
2
|
+
// accessing /site/login will serve the `Hello, world!` as plain text
|
|
3
|
+
router.get("/site/login", () => "Hello, world!");
|
|
7
4
|
};
|
package/src/bundle.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import {File} from "runtime-compat/filesystem";
|
|
2
2
|
|
|
3
|
-
export default async
|
|
4
|
-
const {paths} =
|
|
5
|
-
|
|
3
|
+
export default async env => {
|
|
4
|
+
const {paths} = env;
|
|
6
5
|
if (await paths.static.exists) {
|
|
7
6
|
// remove public directory in case exists
|
|
8
7
|
if (await paths.public.exists) {
|
package/src/conf.js
CHANGED
|
@@ -2,7 +2,9 @@ import {Path} from "runtime-compat/filesystem";
|
|
|
2
2
|
import {EagerEither} from "runtime-compat/functional";
|
|
3
3
|
import cache from "./cache.js";
|
|
4
4
|
import extend from "./extend.js";
|
|
5
|
-
import preset from "./preset/primate.js";
|
|
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,15 +15,16 @@ const qualify = (root, paths) =>
|
|
|
13
15
|
return sofar;
|
|
14
16
|
}, {});
|
|
15
17
|
|
|
16
|
-
export default async (filename = "primate.js") => {
|
|
18
|
+
export default async (filename = "primate.conf.js") => {
|
|
17
19
|
const root = Path.resolve();
|
|
18
20
|
const conffile = root.join(filename);
|
|
19
21
|
const conf = await EagerEither
|
|
20
22
|
.try(async () => extend(preset, (await import(conffile)).default))
|
|
21
23
|
.match({left: () => preset})
|
|
22
24
|
.get();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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}));
|
|
27
30
|
};
|
package/src/handlers/http404.js
CHANGED
package/src/handlers/json.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export default (_, ...keys) => async () => [JSON.stringify(await keys[0]), {
|
|
2
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
package/src/handlers/text.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
const last = -1;
|
|
2
|
-
const response = {
|
|
3
|
-
status: 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
|
+
};
|
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);
|
|
@@ -48,7 +52,9 @@ export default async definitions => {
|
|
|
48
52
|
const url = new URL(`https://primatejs.com${request.pathname}`);
|
|
49
53
|
const {pathname, searchParams} = url;
|
|
50
54
|
const params = Object.fromEntries(searchParams);
|
|
51
|
-
const verb = find(method, pathname, {handler: () =>
|
|
55
|
+
const verb = find(method, pathname, {handler: () => {
|
|
56
|
+
throw new RouteError(`no ${method.toUpperCase()} route to ${pathname}`);
|
|
57
|
+
}});
|
|
52
58
|
const path = pathname.split("/").filter(part => part !== "");
|
|
53
59
|
const named = verb.path?.exec(pathname)?.groups ?? {};
|
|
54
60
|
|
package/src/run.js
CHANGED
|
@@ -1,22 +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
5
|
const extract = (modules, key) => modules.flatMap(module => module[key] ?? []);
|
|
8
6
|
|
|
9
|
-
export default async
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const {paths} = conf;
|
|
7
|
+
export default async env => {
|
|
8
|
+
const {paths} = env;
|
|
13
9
|
const router = await route(paths.routes);
|
|
14
|
-
await bundle(
|
|
10
|
+
await bundle(env);
|
|
15
11
|
|
|
16
|
-
await serve({router,
|
|
17
|
-
paths: conf.paths,
|
|
18
|
-
from: conf.paths.public,
|
|
19
|
-
http: conf.http,
|
|
20
|
-
modules: extract(conf.modules ?? [], "serve"),
|
|
21
|
-
});
|
|
12
|
+
await serve({router, ...env, modules: extract(env.modules ?? [], "serve")});
|
|
22
13
|
};
|
package/src/serve.js
CHANGED
|
@@ -3,7 +3,6 @@ import {serve, Response} from "runtime-compat/http";
|
|
|
3
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;
|
|
@@ -15,25 +14,23 @@ const contents = {
|
|
|
15
14
|
"application/json": body => JSON.parse(body),
|
|
16
15
|
};
|
|
17
16
|
|
|
18
|
-
export default
|
|
17
|
+
export default env => {
|
|
19
18
|
const route = async request => {
|
|
20
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
|
+
};
|
|
26
|
+
|
|
21
27
|
try {
|
|
22
|
-
result = await (await
|
|
28
|
+
result = await (await env.router.process(request))(env, headers);
|
|
23
29
|
} catch (error) {
|
|
24
|
-
|
|
25
|
-
result = http404()``;
|
|
30
|
+
env.error(error.message);
|
|
31
|
+
result = http404(env, headers)``;
|
|
26
32
|
}
|
|
27
|
-
|
|
28
|
-
`${policy_string}${key} ${conf.http.csp[key]};`, "");
|
|
29
|
-
return new Response(result.body, {
|
|
30
|
-
status: result.status,
|
|
31
|
-
headers: {
|
|
32
|
-
...result.headers,
|
|
33
|
-
"Content-Security-Policy": csp,
|
|
34
|
-
"Referrer-Policy": "same-origin",
|
|
35
|
-
},
|
|
36
|
-
});
|
|
33
|
+
return new Response(...result);
|
|
37
34
|
};
|
|
38
35
|
|
|
39
36
|
const resource = async file => new Response(file.readable, {
|
|
@@ -45,7 +42,7 @@ export default conf => {
|
|
|
45
42
|
});
|
|
46
43
|
|
|
47
44
|
const _serve = async request => {
|
|
48
|
-
const path = new Path(
|
|
45
|
+
const path = new Path(env.paths.public, request.pathname);
|
|
49
46
|
return await path.isFile ? resource(path.file) : route(request);
|
|
50
47
|
};
|
|
51
48
|
|
|
@@ -53,7 +50,7 @@ export default conf => {
|
|
|
53
50
|
try {
|
|
54
51
|
return await _serve(request);
|
|
55
52
|
} catch (error) {
|
|
56
|
-
|
|
53
|
+
env.error(error.message);
|
|
57
54
|
return new Response(null, {status: statuses.InternalServerError});
|
|
58
55
|
}
|
|
59
56
|
};
|
|
@@ -63,7 +60,7 @@ export default conf => {
|
|
|
63
60
|
return type === undefined ? body : type(body);
|
|
64
61
|
};
|
|
65
62
|
|
|
66
|
-
const {http, modules} =
|
|
63
|
+
const {http, modules} = env;
|
|
67
64
|
|
|
68
65
|
// handle is the last module to be executed
|
|
69
66
|
const handlers = [...modules, handle].reduceRight((acc, handler) =>
|
|
@@ -87,8 +84,8 @@ export default conf => {
|
|
|
87
84
|
|
|
88
85
|
const {pathname, search} = new URL(`https://example.com${request.url}`);
|
|
89
86
|
|
|
90
|
-
return
|
|
87
|
+
return handlers({original: request, pathname: pathname + search, body});
|
|
91
88
|
}, http);
|
|
92
89
|
|
|
93
|
-
|
|
90
|
+
env.info(`running on ${http.host}:${http.port}`);
|
|
94
91
|
};
|