primate 0.13.0 → 0.13.2

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
@@ -190,32 +190,29 @@ export default router => {
190
190
  ### Sharing logic across requests
191
191
 
192
192
  ```js
193
- import html from "@primate/html";
194
- import redirect from "@primate/redirect";
195
-
196
193
  export default router => {
197
- // declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
194
+ // Declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
198
195
  router.alias("edit-user", "/user/edit/([0-9])+");
199
196
 
200
- // pass user instead of request to all verbs with this route
201
- router.map("edit-user", () => ({name: "Donald"}));
197
+ // Pass user instead of request to all verbs on this route
198
+ router.map("edit-user", ({body}) => body?.name ?? "Donald");
202
199
 
203
- // show user edit form
204
- router.get("edit-user", user => html("user-edit", {user}));
200
+ // Show user as plain text
201
+ router.get("edit-user", user => user);
205
202
 
206
- // verify form and save, or show errors
207
- router.post("edit-user", async user => await user.save()
208
- ? redirect("/users")
209
- : html`<user-edit user="${user}" />`);
203
+ // Verify or show error
204
+ router.post("edit-user", user => user === "Donald"
205
+ ? "Hi Donald!"
206
+ : {message: "Error saving user"});
210
207
  };
211
208
 
212
209
  ```
213
210
 
214
211
  ### Explicit handlers
215
212
 
216
- Most often we can figure the content type to respond with based on the return
217
- type from the handler. To handle content not automatically detected, use the
218
- second argument of the exported function.
213
+ Most often we can figure out the content type to respond with based on the
214
+ return type from the handler. To handle content not automatically detected, use
215
+ the second argument of the exported function.
219
216
 
220
217
  ```js
221
218
  export default (router, {redirect}) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Expressive, minimal and extensible framework for JavaScript",
5
5
  "homepage": "https://primatejs.com",
6
6
  "bugs": "https://github.com/primatejs/primate/issues",
@@ -1,18 +1,15 @@
1
- import html from "@primate/html";
2
- import redirect from "@primate/redirect";
3
-
4
1
  export default router => {
5
- // declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
2
+ // Declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
6
3
  router.alias("edit-user", "/user/edit/([0-9])+");
7
4
 
8
- // pass user instead of request to all verbs with this route
9
- router.map("edit-user", () => ({name: "Donald"}));
5
+ // Pass user instead of request to all verbs on this route
6
+ router.map("edit-user", ({body}) => body?.name ?? "Donald");
10
7
 
11
- // show user edit form
12
- router.get("edit-user", user => html("user-edit", {user}));
8
+ // Show user as plain text
9
+ router.get("edit-user", user => user);
13
10
 
14
- // verify form and save, or show errors
15
- router.post("edit-user", async user => await user.save()
16
- ? redirect("/users")
17
- : html`<user-edit user="${user}" />`);
11
+ // Verify or show error
12
+ router.post("edit-user", user => user === "Donald"
13
+ ? "Hi Donald!"
14
+ : {message: "Error saving user"});
18
15
  };
@@ -117,9 +117,9 @@ to the content type sent along the request. Currently supported are
117
117
 
118
118
  ### Explicit handlers
119
119
 
120
- Most often we can figure the content type to respond with based on the return
121
- type from the handler. To handle content not automatically detected, use the
122
- second argument of the exported function.
120
+ Most often we can figure out the content type to respond with based on the
121
+ return type from the handler. To handle content not automatically detected, use
122
+ the second argument of the exported function.
123
123
 
124
124
  ```js
125
125
  // routing/explicit-handlers.js
package/src/bundle.js CHANGED
@@ -16,6 +16,8 @@ const makePublic = async env => {
16
16
  }
17
17
  };
18
18
 
19
- export default async env => await makePublic(env) &&
19
+ export default async env => {
20
+ await makePublic(env);
20
21
  [...filter("bundle", env.modules), _ => _].reduceRight((acc, handler) =>
21
22
  input => handler(input, acc))(env);
23
+ };
package/src/config.js CHANGED
@@ -1,5 +1,6 @@
1
- import {File, Path} from "runtime-compat/fs";
1
+ import crypto from "runtime-compat/crypto";
2
2
  import {is} from "runtime-compat/dyndef";
3
+ import {File, Path} from "runtime-compat/fs";
3
4
  import cache from "./cache.js";
4
5
  import extend from "./extend.js";
5
6
  import defaults from "./primate.config.js";
@@ -45,11 +46,19 @@ const index = async env => {
45
46
  }
46
47
  };
47
48
 
