primate 0.5.1 → 0.6.1

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.
Files changed (81) hide show
  1. package/README.md +21 -36
  2. package/debris.json +1 -2
  3. package/package.json +7 -10
  4. package/source/{server/App.js → App.js} +14 -19
  5. package/source/Bundler.js +43 -0
  6. package/source/{server/Directory.js → Directory.js} +0 -0
  7. package/source/{server/EagerPromise.js → EagerPromise.js} +0 -0
  8. package/source/{server/File.js → File.js} +0 -0
  9. package/source/Router.js +28 -0
  10. package/source/Server.js +103 -0
  11. package/source/Session.js +26 -0
  12. package/source/{server/attributes.js → attributes.js} +0 -0
  13. package/source/{server/cache.js → cache.js} +0 -0
  14. package/source/{server/conf.js → conf.js} +1 -1
  15. package/source/{server/crypto.js → crypto.js} +0 -0
  16. package/source/{server/domain → domain}/Domain.js +8 -10
  17. package/source/{server/domain → domain}/Field.js +0 -0
  18. package/source/{server/domain → domain}/Predicate.js +0 -0
  19. package/source/{server/domain → domain}/domains.js +0 -0
  20. package/source/{server/errors → errors}/InternalServer.js +0 -0
  21. package/source/{server/errors → errors}/Predicate.js +0 -0
  22. package/source/{server/errors.js → errors.js} +0 -0
  23. package/source/{server/exports.js → exports.js} +4 -2
  24. package/source/{server/extend_object.js → extend_object.js} +0 -0
  25. package/source/handlers/DOM/Node.js +184 -0
  26. package/source/{server/view → handlers/DOM}/Parser.js +22 -18
  27. package/source/handlers/html.js +27 -0
  28. package/source/handlers/http.js +6 -0
  29. package/source/handlers/redirect.js +10 -0
  30. package/source/{server/servers/http-codes.json → http-codes.json} +0 -0
  31. package/source/{server/invariants.js → invariants.js} +0 -0
  32. package/source/{server/log.js → log.js} +0 -0
  33. package/source/{server/servers/mimes.json → mimes.json} +0 -0
  34. package/source/preset/primate.json +12 -4
  35. package/source/preset/static/index.html +0 -2
  36. package/source/preset/stores/default.js +2 -0
  37. package/source/{server/sanitize.js → sanitize.js} +0 -0
  38. package/source/{server/store → store}/Memory.js +0 -0
  39. package/source/{server/store → store}/Store.js +1 -1
  40. package/source/{server/types → types}/Array.js +0 -0
  41. package/source/{server/types → types}/Boolean.js +0 -0
  42. package/source/{server/types → types}/Date.js +0 -0
  43. package/source/{server/types → types}/Domain.js +0 -0
  44. package/source/{server/types → types}/Instance.js +0 -0
  45. package/source/{server/types → types}/Number.js +0 -0
  46. package/source/{server/types → types}/Object.js +0 -0
  47. package/source/{server/types → types}/Primitive.js +0 -0
  48. package/source/{server/types → types}/Storeable.js +0 -0
  49. package/source/{server/types → types}/String.js +0 -0
  50. package/source/{server/types → types}/errors/Array.json +0 -0
  51. package/source/{server/types → types}/errors/Boolean.json +0 -0
  52. package/source/{server/types → types}/errors/Date.json +0 -0
  53. package/source/{server/types → types}/errors/Number.json +0 -0
  54. package/source/{server/types → types}/errors/Object.json +0 -0
  55. package/source/{server/types → types}/errors/String.json +0 -0
  56. package/source/{server/types.js → types.js} +0 -0
  57. package/source/client/Action.js +0 -157
  58. package/source/client/App.js +0 -16
  59. package/source/client/Client.js +0 -61
  60. package/source/client/Context.js +0 -47
  61. package/source/client/Element.js +0 -249
  62. package/source/client/Node.js +0 -13
  63. package/source/client/Session.js +0 -27
  64. package/source/client/View.js +0 -89
  65. package/source/client/document.js +0 -6
  66. package/source/client/exports.js +0 -15
  67. package/source/preset/client/Element.js +0 -2
  68. package/source/preset/client/app.js +0 -2
  69. package/source/preset/data/stores/default.js +0 -2
  70. package/source/server/Action.js +0 -100
  71. package/source/server/Bundler.js +0 -177
  72. package/source/server/Context.js +0 -97
  73. package/source/server/Projector.js +0 -86
  74. package/source/server/Router.js +0 -52
  75. package/source/server/Session.js +0 -45
  76. package/source/server/servers/Dynamic.js +0 -57
  77. package/source/server/servers/Server.js +0 -5
  78. package/source/server/servers/Static.js +0 -118
  79. package/source/server/servers/content-security-policy.json +0 -7
  80. package/source/server/view/TreeNode.js +0 -197
  81. package/source/server/view/View.js +0 -35
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # Primate: a JavaScript framework
1
+ # Primate, A JavaScript Framework
2
2
 
