primate 0.20.2 → 0.20.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.20.2",
3
+ "version": "0.20.4",
4
4
  "description": "Expressive, minimal and extensible web framework",
5
5
  "homepage": "https://primatejs.com",
6
6
  "bugs": "https://github.com/primatejs/primate/issues",
@@ -18,7 +18,7 @@
18
18
  "directory": "packages/primate"
19
19
  },
20
20
  "dependencies": {
21
- "runtime-compat": "^0.20.2"
21
+ "runtime-compat": "^0.21.0"
22
22
  },
23
23
  "engines": {
24
24
  "node": ">=18.16"
package/src/Logger.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {assert, is} from "runtime-compat/dyndef";
2
2
  import {blue, bold, green, red, yellow, dim} from "runtime-compat/colors";
3
- import {map, valmap} from "runtime-compat/object";
3
+ import {map} from "runtime-compat/object";
4
4
 
5
5
  const levels = {
6
6
  Error: 0,
@@ -17,8 +17,7 @@ const reference = "https://primatejs.com/reference/errors";
17
17
 
18
18
  const hyphenate = classCased => classCased
19
19
  .split("")
20
- .map(character => character
21
- .replace(/[A-Z]/u, capital => `-${capital.toLowerCase()}`))
20
+ .map(letter => letter.replace(/[A-Z]/u, upper => `-${upper.toLowerCase()}`))
22
21
  .join("")
23
22
  .slice(1);
24
23
 
package/src/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import crypto from "runtime-compat/crypto";
2
- import {tryreturn} from "runtime-compat/flow";
2
+ import {tryreturn} from "runtime-compat/async";
3
3
  import {File, Path} from "runtime-compat/fs";
4
4
  import {bold, blue} from "runtime-compat/colors";
5
5
  import {transform, valmap} from "runtime-compat/object";
@@ -17,18 +17,18 @@ const library = import.meta.runtime?.library ?? "node_modules";
17
17
 
18
18
  // use user-provided file or fall back to default
19
19
  const index = (app, name) =>
20
- tryreturn(async _ => File.read(`${app.paths.pages.join(name)}`))
21
- .orelse(async _ => base.join("defaults", app.config.index).text());
20
+ tryreturn(_ => File.read(`${app.paths.pages.join(name)}`))
21
+ .orelse(_ => base.join("defaults", app.config.index).text());
22
22
 
23
+ const encoder = new TextEncoder();
23
24
  const hash = async (string, algorithm = "sha-384") => {
24
- const encoder = new TextEncoder();
25
25
  const bytes = await crypto.subtle.digest(algorithm, encoder.encode(string));
26
26
  const algo = algorithm.replace("-", _ => "");
27
27
  return `${algo}-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}`;
28
28
  };
29
29
 
30
- const attribute = attributes => Object.keys(attributes).length > 0 ?
31
- " ".concat(Object.entries(attributes)
30
+ const attribute = attributes => Object.keys(attributes).length > 0
31
+ ? " ".concat(Object.entries(attributes)
32
32
  .map(([key, value]) => `${key}="${value}"`).join(" "))
33
33
  : "";
34
34
  const tag = ({name, attributes = {}, code = "", close = true}) =>
@@ -75,22 +75,25 @@ export default async (config, root, log) => {
75
75
  },
76
76
  headers: _ => {
77
77
  const csp = Object.keys(http.csp).reduce((policy_string, key) =>
78
- `${policy_string}${key} ${http.csp[key]};`, "");
79
- const scripts = app.assets
80
- .filter(({type}) => type !== "style")
81
- .map(asset => `'${asset.integrity}'`).join(" ");
82
- const _csp = scripts === "" ? csp : `${csp}script-src 'self' ${scripts};`;
83
- // remove inline assets
84
- app.assets = app.assets.filter(({inline, type}) => !inline
85
- || type === "importmap");
78
+ `${policy_string}${key} ${http.csp[key]};`, "")
79
+ .replace("script-src 'self'", `script-src 'self' ${
80
+ app.assets
81
+ .filter(({type}) => type !== "style")
82
+ .map(asset => `'${asset.integrity}'`).join(" ")
83
+ } `)
84
+ .replace("style-src 'self'", `style-src 'self' ${
85
+ app.assets
86
+ .filter(({type}) => type === "style")
87
+ .map(asset => `'${asset.integrity}'`).join(" ")
88
+ } `);
86
89
 
87
90
  return {
88
- "Content-Security-Policy": _csp,
91
+ "Content-Security-Policy": csp,
89
92
  "Referrer-Policy": "same-origin",
90
93
  };
91
94
  },
92
95
  handlers: {...handlers},
93
- render: async ({body = "", head = "", page} = {}) => {
96
+ render: async ({body = "", page} = {}) => {
94
97
  const html = await index(app, page ?? config.index);
95
98
  // inline: <script type integrity>...</script>
96
99
  // outline: <script type integrity src></script>
@@ -102,16 +105,17 @@ export default async (config, root, log) => {
102
105
  const style = ({inline, code, href, rel = "stylesheet"}) => inline
103
106
  ? tag({name: "style", code})
104
107
  : tag({name: "link", attributes: {rel, href}, close: false});
105
- const heads = toSorted(app.assets,
108
+ const head = toSorted(app.assets,
106
109
  ({type}) => -1 * (type === "importmap"))
107
110
  .map(({src, code, type, inline, integrity}) =>
108
111
  type === "style"
109
112
  ? style({inline, code, href: src})
110
113
  : script({inline, code, type, integrity, src})
111
114
  ).join("\n");
112
- return html
113
- .replace("%body%", _ => body)
114
- .replace("%head%", _ => `${head}${heads}`);
115
+ // remove inline assets
116
+ app.assets = app.assets.filter(({inline, type}) => !inline
117
+ || type === "importmap");
118
+ return html.replace("%body%", _ => body).replace("%head%", _ => head);
115
119
  },
116
120
  publish: async ({src, code, type = "", inline = false}) => {
117
121
  if (!inline) {
@@ -119,10 +123,10 @@ export default async (config, root, log) => {
119
123
  await base.directory.file.create();
120
124
  await base.file.write(code);
121
125
  }
122
- const integrity = await hash(code);
123
- const _src = new Path(http.static.root).join(src ?? "");
124
- app.assets.push({src: `${_src}`, code: inline ? code : "", type, inline, integrity});
125
- return integrity;
126
+ if (inline || type === "style") {
127
+ app.assets.push({src: new Path(http.static.root).join(src ?? "").path,
128
+ code: inline ? code : "", type, inline, integrity: await hash(code)});
129
+ }
126
130
  },
127
131
  bootstrap: ({type, code}) => {
128
132
  app.entrypoints.push({type, code});
@@ -135,16 +139,14 @@ export default async (config, root, log) => {
135
139
  const exports = pkg.exports === undefined
136
140
  ? {[module]: `/${module}/${pkg.main}`}
137
141
  : transform(pkg.exports, entry => entry
138
- .filter(([, _export]) => _export.import !== undefined)
142
+ .filter(([, _export]) => _export.import !== undefined || _export.default !== undefined)
139
143
  .map(([key, value]) => [
140
144
  key.replace(".", module),
141
- value.import.replace(".", `./${module}`),
145
+ value.import?.replace(".", `./${module}`) ?? value.default.replace(".", `./${module}`),
142
146
  ]));
143
- await Promise.all(Object.values(exports).map(async name => app.publish({
144
- code: await Path.resolve().join(library, name).text(),
145
- src: new Path(root, build.modules, name),
146
- type: "module",
147
- })));
147
+ const dependency = Path.resolve().join(...path);
148
+ const to = new Path(paths.client, build.modules, dependency.name);
149
+ await dependency.file.copy(to);
148
150
  this.importmaps = {
149
151
  ...valmap(exports, value => new Path(root, build.modules, value).path),
150
152
  ...this.importmaps};
@@ -4,6 +4,7 @@ export default {
4
4
  base: "/",
5
5
  logger: {
6
6
  level: Logger.Warn,
7
+ trace: false,
7
8
  },
8
9
  http: {
9
10
  host: "localhost",
@@ -11,6 +12,7 @@ export default {
11
12
  csp: {
12
13
  "default-src": "'self'",
13
14
  "style-src": "'self'",
15
+ "script-src": "'self'",
14
16
  "object-src": "'none'",
15
17
  "frame-ancestors": "'none'",
16
18
  "form-action": "'self'",
@@ -23,12 +25,11 @@ export default {
23
25
  index: "app.html",
24
26
  paths: {
25
27
  build: "build",
26
- static: "static",
27
28
  components: "components",
29
+ pages: "pages",
28
30
  routes: "routes",
31
+ static: "static",
29
32
  types: "types",
30
- pages: "pages",
31
- layouts: "layouts",
32
33
  },
33
34
  build: {
34
35
  includes: [],
package/src/dispatch.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import {is, maybe} from "runtime-compat/dyndef";
2
- import {tryreturn} from "runtime-compat/flow";
2
+ import {tryreturn} from "runtime-compat/sync";
3
3
  import {map} from "runtime-compat/object";
4
4
  import errors from "./errors.js";
5
5
 
@@ -1,19 +1,11 @@
1
1
  const script = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
2
2
  const style = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
3
3
 
4
- const integrate = async (html, publish, headers) => {
5
- const scripts = await Promise.all([...html.matchAll(script)]
4
+ const integrate = async (html, publish) => {
5
+ await Promise.all([...html.matchAll(script)]
6
6
  .map(({groups: {code}}) => publish({code, inline: true})));
7
- for (const integrity of scripts) {
8
- headers["Content-Security-Policy"] = headers["Content-Security-Policy"]
9
- .replace("script-src 'self' ", `script-src 'self' '${integrity}' `);
10
- }
11
- const styles = await Promise.all([...html.matchAll(style)]
7
+ await Promise.all([...html.matchAll(style)]
12
8
  .map(({groups: {code}}) => publish({code, type: "style", inline: true})));
13
- for (const integrity of styles) {
14
- headers["Content-Security-Policy"] = headers["Content-Security-Policy"]
15
- .replace("style-src 'self'", `style-src 'self' '${integrity}' `);
16
- }
17
9
  return html.replaceAll(/<(?<tag>script|style)>.*?<\/\k<tag>>/gus, _ => "");
18
10
  };
19
11
 
@@ -21,10 +13,11 @@ export default (component, options = {}) => {
21
13
  const {status = 200, partial = false, load = false} = options;
22
14
 
23
15
  return async app => {
24
- const headers = app.headers();
25
16
  const body = await integrate(await load ?
26
17
  await app.paths.components.join(component).text() : component,
27
- app.publish, headers);
18
+ app.publish);
19
+ // needs to happen before app.render()
20
+ const headers = app.headers();
28
21
 
29
22
  return [partial ? body : await app.render({body}), {
30
23
  status,
@@ -1,8 +1,7 @@
1
- import copy_includes from "./copy_includes.js"
1
+ import copy_includes from "./copy_includes.js";
2
2
 
3
3
  const pre = async app => {
4
- const {paths, config} = app;
5
- const build = config.build;
4
+ const {paths, config: {build}} = app;
6
5
 
7
6
  // remove build directory in case exists
8
7
  if (await paths.build.exists) {
@@ -1,5 +1,5 @@
1
1
  import {Response, Status} from "runtime-compat/http";
2
- import {tryreturn} from "runtime-compat/flow";
2
+ import {tryreturn} from "runtime-compat/async";
3
3
  import {mime, isResponse, respond} from "./respond/exports.js";
4
4
  import {invalid} from "./route.js";
5
5
  import {error as clientError} from "../handlers/exports.js";
@@ -8,7 +8,7 @@ import errors from "../errors.js";
8
8
  const guardError = Symbol("guardError");
9
9
 
10
10
  export default app => {
11
- const {config: {http, build}, paths} = app;
11
+ const {config: {http: {static: {root}}, build}, paths} = app;
12
12
 
13
13
  const run = async request => {
14
14
  const {pathname} = request.url;
@@ -18,10 +18,10 @@ export default app => {
18
18
 
19
19
  // handle guards
20
20
  try {
21
- guards.map(guard => {
21
+ guards.every(guard => {
22
22
  const result = guard(request);
23
23
  if (result === true) {
24
- return undefined;
24
+ return true;
25
25
  }
26
26
  const error = new Error();
27
27
  error.result = result;
@@ -38,7 +38,7 @@ export default app => {
38
38
 
39
39
  // handle request
40
40
  const handlers = [...app.modules.route, handler]
41
- .reduceRight((chain, next) => input => next(input, chain));
41
+ .reduceRight((next, last) => input => last(input, next));
42
42
 
43
43
  return (await respond(await handlers({...request, path})))(app, {
44
44
  layouts: await Promise.all(layouts.map(layout => layout(request))),
@@ -64,7 +64,6 @@ export default app => {
64
64
 
65
65
  const handle = async request => {
66
66
  const {pathname} = request.url;
67
- const {root} = http.static;
68
67
  if (pathname.startsWith(root)) {
69
68
  const debased = pathname.replace(root, _ => "");
70
69
  // try static first
@@ -79,5 +78,5 @@ export default app => {
79
78
  };
80
79
 
81
80
  return [...app.modules.handle, handle]
82
- .reduceRight((acc, handler) => input => handler(input, acc));
81
+ .reduceRight((next, last) => input => last(input, next));
83
82
  };
@@ -1,5 +1,6 @@
1
1
  import {URL} from "runtime-compat/http";
2
- import {tryreturn} from "runtime-compat/flow";
2
+ import {tryreturn} from "runtime-compat/sync";
3
+ import {stringify} from "runtime-compat/streams";
3
4
  import errors from "../errors.js";
4
5
 
5
6
  const {fromEntries: from} = Object;
@@ -10,47 +11,28 @@ const contents = {
10
11
  .map(subpart => decodeURIComponent(subpart).replaceAll("+", " ")))),
11
12
  "application/json": body => JSON.parse(body),
12
13
  };
13
- const decoder = new TextDecoder();
14
14
 
15
- export default dispatch => async request => {
16
- const parseContentType = (contentType, body) => {
17
- const type = contents[contentType];
18
- return type === undefined ? body : type(body);
19
- };
20
-
21
- const parseContent = async (contentType, body) =>
22
- tryreturn(_ => parseContentType(contentType, body))
23
- .orelse(_ => errors.CannotParseBody.throw(body, contentType));
24
-
25
- const parseBody = async request => {
26
- if (request.body === null) {
27
- return null;
28
- }
29
- const reader = request.body.getReader();
30
- const chunks = [];
31
- let result;
32
- do {
33
- result = await reader.read();
34
- if (result.value !== undefined) {
35
- chunks.push(decoder.decode(result.value));
36
- }
37
- } while (!result.done);
38
-
39
- return parseContent(request.headers.get("content-type"), chunks.join());
40
- };
15
+ const parse = {
16
+ content(content_type, body) {
17
+ return tryreturn(_ => {
18
+ const type = contents[content_type];
19
+ return type === undefined ? body : type(body);
20
+ }).orelse(_ => errors.CannotParseBody.throw(body, content_type));
21
+ },
22
+ async body({body, headers}) {
23
+ return body === null
24
+ ? null
25
+ : this.content(headers.get("content-type"), await stringify(body));
26
+ },
27
+ };
41
28
 
42
- const cookies = request.headers.get("cookie");
29
+ export default dispatch => async request => {
30
+ const body = dispatch(await parse.body(request));
31
+ const cookies = dispatch(from(request.headers.get("cookie")?.split(";")
32
+ .map(cookie => cookie.trim().split("=")) ?? []));
33
+ const headers = dispatch(from(request.headers));
43
34
  const url = new URL(request.url);
35
+ const query = dispatch(from(url.searchParams));
44
36
 
45
- const body = await parseBody(request);
46
- return {
47
- original: request,
48
- url,
49
- body: dispatch(body),
50
- cookies: dispatch(cookies === null
51
- ? {}
52
- : from(cookies.split(";").map(c => c.trim().split("=")))),
53
- headers: dispatch(from(request.headers)),
54
- query: dispatch(from(url.searchParams)),
55
- };
37
+ return {original: request, url, body, cookies, headers, query};
56
38
  };
@@ -22,6 +22,7 @@ const post = async app => {
22
22
  });
23
23
  }
24
24
 
25
+ // copy JavaScript and CSS files from `app.paths.static`
25
26
  const imports = await Path.collect(app.paths.static, /\.(?:js|css)$/u);
26
27
  await Promise.all(imports.map(async file => {
27
28
  const code = await file.text();
@@ -40,9 +41,8 @@ const post = async app => {
40
41
  // copy additional subdirectories to build/client
41
42
  await copy_includes(app, "client", async to =>
42
43
  Promise.all((await to.collect(/\.js$/u)).map(async script => {
43
- const code = await script.text();
44
44
  const src = new Path(root, script.path.replace(source, () => ""));
45
- await app.publish({src, code, type: "module"});
45
+ await app.publish({src, code: await script.text(), type: "module"});
46
46
  }))
47
47
  );
48
48
  };
@@ -1,5 +1,5 @@
1
1
  import {keymap} from "runtime-compat/object";
2
- import {tryreturn} from "runtime-compat/flow";
2
+ import {tryreturn} from "runtime-compat/sync";
3
3
  import errors from "../errors.js";
4
4
 
5
5
  // insensitive-case equal
@@ -18,8 +18,9 @@ export default app => {
18
18
  .filter(([name]) => name.includes("$"))
19
19
  .map(([name, value]) => [name.split("$")[1], value])
20
20
  .every(([name, value]) =>
21
- tryreturn(_ => types?.[name](value) === true)
22
- .orelse(({message}) => errors.MismatchedPath.throw(pathname, message)));
21
+ tryreturn(_ => types?.[name].type(value) === true)
22
+ .orelse(({message}) => errors.MismatchedPath.throw(pathname, message))
23
+ );
23
24
  const isPath = ({route, pathname}) => {
24
25
  const result = route.pathname.exec(pathname);
25
26
  return result === null ? false : isType(result.groups, pathname);
@@ -29,7 +30,7 @@ export default app => {
29
30
  const find = (method, pathname) => routes.find(route =>
30
31
  isMethod({route, method, pathname}));
31
32
 
32
- const index = path => `${paths.routes}${path === "" ? "index" : path}`;
33
+ const index = path => `${paths.routes}${path === "/" ? "/index" : path}`;
33
34
  const deroot = pathname => pathname.endsWith("/") && pathname !== "/"
34
35
  ? pathname.slice(0, -1) : pathname;
35
36
 
@@ -1,8 +1,8 @@
1
- const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
1
+ import {identity} from "runtime-compat/function";
2
2
 
3
3
  export default async (app, server) => {
4
4
  app.log.info("running serve hooks", {module: "primate"});
5
- await [...filter("serve", app.modules), _ => _]
6
- .reduceRight((acc, handler) => input =>
7
- handler(input, acc))({...app, server});
5
+ await [...app.modules.serve, identity]
6
+ .reduceRight((next, previous) =>
7
+ input => previous(input, next))({...app, server});
8
8
  };
@@ -28,7 +28,5 @@ export default async ({
28
28
  return objects;
29
29
  };
30
30
 
31
- export const lc_first = path => /^[a-z]/u.test(path.name);
32
-
33
31
  export const doubled = set => set.find((part, i, array) =>
34
32
  array.filter((_, j) => i !== j).includes(part));
@@ -8,7 +8,8 @@ const normalize = route => {
8
8
  };
9
9
 
10
10
  // index -> ""
11
- const deindex = path => path.endsWith("index") ? path.replace("index", "") : path;
11
+ const deindex = path => path.endsWith("index") ?
12
+ path.replace("index", "") : path;
12
13
 
13
14
  export default async (log, directory, load) => {
14
15
  const filter = path => /^[^+].*.js$/u.test(path.name);
@@ -1,4 +1,4 @@
1
- import {tryreturn} from "runtime-compat/flow";
1
+ import {tryreturn} from "runtime-compat/sync";
2
2
  import errors from "../errors.js";
3
3
  import {invalid} from "../hooks/route.js";
4
4
  import {default as fs, doubled} from "./common.js";
@@ -1,6 +1,8 @@
1
1
  import {Path} from "runtime-compat/fs";
2
2
  import errors from "../errors.js";
3
- import {default as fs, lc_first as filter} from "./common.js";
3
+ import fs from "./common.js";
4
+
5
+ const filter = path => /^[a-z]/u.test(path.name);
4
6
 
5
7
  export default async (log, directory, load = fs) => {
6
8
  const types = await load({log, directory, name: "types", filter});
package/src/run.js CHANGED
@@ -1,4 +1,4 @@
1
- import {tryreturn} from "runtime-compat/flow";
1
+ import {tryreturn} from "runtime-compat/async";
2
2
  import {Path} from "runtime-compat/fs";
3
3
  import {extend} from "runtime-compat/object";
4
4
  import app from "./app.js";
package/src/start.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import {serve, Response, Status} from "runtime-compat/http";
2
- import {tryreturn} from "runtime-compat/flow";
2
+ import {tryreturn} from "runtime-compat/async";
3
3
  import {identity} from "runtime-compat/function";
4
4
  import * as hooks from "./hooks/exports.js";
5
5