49
+ const hash = async (string, algorithm = "sha-384") => {
50
+ const encoder = new TextEncoder();
51
+ const bytes = await crypto.subtle.digest(algorithm, encoder.encode(string));
52
+ const algo = algorithm.replace("-", () => "");
53
+ return `${algo}-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}`;
54
+ };
55
+
48
56
  export default async (filename = "primate.config.js") => {
49
57
  is(filename).string();
50
58
  const root = await getRoot();
51
59
  const config = await getConfig(root, filename);
52
60
 
61
+ const resources = [];
53
62
  const env = {
54
63
  ...config,
55
64
  paths: qualify(root, config.paths),
@@ -59,7 +68,23 @@ export default async (filename = "primate.config.js") => {
59
68
  env.handlers[name] = handler;
60
69
  },
61
70
  handlers: {...handlers},
62
- render: async html => (await index(env)).replace("%body%", () => html),
71
+ render: async ({body = "", head = ""} = {}) => {
72
+ const html = await index(env);
73
+ const heads = resources.map(({src, code, type, inline, integrity}) => {
74
+ const tag = "script";
75
+ const pre = `<${tag} type="${type}" integrity="${integrity}"`;
76
+ const post = `</${tag}>`;
77
+ return inline ? `${pre}>${code}${post}` : `${pre} src="${src}">${post}`;
78
+ }).join("\n");
79
+ return html
80
+ .replace("%body%", () => body)
81
+ .replace("%head%", () => `${head}${heads}`);
82
+ },
83
+ publish: async ({src, code, type = "", inline = false}) => {
84
+ const integrity = await hash(code);
85
+ resources.push({src, code, type, inline, integrity});
86
+ return integrity;
87
+ },
63
88
  };
64
89
  env.log.info(`${package_json.name} \x1b[34m${package_json.version}\x1b[0m`);
65
90
  const modules = await Promise.all(config.modules.map(module => module(env)));
@@ -68,6 +93,6 @@ export default async (filename = "primate.config.js") => {
68
93
  .filter(module => module.load !== undefined)
69
94
  .map(module => module.load()(env)));
70
95
 
71
- return cache("config", filename, () => ({...env,
96
+ return cache("config", filename, () => ({...env, resources,
72
97
  modules: modules.concat(loads)}));
73
98
  };
@@ -8,9 +8,9 @@ const getContent = async (env, name) => {
8
8
 
9
9
  export default (content, {status = 200, partial = false, adhoc = false} = {}) =>
10
10
  async (env, headers) => {
11
- const html = adhoc ? content : await getContent(env, content);
11
+ const body = adhoc ? content : await getContent(env, content);
12
12
  return [
13
- partial ? html : await env.render(html), {
13
+ partial ? body : await env.render({body}), {
14
14
  status,
15
15
  headers: {...headers, "Content-Type": "text/html"},
16
16
  },
package/src/index.html CHANGED
@@ -3,6 +3,7 @@
3
3
  <head>
4
4
  <title>Primate app</title>
5
5
  <meta charset="utf-8" />
6
+ %head%
6
7
  </head>
7
8
  <body>%body%</body>
8
9
  </html>
package/src/publish.js ADDED
@@ -0,0 +1,5 @@
1
+ const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
2
+
3
+ export default async env =>
4
+ [...filter("publish", env.modules), _ => _].reduceRight((acc, handler) =>
5
+ input => handler(input, acc))(env);
package/src/run.js CHANGED
@@ -1,14 +1,21 @@
1
1
  import config from "./config.js";
2
2
  import register from "./register.js";
3
3
  import compile from "./compile.js";
4
+ import publish from "./publish.js";
4
5
  import bundle from "./bundle.js";
5
6
  import route from "./route.js";
6
7
  import serve from "./serve.js";
7
8
 
8
9
  export default async () => {
9
10
  const env = await config();
11
+ // register handlers
10
12
  await register(env);
13
+ // compile server-side code
11
14
  await compile(env);
15
+ // publish client-side code
16
+ await publish(env);
17
+ // bundle client-side code
12
18
  await bundle(env);
19
+ // serve
13
20
  serve({router: await route(env.paths.routes, env.handlers), ...env});
14
21
  };
package/src/serve.js CHANGED
@@ -22,8 +22,19 @@ export default env => {
22
22
  const _respond = async request => {
23
23
  const csp = Object.keys(env.http.csp).reduce((policy_string, key) =>
24
24
  `${policy_string}${key} ${env.http.csp[key]};`, "");
25
+ const scripts = env.resources
26
+ .map(resource => `'${resource.integrity}'`).join(" ");
27
+ const _csp = scripts === "" ? csp : `${csp}script-src 'self' ${scripts};`;
28
+ // remove inline resources
29
+ for (let i = env.resources.length - 1; i >= 0; i--) {
30
+ const resource = env.resources[i];
31
+ if (resource.inline) {
32
+ env.resources.splice(i, 1);
33
+ }
34
+ }
35
+
25
36
  const headers = {
26
- "Content-Security-Policy": csp,
37
+ "Content-Security-Policy": _csp,
27
38
  "Referrer-Policy": "same-origin",
28
39
  };
29
40
 
@@ -53,9 +64,24 @@ export default env => {
53
64
  },
54
65
  });
55
66
 
67
+ const publishedResource = request => {
68
+ const published = env.resources.find(resource =>
69
+ `/${resource.src}` === request.pathname);
70
+ if (published !== undefined) {
71
+ return new Response(published.code, {
72
+ status: statuses.OK,
73
+ headers: {
74
+ "Content-Type": mime(published.src),
75
+ },
76
+ });
77
+ }
78
+
79
+ return route(request);
80
+ };
81
+
56
82
  const _serve = async request => {
57
83
  const path = new Path(env.paths.public, request.pathname);
58
- return await path.isFile ? resource(path.file) : route(request);
84
+ return await path.isFile ? resource(path.file) : publishedResource(request);
59
85
  };
60
86
 
61
87
  const handle = async request => {
@@ -67,11 +93,20 @@ export default env => {
67
93
  }
68
94
  };
69
95
 
70
- const parseContent = (request, body) => {
71
- const type = contents[request.headers.get("content-type")];
96
+ const parseContentType = (contentType, body) => {
97
+ const type = contents[contentType];
72
98
  return type === undefined ? body : type(body);
73
99
  };
74
100
 
101
+ const parseContent = (request, body) => {
102
+ try {
103
+ return parseContentType(request.headers.get("content-type"), body);
104
+ } catch (error) {
105
+ env.log.warn(error);
106
+ return body;
107
+ }
108
+ };
109
+
75
110
  const {http} = env;
76
111
  const modules = filter("serve", env.modules);
77
112