primate 0.20.5 → 0.21.1

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.5",
3
+ "version": "0.21.1",
4
4
  "description": "Expressive, minimal and extensible web framework",
5
5
  "homepage": "https://primatejs.com",
6
6
  "bugs": "https://github.com/primatejs/primate/issues",
@@ -8,7 +8,7 @@
8
8
  "files": [
9
9
  "src/**/*.js",
10
10
  "src/errors.json",
11
- "src/defaults/app.html",
11
+ "src/defaults/*.html",
12
12
  "!src/**/*.spec.js"
13
13
  ],
14
14
  "bin": "src/bin.js",
@@ -18,7 +18,7 @@
18
18
  "directory": "packages/primate"
19
19
  },
20
20
  "dependencies": {
21
- "runtime-compat": "^0.21.0"
21
+ "runtime-compat": "^0.24.1"
22
22
  },
23
23
  "engines": {
24
24
  "node": ">=18.16"
package/src/Logger.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {assert, is} from "runtime-compat/dyndef";
2
2
  import {blue, bold, green, red, yellow, dim} from "runtime-compat/colors";
3
3
  import {map} from "runtime-compat/object";
4
+ import console from "runtime-compat/console";
4
5
 
5
6
  const levels = {
6
7
  Error: 0,
package/src/app.js CHANGED
@@ -15,10 +15,14 @@ const base = new Path(import.meta.url).up(1);
15
15
  const packager = import.meta.runtime?.packager ?? "package.json";
16
16
  const library = import.meta.runtime?.library ?? "node_modules";
17
17
 
18
+ const fallback = (app, page) =>
19
+ tryreturn(_ => base.join("defaults", page).text())
20
+ .orelse(_ => base.join("defaults", app.config.pages.index).text());
21
+
18
22
  // use user-provided file or fall back to default
19
- const index = (app, name) =>
23
+ const index = (app, page) =>
20
24
  tryreturn(_ => File.read(`${app.paths.pages.join(name)}`))
21
- .orelse(_ => base.join("defaults", app.config.index).text());
25
+ .orelse(_ => fallback(app, page));
22
26
 
23
27
  const encoder = new TextEncoder();
24
28
  const hash = async (string, algorithm = "sha-384") => {
@@ -39,8 +43,6 @@ export default async (config, root, log) => {
39
43
  const secure = http?.ssl !== undefined;
40
44
  const {name, version} = await base.up(1).join(packager).json();
41
45
  const paths = valmap(config.paths, value => root.join(value));
42
- paths.client = paths.build.join("client");
43
- paths.server = paths.build.join("server");
44
46
 
45
47
  const at = `at http${secure ? "s" : ""}://${http.host}:${http.port}\n`;
46
48
  print(blue(bold(name)), blue(version), at);
@@ -54,6 +56,13 @@ export default async (config, root, log) => {
54
56
  const types = await loaders.types(log, paths.types);
55
57
 
56
58
  const app = {
59
+ build: {
60
+ paths: {
61
+ client: paths.build.join("client"),
62
+ server: paths.build.join("server"),
63
+ components: paths.build.join("components"),
64
+ },
65
+ },
57
66
  config,
58
67
  secure,
59
68
  name,
@@ -73,7 +82,7 @@ export default async (config, root, log) => {
73
82
  await to.file.write(file);
74
83
  }));
75
84
  },
76
- headers: _ => {
85
+ headers() {
77
86
  const csp = Object.keys(http.csp).reduce((policy_string, key) =>
78
87
  `${policy_string}${key} ${http.csp[key]};`, "")
79
88
  .replace("script-src 'self'", `script-src 'self' ${
@@ -93,8 +102,8 @@ export default async (config, root, log) => {
93
102
  };
94
103
  },
95
104
  handlers: {...handlers},
96
- render: async ({body = "", page} = {}) => {
97
- const html = await index(app, page ?? config.index);
105
+ async render({body = "", page} = {}) {
106
+ const html = await index(app, page ?? config.pages.index);
98
107
  // inline: <script type integrity>...</script>
99
108
  // outline: <script type integrity src></script>
100
109
  const script = ({inline, code, type, integrity, src}) => inline
@@ -105,7 +114,7 @@ export default async (config, root, log) => {
105
114
  const style = ({inline, code, href, rel = "stylesheet"}) => inline
106
115
  ? tag({name: "style", code})
107
116
  : tag({name: "link", attributes: {rel, href}, close: false});
108
- const head = toSorted(app.assets,
117
+ const head = toSorted(this.assets,
109
118
  ({type}) => -1 * (type === "importmap"))
110
119
  .map(({src, code, type, inline, integrity}) =>
111
120
  type === "style"
@@ -113,28 +122,28 @@ export default async (config, root, log) => {
113
122
  : script({inline, code, type, integrity, src})
114
123
  ).join("\n");
115
124
  // remove inline assets
116
- app.assets = app.assets.filter(({inline, type}) => !inline
125
+ this.assets = this.assets.filter(({inline, type}) => !inline
117
126
  || type === "importmap");
118
127
  return html.replace("%body%", _ => body).replace("%head%", _ => head);
119
128
  },
120
- publish: async ({src, code, type = "", inline = false}) => {
129
+ async publish({src, code, type = "", inline = false}) {
121
130
  if (!inline) {
122
- const base = paths.client.join(src);
131
+ const base = this.build.paths.client.join(src);
123
132
  await base.directory.file.create();
124
133
  await base.file.write(code);
125
134
  }
126
135
  if (inline || type === "style") {
127
- app.assets.push({src: new Path(http.static.root).join(src ?? "").path,
136
+ this.assets.push({src: new Path(http.static.root).join(src ?? "").path,
128
137
  code: inline ? code : "", type, inline, integrity: await hash(code)});
129
138
  }
130
139
  },
131
- bootstrap: ({type, code}) => {
132
- app.entrypoints.push({type, code});
140
+ bootstrap({type, code}) {
141
+ this.entrypoints.push({type, code});
133
142
  },
134
143
  async import(module) {
135
144
  const {build} = config;
136
145
  const {root} = http.static;
137
- const path = [library, module];
146
+ const path = [library, ...module.split("/")];
138
147
  const pkg = await Path.resolve().join(...path, packager).json();
139
148
  const exports = pkg.exports === undefined
140
149
  ? {[module]: `/${module}/${pkg.main}`}
@@ -149,7 +158,7 @@ export default async (config, root, log) => {
149
158
  ?? value.import?.replace(".", `./${module}`),
150
159
  ]));
151
160
  const dependency = Path.resolve().join(...path);
152
- const to = new Path(paths.client, build.modules, dependency.name);
161
+ const to = new Path(this.build.paths.client, build.modules, ...module.split("/"));
153
162
  await dependency.file.copy(to);
154
163
  this.importmaps = {
155
164
  ...valmap(exports, value => new Path(root, build.modules, value).path),
@@ -1,8 +1,4 @@
1
1
  import {default as dev} from "./dev.js";
2
2
  import {default as serve} from "./serve.js";
3
3
 
4
- const commands = {dev, serve};
5
-
6
- const run = name => commands[name] ?? dev;
7
-
8
- export default name => name === undefined ? dev : run(name);
4
+ export default name => ({dev, serve})[name] ?? dev;
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Error page</title>
5
+ <meta charset="utf-8" />
6
+ %head%
7
+ </head>
8
+ <body>
9
+ <h1>Error page</h1>
10
+ <p>
11
+ %body%
12
+ </p>
13
+ </body>
14
+ </html>
@@ -2,6 +2,11 @@ import Logger from "../Logger.js";
2
2
 
3
3
  export default {
4
4
  base: "/",
5
+ modules: [],
6
+ pages: {
7
+ index: "app.html",
8
+ error: "error.html",
9
+ },
5
10
  logger: {
6
11
  level: Logger.Warn,
7
12
  trace: false,
@@ -22,7 +27,6 @@ export default {
22
27
  root: "/",
23
28
  },
24
29
  },
25
- index: "app.html",
26
30
  paths: {
27
31
  build: "build",
28
32
  components: "components",
@@ -38,7 +42,6 @@ export default {
38
42
  modules: "modules",
39
43
  index: "index.js",
40
44
  },
41
- modules: [],
42
45
  types: {
43
46
  explicit: false,
44
47
  },
package/src/dispatch.js CHANGED
@@ -1,19 +1,22 @@
1
1
  import {is, maybe} from "runtime-compat/dyndef";
2
2
  import {tryreturn} from "runtime-compat/sync";
3
3
  import {map} from "runtime-compat/object";
4
+ import {camelcased} from "runtime-compat/string";
4
5
  import errors from "./errors.js";
6
+ import validate from "./validate.js";
5
7
 
6
- export default (patches = {}) => value => {
7
- is(patches.get).undefined();
8
+ export default (patches = {}) => (value, raw, cased = true) => {
8
9
  return Object.assign(Object.create(null), {
9
- ...map(patches, ([name, patch]) => [name, property => {
10
+ ...map(patches, ([name, patch]) => [`get${camelcased(name)}`, property => {
10
11
  is(property).defined(`\`${name}\` called without property`);
11
- return tryreturn(_ => patch(value[property], property))
12
+ return tryreturn(_ => validate(patch, value[property], property))
12
13
  .orelse(({message}) => errors.MismatchedType.throw(message));
13
14
  }]),
14
15
  get(property) {
15
16
  maybe(property).string();
16
- return property === undefined ? value : value[property];
17
+ return property === undefined ? value :
18
+ value[cased ? property : property.toLowerCase()];
17
19
  },
20
+ raw,
18
21
  });
