primate 0.31.13 → 0.32.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.
Files changed (46) hide show
  1. package/LICENSE +19 -0
  2. package/package.json +13 -6
  3. package/src/bin.js +3 -3
  4. package/src/commands/build.js +4 -0
  5. package/src/commands/dev.js +7 -2
  6. package/src/commands/exports.js +2 -1
  7. package/src/commands/serve.js +7 -2
  8. package/src/handlers/error.js +1 -0
  9. package/src/handlers/json.js +1 -0
  10. package/src/handlers/redirect.js +1 -0
  11. package/src/handlers/sse.js +1 -0
  12. package/src/handlers/stream.js +1 -0
  13. package/src/handlers/text.js +1 -0
  14. package/src/handlers/view.js +1 -0
  15. package/src/handlers/ws.js +1 -0
  16. package/src/init.js +12 -0
  17. package/README.md +0 -14
  18. package/src/Logger.js +0 -111
  19. package/src/app.js +0 -220
  20. package/src/defaults/app.html +0 -9
  21. package/src/defaults/error.html +0 -14
  22. package/src/defaults/primate.config.js +0 -55
  23. package/src/dispatch.js +0 -28
  24. package/src/errors.js +0 -8
  25. package/src/errors.json +0 -130
  26. package/src/exports.js +0 -9
  27. package/src/handlers.js +0 -93
  28. package/src/hooks/copy_includes.js +0 -20
  29. package/src/hooks/exports.js +0 -7
  30. package/src/hooks/handle.js +0 -113
  31. package/src/hooks/init.js +0 -3
  32. package/src/hooks/parse.js +0 -17
  33. package/src/hooks/publish.js +0 -3
  34. package/src/hooks/register.js +0 -60
  35. package/src/hooks/respond.js +0 -29
  36. package/src/hooks/route.js +0 -49
  37. package/src/hooks/stage.js +0 -64
  38. package/src/loaders/common.js +0 -32
  39. package/src/loaders/exports.js +0 -2
  40. package/src/loaders/modules.js +0 -33
  41. package/src/loaders/types.js +0 -29
  42. package/src/run.js +0 -42
  43. package/src/start.js +0 -66
  44. package/src/to_sorted.js +0 -1
  45. package/src/validate.js +0 -10
  46. package/types/index.d.ts +0 -69
