primate 0.20.1 → 0.20.3

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.1",
3
+ "version": "0.20.3",
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,10 @@
18
18
  "directory": "packages/primate"
19
19
  },
20
20
  "dependencies": {
21
- "runtime-compat": "^0.20.2"
21
+ "runtime-compat": "^0.21.0"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.16"
22
25
  },
23
26
  "type": "module",
24
27
  "exports": "./src/exports.js"
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";
@@ -8,6 +8,7 @@ import * as hooks from "./hooks/exports.js";
8
8
  import * as loaders from "./loaders/exports.js";
9
9
  import dispatch from "./dispatch.js";
10
10
  import {print} from "./Logger.js";
11
+ import toSorted from "./toSorted.js";
11
12
 
12
13
  const base = new Path(import.meta.url).up(1);
13
14
  // do not hard-depend on node
@@ -16,18 +17,18 @@ const library = import.meta.runtime?.library ?? "node_modules";
16
17
 
17
18
  // use user-provided file or fall back to default
18
19
  const index = (app, name) =>
19
- tryreturn(async _ => File.read(`${app.paths.pages.join(name)}`))
20
- .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());
21
22
 
23
+ const encoder = new TextEncoder();
22
24
  const hash = async (string, algorithm = "sha-384") => {
23
- const encoder = new TextEncoder();
24
25
  const bytes = await crypto.subtle.digest(algorithm, encoder.encode(string));
25
26
  const algo = algorithm.replace("-", _ => "");
26
27
  return `${algo}-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}`;
27
28
  };
28
29
 
29
- const attribute = attributes => Object.keys(attributes).length > 0 ?
30
- " ".concat(Object.entries(attributes)
30
+ const attribute = attributes => Object.keys(attributes).length > 0
31
+ ? " ".concat(Object.entries(attributes)
31
32
  .map(([key, value]) => `${key}="${value}"`).join(" "))
32
33
  : "";
33
34
  const tag = ({name, attributes = {}, code = "", close = true}) =>
@@ -74,22 +75,25 @@ export default async (config, root, log) => {
74
75
  },
75
76
  headers: _ => {
76
77
  const csp = Object.keys(http.csp).reduce((policy_string, key) =>
77
- `${policy_string}${key} ${http.csp[key]};`, "");
78
- const scripts = app.assets
79
- .filter(({type}) => type !== "style")
80
- .map(asset => `'${asset.integrity}'`).join(" ");
81
- const _csp = scripts === "" ? csp : `${csp}script-src 'self' ${scripts};`;
82
- // remove inline assets
83
- app.assets = app.assets.filter(({inline, type}) => !inline
84
- || 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
+ } `);
85
89
 
86
90
  return {
87
- "Content-Security-Policy": _csp,
91
+ "Content-Security-Policy": csp,
88
92
  "Referrer-Policy": "same-origin",
89
93
  };
90
94
  },
91
95
  handlers: {...handlers},
92
- render: async ({body = "", head = "", page} = {}) => {
96
+ render: async ({body = "", page} = {}) => {
93
97
  const html = await index(app, page ?? config.index);
94
98
  // inline: <script type integrity>...</script>
95
99
  // outline: <script type integrity src></script>
@@ -101,16 +105,17 @@ export default async (config, root, log) => {
101
105
  const style = ({inline, code, href, rel = "stylesheet"}) => inline
102
106
  ? tag({name: "style", code})
103
107
  : tag({name: "link", attributes: {rel, href}, close: false});
104
- const heads = app.assets
105
- .toSorted(({type}) => -1 * (type === "importmap"))
108
+ const head = toSorted(app.assets,
109
+ ({type}) => -1 * (type === "importmap"))
106
110
  .map(({src, code, type, inline, integrity}) =>
107
111
  type === "style"
108
112
  ? style({inline, code, href: src})
109
113
  : script({inline, code, type, integrity, src})
110
114
  ).join("\n");
111
- return html
112
- .replace("%body%", _ => body)
113
- .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);
114
119
  },
115
120
  publish: async ({src, code, type = "", inline = false}) => {
116
121
  if (!inline) {
@@ -118,10 +123,10 @@ export default async (config, root, log) => {
118
123
  await base.directory.file.create();
119
124
  await base.file.write(code);
120
125
  }
121
- const integrity = await hash(code);
122
- const _src = new Path(http.static.root).join(src ?? "");
123
- app.assets.push({src: `${_src}`, code: inline ? code : "", type, inline, integrity});
124
- 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
+ }
125
130
  },
126
131
  bootstrap: ({type, code}) => {
127
132
  app.entrypoints.push({type, code});
@@ -134,10 +139,10 @@ export default async (config, root, log) => {
134
139
  const exports = pkg.exports === undefined
135
140
  ? {[module]: `/${module}/${pkg.main}`}
136
141
  : transform(pkg.exports, entry => entry
137
- .filter(([, _export]) => _export.import !== undefined)
142
+ .filter(([, _export]) => _export.import !== undefined || _export.default !== undefined)
138
143
  .map(([key, value]) => [
139
144
  key.replace(".", module),
140
- value.import.replace(".", `./${module}`),
145
+ value.import?.replace(".", `./${module}`) ?? value.default.replace(".", `./${module}`),
141
146
  ]));
142
147
  await Promise.all(Object.values(exports).map(async name => app.publish({
143
148
  code: await Path.resolve().join(library, name).text(),
@@ -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));
@@ -1,13 +1,14 @@
1
1
  import {Path} from "runtime-compat/fs";
2
2
  import errors from "../../errors.js";
3
+ import toSorted from "../../toSorted.js";
3
4
 
4
5
  export default type => async (log, directory, load) => {
5
6
  const filter = path => new RegExp(`^\\+${type}.js$`, "u").test(path.name);
6
7
 
7
8
  const replace = new RegExp(`\\+${type}`, "u");
8
- const objects = (await load({log, directory, filter, warn: false}))
9
- .map(([name, object]) => [name.replace(replace, () => ""), object])
10
- .toSorted(([a], [b]) => a.length - b.length);
9
+ const objects = toSorted((await load({log, directory, filter, warn: false}))
10
+ .map(([name, object]) => [name.replace(replace, () => ""), object]),
11
+ ([a], [b]) => a.length - b.length);
11
12
 
12
13
  const resolve = name => new Path(directory, name, `+${type}.js`);
13
14
  objects.some(([name, value]) => typeof value !== "function"
@@ -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
 
@@ -0,0 +1 @@
1
+ export default (array, compareFn) => [...array].sort(compareFn);