19
22
  };
package/src/errors.json CHANGED
@@ -32,7 +32,7 @@
32
32
  "level": "Warn"
33
33
  },
34
34
  "ErrorInConfigFile": {
35
- "message": "error in config file :: {0}",
35
+ "message": "error in config file: {0}",
36
36
  "fix": "check errors in config file by running {1}",
37
37
  "level": "Error"
38
38
  },
@@ -57,12 +57,12 @@
57
57
  "level": "Error"
58
58
  },
59
59
  "MismatchedPath": {
60
- "message": "mismatched {0} path :: {1}",
60
+ "message": "mismatched path {0}: {1}",
61
61
  "fix": "if unintentional, fix the type or the caller",
62
62
  "level": "Info"
63
63
  },
64
64
  "MismatchedType": {
65
- "message": "mismatched type :: {0}",
65
+ "message": "mismatched type: {0}",
66
66
  "fix": "if unintentional, fix the type or the caller",
67
67
  "level": "Info"
68
68
  },
@@ -98,7 +98,7 @@
98
98
  },
99
99
  "NoRouteToPath": {
100
100
  "message": "no {0} route to {1}",
101
- "fix": "if unintentional create a route at {2}.js",
101
+ "fix": "if unintentional create a {3} route function at {2}.js",
102
102
  "level": "Info"
103
103
  },
