primate 0.5.0 → 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 (82) hide show
  1. package/README.md +18 -34
  2. package/debris.json +4 -0
  3. package/jsconfig.json +8 -0
  4. package/package.json +14 -11
  5. package/source/{server/App.js → App.js} +14 -19
  6. package/source/Bundler.js +43 -0
  7. package/source/{server/Directory.js → Directory.js} +0 -0
  8. package/source/{server/EagerPromise.js → EagerPromise.js} +0 -0
  9. package/source/{server/File.js → File.js} +4 -0
  10. package/source/Router.js +23 -0
  11. package/source/Server.js +103 -0
  12. package/source/Session.js +26 -0
  13. package/source/{server/attributes.js → attributes.js} +0 -0
  14. package/source/{server/cache.js → cache.js} +0 -0
  15. package/source/{server/conf.js → conf.js} +1 -1
  16. package/source/{server/crypto.js → crypto.js} +0 -0
  17. package/source/{server/domain → domain}/Domain.js +8 -10
  18. package/source/{server/domain → domain}/Field.js +0 -0
  19. package/source/{server/domain → domain}/Predicate.js +0 -0
  20. package/source/{server/domain → domain}/domains.js +1 -1
  21. package/source/{server/errors → errors}/InternalServer.js +0 -0
  22. package/source/{server/errors → errors}/Predicate.js +0 -0
  23. package/source/{server/errors.js → errors.js} +0 -0
  24. package/source/{server/exports.js → exports.js} +4 -2
  25. package/source/{server/extend_object.js → extend_object.js} +0 -0
  26. package/source/handlers/DOM/Node.js +188 -0
  27. package/source/{server/view → handlers/DOM}/Parser.js +22 -18
  28. package/source/handlers/html.js +27 -0
  29. package/source/handlers/http.js +6 -0
  30. package/source/handlers/redirect.js +10 -0
  31. package/source/{server/servers/http-codes.json → http-codes.json} +0 -0
  32. package/source/{server/invariants.js → invariants.js} +1 -1
  33. package/source/{server/log.js → log.js} +0 -0
  34. package/source/{server/servers/mimes.json → mimes.json} +0 -0
  35. package/source/preset/primate.json +13 -7
  36. package/source/preset/static/index.html +0 -2
  37. package/source/preset/stores/default.js +2 -0
  38. package/source/{server/sanitize.js → sanitize.js} +0 -0
  39. package/source/{server/store → store}/Memory.js +0 -0
  40. package/source/{server/store → store}/Store.js +1 -1
  41. package/source/{server/types → types}/Array.js +0 -0
  42. package/source/{server/types → types}/Boolean.js +0 -0
  43. package/source/{server/types → types}/Date.js +0 -0
  44. package/source/{server/types → types}/Domain.js +0 -0
  45. package/source/{server/types → types}/Instance.js +0 -0
  46. package/source/{server/types → types}/Number.js +0 -0
  47. package/source/{server/types → types}/Object.js +0 -0
  48. package/source/{server/types → types}/Primitive.js +0 -0
  49. package/source/{server/types → types}/Storeable.js +0 -0
  50. package/source/{server/types → types}/String.js +0 -0
  51. package/source/{server/types → types}/errors/Array.json +0 -0
  52. package/source/{server/types → types}/errors/Boolean.json +0 -0
  53. package/source/{server/types → types}/errors/Date.json +0 -0
  54. package/source/{server/types → types}/errors/Number.json +0 -0
  55. package/source/{server/types → types}/errors/Object.json +0 -0
  56. package/source/{server/types → types}/errors/String.json +0 -0
  57. package/source/{server/types.js → types.js} +0 -0
  58. package/source/client/Action.js +0 -157
  59. package/source/client/App.js +0 -16
  60. package/source/client/Client.js +0 -61
  61. package/source/client/Context.js +0 -47
  62. package/source/client/Element.js +0 -249
  63. package/source/client/Node.js +0 -13
  64. package/source/client/Session.js +0 -27
  65. package/source/client/View.js +0 -89
  66. package/source/client/document.js +0 -6
  67. package/source/client/exports.js +0 -15
  68. package/source/preset/client/Element.js +0 -2
  69. package/source/preset/client/app.js +0 -2
  70. package/source/preset/data/stores/default.js +0 -2
  71. package/source/server/Action.js +0 -99
  72. package/source/server/Bundler.js +0 -177
  73. package/source/server/Context.js +0 -97
  74. package/source/server/Projector.js +0 -86
  75. package/source/server/Router.js +0 -52
  76. package/source/server/Session.js +0 -45
  77. package/source/server/servers/Dynamic.js +0 -57
  78. package/source/server/servers/Server.js +0 -5
  79. package/source/server/servers/Static.js +0 -118
  80. package/source/server/servers/content-security-policy.json +0 -7
  81. package/source/server/view/TreeNode.js +0 -197
  82. package/source/server/view/View.js +0 -35
