primate 0.7.0 → 0.8.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
@@ -1,8 +1,7 @@
1
- # Primate, A JavaScript Framework
1
+ # Primate
2
2
 
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.
3
+ A full-stack Javascript framework with data verification and server-side HTML
4
+ rendering.
6
5
 
7
6
  ## Highlights
8
7
 
@@ -15,10 +14,12 @@ expressive code.
15
14
  [MongoDB][primate-mongodb-store]
16
15
  * Easy modelling of`1:1`, `1:n` and `n:m` relationships
17
16
  * Minimally opinionated with sane, overrideable defaults
18
- * No dependencies
17
+ * Supports both Node.js and Deno
19
18
 
20
19
  ## Getting started
21
20
 
21
+ ### Prepare
22
+
22
23
  Lay out your app
23
24
 
24
25
  ```sh
@@ -32,13 +33,13 @@ Create a route for `/`
32
33
 
33
34
  import {router, html} from "primate";
34
35
 
35
- router.get("/", () => html`<site-index date=${new Date()} />`);
36
+ router.get("/", () => html`<site-index date="${new Date()}" />`);
36
37
  ```
37
38
 
38
39
  Create a component for your route (in `components/site-index.html`)
39
40
 
40
41
  ```html
41
- Today's date is <span data-value="date"></span>.
42
+ Today's date is <span value="${date}"></span>.
42
43
  ```
43
44
 
44
45
  Generate SSL key/certificate
@@ -56,6 +57,8 @@ import {app} from "primate";
56
57
  app.run();
57
58
  ```
58
59
 
60
+ ### Run on Node.js
61
+
59
62
  Create a start script and enable ES modules (in `package.json`)
60
63
 
61
64
  ```json
@@ -79,15 +82,33 @@ Run app
79
82
  $ npm start
80
83
  ```
81
84
 
82
- Visit `https://localhost:9999`
85
+ ### Run on Deno
86
+
87
+ Create an import map file (`import-map.json`)
88
+
89
+ ```json
90
+ {
91
+ "imports": {
92
+ "runtime-compat": "https://deno.land/x/runtime_compat/exports.js",
93
+ "primate": "https:/deno.land/x/primate/exports.js"
94
+ }
95
+ }
96
+ ```
97
+
98
+ Run app
99
+
100
+ ```
101
+ deno run --import-map=import-map.json app.js
102
+ ```
103
+
104
+ You will typically need the `allow-read`, `allow-write` and `allow-net`
105
+ permissions.
83
106
 
84
107
  ## Table of Contents
85
108
 