package/src/errors.json DELETED
@@ -1,130 +0,0 @@
1
- {
2
- "module": "primate",
3
- "errors": {
4
- "DoubleFileExtension": {
5
- "message": "double file extension {0}",
6
- "fix": "unload one of the two handlers registering the file extension",
7
- "level": "Error"
8
- },
9
- "DoubleModule": {
10
- "message": "double module {0} in {1}",
11
- "fix": "load {0} only once",
12
- "level": "Error"
13
- },
14
- "DoublePathParameter": {
15
- "message": "double path parameter {0} in route {1}",
16
- "fix": "disambiguate path parameters in route names",
17
- "level": "Error"
18
- },
19
- "DoubleRoute": {
20
- "message": "double route of the form {0}",
21
- "fix": "disambiguate routes",
22
- "level": "Error"
23
- },
24
- "EmptyConfigFile": {
25
- "message": "empty config file at {0}",
26
- "fix": "add configuration options to the file or remove it",
27
- "level": "Warn"
28
- },
29
- "EmptyPathParameter": {
30
- "message": "empty path parameter {0} in route {1}",
31
- "fix": "name the parameter or remove it",
32
- "level": "Error"
33
- },
34
- "EmptyRouteFile": {
35
- "message": "empty route file at {0}",
36
- "fix": "add routes to the file or remove it",
37
- "level": "Warn"
38
- },
39
- "EmptyDirectory": {
40
- "message": "empty {0} directory",
41
- "fix": "populate {1} or remove it",
42
- "level": "Warn"
43
- },
44
- "ErrorInConfigFile": {
45
- "message": "error in config file: {0}",
46
- "fix": "check errors in config file by running {1}",
47
- "level": "Error"
48
- },
49
- "InvalidBodyReturned": {
50
- "message": "invalid body returned from route, got {0}",
51
- "fix": "return a proper body from route",
52
- "level": "Error"
53
- },
54
- "InvalidDefaultExport": {
55
- "message": "invalid default export at {0}",
56
- "fix": "use only functions for the default export",
57
- "level": "Error"
58
- },
59
- "InvalidPath": {
60
- "message": "invalid path {0}",
61
- "fix": "use only letters, digits, '_', '[', ']' or '=' in path filenames",
62
- "level": "Error"
63
- },
64
- "InvalidTypeExport": {
65
- "message": "invalid type export at {0}",
66
- "fix": "export object with a `base` string and a `validate` function",
67
- "level": "Error"
68
- },
69
- "InvalidTypeName": {
70
- "message": "invalid type name {0}",
71
- "fix": "use lowercase-first latin letters and decimals in type names",
72
- "level": "Error"
73
- },
74
- "MismatchedBody": {
75
- "message": "{0}: {1}",
76
- "fix": "make sure the body payload corresponds to the used content type",
77
- "level": "Error"
78
- },
79
- "MismatchedPath": {
80
- "message": "mismatched path {0}: {1}",
81
- "fix": "fix the type or the caller",
82
- "level": "Info"
83
- },
84
- "MismatchedType": {
85
- "message": "mismatched type: {0}",
86
- "fix": "fix the type or the caller",
87
- "level": "Info"
88
- },
89
- "ModuleHasNoHooks": {
90
- "message": "module {0} has no hooks",
91
- "fix": "ensure every module uses at least one hook or deactivate it",
92
- "level": "Warn"
93
- },
94
- "ModuleHasNoName": {
95
- "message": "module at index {0} has no name",
96
- "fix": "update module at index {0} and inform maintainer",
97
- "level": "Error"
98
- },
99
- "ModulesMustBeArray" :{
100
- "message": "the {0} config property must be an array",
101
- "fix": "change {0} to an array in the config or remove this property",
102
- "level": "Error"
103
- },
104
- "NoHandlerForComponent": {
105
- "message": "no handler for {0}",
106
- "fix": "add handler module for this component or remove {0}",
107
- "level": "Error"
108
- },
109
- "NoRouteToPath": {
110
- "message": "no {0} route to {1}",
111
- "fix": "create a {0} route function at {2}.js",
112
- "level": "Info"
113
- },
114
- "OptionalRoute": {
115
- "message": "optional route {0} must be a leaf",
116
- "fix": "move route to leaf (last) position in filesystem hierarchy",
117
- "level": "Error"
118
- },
119
- "ReservedTypeName": {
120
- "message": "reserved type name {0}",
121
- "fix": "do not use any reserved type names",
122
- "level": "Error"
123
- },
124
- "RestRoute": {
125
- "message": "rest route {0} must be a leaf",
126
- "fix": "move route to leaf (last) position in filesystem hierarchy",
127
- "level": "Error"
128
- }
129
- }
130
- }
package/src/exports.js DELETED
@@ -1,9 +0,0 @@
1
- import run from "./run.js";
2
-
3
- export * from "./handlers.js";
4
-
5
- export { default as Logger } from "./Logger.js";
6
-
7
- export { URL, Response, Status, MediaType } from "rcompat/http";
8
-
9
- export default command => run(command);
package/src/handlers.js DELETED
@@ -1,93 +0,0 @@
1
- import FS from "rcompat/fs";
2
- import { MediaType, Status } from "rcompat/http";
3
- import { identity } from "rcompat/function";
4
- import { HTML } from "rcompat/string";
5
- import errors from "./errors.js";
6
-
7
- const handle = (mediatype, mapper = identity) => (body, options) => app =>
8
- app.respond(mapper(body), app.media(mediatype, options));
9
-
10
- // {{{ text
11
- const text = handle(MediaType.TEXT_PLAIN);
12
- // }}}
13
- // {{{ json
14
- const json = handle(MediaType.APPLICATION_JSON, JSON.stringify);
15
- // }}}
16
- // {{{ stream
17
- const stream = handle(MediaType.APPLICATION_OCTET_STREAM);
18
- // }}}
19
- // {{{ ws
20
- const ws = implementation => ({ server }, _, { original }) =>
21
- server.upgrade(original, implementation);
22
- // }}}
23
- // {{{ sse
24
- const sse = handle(MediaType.TEXT_EVENT_STREAM, implementation =>
25
- new ReadableStream({
26
- start(controller) {
27
- implementation.open({
28
- send(name, data) {
29
- const event = data === undefined ? "" : `event: ${name}\n`;
30
- const $data = data === undefined ? name : data;
31
- controller.enqueue(`${event}data:${JSON.stringify($data)}\n\n`);
32
- },
33
- });
34
- },
35
- cancel() {
36
- implementation.close?.();
37
- },
38
- }));
39
- // }}}
40
- // {{{ redirect
41
- const redirect = (Location, { status = Status.FOUND } = {}) => app =>
42
- /* no body */
43
- app.respond(null, { status, headers: { Location } });
44
- // }}}
45
- // {{{ error
46
- const error = (body = "Not Found", { status = Status.NOT_FOUND, page } = {}) =>
47
- app => app.view({ body, status, page: page ?? app.get("pages.error") });
48
- // }}}
49
- // {{{ html
50
- const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
51
- const style_re = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
52
- const remove = /<(?<tag>script|style)>.*?<\/\k<tag>>/gus;
53
- const render = (component, props = {}) => {
54
- const encoded = JSON.parse(HTML.escape(JSON.stringify(props)));
55
- const keys = Object.keys(encoded);
56
- const values = Object.values(encoded);
57
- return new Function(...keys, `return \`${component}\`;`)(...values);
58
- };
59
- const html = (name, props, options = {}) => async app => {
60
- const location = app.get("location");
61
- const components = app.runpath(location.server, location.components);
62
- const component = await components.join(name).text();
63
- const { head: xhead = [], csp = {}, headers, ...rest } = options;
64
- const { script_src: xscript_src = [], style_src: xstyle_src = [] } = csp;
65
- const scripts = await Promise.all([...component.matchAll(script_re)]
66
- .map(({ groups: { code } }) => app.inline(code, "module")));
67
- const styles = await Promise.all([...component.matchAll(style_re)]
68
- .map(({ groups: { code } }) => app.inline(code, "style")));
69
- const style_src = styles.map(asset => asset.integrity).concat(xstyle_src);
70
- const script_src = scripts.map(asset => asset.integrity).concat(xscript_src);
71
- const head = [...scripts, ...styles].map(asset => asset.head);
72
-
73
- return app.view({
74
- body: render(component.replaceAll(remove, _ => ""), props),
75
- head: [...head, ...xhead].join("\n"),
76
- headers: {
77
- ...app.headers({ "style-src": style_src, "script-src": script_src }),
78
- ...headers,
79
- },
80
- ...rest,
81
- });
82
- };
83
- // }}}
84
- // {{{ view
85
- const extensions = ["fullExtension", "extension"];
86
- const view = (name, props, options) => (app, ...rest) => extensions
87
- .map(extension => app.extensions[new FS.File(name)[extension]])
88
- .find(extension => extension?.handle)
89
- ?.handle(name, props, options)(app, ...rest)
90
- ?? errors.NoHandlerForComponent.throw(name);
91
- // }}}
92
-
93
- export { text, json, stream, redirect, error, html, view, ws, sse };
@@ -1,20 +0,0 @@
1
- import FS from "rcompat/fs";
2
-
3
- export default async (app, type, post = () => undefined) => {
4
- const includes = app.get("build.includes");
5
- const reserved = Object.values(app.get("location"));
6
-
7
- if (Array.isArray(includes)) {
8
- await Promise.all(includes
9
- .filter(include => !reserved.includes(include))
10
- .filter(include => /^[^/]*$/u.test(include))
11
- .map(async include => {
12
- const path = app.root.join(include);
13
- if (await path.exists()) {
14
- const target = FS.File.join(type, include);
15
- await app.stage(path, target);
16
- await post(target);
17
- }
18
- }));
19
- }
20
- };
@@ -1,7 +0,0 @@
1
- export { default as init } from "./init.js";
2
- export { default as stage } from "./stage.js";
3
- export { default as register } from "./register.js";
4
- export { default as publish } from "./publish.js";
5
- export { default as route } from "./route.js";
6
- export { default as handle } from "./handle.js";
7
- export { default as parse } from "./parse.js";
@@ -1,113 +0,0 @@
1
- import { Response, Status, MediaType, fetch } from "rcompat/http";
2
- import { cascade, tryreturn } from "rcompat/async";
3
- import respond from "./respond.js";
4
- import { error as clientError } from "../handlers.js";
5
-
6
- const guard_error = Symbol("guard_error");
7
- const guard = (app, guards) => async (request, next) => {
8
- // handle guards
9
- try {
10
- for (const guard of guards) {
11
- const result = await guard(request);
12
- if (result !== true) {
13
- const error = new Error();
14
- error.result = result;
15
- error.type = guard_error;
16
- throw error;
17
- }
18
- }
19
-
20
- return next(request);
21
- } catch (error) {
22
- if (error.type === guard_error) {
23
- return { request, response: respond(error.result)(app) };
24
- }
25
- // rethrow if not guard error
26
- throw error;
27
- }
28
- };
29
-
30
- const get_layouts = async (layouts, request) => {
31
- const stop_at = layouts.findIndex(({ recursive }) => recursive === false);
32
- return Promise.all(layouts
33
- .slice(stop_at === -1 ? 0 : stop_at)
34
- .map(layout => layout(request)));
35
- };
36
- // last handler, preserve final request form
37
- const last = handler => async request => {
38
- const response = await handler(request);
39
- return { request, response };
40
- };
41
-
42
- export default app => {
43
- const route = request => app.route(request);
44
-
45
- const as_route = async request => {
46
- // if tryreturn throws, this will default
47
- let error_handler = app.error.default;
48
-
49
- return tryreturn(async _ => {
50
- const { body, path, guards, errors, layouts, handler } =
51
- await route(request);
52
-
53
- error_handler = errors?.at(-1);
54
-
55
- const hooks = [...app.modules.route, guard(app, guards), last(handler)];
56
-
57
- // handle request
58
- const routed = await (await cascade(hooks))({ ...request, body, path });
59
-
60
- const $layouts = { layouts: await get_layouts(layouts, routed.request) };
61
- return respond(routed.response)(app, $layouts, routed.request);
62
- }).orelse(async error => {
63
- app.log.auto(error);
64
-
65
- // the +error.js page itself could fail
66
- return tryreturn(_ => respond(error_handler(request))(app, {}, request))
67
- .orelse(_ => clientError()(app, {}, request));
68
- });
69
- };
70
-
71
- const as_asset = async path => new Response(path.stream(), {
72
- status: Status.OK,
73
- headers: {
74
- "Content-Type": MediaType.resolve(path.name),
75
- Etag: await path.modified(),
76
- },
77
- });
78
-
79
- const location = app.get("location");
80
- const root = app.get("http.static.root");
81
- const client = app.runpath(location.client);
82
- const handle = async request => {
83
- const { pathname } = request.url;
84
- if (pathname.startsWith(root)) {
85
- const debased = pathname.replace(root, _ => "");
86
- // try static first
87
- const asset = client.join(location.static, debased);
88
- if (await asset.isFile) {
89
- return as_asset(asset);
90
- }
91
- const path = client.join(debased);
92
- if (await path.isFile) {
93
- return as_asset(path);
94
- }
95
- }
96
- return as_route(request);
97
- };
98
- // first hook
99
- const pass = (request, next) => next({
100
- ...request,
101
- pass(to) {
102
- const { method, headers, body } = request.original;
103
- const input = `${to}${request.url.pathname}`;
104
-
105
- return fetch(input, { headers, method, body, duplex: "half" });
106
- },
107
- });
108
- const hotreload = (request, next) => app.mode === "development"
109
- ? app.build.proxy(request, next)
110
- : next(request);
111
-
112
- return cascade([pass, hotreload, ...app.modules.handle], handle);
113
- };
package/src/hooks/init.js DELETED
@@ -1,3 +0,0 @@
1
- import { cascade } from "rcompat/async";
2
-
3
- export default async app => (await cascade(app.modules.init))(app);
@@ -1,17 +0,0 @@
1
- import { URL } from "rcompat/http";
2
- import o from "rcompat/object";
3
-
4
- export default app => async original => {
5
- const { headers } = original;
6
-
7
- const url = new URL(original.url);
8
- const cookies = headers.get("cookie");
9
-
10
- return { original, url,
11
- ...o.valmap({
12
- query: [o.from(url.searchParams), url.search],
13
- headers: [o.from(headers), headers, false],
14
- cookies: [o.from(cookies?.split(";").map(cookie =>
15
- cookie.trim().split("=")) ?? []), cookies],
16
- }, value => app.dispatch(...value)) };
17
- };
@@ -1,3 +0,0 @@
1
- import { cascade } from "rcompat/async";
2
-
3
- export default async app => (await cascade(app.modules.publish))(app);
@@ -1,60 +0,0 @@
1
- import FS from "rcompat/fs";
2
- import { cascade } from "rcompat/async";
3
- import copy_includes from "./copy_includes.js";
4
-
5
- const html = /^.*.html$/u;
6
- const defaults = new FS.File(import.meta.url).up(2).join("defaults");
7
-
8
- const pre = async app => {
9
- const pages = app.get("location.pages");
10
-
11
- // copy framework pages
12
- await app.stage(defaults, pages, html);
13
- // overwrite transformed pages to build
14
- await app.path.pages.exists() && await app.stage(app.path.pages, pages, html);
15
-
16
- return app;
17
- };
18
-
19
- const post = async app => {
20
- const _static = app.path.static;
21
- const location = app.get("location");
22
-
23
- if (await _static.exists()) {
24
- // copy static files to build/server/static
25
- await app.stage(_static, FS.File.join(location.server, location.static));
26
-
27
- // copy static files to build/client/static
28
- await app.stage(_static, FS.File.join(location.client, location.static));
29
-
30
- // copy static files to build/static
31
- await app.stage(_static, FS.File.join(location.static));
32
-
33
- // publish JavaScript and CSS files
34
- const imports = await FS.File.collect(_static, /\.(?:css)$/u);
35
- await Promise.all(imports.map(async file => {
36
- const src = file.debase(_static);
37
- app.build.export(`import "./${location.static}${src}";`);
38
- }));
39
- }
40
-
41
- // copy additional subdirectories to build/server
42
- await copy_includes(app, location.server);
43
- // copy additional subdirectories to build
44
- await copy_includes(app, "");
45
-
46
- if (await app.path.components.exists()) {
47
- // copy components to build/components
48
- await app.stage(app.path.components, FS.File.join(location.components));
49
-
50
- const components = await app.runpath(location.components).collect();
51
-
52
- // from the build directory, compile to server and client
53
- await Promise.all(components.map(component => app.compile(component)));
54
- }
55
-
56
- return app;
57
- };
58
-
59
- export default async app =>
60
- post(await (await cascade(app.modules.register))(await pre(app)));
@@ -1,29 +0,0 @@
1
- import FS from "rcompat/fs";
2
- import { URL, Response } from "rcompat/http";
3
- import { identity } from "rcompat/function";
4
- import o from "rcompat/object";
5
- import { text, json, stream, redirect } from "primate";
6
- import errors from "../errors.js";
7
-
8
- const not_found = value => errors.InvalidBodyReturned.throw(value);
9
- const is_text = value => typeof value === "string";
10
- const is_instance = of => value => value instanceof of;
11
- const is_response = is_instance(globalThis.Response);
12
- const is_fake_response = is_instance(Response);
13
- const is_streamable =
14
- value => value instanceof FS.Blob || value?.streamable === FS.s_streamable;
15
-
16
- // [if, then]
17
- const guesses = [
18
- [is_instance(URL), redirect],
19
- [is_streamable, value => stream(value.stream())],
20
- [is_instance(ReadableStream), stream],
21
- [value => is_response(value) || is_fake_response(value), value => _ => value],
22
- [o.proper, json],
23
- [is_text, text],
24
- [not_found, identity],
25
- ];
26
-
27
- const guess = value => guesses.find(([check]) => check(value))?.[1](value);
28
-
29
- export default result => typeof result === "function" ? result : guess(result);
@@ -1,49 +0,0 @@
1
- import o from "rcompat/object";
2
- import { tryreturn } from "rcompat/sync";
3
- import { Body } from "rcompat/http";
4
- import $errors from "../errors.js";
5
- import validate from "../validate.js";
6
-
7
- const { MismatchedBody, MismatchedPath, NoRouteToPath } = $errors;
8
-
9
- const deroot = pathname => pathname.endsWith("/") && pathname !== "/"
10
- ? pathname.slice(0, -1) : pathname;
11
-
12
- const parse_body = (request, url) =>
13
- tryreturn(async _ => await Body.parse(request) ?? {})
14
- .orelse(error => MismatchedBody.throw(url.pathname, error.message));
15
-
16
- export default app => {
17
- const $request_body_parse = app.get("request.body.parse");
18
- const $location = app.get("location");
19
-
20
- const index = path => `${$location.routes}${path === "/" ? "/index" : path}`;
21
- // remove excess slashes
22
- const deslash = url => url.replaceAll(/\/{2,}/gu, _ => "/");
23
-
24
- return async ({ original, url }) => {
25
- const pathname = deroot(deslash(url.pathname));
26
- const route = await app.router.match(original) ?? NoRouteToPath
27
- .throw(original.method.toLowerCase(), pathname, index(pathname));
28
- const { params } = route;
29
- const untyped_path = Object.fromEntries(Object.entries(params)
30
- .filter(([name]) => !name.includes("="))
31
- .map(([key, value]) => [key, value]));
32
- const typed_path = Object.fromEntries(Object.entries(params)
33
- .filter(([name]) => name.includes("="))
34
- .map(([name, value]) => [name.split("="), value])
35
- .map(([[name, type], value]) =>
36
- tryreturn(_ => {
37
- validate(app.types[type], value, name);
38
- return [name, value];
39
- }).orelse(({ message }) => MismatchedPath.throw(pathname, message))));
40
- const path = app.dispatch({ ...untyped_path, ...typed_path });
41
- const local_parse_body = route.file.body?.parse ?? $request_body_parse;
42
- const body = local_parse_body ? await parse_body(original, url) : null;
43
- const { guards = [], errors = [], layouts = [] } = o.map(route.specials,
44
- ([key, value]) => [`${key}s`, value.map(i => i.default)]);
45
- const handler = route.file.default[original.method.toLowerCase()];
46
-
47
- return { body, path, guards, errors, layouts, handler };
48
- };
49
- };
@@ -1,64 +0,0 @@
1
- import FS from "rcompat/fs";
2
- import { cascade } from "rcompat/async";
3
- import dispatch from "../dispatch.js";
4
- import * as loaders from "../loaders/exports.js";
5
- import { doubled } from "../loaders/common.js";
6
- import errors from "../errors.js";
7
-
8
- const pre = async app => {
9
- // remove build directory in case exists
10
- await app.path.build.remove();
11
- await app.path.build.create();
12
- await Promise.all(["server", "client", "pages", "components"]
13
- .map(directory => app.runpath(directory).create()));
14
-
15
- return { ...app };
16
- };
17
-
18
- const post = async app => {
19
- const $location = app.get("location");
20
-
21
- // stage routes
22
- if (await app.path.routes.exists()) {
23
- await app.stage(app.path.routes, $location.routes);
24
- }
25
- if (await app.path.types.exists()) {
26
- await app.stage(app.path.types, $location.types);
27
- }
28
- const user_types = await loaders.types(app.log, app.runpath($location.types));
29
- const types = { ...app.types, ...user_types };
30
-
31
- const directory = app.runpath($location.routes);
32
- for (const path of await directory.collect()) {
33
- await app.extensions[path.extension]
34
- ?.route(directory, path.debase(`${directory}/`), types);
35
- }
36
-
37
- let router;
38
-
39
- try {
40
- router = await FS.Router.load({
41
- directory,
42
- specials: {
43
- guard: { recursive: true },
44
- error: { recursive: false },
45
- layout: { recursive: true },
46
- },
47
- predicate(route, request) {
48
- return route.default[request.method.toLowerCase()] !== undefined;
49
- },
50
- });
51
- } catch (error) {
52
- const { DoubleRoute, OptionalRoute, RestRoute } = FS.Router.Error;
53
- error instanceof DoubleRoute && errors.DoubleRoute.throw(error.route);
54
- error instanceof OptionalRoute && errors.OptionalRoute.throw(error.route);
55
- error instanceof RestRoute && errors.RestRoute.throw(error.route);
56
- // rethrow original error
57
- throw error;
58
- }
59
- const layout = { depth: router.depth("layout") };
60
- return { ...app, types, dispatch: dispatch(types), layout, router };
61
- };
62
-
63
- export default async app =>
64
- post(await (await cascade(app.modules.stage))(await pre(app)));
@@ -1,32 +0,0 @@
1
- import FS from "rcompat/fs";
2
- import { identity } from "rcompat/function";
3
- import errors from "../errors.js";
4
-
5
- const ending = ".js";
6
-
7
- const empty = log => (objects, name, path) =>
8
- Object.keys(objects).length === 0
9
- && errors.EmptyDirectory.warn(log, name, path);
10
-
11
- export default async ({
12
- log,
13
- directory,
14
- name = "routes",
15
- filter = identity,
16
- recursive = true,
17
- warn = true,
18
- } = {}) => {
19
- const objects = directory === undefined ? [] : await Promise.all(
20
- (await FS.File.collect(directory, /^.*.js$/u, { recursive }))
21
- .filter(filter)
22
- .map(async file => [
23
- `${file}`.replace(directory, _ => "").slice(1, -ending.length),
24
- await file.import(),
25
- ]));
26
- warn && await directory.exists() && empty(log)(objects, name, directory);
27
-
28
- return objects;
29
- };
30
-
31
- export const doubled = set => set.find((part, i, array) =>
32
- array.filter((_, j) => i !== j).includes(part));
@@ -1,2 +0,0 @@
1
- export { default as modules } from "./modules.js";
2
- export { default as types } from "./types.js";
@@ -1,33 +0,0 @@
1
- import * as hooks from "../hooks/exports.js";
2
- import { doubled } from "./common.js";
3
- import errors from "../errors.js";
4
-
5
- const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
6
-
7
- const load = (modules = []) => modules.map(module =>
8
- [module, load(module.load?.() ?? [])]).flat();
9
-
10
- export default async (log, root, modules) => {
11
- Array.isArray(modules) || errors.ModulesMustBeArray.throw("modules");
12
-
13
- modules.some(({ name }, n) => name === undefined &&
14
- errors.ModuleHasNoName.throw(n));
15
-
16
- const names = modules.map(({ name }) => name);
17
- new Set(names).size !== modules.length &&
18
- errors.DoubleModule.throw(doubled(names), root.join("primate.config.js"));
19
-
20
- const hookless = modules.filter(module => !Object.keys(module).some(key =>
21
- [...Object.keys(hooks), "load", "context"].includes(key)));
22
- hookless.length > 0 && errors.ModuleHasNoHooks.warn(log,
23
- hookless.map(({ name }) => name).join(", "));
24
-
25
- // collect modules
26
- const loaded = load(modules).flat(2);
27
-
28
- return {
29
- names: loaded.map(module => module.name),
30
- ...Object.fromEntries([...Object.keys(hooks), "context"]
31
- .map(hook => [hook, filter(hook, loaded)])),
32
- };
33
- };