package/README.md CHANGED
@@ -1,33 +1,23 @@
1
- # Primate: a JavaScript framework
1
+ # Primate, A JavaScript Framework
2
2
 
3
- ⚠️ **This software is currently its initial development phase. It is not
4
- considered production-ready. Expect breakage.** ⚠️
5
-
6
- Primate is a server-client JavaScript framework for web applications aimed at
7
- relieving you of dealing with repetitive, error-prone tasks and letting you
8
- 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.
9
4
 
10
5
  ## Installing
11
6
 
12
7
  ```
13
- yarn add primate
8
+ npm install primate
14
9
  ```
15
10
 
16
- ## Concepts
17
-
18
- * **Minimal** just one dependency ([ws][])
19
- * **Simple** only native web technologies (JavaScript, HTML)
20
- * **Full-stack** server and client, both in JavaScript
21
- * **Layered** separation of data (domains), logic (actions) and presentation
22
- (views)
23
- * **Correct** data verification using field definitions in domains
24
- * **Secure** serving hash-verified scripts on secure HTTP with a strong CSP
25
- * **Roles** access control with contexts
26
- * **Sessions** using cookies that are `HttpOnly`, `Secure` and `Path`-limited
27
- * **Sane defaults** minimally opinionated with good, overrideable defaults
28
- for most features
29
- * **Data linking** modeling `1:1`, `1:n` and `n:m` relationships on domains via
30
- 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
31
21
 
32
22
  ## Getting started
33
23
 
@@ -38,19 +28,13 @@ See the [getting started][getting-started] guide.
38
28
  * [Source code][source-code]
39
29
  * [Issues][issues]
40
30
 
41
- A full guide is coming soon.
42
-
43
- ## Versioning
44
-
45
- This software is still in its initial development phase and is not considered
46
- stable or recommended for production. Once we release a stable version `1`, we
47
- will move over to semantic versioning.
48
-
49
31
  ## License
50
32
 
51
33
  BSD-3-Clause
52
34
 
53
- [ws]: https://github.com/websockets/ws
54
35
  [getting-started]: https://primatejs.com/getting-started
55
- [source-code]: https://adaptivecloud.dev/primate/primate
56
- [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 ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "suites": "test/suites",
3
+ "fixtures": "test/fixtures"
4
+ }
package/jsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "target": "esnext",
5
+ "module": "esnext"
6
+ },
7
+ "exclude": ["node_modules"]
8
+ }
package/package.json CHANGED
@@ -1,25 +1,28 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.5.0",
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",
16
- "stick": "node --experimental-json-modules node_modules/stick stick.json",
17
- "coverage": "yarn instrument && nyc yarn run stick",
18
- "test": "yarn copy && yarn run stick"
13
+ "debris": "node --experimental-json-modules node_modules/debris debris.json",
14
+ "coverage": "npm run instrument && nyc npm run debris",
15
+ "test": "npm run copy && npm run debris"
16
+ },
17
+ "devDependencies": {
18
+ "debris": "^0.2.2",
19
+ "eslint": "^8.13.0",
20
+ "eslint-plugin-json": "^3.1.0",
21
+ "nyc": "^15.1.0"
19
22
  },
20
23
  "type": "module",
21
- "exports": "./source/server/exports.js",
24
+ "exports": "./source/exports.js",
22
25
  "engines": {
23
- "node": ">=17.3.0"
26
+ "node": ">=17.9.0"
24
27
  }
25
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
@@ -31,6 +31,10 @@ export default class File {
31
31
  return this.exists && !this.stats.isDirectory();
32
32
  }
33
33
 
34
+ get stream() {
35
+ return this.read_stream;
36
+ }
37
+
34
38
  get read_stream() {
35
39
  return fs.createReadStream(this.path, {"flags": "r"});
36
40
  }
@@ -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
+ }
@@ -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
@@ -4,7 +4,7 @@ import Field from "./Field.js";
4
4
  import Domain from "./Domain.js";
5
5
 
6
6
  const domains = {};
7
- const base = conf().paths.data.domains;
7
+ const base = conf().paths.domains;
8
8
 
9
9
  for (const domain of await new File(base).list(".js")) {
10
10
  const name = domain.slice(0, -3);
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};