86
109
  * [Routes](#routes)
87
110
  * [Handlers](#handlers)
88
111
  * [Components](#components)
89
- * [Domains](#domains)
90
- * [Verification](#verification)
91
112
 
92
113
  ## Routes
93
114
 
@@ -171,13 +192,15 @@ router.map("/user/edit/_id", request => {
171
192
 
172
193
  router.get("/user/edit/_id", request => {
173
194
  // show user edit form
174
- return html`<user-edit user=${request.user} />`
195
+ return html`<user-edit user="${request.user}" />`
175
196
  });
176
197
 
177
198
  router.post("/user/edit/_id", request => {
178
199
  const {user} = request;
179
200
  // verify form and save / show errors
180
- return await user.save() ? redirect`/users` : html`<user-edit user=${user} />`
201
+ return await user.save()
202
+ ? redirect`/users`
203
+ : html`<user-edit user="${user}" />`
181
204
  });
182
205
  ```
183
206
 
@@ -185,7 +208,7 @@ router.post("/user/edit/_id", request => {
185
208
 
186
209
  Handlers are tagged template functions usually associated with data.
187
210
 
188
- ### ``html`<component-name attribute=${value} />` ``
211
+ ### ``html`<component-name attribute="${value}" />` ``
189
212
 
190
213
  Compiles and serves a component from the `components` directory and with the
191
214
  specified attributes and their values. Returns an HTTP 200 response with the
@@ -202,8 +225,8 @@ Redirects to `url`. Returns an HTTP 302 response.
202
225
 
203
226
  ## Components
204
227
 
205
- Create HTML components in the `components` directory. Use `data`-attributes to
206
- show data within your component.
228
+ Create HTML components in the `components` directory. Use attributes to expose
229
+ passed data within your component.
207
230
 
208
231
  ```js
209
232
  // in routes/user.js
@@ -219,14 +242,16 @@ router.map("/user/edit/_id", request => {
219
242
 
220
243
  router.get("/user/edit/_id", request => {
221
244
  // show user edit form
222
- return html`<user-edit user=${request.user} />`
245
+ return html`<user-edit user="${request.user}" />`
223
246
  });
224
247
 
225
248
  router.post("/user/edit/_id", request => {
226
249
  const {user, payload} = request;
227
250
  // verify form and save / show errors
228
251
  // this assumes `user` has a method `save` to verify data
229
- return await user.save(payload) ? redirect`/users` : html`<user-edit user=${user} />`
252
+ return await user.save(payload)
253
+ ? redirect`/users`
254
+ : html`<user-edit user="${user}" />`
230
255
  });
231
256
  ```
232
257
 
@@ -235,28 +260,28 @@ router.post("/user/edit/_id", request => {
235
260
  <form method="post">
236
261
  <h1>Edit user</h1>
237
262
  <p>
238
- <input name="user.name" data-value="user.name"></textarea>
263
+ <input name="name" value="${user.name}"></textarea>
239
264
  </p>
240
265
  <p>
241
- <input name="user.email" data-value="user.email"></textarea>
266
+ <input name="email" value="${user.email}"></textarea>
242
267
  </p>
243
268
  <input type="submit" value="Save user" />
244
269
  </form>
245
270
  ```
246
271
 
247
- ### Grouping objects with `data-for`
272
+ ### Grouping objects with `for`
248
273
 
249
- You can use the special attribute `data-for` to group objects.
274
+ You can use the special attribute `for` to group objects.
250
275
 
251
276
  ```html
252
277
  <!-- components/edit-user.html -->
253
- <form data-for="user" method="post">
278
+ <form for="${user}" method="post">
254
279
  <h1>Edit user</h1>
255
280
  <p>
256
- <input name="name" data-value="name" />
281
+ <input name="name" value="${name}" />
257
282
  </p>
258
283
  <p>
259
- <input name="email" data-value="email" />
284
+ <input name="email" value="${email}" />
260
285
  </p>
261
286
  <input type="submit" value="Save user" />
262
287
  </form>
@@ -264,7 +289,7 @@ You can use the special attribute `data-for` to group objects.
264
289
 
265
290
  ### Expanding arrays
266
291
 
267
- `data-for` can also be used to expand arrays.
292
+ `for` can also be used to expand arrays.
268
293
 
269
294
  ```js
270
295
  // in routes/user.js
@@ -275,15 +300,15 @@ router.get("/users", request => {
275
300
  {"name": "Donald", "email": "donald@was.here"},
276
301
  {"name": "Ryan", "email": "ryan@was.here"},
277
302
  ];
278
- return html`<user-index users=${users} />`;
303
+ return html`<user-index users="${users}" />`;
279
304
  });
280
305
  ```
281
306
 
282
307
  ```html
283
308
  <!-- in components/user-index.html -->
284
- <div data-for="users">
285
- User <span data-value="name"></span>
286
- Email <span data-value="email"></span>
309
+ <div for="${users}">
310
+ User <span value="${name}"></span>
311
+ Email <span value="${email}"></span>
287
312
  </div>
288
313
  ```
289
314
 
package/exports.js ADDED
@@ -0,0 +1,28 @@
1
+ import conf from "./source/conf.js";
2
+ import App from "./source/App.js";
3
+
4
+ export {App};
5
+ export {default as Bundler} from "./source/Bundler.js";
6
+ export {default as EagerPromise, eager} from "./source/EagerPromise.js" ;
7
+
8
+ export {default as Domain} from "./source/domain/Domain.js";
9
+ export {default as Storeable} from "./source/types/Storeable.js";
10
+
11
+ export * from "./source/errors.js";
12
+ export * from "./source/invariants.js";
13
+
14
+ export {default as MemoryStore} from "./source/store/Memory.js";
15
+ export {default as Store} from "./source/store/Store.js";
16
+
17
+ export {default as extend_object} from "./source/extend_object.js";
18
+ export {default as sanitize} from "./source/sanitize.js";
19
+
20
+ export {default as html} from "./source/handlers/html.js";
21
+ export {default as json} from "./source/handlers/json.js";
22
+ export {default as redirect} from "./source/handlers/redirect.js";
23
+
24
+ export {default as router} from "./source/Router.js";
25
+
26
+ const app = new App(conf());
27
+
28
+ export {app};
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "author": "Primate core team <core@primatejs.com>",
5
5
  "homepage": "https://primatejs.com",
6
6
  "bugs": "https://github.com/primatejs/primate/issues",
7
7
  "repository": "https://github.com/primatejs/primate",
8
- "description": "Full-stack JavaScript framework",
8
+ "description": "Cross-runtime JavaScript framework",
9
9
  "license": "BSD-3-Clause",
10
10
  "scripts": {
11
11
  "copy": "rm -rf output && mkdir output && cp source/* output -a",
@@ -15,16 +15,16 @@
15
15
  "test": "npm run copy && npm run debris"
16
16
  },
17
17
  "dependencies": {
18
- "runtime-compat": "^0.1.0"
18
+ "runtime-compat": "^0.1.5"
19
19
  },
20
20
  "devDependencies": {
21
21
  "debris": "^0.2.2",
22
- "eslint": "^8.13.0",
22
+ "eslint": "^8.14.0",
23
23
  "eslint-plugin-json": "^3.1.0",
24
24
  "nyc": "^15.1.0"
25
25
  },
26
26
  "type": "module",
27
- "exports": "./source/exports.js",
27
+ "exports": "./exports.js",
28
28
  "engines": {
29
29
  "node": ">=17.9.0"
30
30
  }
package/source/App.js CHANGED
@@ -13,7 +13,7 @@ export default class App {
13
13
  log.reset("Primate").yellow(package_json.version);
14
14
  const routes = await File.list(this.conf.paths.routes);
15
15
  for (const route of routes) {
16
- await import(`${this.conf.paths.routes}/${route}`);
16
+ await import(`file://${this.conf.paths.routes}/${route}`);
17
17
  }
18
18
  await new Bundler(this.conf).bundle();
19
19
 
package/source/Bundler.js CHANGED
@@ -12,25 +12,16 @@ export default class Bundler {
12
12
  this.scripts = [];
13
13
  }
14
14
 
15
- async copy_with_preset(subdirectory, to) {
16
- const {paths} = this.conf;
17
-
18
- // copy files preset files first
19
- await File.copy(`${preset}/${subdirectory}`, to);
20
-
21
- // copy any user code over it, not recreating the folder
22
- try {
23
- await File.copy(paths[subdirectory], to);
24
- } catch(error) {
25
- // directory doesn't exist
26
- }
27
- }
28
-
29
15
  async bundle() {
30
16
  const {paths} = this.conf;
31
17
 
18
+ // remove public directory in case exists
19
+ await File.remove(paths.public);
20
+ // create public directory
21
+ await File.create(paths.public);
22
+
32
23
  // copy static files to public
33
- await this.copy_with_preset("static", paths.public);
24
+ await File.copy(paths.static, paths.public);
34
25
 
35
26
  // read index.html from public, then remove it (we serve it dynamically)
36
27
  await File.remove(`${paths.public}/${this.index}`);
package/source/Router.js CHANGED
@@ -6,8 +6,8 @@ const dealias = path => aliases.reduce((dealiased, {key, value}) =>
6
6
  dealiased.replace(key, () => value), path);
7
7
  const push = (type, path, handler) =>
8
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;
9
+ const find = (type, path, fallback = {"handler": r => r}) => routes.find(route =>
10
+ route.type === type && route.path.test(path)) ?? fallback;
11
11
 
12
12
  export default {
13
13
  "map": (path, callback) => push("map", path, callback),
@@ -19,10 +19,13 @@ export default {
19
19
  const url = new URL(`https://primatejs.com${original_request.pathname}`);
20
20
  const {pathname, searchParams} = url;
21
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));
22
+ const verb = find(method, pathname, () => ({"handler": http404``}));
23
+ const path = pathname.split("/").filter(path => path !== "");
24
+ Object.entries(verb.path.exec(pathname)?.groups ?? [])
25
+ .filter(([key]) => path[key] === undefined)
26
+ .forEach(([key, value]) => Object.defineProperty(path, key, {value}));
27
+
28
+ const request = {...original_request, pathname, params, path};
29
+ return verb.handler(await find("map", pathname).handler(request));
27
30
  },
28
31
  };
package/source/Server.js CHANGED
@@ -30,12 +30,12 @@ export default class Server {
30
30
  response.setHeader("Set-Cookie", `${cookie}; SameSite=${same_site}`);
31
31
  }
32
32
  response.session = session;
33
- const body = await request.body;
34
- const payload = Object.fromEntries(decodeURI(body).replaceAll("+", " ")
33
+ const text = await request.text();
34
+ const payload = Object.fromEntries(decodeURI(text).replaceAll("+", " ")
35
35
  .split("&")
36
36
  .map(part => part.split("="))
37
37
  .filter(([, value]) => value !== ""));
38
- const {pathname, search} = new URL(`https://1${request.url}`);
38
+ const {pathname, search} = request.url;
39
39
  return this.try(pathname + search, request, response, payload);
40
40
  });
41
41
  }
@@ -2,6 +2,7 @@ import Parser from "./Parser.js";
2
2
 
3
3
  const replacement_regex = /^\$([0-9]*)$/;
4
4
  const data_regex = /\${([^}]*)}/g;
5
+ const attributes_regex = /([-a-z]*="[^"]+")/g;
5
6
  const replace = (attribute, source) => {
6
7
  if (attribute.includes(".")) {
7
8
  const index = attribute.indexOf(".");
@@ -18,16 +19,17 @@ const fulfill = (attribute, source) => {
18
19
  if (source === undefined) {
19
20
  return undefined;
20
21
  }
21
- let value;
22
+ let value = attribute;
22
23
  const matches = [...attribute.matchAll(data_regex)];
23
24
  if (matches.length > 0) {
24
25
  for (const match of matches) {
25
26
  const [key] = match;
26
27
  const new_value = replace(match[1], source);
28
+ if (attribute === key) {
29
+ return new_value;
30
+ }
27
31
  value = attribute.replace(key, new_value);
28
32
  }
29
- } else {
30
- value = replace(attribute, source);
31
33
  }
32
34
  return value;
33
35
  };
@@ -43,7 +45,8 @@ export default class Node {
43
45
  this.#data = data;
44
46
  this.attributes = {};
45
47
  if (content !== undefined) {
46
- const [tag_name, ...attributes] = content.split(" ");
48
+ const [tag_name] = content.split(" ");
49
+ const attributes = content.match(attributes_regex) ?? [];
47
50
  this.tag_name = tag_name;
48
51
  for (const attribute of attributes
49
52
  .map(a => a.replaceAll("\"", ""))
@@ -64,11 +67,7 @@ export default class Node {
64
67
  }
65
68
 
66
69
  get data() {
67
- if (this.#data !== undefined) {
68
- return this.#data;
69
- } else {
70
- return this.parent?.data;
71
- }
70
+ return this.#data ?? this.parent?.data;
72
71
  }
73
72
 
74
73
  set data(value) {
@@ -119,13 +118,13 @@ export default class Node {
119
118
  return tag;
120
119
  }
121
120
 
122
- compose(components) {
121
+ async compose(components) {
123
122
  if (components[this.tag_name]) {
124
- return Parser.parse(components[this.tag_name], this.attributes);
125
- }
126
- for (let i = 0; i < this.children.length; i++) {
127
- this.children[i] = this.children[i].compose(components);
123
+ return Parser.parse(components[this.tag_name], this.attributes)
124
+ .compose(components);
128
125
  }
126
+ this.children = await Promise.all(this.children.map(child =>
127
+ child.compose(components)));
129
128
  return this;
130
129
  }
131
130
 
@@ -136,15 +135,15 @@ export default class Node {
136
135
  cloned.attributes[attribute] = this.attributes[attribute];
137
136
  }
138
137
  for (const child of this.children) {
139
- child.clone(cloned);
138
+ child.clone(cloned, child.data !== this.data ? child.data : undefined);
140
139
  }
141
140
  }
142
141
 
143
142
  async expand() {
144
- if (this.attributes["data-for"] !== undefined) {
145
- const key = this.attributes["data-for"];
146
- delete this.attributes["data-for"];
147
- const value = await this.data[key];
143
+ if (this.attributes["for"] !== undefined) {
144
+ const key = this.attributes["for"];
145
+ delete this.attributes["for"];
146
+ const value = await fulfill(key, this.data);
148
147
  const arr = Array.isArray(value) ? value : [value];
149
148
  const newparent = new Node();
150
149
  for (const val of arr) {
@@ -154,21 +153,18 @@ export default class Node {
154
153
  return newparent.expand();
155
154
  }
156
155
  for (const attribute in this.attributes) {
157
- if (attribute.startsWith("data-")) {
158
- const fulfilled = fulfill(this.attributes[attribute], this.data);
159
- switch(attribute) {
160
- case "data-value":
161
- if (this.tag_name === "input") {
162
- this.attributes.value = fulfilled;
163
- } else {
164
- this.text = fulfilled;
165
- }
166
- break;
167
- default:
168
- this.attributes[attribute.slice(5)] = fulfilled;
169
- break;
170
- }
171
- delete this.attributes[attribute];
156
+ const fulfilled = fulfill(this.attributes[attribute], this.data);
157
+ switch(attribute) {
158
+ case "value":
159
+ if (this.tag_name === "input") {
160
+ this.attributes.value = fulfilled;
161
+ } else {
162
+ this.text = fulfilled;
163
+ }
164
+ break;
165
+ default:
166
+ this.attributes[attribute] = fulfilled;
167
+ break;
172
168
  }
173
169
  }
174
170
  for (let i = 0; i < this.children.length; i++) {
@@ -177,7 +173,7 @@ export default class Node {
177
173
  return this;
178
174
  }
179
175
 
180
- unfold(components) {
181
- return this.compose(components).expand();
176
+ async unfold(components) {
177
+ return (await this.compose(components)).expand();
182
178
  }
183
179
  }
@@ -16,11 +16,11 @@ if (await File.exists(path)) {
16
16
 
17
17
  export default async (strings, ...keys) => {
18
18
  const awaited_keys = await Promise.all(keys);
19
- const rendered = await (await Parser.parse(strings
19
+ const rendered = await (await (await Parser.parse(strings
20
20
  .slice(0, last)
21
21
  .map((string, i) => `${string}$${i}`)
22
22
  .join("") + strings[strings.length+last], awaited_keys)
23
- .unfold(components))
23
+ .unfold(components)))
24
24
  .render();
25
25
  const body = (await index(conf)).replace("<body>", () => `<body>${rendered}`);
26
26
  const code = 200;
package/debris.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "suites": "test/suites",
3
- "fixtures": "test/fixtures"
4
- }
package/jsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "baseUrl": ".",
4
- "target": "esnext",
5
- "module": "esnext"
6
- },
7
- "exclude": ["node_modules"]
8
- }
package/source/exports.js DELETED
@@ -1,28 +0,0 @@
1
- import conf from "./conf.js";
2
- import App from "./App.js";
3
-
4
- export {App};
5
- export {default as Bundler} from "./Bundler.js";
6
- export {default as EagerPromise, eager} from "./EagerPromise.js" ;
7
-
8
- export {default as Domain} from "./domain/Domain.js";
9
- export {default as Storeable} from "./types/Storeable.js";
10
-
11
- export * from "./errors.js";
12
- export * from "./invariants.js";
13
-
14
- export {default as MemoryStore} from "./store/Memory.js";
15
- export {default as Store} from "./store/Store.js";
16
-
17
- export {default as extend_object} from "./extend_object.js";
18
- export {default as sanitize} from "./sanitize.js";
19
-
20
- export {default as html} from "./handlers/html.js";
21
- export {default as json} from "./handlers/json.js";
22
- export {default as redirect} from "./handlers/redirect.js";
23
-
24
- export {default as router} from "./Router.js";
25
-
26
- const app = new App(conf());
27
-
28
- export {app};
@@ -1,8 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <title>Primate app</title>
5
- <meta charset="utf-8" />
6
- </head>
7
- <body></body>
8
- </html>