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 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
- ```js
121
- import html from "@primate/html";
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 contents of
125
- // `components/site-login.html` as HTML
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
  ```
@@ -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.10.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.34.0",
18
+ "eslint": "^8.36.0",
19
19
  "eslint-plugin-json": "^3.1.0"
20
20
  },
21
21
  "scripts": {
@@ -0,0 +1,3 @@
1
+ export default {
2
+ modules: [],
3
+ };
@@ -0,0 +1,5 @@
1
+ import domains from "@primate/domains";
2
+
3
+ export default {
4
+ modules: [domains()],
5
+ };
@@ -0,0 +1,3 @@
1
+ export default router => {
2
+ router.post("/site/login", ({body}) => `submitted user: ${body.username}`);
3
+ };
@@ -1,7 +1,4 @@
1
- import html from "@primate/html";
2
-
3
1
  export default router => {
4
- // accessing /site/login will serve the contents of
5
- // `components/site-login.html` as HTML
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 conf => {
4
- const {paths} = conf;
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
- const paths = qualify(root, conf.paths);
24
- return cache("conf", filename, () => {
25
- return {...conf, paths, root};
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
  };
@@ -1,7 +1,4 @@
1
- const response = {
2
- body: "Page not found",
1
+ export default () => () => ["Page not found", {
3
2
  status: 404,
4
3
  headers: {"Content-Type": "text/html"},
5
- };
6
-
7
- export default () => () => ({...response});
4
+ }];
@@ -1,7 +1,4 @@
1
- const response = {
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
+ }];
@@ -1,7 +1,4 @@
1
- const response = {
1
+ export default (_, ...keys) => async () => [await keys[0], {
2
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
+ }];
@@ -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
- return {...response, body};
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 log;
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
+ };
@@ -18,4 +18,5 @@ export default {
18
18
  routes: "routes",
19
19
  components: "components",
20
20
  },
21
+ modules: [],
21
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 => typeof value === "string" ? text`${value}` : http404``;
11
- const isObject = value => typeof value === "object" && value !== null
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: () => http404``});
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 conf => {
10
- log.reset("Primate").yellow(package_json.version);
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(conf);
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 conf => {
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 conf.router.process(request))(conf);
28
+ result = await (await env.router.process(request))(env, headers);
23
29
  } catch (error) {
24
- console.log(error);
25
- result = http404()``;
30
+ env.error(error.message);
31
+ result = http404(env, headers)``;
26
32
  }
27
- const csp = Object.keys(conf.http.csp).reduce((policy_string, key) =>
28
- `${policy_string}${key} ${conf.http.csp[key]};`, "");
29
- return new Response(result.body, {
30
- status: result.status,
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(conf.from, request.pathname);
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
- console.log(error);
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} = conf;
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 await handlers({original: request, pathname: pathname + search, body});
87
+ return handlers({original: request, pathname: pathname + search, body});
91
88
  }, http);
92
89
 
93
- log.reset("on").yellow(`${http.host}:${http.port}`).nl();
90
+ env.info(`running on ${http.host}:${http.port}`);
94
91
  };