3
- Primate is a server-client JavaScript framework for web applications aimed at
4
- relieving you of dealing with repetitive, error-prone tasks and letting you
5
- concentrate on writing effective, expressive code.
3
+ A full-stack Javascript framework, Primate relieves you of dealing with
4
+ repetitive, error-prone tasks and lets you concentrate on writing effective,
5
+ expressive code.
6
6
 
7
7
  ## Installing
8
8
 
@@ -10,45 +10,30 @@ concentrate on writing effective, expressive code.
10
10
  npm install primate
11
11
  ```
12
12
 
13
- ## Concepts
14
-
15
- * **Minimal** just one dependency ([ws][])
16
- * **Simple** only native web technologies (JavaScript, HTML)
17
- * **Full-stack** server and client, both in JavaScript
18
- * **Layered** separation of data (domains), logic (actions) and presentation
19
- (views)
20
- * **Correct** data verification using field definitions in domains
21
- * **Secure** serving hash-verified scripts on secure HTTP with a strong CSP
22
- * **Roles** access control with contexts
23
- * **Sessions** using cookies that are `Secure`, `SameSite=Strict`, `HttpOnly`
24
- and `Path`-limited
25
- * **Sane defaults** minimally opinionated with good, overrideable defaults
26
- for most features
27
- * **Data linking** modeling `1:1`, `1:n` and `n:m` relationships on domains via
28
- cacheable ad-hoc getters
13
+ ## Highlights
14
+
15
+ * Flexible HTTP routing, returning HTML, JSON or a custom handler
16
+ * Secure by default with HTTPS, hash-verified scripts and a strong CSP
17
+ * Built-in support for sessions with secure cookies
18
+ * Input verification using data domains
19
+ * Many different data store modules: In-Memory (built-in),
20
+ [File][primate-store-file], [JSON][primate-store-json],
21
+ [MongoDB][primate-store-mongodb]
22
+ * Easy modelling of`1:1`, `1:n` and `n:m` relationships
23
+ * Minimally opinionated with sane, overrideable defaults
24
+ * No dependencies
29
25
 
30
26
  ## Getting started
31
27
 
32
28
  See the [getting started][getting-started] guide.
33
29
 
34
- ## Resources
35
-
36
- * [Source code][source-code]
37
- * [Issues][issues]
38
-
39
- A full guide is coming soon.
40
-
41
- ## Versioning
42
-
43
- This software is still in its initial development phase and is not considered
44
- stable or recommended for production. Once we release a stable version `1`, we
45
- will move over to semantic versioning.
46
-
47
30
  ## License
48
31
 
49
32
  BSD-3-Clause
50
33
 
51
- [ws]: https://github.com/websockets/ws
52
34
  [getting-started]: https://primatejs.com/getting-started
53
- [source-code]: https://adaptivecloud.dev/primate/primate
54
- [issues]: https://adaptivecloud.dev/primate/primate/issues
35
+ [source-code]: https://github.com/primatejs/primate
36
+ [issues]: https://github.com/primatejs/primate/issues
37
+ [primate-store-file]: https://npmjs.com/primate-store-file
38
+ [primate-store-json]: https://npmjs.com/primate-store-json
39
+ [primate-store-mongodb]: https://npmjs.com/primate-store-mongodb
package/debris.json CHANGED
@@ -1,5 +1,4 @@
1
1
  {
2
2
  "suites": "test/suites",
3
- "fixtures": "test/fixtures",
4
- "explicit": false
3
+ "fixtures": "test/fixtures"
5
4
  }
package/package.json CHANGED
@@ -1,15 +1,12 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "author": "Primate core team <core@primatejs.com>",
5
5
  "homepage": "https://primatejs.com",
6
- "bugs": "https://adaptivecloud.dev/primate/primate/issues",
7
- "repository": "https://adaptivecloud.dev/primate/primate",
6
+ "bugs": "https://github.com/primatejs/primate/issues",
7
+ "repository": "https://github.com/primatejs/primate",
8
8
  "description": "Server-client framework",
9
9
  "license": "BSD-3-Clause",
10
- "dependencies": {
11
- "ws": "^8.5.0"
12
- },
13
10
  "scripts": {
14
11
  "copy": "rm -rf output && mkdir output && cp source/* output -a",
15
12
  "instrument": "nyc instrument source ./output",
@@ -18,14 +15,14 @@
18
15
  "test": "npm run copy && npm run debris"
19
16
  },
20
17
  "devDependencies": {
21
- "debris": "^0.2.0",
22
- "eslint": "^8.11.0",
18
+ "debris": "^0.2.2",
19
+ "eslint": "^8.13.0",
23
20
  "eslint-plugin-json": "^3.1.0",
24
21
  "nyc": "^15.1.0"
25
22
  },
26
23
  "type": "module",
27
- "exports": "./source/server/exports.js",
24
+ "exports": "./source/exports.js",
28
25
  "engines": {
29
- "node": ">=17.7.2"
26
+ "node": ">=17.9.0"
30
27
  }
31
28
  }
@@ -1,12 +1,12 @@
1
1
  import {resolve} from "path";
2
2
  import Bundler from "./Bundler.js";
3
3
  import File from "./File.js";
4
+ import Directory from "./Directory.js";
4
5
  import Router from "./Router.js";
5
- import DynamicServer from "./servers/Dynamic.js";
6
- import StaticServer from "./servers/Static.js";
6
+ import Server from "./Server.js";
7
7
  import cache from "./cache.js";
8
8
  import log from "./log.js";
9
- import package_json from "../../package.json" assert {"type": "json"};
9
+ import package_json from "../package.json" assert {"type": "json"};
10
10
 
11
11
  export default class App {
12
12
  constructor(conf) {
@@ -29,33 +29,28 @@ export default class App {
29
29
  async run() {
30
30
  log.reset("Primate").yellow(package_json.version);
31
31
 
32
- this.router = new Router(await this.routes, this.conf);
33
- const {index, hashes} = await new this.Bundler(this.conf).bundle();
34
- const {router} = this;
32
+ const routes = await Directory.list(this.conf.paths.routes);
33
+ for (const route of routes) {
34
+ await import(`${this.conf.paths.routes}/${route}`);
35
+ }
36
+ const index = await new this.Bundler(this.conf).bundle();
35
37
 
36
- const conf = {index, hashes, router,
38
+ const conf = {index, "router": Router,
37
39
  "serve_from": this.conf.paths.public,
38
40
  "http": {
41
+ ...this.conf.http,
39
42
  "key": File.read_sync(resolve(this.conf.http.ssl.key)),
40
43
  "cert": File.read_sync(resolve(this.conf.http.ssl.cert)),
41
44
  },
42
- "context": this.conf.defaults.context,
43
45
  };
44
- this.static_server = new StaticServer(conf);
45
- await this.static_server.run();
46
-
47
- this.dynamic_server = new DynamicServer({router,
48
- "path": this.conf.base,
49
- "server": this.static_server.server,
50
- "context": this.conf.defaults.context,
51
- });
52
- await this.dynamic_server.run();
46
+ this.server = new Server(conf);
47
+ await this.server.run();
53
48
 
54
49
  const {port, host} = this.conf.http;
55
- this.static_server.listen(port, host);
50
+ this.server.listen(port, host);
56
51
  }
57
52
 
58
53
  stop() {
59
- this.static_server.close();
54
+ this.server.close();
60
55
  }
61
56
  }
@@ -0,0 +1,43 @@
1
+ import {dirname} from "path";
2
+ import {fileURLToPath} from "url";
3
+ import File from "./File.js";
4
+
5
+ const meta_url = fileURLToPath(import.meta.url);
6
+ const directory = dirname(meta_url);
7
+ const preset = `${directory}/preset`;
8
+
9
+ export default class Bundler {
10
+ constructor(conf) {
11
+ this.conf = conf;
12
+ this.debug = conf.debug;
13
+ this.index = conf.files.index;
14
+ this.scripts = [];
15
+ }
16
+
17
+ async copy_with_preset(subdirectory, to) {
18
+ const {paths} = this.conf;
19
+
20
+ // copy files preset files first
21
+ await File.copy(`${preset}/${subdirectory}`, to);
22
+
23
+ // copy any user code over it, not recreating the folder
24
+ try {
25
+ await File.copy(paths[subdirectory], to, false);
26
+ } catch(error) {
27
+ // directory doesn't exist
28
+ }
29
+ }
30
+
31
+ async bundle() {
32
+ const {paths} = this.conf;
33
+
34
+ // copy static files to public
35
+ await this.copy_with_preset("static", paths.public);
36
+
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
+ await File.remove(`${paths.public}/${this.index}`);
40
+
41
+ return index_html;
42
+ }
43
+ }
File without changes
File without changes
@@ -0,0 +1,28 @@
1
+ import {http404} from "./handlers/http.js";
2
+
3
+ const aliases = [];
4
+ const routes = [];
5
+ const dealias = path => aliases.reduce((dealiased, {key, value}) =>
6
+ dealiased.replace(key, () => value), path);
7
+ const push = (type, path, handler) =>
8
+ routes.push({type, "path": new RegExp(`^${dealias(path)}$`, "u"), handler});
9
+ const find = (type, path, fallback) => routes.find(route =>
10
+ route.type === type && route.path.test(path))?.handler ?? fallback;
11
+
12
+ export default {
13
+ "map": (path, callback) => push("map", path, callback),
14
+ "get": (path, callback) => push("get", path, callback),
15
+ "post": (path, callback) => push("post", path, callback),
16
+ "alias": (key, value) => aliases.push({key, value}),
17
+ "process": async original_request => {
18
+ const {method} = original_request;
19
+ const url = new URL(`https://primatejs.com${original_request.pathname}`);
20
+ const {pathname, searchParams} = url;
21
+ const params = Object.fromEntries(searchParams);
22
+ const request = {...original_request, pathname, params,
23
+ "path": pathname.split("/").filter(path => path !== ""),
24
+ };
25
+ const verb = find(method, pathname, () => http404``);
26
+ return verb(await find("map", pathname, _ => _)(request));
27
+ },
28
+ };
@@ -0,0 +1,103 @@
1
+ import zlib from "zlib";
2
+ import {Readable} from "stream";
3
+ import {createServer} from "https";
4
+ import {join} from "path";
5
+ import {parse} from "url";
6
+ import Session from "./Session.js";
7
+ import File from "./File.js";
8
+ import {algorithm, hash} from "./crypto.js";
9
+ import log from "./log.js";
10
+ import codes from "./http-codes.json" assert {"type": "json"};
11
+ import mimes from "./mimes.json" assert {"type": "json"};
12
+
13
+ const regex = /\.([a-z1-9]*)$/u;
14
+ const mime = filename => mimes[filename.match(regex)[1]] ?? mimes.binary;
15
+
16
+ const stream = (from, response) => {
17
+ response.setHeader("Content-Encoding", "br");
18
+ response.writeHead(codes.OK);
19
+ return from.pipe(zlib.createBrotliCompress())
20
+ .pipe(response)
21
+ .on("close", () => response.end());
22
+ };
23
+
24
+ export default class Server {
25
+ constructor(conf) {
26
+ this.conf = conf;
27
+ }
28
+
29
+ async run() {
30
+ const {http} = this.conf;
31
+ const {csp, "same-site": same_site = "Strict"} = http;
32
+ this.csp = Object.keys(csp).reduce((policy_string, key) =>
33
+ policy_string + `${key} ${csp[key]};`, "");
34
+
35
+ this.server = await createServer(http, async (request, response) => {
36
+ const session = await Session.get(request.headers.cookie);
37
+ if (!session.has_cookie) {
38
+ const {cookie} = session;
39
+ response.setHeader("Set-Cookie", `${cookie}; SameSite=${same_site}`);
40
+ }
41
+ response.session = session;
42
+ const buffers = [];
43
+
44
+ for await (const chunk of request) {
45
+ buffers.push(chunk);
46
+ }
47
+
48
+ const data = Buffer.concat(buffers).toString();
49
+ const payload = Object.fromEntries(decodeURI(data).replaceAll("+", " ")
50
+ .split("&")
51
+ .map(part => part.split("="))
52
+ .filter(([, value]) => value !== ""));
53
+ this.try(parse(request.url).path, request, response, payload);
54
+ });
55
+ }
56
+
57
+ async try(url, request, response, payload) {
58
+ try {
59
+ await this.serve(url, request, response, payload);
60
+ } catch (error) {
61
+ console.log(error);
62
+ // await response.session.log("red", error.message);
63
+ response.writeHead(codes.InternalServerError);
64
+ response.end();
65
+ }
66
+ }
67
+
68
+ async serve_file(url, filename, file, response) {
69
+ response.setHeader("Content-Type", mime(filename));
70
+ response.setHeader("Etag", file.modified);
71
+ await response.session.log("green", url);
72
+ return stream(file.read_stream, response);
73
+ }
74
+
75
+ async serve(url, request, response, payload) {
76
+ const filename = join(this.conf.serve_from, url);
77
+ const file = await new File(filename);
78
+ return await file.is_file
79
+ ? this.serve_file(url, filename, file, response, payload)
80
+ : this.serve_data(url, request, response, payload);
81
+ }
82
+
83
+ async serve_data(pathname, request, response, payload) {
84
+ const {session} = response;
85
+ const request2 = {pathname, "method": request.method.toLowerCase(), payload};
86
+ const res = await this.conf.router.process(request2);
87
+ const {body, code, headers} = res;
88
+
89
+ const result = this.conf.index.replace("<body>", () => `<body>${body}`);
90
+ for (const [key, value] of Object.entries(headers)) {
91
+ response.setHeader(key, value);
92
+ }
93
+ response.setHeader("Content-Security-Policy", this.csp);
94
+ response.setHeader("Referrer-Policy", "same-origin");
95
+ response.writeHead(code);
96
+ response.end(result);
97
+ }
98
+
99
+ listen(port, host) {
100
+ log.reset("on").yellow(`https://${host}:${port}`).nl();
101
+ this.server.listen(port, host);
102
+ }
103
+ }
@@ -0,0 +1,26 @@
1
+ import Domain from "./domain/Domain.js";
2
+
3
+ const extract_id = cookie_header => cookie_header
4
+ ?.split(";").filter(text => text.includes("session_id="))[0]?.split("=")[1];
5
+
6
+ export default class Session extends Domain {
7
+ static get fields() {
8
+ return {
9
+ "?data": Object,
10
+ "created": value => value ?? new Date(),
11
+ };
12
+ }
13
+
14
+ static async get(cookie_header) {
15
+ const session = await Session.touch({"_id": extract_id(cookie_header)});
16
+ await session.save();
17
+ if (session.new) {
18
+ session.has_cookie = false;
19
+ }
20
+ return session;
21
+ }
22
+
23
+ get cookie() {
24
+ return `session_id=${this._id}; Path=/; Secure; HttpOnly`;
25
+ }
26
+ }
File without changes
File without changes
@@ -2,7 +2,7 @@ import {join, resolve} from "path";
2
2
  import cache from "./cache.js";