104
104
  "ReservedTypeName": {
package/src/exports.js CHANGED
@@ -4,6 +4,6 @@ export * from "./handlers/exports.js";
4
4
 
5
5
  export {default as Logger} from "./Logger.js";
6
6
 
7
- export {URL, Response, Status} from "runtime-compat/http";
7
+ export {URL, Response, Status, MediaType} from "runtime-compat/http";
8
8
 
9
9
  export default command => run(command);
@@ -1,9 +1,9 @@
1
- import {Status} from "runtime-compat/http";
1
+ import {Response, Status, MediaType} from "runtime-compat/http";
2
2
 
3
- export default (body = "Not Found", {status = Status.NotFound} = {}) =>
4
- async app => [
5
- await app.render({body}), {
3
+ export default (body = "Not Found", {status = Status.NOT_FOUND, page} = {}) =>
4
+ async app => new Response(await app.render({
5
+ body,
6
+ page: page ?? app.config.pages.error}), {
6
7
  status,
7
- headers: {...app.headers(), "Content-Type": "text/html"},
8
- },
9
- ];
8
+ headers: {...app.headers(), "Content-Type": MediaType.TEXT_HTML},
9
+ });
@@ -1,3 +1,5 @@
1
+ import {Response, Status, MediaType} from "runtime-compat/http";
2
+
1
3
  const script = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
2
4
  const style = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
3
5
 
@@ -10,7 +12,7 @@ const integrate = async (html, publish) => {
10
12
  };
11
13
 
12
14
  export default (component, options = {}) => {
13
- const {status = 200, partial = false, load = false} = options;
15
+ const {status = Status.OK, partial = false, load = false} = options;
14
16
 
15
17
  return async app => {
16
18
  const body = await integrate(await load ?
@@ -19,9 +21,9 @@ export default (component, options = {}) => {
19
21
  // needs to happen before app.render()
20
22
  const headers = app.headers();
21
23
 
22
- return [partial ? body : await app.render({body}), {
24
+ return new Response(partial ? body : await app.render({body}), {
23
25
  status,
24
- headers: {...headers, "Content-Type": "text/html"},
25
- }];
26
+ headers: {...headers, "Content-Type": MediaType.TEXT_HTML},
27
+ });
26
28
  };
27
29
  };
@@ -1,6 +1,7 @@
1
- export default (body, {status = 200} = {}) => app => [
2
- JSON.stringify(body), {
1
+ import {Response, Status, MediaType} from "runtime-compat/http";
2
+
3
+ export default (body, {status = Status.OK} = {}) => app =>
4
+ new Response(JSON.stringify(body), {
3
5
  status,
4
- headers: {...app.headers(), "Content-Type": "application/json"},
5
- },
6
- ];
6
+ headers: {...app.headers(), "Content-Type": MediaType.APPLICATION_JSON},
7
+ });
@@ -1,9 +1,8 @@
1
- import {Status} from "runtime-compat/http";
1
+ import {Response, Status} from "runtime-compat/http";
2
2
 
3
- export default (Location, {status = Status.Found} = {}) => app => [
3
+ export default (Location, {status = Status.FOUND} = {}) => app =>
4
4
  /* no body */
5
- null, {
5
+ new Response(null, {
6
6
  status,
7
7
  headers: {...app.headers(), Location},
8
- },
9
- ];
8
+ });
@@ -1,6 +1,8 @@
1
- export default (body, {status = 200} = {}) => app => [
2
- body, {
1
+ import {Response, Status, MediaType} from "runtime-compat/http";
2
+
3
+ export default (body, {status = Status.OK} = {}) => app =>
4
+ new Response(body, {
3
5
  status,
4
- headers: {...app.headers(), "Content-Type": "application/octet-stream"},
5
- },
6
- ];
6
+ headers: {...app.headers(), "Content-Type":
7
+ MediaType.APPLICATION_OCTET_STREAM},
8
+ });
@@ -1,6 +1,7 @@
1
- export default (body, {status = 200} = {}) => app => [
2
- body, {
1
+ import {Response, Status, MediaType} from "runtime-compat/http";
2
+
3
+ export default (body, {status = Status.OK} = {}) => app =>
4
+ new Response(body, {
3
5
  status,
4
- headers: {...app.headers(), "Content-Type": "text/plain"},
5
- },
6
- ];
6
+ headers: {...app.headers(), "Content-Type": MediaType.TEXT_PLAIN},
7
+ });
@@ -1,4 +1,4 @@
1
- import errors from "../errors.js";
1
+ import errors from "../errors.js";
2
2
 
3
3
  export default (name, props, options) => async (app, ...rest) => {
4
4
  const ending = name.slice(name.lastIndexOf(".") + 1);
@@ -1,10 +1,10 @@
1
1
  import {File} from "runtime-compat/fs";
2
2
 
3
3
  const pre = async app => {
4
- const {paths, config} = app;
4
+ const {paths, config, build} = app;
5
5
  if (await paths.static.exists) {
6
- // copy static files to build/client/_static
7
- await File.copy(paths.static, paths.client.join(config.build.static));
6
+ // copy static files to build/client/static
7
+ await File.copy(paths.static, build.paths.client.join(config.build.static));
8
8
  }
9
9
  };
10
10
 
@@ -1,16 +1,20 @@
1
1
  import copy_includes from "./copy_includes.js";
2
2
 
3
3
  const pre = async app => {
4
- const {paths, config: {build}} = app;
4
+ const {build, paths, config} = app;
5
5
 
6
6
  // remove build directory in case exists
7
7
  if (await paths.build.exists) {
8
8
  await paths.build.file.remove();
9
9
  }
10
- await paths.server.file.create();
10
+ await build.paths.server.file.create();
11
+ await build.paths.components.file.create();
11
12
 
12
13
  if (await paths.components.exists) {
13
- await app.copy(paths.components, paths.server.join(build.app));
14
+ // copy all files to build/components
15
+ await app.copy(paths.components, build.paths.components, /^.*$/u);
16
+ // copy .js files from components to build/server
17
+ await app.copy(paths.components, build.paths.server.join(config.build.app));
14
18
  }
15
19
 
16
20
  // copy additional subdirectories to build/server
@@ -1,7 +1,7 @@
1
1
  const system = ["routes", "components", "build"];
2
2
 
3
3
  export default async (app, type, post = () => undefined) => {
4
- const {paths, config} = app;
4
+ const {config} = app;
5
5
  const {build} = config;
6
6
  const {includes} = build;
7
7
 
@@ -14,7 +14,7 @@ export default async (app, type, post = () => undefined) => {
14
14
  .map(async include => {
15
15
  const path = app.root.join(include);
16
16
  if (await path.exists) {
17
- const to = paths[type].join(include);
17
+ const to = app.build.paths[type].join(include);
18
18
  await to.file.create();
19
19
  await app.copy(path, to);
20
20
  await post(to);
@@ -1,63 +1,66 @@
1
- import {Response, Status} from "runtime-compat/http";
1
+ import {Response, Status, MediaType} from "runtime-compat/http";
2
2
  import {tryreturn} from "runtime-compat/async";
3
- import {mime, isResponse, respond} from "./respond/exports.js";
3
+ import {respond} from "./respond/exports.js";
4
4
  import {invalid} from "./route.js";
5
5
  import {error as clientError} from "../handlers/exports.js";
6
- import errors from "../errors.js";
6
+ import _errors from "../errors.js";
7
+ const {NoFileForPath} = _errors;
7
8
 
8
9
  const guardError = Symbol("guardError");
9
10
 
10
11
  export default app => {
11
12
  const {config: {http: {static: {root}}, build}, paths} = app;
12
13
 
13
- const run = async request => {
14
+ const route = async request => {
14
15
  const {pathname} = request.url;
15
- const {path, guards, layouts, handler} = invalid(pathname)
16
- ? errors.NoFileForPath.throw(pathname, paths.static)
17
- : await app.route(request);
16
+ // if NoFileForPath is thrown, this will remain undefined
17
+ let errorHandler = undefined;
18
18
 
19
- // handle guards
20
- try {
21
- guards.every(guard => {
22
- const result = guard(request);
23
- if (result === true) {
24
- return true;
19
+ return tryreturn(async _ => {
20
+ const {path, guards, errors, layouts, handler} = invalid(pathname)
21
+ ? NoFileForPath.throw(pathname, paths.static)
22
+ : await app.route(request);
23
+ errorHandler = errors?.at(-1);
24
+
25
+ // handle guards
26
+ try {
27
+ guards.every(guard => {
28
+ const result = guard(request);
29
+ if (result === true) {
30
+ return true;
31
+ }
32
+ const error = new Error();
33
+ error.result = result;
34
+ error.type = guardError;
35
+ throw error;
36
+ });
37
+ } catch (error) {
38
+ if (error.type === guardError) {
39
+ return (await respond(error.result))(app);
25
40
  }
26
- const error = new Error();
27
- error.result = result;
28
- error.type = guardError;
41
+ // rethrow if not guard error
29
42
  throw error;
30
- });
31
- } catch (error) {
32
- if (error.type === guardError) {
33
- return (await respond(error.result))(app);
34
43
  }
35
- // rethrow if not guard error
36
- throw error;
37
- }
38
44
 
39
- // handle request
40
- const handlers = [...app.modules.route, handler]
41
- .reduceRight((next, last) => input => last(input, next));
42
-
43
- return (await respond(await handlers({...request, path})))(app, {
44
- layouts: await Promise.all(layouts.map(layout => layout(request))),
45
- });
46
- };
47
-
48
- const route = async request =>
49
- tryreturn(async _ => {
50
- const response = await run(request);
51
- return isResponse(response) ? response : new Response(...response);
45
+ // handle request
46
+ const handlers = [...app.modules.route, handler]
47
+ .reduceRight((next, last) => input => last(input, next));
48
+ return (await respond(await handlers({...request, path})))(app, {
49
+ layouts: await Promise.all(layouts.map(layout => layout(request))),
50
+ }, request);
52
51
  }).orelse(async error => {
53
52
  app.log.auto(error);
54
- return new Response(...await clientError()(app, {}));
53
+ console.log("HI")
54
+ // the +error.js page itself could fail
55
+ return tryreturn(_ => respond(errorHandler())(app, {}, request))
56
+ .orelse(_ => clientError()(app, {}, request));
55
57
  });
58
+ };
56
59
 
57
60
  const asset = async file => new Response(file.readable, {
58
61
  status: Status.OK,
59
62
  headers: {
60
- "Content-Type": mime(file.name),
63
+ "Content-Type": MediaType.resolve(file.name),
61
64
  Etag: await file.modified,
62
65
  },
63
66
  });
@@ -66,12 +69,13 @@ export default app => {
66
69
  const {pathname} = request.url;
67
70
  if (pathname.startsWith(root)) {
68
71
  const debased = pathname.replace(root, _ => "");
72
+ const {client} = app.build.paths;
69
73
  // try static first
70
- const _static = paths.client.join(build.static, debased);
74
+ const _static = client.join(build.static, debased);
71
75
  if (await _static.isFile) {
72
76
  return asset(_static.file);
73
77
  }
74
- const _app = app.paths.client.join(debased);
78
+ const _app = client.join(debased);
75
79
  return await _app.isFile ? asset(_app.file) : route(request);
76
80
  }
77
81
  return route(request);
@@ -1,38 +1,35 @@
1
- import {URL} from "runtime-compat/http";
1
+ import {URL, MediaType} from "runtime-compat/http";
2
2
  import {tryreturn} from "runtime-compat/sync";
3
3
  import {stringify} from "runtime-compat/streams";
4
+ import {from, valmap} from "runtime-compat/object";
4
5
  import errors from "../errors.js";
5
6
 
6
- const {fromEntries: from} = Object;
7
+ const {APPLICATION_FORM_URLENCODED, APPLICATION_JSON} = MediaType;
7
8
 
8
9
  const contents = {
9
- "application/x-www-form-urlencoded": body => from(body.split("&")
10
+ [APPLICATION_FORM_URLENCODED]: body => from(body.split("&")
10
11
  .map(part => part.split("=")
11
12
  .map(subpart => decodeURIComponent(subpart).replaceAll("+", " ")))),
12
- "application/json": body => JSON.parse(body),
13
+ [APPLICATION_JSON]: body => JSON.parse(body),
13
14
  };
14
15
 
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
- };
16
+ const content = (type, body) =>
17
+ tryreturn(_ => contents[type?.split(";")[0]]?.(body) ?? body)
18
+ .orelse(_ => errors.CannotParseBody.throw(body, type));
28
19
 
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));
34
- const url = new URL(request.url);
35
- const query = dispatch(from(url.searchParams));
20
+ export default dispatch => async original => {
21
+ const {headers} = original;
22
+ const url = new URL(original.url);
23
+ const body = await stringify(original.body);
24
+ const cookies = headers.get("cookie");
36
25
 
37
- return {original: request, url, body, cookies, headers, query};
26
+ return {original, url,
27
+ ...valmap({
28
+ body: [content(headers.get("content-type"), body), body],
29
+ query: [from(url.searchParams), url.search],
30
+ headers: [from(headers), headers, false],
31
+ cookies: [from(cookies?.split(";").map(cookie => cookie.trim().split("="))
32
+ ?? []), cookies],
33
+ }, value => dispatch(...value)),
34
+ };
38
35
  };
@@ -1,9 +1,9 @@
1
1
  import {Path} from "runtime-compat/fs";
2
2
  import {identity} from "runtime-compat/function";
3
- import copy_includes from "./copy_includes.js"
3
+ import copy_includes from "./copy_includes.js";
4
4
 
5
5
  const post = async app => {
6
- const {config} = app;
6
+ const {config, paths} = app;
7
7
  const build = config.build.app;
8
8
  {
9
9
  // after hook, publish a zero assumptions app.js (no css imports)
@@ -12,7 +12,10 @@ const post = async app => {
12
12
  const src = new Path(config.http.static.root, build, config.build.index);
13
13
  await app.publish({src, code, type: "module"});
14
14
 
15
- await app.copy(app.paths.components, app.paths.client.join(build));
15
+ if (await paths.components.exists) {
16
+ // copy .js files from components to build/server
17
+ await app.copy(app.paths.components, app.build.paths.client.join(build));
18
+ }
16
19
 
17
20
  const imports = {...app.importmaps, app: src.path};
18
21
  await app.publish({
@@ -36,7 +39,7 @@ const post = async app => {
36
39
  }
37
40
  }));
38
41
 
39
- const source = `${app.paths.client}`;
42
+ const source = `${app.build.paths.client}`;
40
43
  const {root} = app.config.http.static;
41
44
  // copy additional subdirectories to build/client
42
45
  await copy_includes(app, "client", async to =>
@@ -1,3 +1,2 @@
1
- export {default as mime} from "./mime.js";
2
1
  export {isResponse} from "./duck.js";
3
2
  export {default as respond} from "./respond.js";
@@ -1,6 +1,7 @@
1
1
  import {keymap} from "runtime-compat/object";
2
2
  import {tryreturn} from "runtime-compat/sync";
3
3
  import errors from "../errors.js";
4
+ import validate from "../validate.js";
4
5
 
5
6
  // insensitive-case equal
6
7
  const ieq = (left, right) => left.toLowerCase() === right.toLowerCase();
@@ -18,9 +19,8 @@ export default app => {
18
19
  .filter(([name]) => name.includes("$"))
19
20
  .map(([name, value]) => [name.split("$")[1], value])
20
21
  .every(([name, value]) =>
21
- tryreturn(_ => types?.[name].type(value) === true)
22
- .orelse(({message}) => errors.MismatchedPath.throw(pathname, message))
23
- );
22
+ tryreturn(_ => validate(types[name], value, name))
23
+ .orelse(({message}) => errors.MismatchedPath.throw(pathname, message)));
24
24
  const isPath = ({route, pathname}) => {
25
25
  const result = route.pathname.exec(pathname);
26
26
  return result === null ? false : isType(result.groups, pathname);
@@ -34,11 +34,10 @@ export default app => {
34
34
  const deroot = pathname => pathname.endsWith("/") && pathname !== "/"
35
35
  ? pathname.slice(0, -1) : pathname;
36
36
 
37
- return request => {
38
- const {original: {method}, url} = request;
37
+ return ({original: {method}, url}) => {
39
38
  const pathname = deroot(url.pathname);
40
- const route = find(method, pathname) ??
41
- errors.NoRouteToPath.throw(method, pathname, index(pathname));
39
+ const route = find(method, pathname) ?? errors.NoRouteToPath
40
+ .throw(method, pathname, index(pathname), method.toLowerCase());
42
41
 
43
42
  const path = app.dispatch(keymap(route.pathname?.exec(pathname).groups,
44
43
  key => key.split("$")[0]));
@@ -0,0 +1,3 @@
1
+ import load from "./load.js";
2
+
3
+ export default load("error");
@@ -1,3 +1,4 @@
1
1
  export {default as routes} from "./routes.js";
2
2
  export {default as guards} from "./guards.js";
3
+ export {default as errors} from "./errors.js";
3
4
  export {default as layouts} from "./layouts.js";
@@ -1,4 +1,5 @@
1
1
  import {tryreturn} from "runtime-compat/sync";
2
+ import {from, filter, valmap} from "runtime-compat/object";
2
3
  import errors from "../errors.js";
3
4
  import {invalid} from "../hooks/route.js";
4
5
  import {default as fs, doubled} from "./common.js";
@@ -22,10 +23,11 @@ const make = path => {
22
23
  return new RegExp(`^/${route}$`, "u");
23
24
  };
24
25
 
26
+ const specials = ["guards", "errors", "layouts"];
25
27
  export default async (log, directory, load = fs) => {
26
28
  const routes = await get.routes(log, directory, load);
27
- const guards = await get.guards(log, directory, load);
28
- const layouts = await get.layouts(log, directory, load);
29
+ const $routes = from(await Promise.all(specials.map(async extra =>
30
+ [extra, await get[extra](log, directory, load)])));
29
31
  const filter = path => ([name]) => path.includes(name);
30
32
 
31
33
  return routes.map(([path, imported]) => {
@@ -38,8 +40,9 @@ export default async (log, directory, load = fs) => {
38
40
  method,
39
41
  handler,
40
42
  pathname: make(path.endsWith("/") ? path.slice(0, -1) : path),
41
- guards: guards.filter(filter(path)).map(([, guard]) => guard),
42
- layouts: layouts.filter(filter(path)).map(([, layout]) => layout),
43
+ guards: $routes.guards.filter(filter(path)).map(([, guard]) => guard),
44
+ errors: $routes.errors.filter(filter(path)).map(([, error]) => error),
45
+ layouts: $routes.layouts.filter(filter(path)).map(([, layout]) => layout),
43
46
  }));
44
47
  }).flat();
45
48
  };
package/src/start.js CHANGED
@@ -21,7 +21,7 @@ export default async (app, operations = {}) => {
21
21
  tryreturn(async _ => hooks.handle(app)(await app.parse(request)))
22
22
  .orelse(error => {
23
23
  app.log.auto(error);
24
- return new Response(null, {status: Status.InternalServerError});
24
+ return new Response(null, {status: Status.INTERNAL_SERVER_ERROR});
25
25
  }),
26
26
  app.config.http);
27
27
 
@@ -0,0 +1,10 @@
1
+ import {is, maybe} from "runtime-compat/dyndef";
2
+
3
+ export default (type, value, name) => {
4
+ maybe(type.validate).function();
5
+ if (type.validate) {
6
+ return type.validate(value, name);
7
+ }
8
+ is(type).function();
9
+ return type(value, name);
10
+ };
@@ -1,18 +0,0 @@
1
- const mimes = {
2
- binary: "application/octet-stream",
3
- css: "text/css",
4
- html: "text/html",
5
- jpg: "image/jpeg",
6
- js: "text/javascript",
7
- mjs: "text/javascript",
8
- json: "application/json",
9
- png: "image/png",
10
- svg: "image/svg+xml",
11
- woff2: "font/woff2",
12
- webp: "image/webp",
13
- };
14
-
15
- const regex = /\.(?<extension>[a-z1-9]*)$/u;
16
- const match = filename => filename.match(regex)?.groups.extension;
17
-
18
- export default filename => mimes[match(filename)] ?? mimes.binary;