primate 0.7.4 → 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 cross-runtime framework
1
+ # Primate
2
2
 
3
- Primate is a full-stack cross-runtime Javascript framework (Node.js and Deno).
4
- It relieves you of dealing with repetitive, error-prone tasks and lets you
5
- concentrate on writing effective, expressive code.
3
+ A full-stack Javascript framework with data verification and server-side HTML
4
+ rendering.
6
5
 
7
6
  ## Highlights
8
7
 
@@ -34,13 +33,13 @@ Create a route for `/`
34
33
 
35
34
  import {router, html} from "primate";
36
35
 
37
- router.get("/", () => html`<site-index date=${new Date()} />`);
36
+ router.get("/", () => html`<site-index date="${new Date()}" />`);
38
37
  ```
39
38
 
40
39
  Create a component for your route (in `components/site-index.html`)
41
40
 
42
41
  ```html
43
- Today's date is <span data-value="date"></span>.
42
+ Today's date is <span value="${date}"></span>.
44
43
  ```
45
44
 
46
45
  Generate SSL key/certificate
@@ -110,8 +109,6 @@ permissions.
110
109
  * [Routes](#routes)
111
110
  * [Handlers](#handlers)
112
111
  * [Components](#components)
113
- * [Domains](#domains)
114
- * [Verification](#verification)
115
112
 
116
113
  ## Routes
117
114
 
@@ -195,13 +192,15 @@ router.map("/user/edit/_id", request => {
195
192
 
196
193
  router.get("/user/edit/_id", request => {
197
194
  // show user edit form
198
- return html`<user-edit user=${request.user} />`
195
+ return html`<user-edit user="${request.user}" />`
199
196
  });
200
197
 
201
198
  router.post("/user/edit/_id", request => {
202
199
  const {user} = request;
203
200
  // verify form and save / show errors
204
- 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}" />`
205
204
  });
206
205
  ```
207
206
 
@@ -209,7 +208,7 @@ router.post("/user/edit/_id", request => {
209
208
 
210
209
  Handlers are tagged template functions usually associated with data.
211
210
 
212
- ### ``html`<component-name attribute=${value} />` ``
211
+ ### ``html`<component-name attribute="${value}" />` ``
213
212
 
214
213
  Compiles and serves a component from the `components` directory and with the
215
214
  specified attributes and their values. Returns an HTTP 200 response with the
@@ -226,8 +225,8 @@ Redirects to `url`. Returns an HTTP 302 response.
226
225
 
227
226
  ## Components
228
227
 
229
- Create HTML components in the `components` directory. Use `data`-attributes to
230
- show data within your component.
228
+ Create HTML components in the `components` directory. Use attributes to expose
229
+ passed data within your component.
231
230
 
232
231
  ```js
233
232
  // in routes/user.js
@@ -243,14 +242,16 @@ router.map("/user/edit/_id", request => {
243
242
 
244
243
  router.get("/user/edit/_id", request => {
245
244
  // show user edit form
246
- return html`<user-edit user=${request.user} />`
245
+ return html`<user-edit user="${request.user}" />`
247
246
  });
248
247
 
249
248
  router.post("/user/edit/_id", request => {
250
249
  const {user, payload} = request;
251
250
  // verify form and save / show errors
252
251
  // this assumes `user` has a method `save` to verify data
253
- 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}" />`
254
255
  });
255
256
  ```
256
257
 
@@ -259,28 +260,28 @@ router.post("/user/edit/_id", request => {
259
260
  <form method="post">
260
261
  <h1>Edit user</h1>
261
262
  <p>
262
- <input name="user.name" data-value="user.name"></textarea>
263
+ <input name="name" value="${user.name}"></textarea>
263
264
  </p>
264
265
  <p>
265
- <input name="user.email" data-value="user.email"></textarea>
266
+ <input name="email" value="${user.email}"></textarea>
266
267
  </p>
267
268
  <input type="submit" value="Save user" />
268
269
  </form>
269
270
  ```
270
271
 
271
- ### Grouping objects with `data-for`
272
+ ### Grouping objects with `for`
272
273
 
273
- You can use the special attribute `data-for` to group objects.
274
+ You can use the special attribute `for` to group objects.
274
275
 
275
276
  ```html
276
277
  <!-- components/edit-user.html -->
277
- <form data-for="user" method="post">
278
+ <form for="${user}" method="post">
278
279
  <h1>Edit user</h1>
279
280
  <p>
280
- <input name="name" data-value="name" />
281
+ <input name="name" value="${name}" />
281
282
  </p>
282
283
  <p>
283
- <input name="email" data-value="email" />
284
+ <input name="email" value="${email}" />
284
285
  </p>
285
286
  <input type="submit" value="Save user" />
286
287
  </form>
@@ -288,7 +289,7 @@ You can use the special attribute `data-for` to group objects.
288
289
 
289
290
  ### Expanding arrays
290
291
 
291
- `data-for` can also be used to expand arrays.
292
+ `for` can also be used to expand arrays.
292
293
 
293
294
  ```js
294
295
  // in routes/user.js
@@ -299,15 +300,15 @@ router.get("/users", request => {
299
300
  {"name": "Donald", "email": "donald@was.here"},
300
301
  {"name": "Ryan", "email": "ryan@was.here"},
301
302
  ];
302
- return html`<user-index users=${users} />`;
303
+ return html`<user-index users="${users}" />`;
303
304
  });
304
305
  ```
305
306
 
306
307
  ```html
307
308
  <!-- in components/user-index.html -->
308
- <div data-for="users">
309
- User <span data-value="name"></span>
310
- Email <span data-value="email"></span>
309
+ <div for="${users}">
310
+ User <span value="${name}"></span>
311
+ Email <span value="${email}"></span>
311
312
  </div>
312
313
  ```
313
314
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.7.4",
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",
@@ -19,7 +19,7 @@
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
  },
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
  };
@@ -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
- }