3
3
  import File from "./File.js";
4
4
  import extend_object from "./extend_object.js";
5
- import primate_json from "../preset/primate.json" assert {"type": "json" };
5
+ import primate_json from "./preset/primate.json" assert {"type": "json" };
6
6
 
7
7
  const qualify = (root, paths) =>
8
8
  Object.keys(paths).reduce((sofar, key) => {
File without changes
@@ -1,4 +1,3 @@
1
- import {resolve as path_resolve} from "path";
2
1
  import Field from "./Field.js";
3
2
  import {PredicateError} from "../errors.js";
4
3
  import EagerPromise from "../EagerPromise.js";
@@ -10,7 +9,7 @@ import {random} from "../crypto.js";
10
9
  const length = 12;
11
10
 
12
11
  export default class Domain {
13
- static stores_directory = "data/stores";
12
+ static stores_directory = "stores";
14
13
  static store_file = "default.js";
15
14
 
16
15
  static {
@@ -27,8 +26,8 @@ export default class Domain {
27
26
  "in": value => value ?? random(length).toString("hex"),
28
27
  });
29
28
  return new Proxy(this, {"get": (target, property, receiver) =>
30
- Reflect.get(target, property, receiver) ?? target.#proxy(property)
31
- }).set({...document, errors})
29
+ Reflect.get(target, property, receiver) ?? target.#proxy(property),
30
+ }).set({...document, errors});
32
31
  }
