primate 0.5.2 → 0.6.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.
Files changed (78) hide show
  1. package/README.md +17 -31
  2. package/debris.json +1 -2
  3. package/package.json +7 -10
  4. package/source/{server/App.js → App.js} +13 -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 +23 -0
  10. package/source/Server.js +103 -0
  11. package/source/{server/Session.js → Session.js} +2 -21
  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 +7 -9
  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 +188 -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 +3 -3
  35. package/source/preset/static/index.html +0 -2
  36. package/source/preset/stores/default.js +1 -1
  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/server/Action.js +0 -100
  70. package/source/server/Bundler.js +0 -177
  71. package/source/server/Context.js +0 -97
  72. package/source/server/Projector.js +0 -86
  73. package/source/server/Router.js +0 -52
  74. package/source/server/servers/Dynamic.js +0 -57
  75. package/source/server/servers/Server.js +0 -5
  76. package/source/server/servers/Static.js +0 -117
  77. package/source/server/view/TreeNode.js +0 -197
  78. package/source/server/view/View.js +0 -35
package/README.md CHANGED
@@ -1,8 +1,6 @@
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 with batteries included, Primate relieves you of dealing with repetitive, error-prone tasks and lets you concentrate on writing effective, expressive code.
6
4
 
7
5
  ## Installing
8
6
 
@@ -10,22 +8,16 @@ concentrate on writing effective, expressive code.
10
8
  npm install primate
11
9
  ```
12
10
 
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
11
+ ## Highlights
12
+
13
+ * Flexible HTTP routing, returning HTML, JSON or a custom handler
14
+ * Secure by default with HTTPS, hash-verified scripts and a strong CSP
15
+ * Built-in support for sessions with secure cookies
16
+ * Input verification using data domains
17
+ * Many different data store modules: In-Memory (built-in), [File][primate-store-file], [JSON][primate-store-json], [MongoDB][primate-store-mongodb]
18
+ * Easy modelling of`1:1`, `1:n` and `n:m` relationships
19
+ * Minimally opinionated with sane, overrideable defaults
20
+ * No dependencies
29
21
 
30
22
  ## Getting started
31
23
 
@@ -36,19 +28,13 @@ See the [getting started][getting-started] guide.
36
28
  * [Source code][source-code]
37
29
  * [Issues][issues]
38
30
 
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
31
  ## License
48
32
 
49
33
  BSD-3-Clause
50
34
 
51
- [ws]: https://github.com/websockets/ws
52
35
  [getting-started]: https://primatejs.com/getting-started
53
- [source-code]: https://adaptivecloud.dev/primate/primate
54
- [issues]: https://adaptivecloud.dev/primate/primate/issues
36
+ [source-code]: https://github.com/primatejs/primate
37
+ [issues]: https://github.com/primatejs/primate/issues
38
+ [primate-store-file]: https://npmjs.com/primate-store-file
39
+ [primate-store-json]: https://npmjs.com/primate-store-json
40
+ [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.2",
3
+ "version": "0.6.0",
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,34 +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": {
39
41
  ...this.conf.http,
40
42
  "key": File.read_sync(resolve(this.conf.http.ssl.key)),
41
43
  "cert": File.read_sync(resolve(this.conf.http.ssl.cert)),
42
44
  },
43
- "context": this.conf.defaults.context,
44
45
  };
45
- this.static_server = new StaticServer(conf);
46
- await this.static_server.run();
47
-
48
- this.dynamic_server = new DynamicServer({router,
49
- "path": this.conf.base,
50
- "server": this.static_server.server,
51
- "context": this.conf.defaults.context,
52
- });
53
- await this.dynamic_server.run();
46
+ this.server = new Server(conf);
47
+ await this.server.run();
54
48
 
55
49
  const {port, host} = this.conf.http;
56
- this.static_server.listen(port, host);
50
+ this.server.listen(port, host);
57
51
  }
58
52
 
59
53
  stop() {
60
- this.static_server.close();
54
+ this.server.close();
61
55
  }
62
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,23 @@
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 request => {
18
+ const {pathname, method} = request;
19
+ request.path = pathname.split("/").filter(path => path !== "");
20
+ const verb = find(method, pathname, () => http404``);
21
+ return verb(await find("map", pathname, _ => _)(request));
22
+ },
23
+ };
@@ -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(data
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
+ }
@@ -1,4 +1,3 @@
1
- import Context from "./Context.js";
2
1
  import Domain from "./domain/Domain.js";
3
2
 
4
3
  const extract_id = cookie_header => cookie_header
@@ -8,23 +7,14 @@ export default class Session extends Domain {
8
7
  static get fields() {
9
8
  return {
10
9
  "?data": Object,
11
- "context" : String,
12
10
  "created": value => value ?? new Date(),
13
11
  };
14
12
  }
15
13
 
16
- async log(color, message) {
17
- (await Context.get(this.context)).log(color, message);
18
- }
19
-
20
- route(router, url) {
21
- return router.route(url, this.context);
22
- }
23
-
24
- static async get(cookie_header, default_context) {
14
+ static async get(cookie_header) {
25
15
  const session = await Session.touch({"_id": extract_id(cookie_header)});
16
+ await session.save();
26
17
  if (session.new) {
27
- await session.save({"context": default_context});
28
18
  session.has_cookie = false;
29
19
  }
30
20
  return session;
@@ -33,13 +23,4 @@ export default class Session extends Domain {
33
23
  get cookie() {
34
24
  return `session_id=${this._id}; Path=/; Secure; HttpOnly`;
35
25
  }
36
-
37
- async switch_context(context, data = {}) {
38
- await this.save({context, data});
39
- return this;
40
- }
41
-
42
- async run(request) {
43
- return (await Context.get(this.context)).run(request, this);
44
- }
45
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";
@@ -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};