primate 0.19.3 → 0.20.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.
Files changed (42) hide show
  1. package/package.json +2 -2
  2. package/src/Logger.js +48 -64
  3. package/src/app.js +86 -121
  4. package/src/bin.js +0 -1
  5. package/src/defaults/primate.config.js +12 -5
  6. package/src/dispatch.js +8 -13
  7. package/src/errors.js +6 -146
  8. package/src/exports.js +1 -1
  9. package/src/handlers/error.js +4 -4
  10. package/src/handlers/html.js +5 -6
  11. package/src/handlers/json.js +2 -2
  12. package/src/handlers/redirect.js +3 -3
  13. package/src/handlers/stream.js +2 -2
  14. package/src/handlers/text.js +2 -2
  15. package/src/handlers/view.js +3 -3
  16. package/src/hooks/bundle.js +4 -15
  17. package/src/hooks/compile.js +21 -2
  18. package/src/hooks/copy_includes.js +24 -0
  19. package/src/hooks/handle.js +55 -42
  20. package/src/hooks/parse.js +13 -16
  21. package/src/hooks/publish.js +42 -23
  22. package/src/hooks/register.js +1 -3
  23. package/src/hooks/{handle → respond}/respond.js +1 -1
  24. package/src/hooks/route.js +25 -85
  25. package/src/loaders/common.js +34 -0
  26. package/src/loaders/exports.js +3 -0
  27. package/src/loaders/modules.js +40 -0
  28. package/src/loaders/routes/exports.js +3 -0
  29. package/src/loaders/routes/guards.js +3 -0
  30. package/src/loaders/routes/layouts.js +3 -0
  31. package/src/loaders/routes/load.js +17 -0
  32. package/src/loaders/routes/routes.js +22 -0
  33. package/src/loaders/routes.js +45 -0
  34. package/src/loaders/types.js +19 -0
  35. package/src/run.js +13 -24
  36. package/src/start.js +11 -15
  37. package/src/defaults/index.html +0 -9
  38. package/src/extend.js +0 -10
  39. package/src/http-statuses.js +0 -4
  40. /package/src/hooks/{handle → respond}/duck.js +0 -0
  41. /package/src/hooks/{handle → respond}/exports.js +0 -0
  42. /package/src/hooks/{handle → respond}/mime.js +0 -0
package/src/errors.js CHANGED
@@ -1,148 +1,8 @@
1
+ import {Path} from "runtime-compat/fs";
1
2
  import Logger from "./Logger.js";
2
3
 