33
32
 
34
33
  get Class() {
@@ -47,7 +46,7 @@ export default class Domain {
47
46
  }
48
47
 
49
48
  static get store() {
50
- return EagerPromise.resolve(cache(this, "store", async () =>
49
+ return EagerPromise.resolve(cache(this, "store", () =>
51
50
  Store.get(this.stores_directory, this.store_file)
52
51
  ));
53
52
  }
@@ -101,8 +100,8 @@ export default class Domain {
101
100
  #link(name) {
102
101
  const field = this.fields[`${name}_id`];
103
102
  if (field?.is_domain) {
104
- const collection = field.Type.collection;
105
- const cache = this.Class.cache;
103
+ const {collection} = field.Type;
104
+ const {cache} = this.Class;
106
105
  if (cache[collection] === undefined) {
107
106
  cache[collection] = {};
108
107
  }
@@ -110,9 +109,8 @@ export default class Domain {
110
109
  cache[collection][this[`${name}_id`]] = field.by_id(this[`${name}_id`]);
111
110
  }
112
111
  return cache[collection][this[`${name}_id`]];
113
- } else {
114
- return undefined
115
112
  }
113
+ return undefined;
116
114
  }
117
115
 
118
116
  // Serializing is done from the instance's point of view.
@@ -163,7 +161,7 @@ export default class Domain {
163
161
 
164
162
  async verify(delta) {
165
163
  this.set(delta);
166
- const fields = this.fields;
164
+ const {fields} = this;
167
165
  this.errors = (await Promise.all(Object.keys(fields).map(async property =>
168
166
  ({property, "value": await fields[property].verify(property, this)}))))
169
167
  .filter(result => typeof result.value === "string")
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -2,9 +2,7 @@ import conf from "./conf.js";
2
2
  import App from "./App.js";
3
3
 
4
4
  export {App};
5
- export {default as Action} from "./Action.js";
6
5
  export {default as Bundler} from "./Bundler.js";
7
- export {default as Context} from "./Context.js";
8
6
  export {default as Directory} from "./Directory.js";
9
7
  export {default as File} from "./File.js";
10
8
  export {default as EagerPromise, eager} from "./EagerPromise.js" ;
@@ -23,6 +21,10 @@ export {default as log} from "./log.js";
23
21
  export {default as extend_object} from "./extend_object.js";
24
22
  export {default as sanitize} from "./sanitize.js";
25
23
 
24
+ export {default as html} from "./handlers/html.js";
25
+ export {default as redirect} from "./handlers/redirect.js";
26
+ export {default as router} from "./Router.js";
27
+
26
28
  const app = new App(conf());
27
29
 
28
30
  export {app};