3
- export default Object.fromEntries(Object.entries({
4
- CannotParseBody({body, contentType}) {
5
- return {
6
- message: ["cannot parse body % as %", body, contentType],
7
- fix: ["use a different content type or fix body"],
8
- level: Logger.Warn,
9
- };
10
- },
11
- DoubleModule({modules, config}) {
12
- const double = modules.find((module, i, array) =>
13
- array.filter((_, j) => i !== j).includes(module));
14
- return {
15
- message: ["double module % in %", double, config],
16
- fix: ["load % only once", double],
17
- level: Logger.Error,
18
- };
19
- },
20
- DoublePathParameter({path, double}) {
21
- return {
22
- message: ["double path parameter % in route %", double, path],
23
- fix: ["disambiguate path parameters in route names"],
24
- level: Logger.Error,
25
- };
26
- },
27
- DoubleRoute({double}) {
28
- return {
29
- message: ["double route %", double],
30
- fix: ["disambiguate route % and %", double, `${double}/index`],
31
- level: Logger.Error,
32
- };
33
- },
34
- EmptyRouteFile({config: {paths}, route}) {
35
- return {
36
- message: ["empty route file at %", `${paths.routes}/${route}.js`],
37
- fix: ["add routes or remove file"],
38
- level: Logger.Warn,
39
- };
40
- },
41
- EmptyTypeDirectory({root}) {
42
- return {
43
- message: ["empty type directory"],
44
- fix: ["populate % with types or remove it", root],
45
- level: Logger.Warn,
46
- };
47
- },
48
- ErrorInConfigFile({config, message}) {
49
- return {
50
- message: ["error in config %", message],
51
- fix: ["check errors in config file by running %", `node ${config}`],
52
- level: Logger.Error,
53
- };
54
- },
55
- InvalidPathParameter({named, path}) {
56
- return {
57
- message: ["invalid path parameter % in route %", named, path],
58
- fix: ["use only latin letters and decimal digits in path parameters"],
59
- level: Logger.Error,
60
- };
61
- },
62
- InvalidRouteName({path}) {
63
- return {
64
- message: ["invalid route name %", path],
65
- fix: ["do not use dots in route names"],
66
- level: Logger.Error,
67
- };
68
- },
69
- InvalidType({name}) {
70
- return {
71
- message: ["invalid type %", name],
72
- fix: ["use only functions for the default export of types"],
73
- level: Logger.Error,
74
- };
75
- },
76
- InvalidTypeName({name}) {
77
- return {
78
- message: ["invalid type name %", name],
79
- fix: ["use only latin letters and decimal digits in types"],
80
- level: Logger.Error,
81
- };
82
- },
83
- MismatchedPath({path, message}) {
84
- return {
85
- message: [`mismatched % path: ${message}`, path],
86
- fix: ["if unintentional, fix the type or the caller"],
87
- level: Logger.Info,
88
- };
89
- },
90
- MismatchedType({message}) {
91
- return {
92
- message: [`mismatched type: ${message}`],
93
- fix: ["if unintentional, fix the type or the caller"],
94
- level: Logger.Info,
95
- };
96
- },
97
- ModuleHasNoHooks({hookless}) {
98
- const modules = hookless.map(({name}) => name).join(", ");
99
- return {
100
- message: ["module % has no hooks", modules],
101
- fix: ["ensure every module uses at least one hook or deactivate it"],
102
- level: Logger.Warn,
103
- };
104
- },
105
- ModulesMustHaveNames({n}) {
106
- return {
107
- message: ["modules must have names"],
108
- fix: ["update module at index % and inform maintainer", n],
109
- level: Logger.Error,
110
- };
111
- },
112
- EmptyConfigFile({config}) {
113
- return {
114
- message: ["empty config file at %", config],
115
- fix: ["add configuration options or remove file"],
116
- level: Logger.Warn,
117
- };
118
- },
119
- NoFileForPath({pathname, config: {paths}}) {
120
- return {
121
- message: ["no file for %", pathname],
122
- fix: ["if unintentional create a file at %%", paths.static, pathname],
123
- level: Logger.Info,
124
- };
125
- },
126
- NoHandlerForExtension({name, ending}) {
127
- return {
128
- message: ["no handler for % extension", ending],
129
- fix: ["add handler module for % files or remove %", `.${ending}`, name],
130
- level: Logger.Error,
131
- };
132
- },
133
- NoRouteToPath({method, pathname, config: {paths}}) {
134
- const route = `${paths.routes}${pathname === "" ? "index" : pathname}.js`;
135
- return {
136
- message: ["no % route to %", method, pathname],
137
- fix: ["if unintentional create a route at %", route],
138
- level: Logger.Info,
139
- };
140
- },
141
- ReservedTypeName({name}) {
142
- return {
143
- message: ["type name % is reserved", name],
144
- fix: ["do not use any reserved type names"],
145
- level: Logger.Error,
146
- };
147
- },
148
- }).map(([name, error]) => [name, Logger.throwable(error, name, "primate")]));
4
+ const json = await new Path(import.meta.url).up(1).join("errors.json").json();
5
+
6
+ const errors = Logger.err(json.errors, json.module);
7
+
8
+ export default errors;
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} from "runtime-compat/http";
7
+ export {URL, Response, Status} from "runtime-compat/http";
8
8
 
9
9
  export default command => run(command);
@@ -1,9 +1,9 @@
1
- import {NotFound} from "../http-statuses.js";
1
+ import {Status} from "runtime-compat/http";
2
2
 
3
- export default (body = "Not Found", {status = NotFound} = {}) =>
4
- async (app, headers) => [
3
+ export default (body = "Not Found", {status = Status.NotFound} = {}) =>
4
+ async app => [
5
5
  await app.render({body}), {
6
6
  status,
7
- headers: {...headers, "Content-Type": "text/html"},
7
+ headers: {...app.headers(), "Content-Type": "text/html"},
8
8
  },
9
9
  ];
@@ -14,20 +14,19 @@ const integrate = async (html, publish, headers) => {
14
14
  headers["Content-Security-Policy"] = headers["Content-Security-Policy"]
15
15
  .replace("style-src 'self'", `style-src 'self' '${integrity}' `);
16
16
  }
17
- return html
18
- .replaceAll(/<script>.*?<\/script>/gus, () => "")
19
- .replaceAll(/<style>.*?<\/style>/gus, () => "");
17
+ return html.replaceAll(/<(?<tag>script|style)>.*?<\/\k<tag>>/gus, _ => "");
20
18
  };
21
19
 
22
20
  export default (component, options = {}) => {
23
- const {status = 200, partial = false, load = false, layout} = options;
21
+ const {status = 200, partial = false, load = false} = options;
24
22
 
25
- return async (app, headers) => {
23
+ return async app => {
24
+ const headers = app.headers();
26
25
  const body = await integrate(await load ?
27
26
  await app.paths.components.join(component).text() : component,
28
27
  app.publish, headers);
29
28
 
30
- return [partial ? body : await app.render({body, layout}), {
29
+ return [partial ? body : await app.render({body}), {
31
30
  status,
32
31
  headers: {...headers, "Content-Type": "text/html"},
33
32
  }];
@@ -1,6 +1,6 @@
1
- export default (body, {status = 200} = {}) => (_, headers) => [
1
+ export default (body, {status = 200} = {}) => app => [
2
2
  JSON.stringify(body), {
3
3
  status,
4
- headers: {...headers, "Content-Type": "application/json"},
4
+ headers: {...app.headers(), "Content-Type": "application/json"},
5
5
  },
6
6
  ];
@@ -1,9 +1,9 @@
1
- import {Found} from "../http-statuses.js";
1
+ import {Status} from "runtime-compat/http";
2
2
 
3
- export default (Location, {status = Found} = {}) => (_, headers) => [
3
+ export default (Location, {status = Status.Found} = {}) => app => [
4
4
  /* no body */
5
5
  null, {
6
6
  status,
7
- headers: {...headers, Location},
7
+ headers: {...app.headers(), Location},
8
8
  },
9
9
  ];
@@ -1,6 +1,6 @@
1
- export default (body, {status = 200} = {}) => (_, headers) => [
1
+ export default (body, {status = 200} = {}) => app => [
2
2
  body, {
3
3
  status,
4
- headers: {...headers, "Content-Type": "application/octet-stream"},
4
+ headers: {...app.headers(), "Content-Type": "application/octet-stream"},
5
5
  },
6
6
  ];
@@ -1,6 +1,6 @@
1
- export default (body, {status = 200} = {}) => (_, headers) => [
1
+ export default (body, {status = 200} = {}) => app => [
2
2
  body, {
3
3
  status,
4
- headers: {...headers, "Content-Type": "text/plain"},
4
+ headers: {...app.headers(), "Content-Type": "text/plain"},
5
5
  },
6
6
  ];
@@ -1,8 +1,8 @@
1
1
  import errors from "../errors.js";
2
2
 
3
- export default (name, props, options) => async (app, headers) => {
3
+ export default (name, props, options) => async (app, ...rest) => {
4
4
  const ending = name.slice(name.lastIndexOf(".") + 1);
5
5
  const handler = app.handlers[ending];
6
- return handler?.(name, {load: true, ...props}, options)(app, headers)
7
- ?? errors.NoHandlerForExtension.throw({name, ending});
6
+ return handler?.(name, {load: true, ...props}, options)(app, ...rest)
7
+ ?? errors.NoHandlerForExtension.throw(ending, name);
8
8
  };
@@ -1,21 +1,10 @@
1
1
  import {File} from "runtime-compat/fs";
2
- const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
3
2
 
4
3
  const pre = async app => {
5
- const {paths} = app;
6
-
7
- // remove public directory in case exists
8
- if (await paths.public.exists) {
9
- await paths.public.file.remove();
10
- }
11
- await paths.public.file.create();
12
-
4
+ const {paths, config} = app;
13
5
  if (await paths.static.exists) {
14
- // copy static files to public
15
- const filter = file => app.config.http.static.pure
16
- ? true
17
- : !file.endsWith(".js") && !file.endsWith(".css");
18
- await File.copy(paths.static, paths.public, filter);
6
+ // copy static files to build/client/_static
7
+ await File.copy(paths.static, paths.client.join(config.build.static));
19
8
  }
20
9
  };
21
10
 
@@ -23,7 +12,7 @@ export default async (app, bundle) => {
23
12
  await pre(app);
24
13
  if (bundle) {
25
14
  app.log.info("running bundle hooks", {module: "primate"});
26
- await [...filter("bundle", app.modules), _ => _]
15
+ await [...app.modules.bundle, _ => _]
27
16
  .reduceRight((acc, handler) => input => handler(input, acc))(app);
28
17
  }
29
18
  };
@@ -1,7 +1,26 @@
1
- const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
1
+ import copy_includes from "./copy_includes.js"
2
+
3
+ const pre = async app => {
4
+ const {paths, config} = app;
5
+ const build = config.build;
6
+
7
+ // remove build directory in case exists
8
+ if (await paths.build.exists) {
9
+ await paths.build.file.remove();
10
+ }
11
+ await paths.server.file.create();
12
+
13
+ if (await paths.components.exists) {
14
+ await app.copy(paths.components, paths.server.join(build.app));
15
+ }
16
+
17
+ // copy additional subdirectories to build/server
18
+ await copy_includes(app, "server");
19
+ };
2
20
 
3
21
  export default async app => {
22
+ await pre(app);
4
23
  app.log.info("running compile hooks", {module: "primate"});
5
- await [...filter("compile", app.modules), _ => _]
24
+ await [...app.modules.compile, _ => _]
6
25
  .reduceRight((acc, handler) => input => handler(input, acc))(app);
7
26
  };
@@ -0,0 +1,24 @@
1
+ const system = ["routes", "components", "build"];
2
+
3
+ export default async (app, type, post = () => undefined) => {
4
+ const {paths, config} = app;
5
+ const {build} = config;
6
+ const {includes} = build;
7
+
8
+ const reserved = system.concat(build.static, build.app, build.modules);
9
+
10
+ if (Array.isArray(includes)) {
11
+ await Promise.all(includes
12
+ .filter(include => !reserved.includes(include))
13
+ .filter(include => /^[^/]*$/u.test(include))
14
+ .map(async include => {
15
+ const path = app.root.join(include);
16
+ if (await path.exists) {
17
+ const to = paths[type].join(include);
18
+ await to.file.create();
19
+ await app.copy(path, to);
20
+ await post(to);
21
+ }
22
+ }));
23
+ }
24
+ };
@@ -1,70 +1,83 @@
1
- import {Response} from "runtime-compat/http";
2
- import {error as clientError} from "../handlers/exports.js";
3
- import {mime, isResponse, respond} from "./handle/exports.js";
1
+ import {Response, Status} from "runtime-compat/http";
2
+ import {tryreturn} from "runtime-compat/flow";
3
+ import {mime, isResponse, respond} from "./respond/exports.js";
4
4
  import {invalid} from "./route.js";
5
+ import {error as clientError} from "../handlers/exports.js";
5
6
  import errors from "../errors.js";
6
- import {OK} from "../http-statuses.js";
7
7
 
8
- const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
8
+ const guardError = Symbol("guardError");
9
9
 
10
10
  export default app => {
11
- const {http} = app.config;
11
+ const {config: {http, build}, paths} = app;
12
12
 
13
- const _respond = async (request, headers) => {
13
+ const run = async request => {
14
14
  const {pathname} = request.url;
15
- return invalid(pathname)
16
- ? errors.NoFileForPath.throw({pathname, config: app.config})
17
- : (await respond(await app.route(request)))(app, headers);
18
- };
19
-
20
- const route = async request => {
21
- const headers = app.generateHeaders();
15
+ const {path, guards, layouts, handler} = invalid(pathname)
16
+ ? errors.NoFileForPath.throw(pathname, paths.static)
17
+ : await app.route(request);
22
18
 
19
+ // handle guards
23
20
  try {
24
- const response = await _respond(request, headers);
25
- return isResponse(response) ? response : new Response(...response);
21
+ guards.map(guard => {
22
+ const result = guard(request);
23
+ if (result === true) {
24
+ return undefined;
25
+ }
26
+ const error = new Error();
27
+ error.result = result;
28
+ error.type = guardError;
29
+ throw error;
30
+ });
26
31
  } catch (error) {
27
- app.log.auto(error);
28
- return new Response(...await clientError()(app, {}));
32
+ if (error.type === guardError) {
33
+ return (await respond(error.result))(app);
34
+ }
35
+ // rethrow if not guard error
36
+ throw error;
29
37
  }
38
+
39
+ // handle request
40
+ const handlers = [...app.modules.route, handler]
41
+ .reduceRight((chain, next) => input => next(input, chain));
42
+
43
+ return (await respond(await handlers({...request, path})))(app, {
44
+ layouts: await Promise.all(layouts.map(layout => layout(request))),
45
+ });
30
46
  };
31
47
 
32
- const staticResource = async file => new Response(file.readable, {
33
- status: OK,
48
+ const route = async request =>
49
+ tryreturn(async _ => {
50
+ const response = await run(request);
51
+ return isResponse(response) ? response : new Response(...response);
52
+ }).orelse(async error => {
53
+ app.log.auto(error);
54
+ return new Response(...await clientError()(app, {}));
55
+ });
56
+
57
+ const asset = async file => new Response(file.readable, {
58
+ status: Status.OK,
34
59
  headers: {
35
60
  "Content-Type": mime(file.name),
36
61
  Etag: await file.modified,
37
62
  },
38
63
  });
39
64
 
40
- const publishedResource = request => {
41
- const published = app.resources.find(({src, inline}) =>
42
- !inline && src === request.url.pathname);
43
- if (published !== undefined) {
44
- return new Response(published.code, {
45
- status: OK,
46
- headers: {
47
- "Content-Type": mime(published.src),
48
- Etag: published.integrity,
49
- },
50
- });
51
- }
52
-
53
- return route(request);
54
- };
55
-
56
- const resource = async request => {
65
+ const handle = async request => {
57
66
  const {pathname} = request.url;
58
67
  const {root} = http.static;
59
68
  if (pathname.startsWith(root)) {
60
- const path = app.paths.public.join(pathname.replace(root, ""));
61
- return await path.isFile
62
- ? staticResource(path.file)
63
- : publishedResource(request);
69
+ const debased = pathname.replace(root, _ => "");
70
+ // try static first
71
+ const _static = paths.client.join(build.static, debased);
72
+ if (await _static.isFile) {
73
+ return asset(_static.file);
74
+ }
75
+ const _app = app.paths.client.join(debased);
76
+ return await _app.isFile ? asset(_app.file) : route(request);
64
77
  }
65
78
  return route(request);
66
79
  };
67
80
 
68
- return [...filter("handle", app.modules), resource]
81
+ return [...app.modules.handle, handle]
69
82
  .reduceRight((acc, handler) => input => handler(input, acc));
70
83
  };
@@ -1,9 +1,12 @@
1
1
  import {URL} from "runtime-compat/http";
2
+ import {tryreturn} from "runtime-compat/flow";
2
3
  import errors from "../errors.js";
3
4
 
5
+ const {fromEntries: from} = Object;
6
+
4
7
  const contents = {
5
- "application/x-www-form-urlencoded": body =>
6
- Object.fromEntries(body.split("&").map(part => part.split("=")
8
+ "application/x-www-form-urlencoded": body => from(body.split("&")
9
+ .map(part => part.split("=")
7
10
  .map(subpart => decodeURIComponent(subpart).replaceAll("+", " ")))),
8
11
  "application/json": body => JSON.parse(body),
9
12
  };
@@ -15,14 +18,9 @@ export default dispatch => async request => {
15
18
  return type === undefined ? body : type(body);
16
19
  };
17
20
 
18
- const parseContent = async (request, body) => {
19
- const contentType = request.headers.get("content-type");
20
- try {
21
- return parseContentType(contentType, body);
22
- } catch (error) {
23
- return errors.CannotParseBody.throw({body, contentType});
24
- }
25
- };
21
+ const parseContent = async (contentType, body) =>
22
+ tryreturn(_ => parseContentType(contentType, body))
23
+ .orelse(_ => errors.CannotParseBody.throw(body, contentType));
26
24
 
27
25
  const parseBody = async request => {
28
26
  if (request.body === null) {
@@ -38,12 +36,11 @@ export default dispatch => async request => {
38
36
  }
39
37
  } while (!result.done);
40
38
 
41
- return parseContent(request, chunks.join());
39
+ return parseContent(request.headers.get("content-type"), chunks.join());
42
40
  };
43
41
 
44
42
  const cookies = request.headers.get("cookie");
45
- const _url = request.url;
46
- const url = new URL(_url.endsWith("/") ? _url.slice(0, -1) : _url);
43
+ const url = new URL(request.url);
47
44
 
48
45
  const body = await parseBody(request);
49
46
  return {
@@ -52,8 +49,8 @@ export default dispatch => async request => {
52
49
  body: dispatch(body),
53
50
  cookies: dispatch(cookies === null
54
51
  ? {}
55
- : Object.fromEntries(cookies.split(";").map(c => c.trim().split("=")))),
56
- headers: dispatch(Object.fromEntries(request.headers)),
57
- query: dispatch(Object.fromEntries(url.searchParams)),
52
+ : from(cookies.split(";").map(c => c.trim().split("=")))),
53
+ headers: dispatch(from(request.headers)),
54
+ query: dispatch(from(url.searchParams)),
58
55
  };
59
56
  };
@@ -1,36 +1,55 @@
1
1
  import {Path} from "runtime-compat/fs";
2
-
3
- const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
2
+ import {identity} from "runtime-compat/function";
3
+ import copy_includes from "./copy_includes.js"
4
4
 
5
5
  const post = async app => {
6
- // after hook, publish a zero assumptions app.js (no css imports)
7
- const code = app.entrypoints.filter(({type}) => type === "script")
8
- .map(entrypoint => entrypoint.code).join("");
9
- await app.publish({src: `${app.config.dist}.js`, code, type: "module"});
6
+ const {config} = app;
7
+ const build = config.build.app;
8
+ {
9
+ // after hook, publish a zero assumptions app.js (no css imports)
10
+ const code = app.entrypoints.filter(({type}) => type === "script")
11
+ .map(entrypoint => entrypoint.code).join("");
12
+ const src = new Path(config.http.static.root, build, config.build.index);
13
+ await app.publish({src, code, type: "module"});
14
+
15
+ await app.copy(app.paths.components, app.paths.client.join(build));
10
16
 
11
- if (!app.config.http.static.pure) {
12
- const memoryFiles = await Path.collect(app.paths.static, /\.(?:js|css)$/u,
13
- {recursive: false});
14
- await Promise.all(memoryFiles.map(async file => {
15
- const code = await file.text();
16
- const src = file.name;
17
- await app.publish({src, code, type: file.extension === ".js" ?
18
- "module" : "style"});
19
- if (file.extension === ".css") {
20
- app.bootstrap({type: "style", code: `import "./${file.name}";`});
21
- }
22
- }));
17
+ const imports = {...app.importmaps, app: src.path};
18
+ await app.publish({
19
+ inline: true,
20
+ code: JSON.stringify({imports}, null, 2),
21
+ type: "importmap",
22
+ });
23
23
  }
24
- await Promise.all(Object.entries(app.library).map(async libfile => {
25
- const [, src] = libfile;
26
- const code = await Path.resolve().join("node_modules", src).text();
27
- await app.publish({src, code, type: "module"});
24
+
25
+ const imports = await Path.collect(app.paths.static, /\.(?:js|css)$/u);
26
+ await Promise.all(imports.map(async file => {
27
+ const code = await file.text();
28
+ const src = file.name;
29
+ const isCSS = file.extension === ".css";
30
+ await app.publish({src: `${config.build.static}/${src}`, code,
31
+ type: isCSS ? "style" : "module"});
32
+ if (isCSS) {
33
+ app.bootstrap({type: "style",
34
+ code: `import "../${config.build.static}/${file.name}";`});
35
+ }
28
36
  }));
37
+
38
+ const source = `${app.paths.client}`;
39
+ const {root} = app.config.http.static;
40
+ // copy additional subdirectories to build/client
41
+ await copy_includes(app, "client", async to =>
42
+ Promise.all((await to.collect(/\.js$/u)).map(async script => {
43
+ const code = await script.text();
44
+ const src = new Path(root, script.path.replace(source, () => ""));
45
+ await app.publish({src, code, type: "module"});
46
+ }))
47
+ );
29
48
  };
30
49
 
31
50
  export default async app => {
32
51
  app.log.info("running publish hooks", {module: "primate"});
33
- await [...filter("publish", app.modules), _ => _]
52
+ await [...app.modules.publish, identity]
34
53
  .reduceRight((acc, handler) => input => handler(input, acc))(app);
35
54
  await post(app);
36
55
  };
@@ -1,7 +1,5 @@
1
- const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
2
-
3
1
  export default async app => {
4
2
  app.log.info("running register hooks", {module: "primate"});
5
- await [...filter("register", app.modules), _ => _]
3
+ await [...app.modules.register, _ => _]
6
4
  .reduceRight((acc, handler) => input => handler(input, acc))(app);
7
5
  };
@@ -14,7 +14,7 @@ const isNonNullObject = value => typeof value === "object" && value !== null;
14
14
  const isObject = value => isNonNullObject(value)
15
15
  ? json(value) : isText(value);
16
16
  const isResponse = value => isResponseDuck(value)
17
- ? () => value : isObject(value);
17
+ ? _ => value : isObject(value);
18
18
  const isStream = value => value instanceof ReadableStream
19
19
  ? stream(value) : isResponse(value);
20
20
  const isBlob = value => value instanceof Blob