@primate/core 0.1.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 (70) hide show
  1. package/LICENSE +19 -0
  2. package/package.json +53 -0
  3. package/src/build/app.js +105 -0
  4. package/src/build/defaults/app.html +9 -0
  5. package/src/build/defaults/error.html +14 -0
  6. package/src/build/hook/build.js +179 -0
  7. package/src/build/hook/copy_includes.js +20 -0
  8. package/src/build/hook/exports.js +2 -0
  9. package/src/build/hook/init.js +3 -0
  10. package/src/build/hook/router.js +32 -0
  11. package/src/build/index.js +41 -0
  12. package/src/build/module_loader.js +30 -0
  13. package/src/build/targets/exports.js +1 -0
  14. package/src/build/targets/web.js +81 -0
  15. package/src/dispatch/index.js +1 -0
  16. package/src/log/index.js +1 -0
  17. package/src/private/bye.js +5 -0
  18. package/src/private/config-filename.js +1 -0
  19. package/src/private/config.js +48 -0
  20. package/src/private/depend.js +20 -0
  21. package/src/private/dispatch.js +18 -0
  22. package/src/private/error/bad-body.js +6 -0
  23. package/src/private/error/bad-default-export.js +6 -0
  24. package/src/private/error/bad-path.js +6 -0
  25. package/src/private/error/bad-type-export.js +6 -0
  26. package/src/private/error/bad-type-name.js +6 -0
  27. package/src/private/error/double-extension.js +6 -0
  28. package/src/private/error/double-module.js +6 -0
  29. package/src/private/error/double-path-parameter.js +6 -0
  30. package/src/private/error/double-route.js +6 -0
  31. package/src/private/error/empty-config-file.js +6 -0
  32. package/src/private/error/empty-directory.js +6 -0
  33. package/src/private/error/empty-path-parameter.js +6 -0
  34. package/src/private/error/empty-route-file.js +6 -0
  35. package/src/private/error/error-in-config-file.js +6 -0
  36. package/src/private/error/mismatched-body.js +6 -0
  37. package/src/private/error/mismatched-path.js +6 -0
  38. package/src/private/error/mismatched-type.js +6 -0
  39. package/src/private/error/module-no-name.js +6 -0
  40. package/src/private/error/modules-array.js +6 -0
  41. package/src/private/error/no-handler.js +6 -0
  42. package/src/private/error/no-route-to-path.js +6 -0
  43. package/src/private/error/optional-route.js +6 -0
  44. package/src/private/error/reserved-type-name.js +6 -0
  45. package/src/private/error/rest-route.js +6 -0
  46. package/src/private/error.js +15 -0
  47. package/src/private/log.js +69 -0
  48. package/src/private/loglevel.js +9 -0
  49. package/src/private/mark.js +5 -0
  50. package/src/private/validate.js +11 -0
  51. package/src/serve/app.js +191 -0
  52. package/src/serve/handler/error.js +10 -0
  53. package/src/serve/handler/json.js +10 -0
  54. package/src/serve/handler/redirect.js +11 -0
  55. package/src/serve/handler/shared/base.js +4 -0
  56. package/src/serve/handler/sse.js +24 -0
  57. package/src/serve/handler/stream.js +4 -0
  58. package/src/serve/handler/text.js +10 -0
  59. package/src/serve/handler/view.js +15 -0
  60. package/src/serve/handler/ws.js +2 -0
  61. package/src/serve/hook/exports.js +5 -0
  62. package/src/serve/hook/handle.js +120 -0
  63. package/src/serve/hook/init.js +3 -0
  64. package/src/serve/hook/parse.js +17 -0
  65. package/src/serve/hook/respond.js +28 -0
  66. package/src/serve/hook/route.js +50 -0
  67. package/src/serve/hook/serve.js +53 -0
  68. package/src/serve/index.js +9 -0
  69. package/src/serve/module_loader.js +16 -0
  70. package/src/serve/to_sorted.js +1 -0
@@ -0,0 +1,120 @@
1
+ import log from "#log";
2
+ import client_error from "@primate/core/handler/error";
3
+ import cascade from "@rcompat/async/cascade";
4
+ import tryreturn from "@rcompat/async/tryreturn";
5
+ import reload_defaults from "@rcompat/build/reload/defaults";
6
+ import reload_path from "@rcompat/build/reload/path";
7
+ import { resolve } from "@rcompat/http/mime";
8
+ import { OK } from "@rcompat/http/status";
9
+ import respond from "./respond.js";
10
+
11
+ const guard_error = Symbol("guard_error");
12
+ const guard = (app, guards) => async (request, next) => {
13
+ // handle guards
14
+ try {
15
+ for (const guard of guards) {
16
+ const result = await guard(request);
17
+ if (result !== true) {
18
+ const error = new Error();
19
+ error.result = result;
20
+ error.type = guard_error;
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ return next(request);
26
+ } catch (error) {
27
+ if (error.type === guard_error) {
28
+ return { request, response: respond(error.result)(app) };
29
+ }
30
+ // rethrow if not guard error
31
+ throw error;
32
+ }
33
+ };
34
+
35
+ const get_layouts = async (layouts, request) => {
36
+ const stop_at = layouts.findIndex(({ recursive }) => recursive === false);
37
+ return Promise.all(layouts
38
+ .slice(stop_at === -1 ? 0 : stop_at)
39
+ .map(layout => layout(request)));
40
+ };
41
+ // last handler, preserve final request form
42
+ const last = handler => async request => {
43
+ const response = await handler(request);
44
+ return { request, response };
45
+ };
46
+
47
+ export default app => {
48
+ const route = request => app.route(request);
49
+
50
+ const as_route = async request => {
51
+ // if tryreturn throws, this will default
52
+ let error_handler = app.error.default;
53
+
54
+ return tryreturn(async _ => {
55
+ const { body, path, guards, errors, layouts, handler } =
56
+ await route(request);
57
+
58
+ error_handler = errors?.at(-1);
59
+
60
+ const hooks = [...app.modules.route, guard(app, guards), last(handler)];
61
+
62
+ // handle request
63
+ const routed = await (await cascade(hooks))({ ...request, body, path });
64
+
65
+ const $layouts = { layouts: await get_layouts(layouts, routed.request) };
66
+ return respond(routed.response)(app, $layouts, routed.request);
67
+ }).orelse(async () => {
68
+ // the +error.js page itself could fail
69
+ return tryreturn(_ => respond(error_handler(request))(app, {}, request))
70
+ .orelse(_ => client_error()(app, {}, request));
71
+ });
72
+ };
73
+
74
+ const as_asset = async (pathname, code) => new Response(code, {
75
+ status: OK,
76
+ headers: {
77
+ "Content-Type": resolve(pathname),
78
+ // Etag: await path.modified(),
79
+ },
80
+ });
81
+
82
+ const handle = async request => {
83
+ const { pathname } = request.url;
84
+
85
+ const asset = app.loader.asset(pathname)?.code;
86
+
87
+ return asset === undefined ? as_route(request) : as_asset(pathname, asset);
88
+ };
89
+ // first hook
90
+ const pass = (request, next) => next({
91
+ ...request,
92
+ pass(to) {
93
+ const { method, headers, body } = request.original;
94
+ const input = `${to}${request.url.pathname}`;
95
+
96
+ return fetch(input, { headers, method, body, duplex: "half" });
97
+ },
98
+ });
99
+
100
+ const paths = [reload_path].concat(app.assets
101
+ .filter(asset => asset.type !== "importmap")
102
+ .map(asset => asset.src));
103
+ const http = app.get("http");
104
+ const url = `http://${http.host}:${reload_defaults.port}`;
105
+
106
+ const proxy = (request, fallback) => {
107
+ const { pathname } = new URL(request.url);
108
+ const { method, headers, body } = request;
109
+
110
+ return paths.includes(pathname)
111
+ ? fetch(`${url}${pathname}`, { headers, method, body, duplex: "half" })
112
+ : fallback();
113
+ };
114
+
115
+ const hotreload = (request, next) => app.mode === "development"
116
+ ? proxy(request.original, () => next(request))
117
+ : next(request);
118
+
119
+ return cascade([pass, hotreload, ...app.modules.handle], handle);
120
+ };
@@ -0,0 +1,3 @@
1
+ import cascade from "@rcompat/async/cascade";
2
+
3
+ export default async app => (await cascade(app.modules.init))(app);
@@ -0,0 +1,17 @@
1
+ import dispatch from "#dispatch";
2
+ import valmap from "@rcompat/object/valmap";
3
+
4
+ export default () => 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
+ ...valmap({
12
+ query: [Object.fromEntries(url.searchParams), url.search],
13
+ headers: [Object.fromEntries(headers), headers, false],
14
+ cookies: [Object.fromEntries(cookies?.split(";").map(cookie =>
15
+ cookie.trim().split("=")) ?? []), cookies],
16
+ }, value => dispatch(...value)) };
17
+ };
@@ -0,0 +1,28 @@
1
+ import bad_body from "#error/bad-body";
2
+ import json from "@primate/core/handler/json";
3
+ import redirect from "@primate/core/handler/redirect";
4
+ import stream from "@primate/core/handler/stream";
5
+ import text from "@primate/core/handler/text";
6
+ import streamable from "@rcompat/fs/streamable";
7
+ import identity from "@rcompat/function/identity";
8
+ import proper from "@rcompat/object/proper";
9
+
10
+ const is_instance = of => value => value instanceof of;
11
+ const is_response = is_instance(Response);
12
+ const is_streamable =
13
+ value => value instanceof Blob || value?.streamable === streamable;
14
+
15
+ // [if, then]
16
+ const guesses = [
17
+ [is_instance(URL), redirect],
18
+ [is_streamable, value => stream(value.stream())],
19
+ [is_instance(ReadableStream), stream],
20
+ [value => is_response(value), value => _ => value],
21
+ [proper, json],
22
+ [value => typeof value === "string", text],
23
+ [bad_body, identity],
24
+ ];
25
+
26
+ const guess = value => guesses.find(([check]) => check(value))?.[1](value);
27
+
28
+ export default result => typeof result === "function" ? result : guess(result);
@@ -0,0 +1,50 @@
1
+ import dispatch from "#dispatch";
2
+ import mismatched_body from "#error/mismatched-body";
3
+ import mismatched_path from "#error/mismatched-path";
4
+ import no_route_to_path from "#error/no-route-to-path";
5
+ import validate from "#validate";
6
+ import Body from "@rcompat/http/body";
7
+ import map from "@rcompat/object/map";
8
+ import tryreturn from "@rcompat/sync/tryreturn";
9
+
10
+ const deroot = pathname => pathname.endsWith("/") && pathname !== "/"
11
+ ? pathname.slice(0, -1) : pathname;
12
+
13
+ const parse_body = (request, url) =>
14
+ tryreturn(async _ => await Body.parse(request) ?? {})
15
+ .orelse(error => mismatched_body(url.pathname, error.message));
16
+
17
+ export default app => {
18
+ const $request_body_parse = app.get("request.body.parse");
19
+ const $location = app.get("location");
20
+
21
+ const index = path => `${$location.routes}${path === "/" ? "/index" : path}`;
22
+ // remove excess slashes
23
+ const deslash = url => url.replaceAll(/\/{2,}/gu, _ => "/");
24
+
25
+ return async ({ original, url }) => {
26
+ const pathname = deroot(deslash(url.pathname));
27
+ const route = await app.router.match(original) ??
28
+ no_route_to_path(original.method.toLowerCase(), pathname, index(pathname));
29
+ const { params } = route;
30
+ const untyped_path = Object.fromEntries(Object.entries(params)
31
+ .filter(([name]) => !name.includes("="))
32
+ .map(([key, value]) => [key, value]));
33
+ const typed_path = Object.fromEntries(Object.entries(params)
34
+ .filter(([name]) => name.includes("="))
35
+ .map(([name, value]) => [name.split("="), value])
36
+ .map(([[name, type], value]) =>
37
+ tryreturn(_ => {
38
+ validate(app.types[type], value, name);
39
+ return [name, value];
40
+ }).orelse(({ message }) => mismatched_path(pathname, message))));
41
+ const path = dispatch({ ...untyped_path, ...typed_path });
42
+ const local_parse_body = route.file.body?.parse ?? $request_body_parse;
43
+ const body = local_parse_body ? await parse_body(original, url) : null;
44
+ const { guards = [], errors = [], layouts = [] } = map(route.specials,
45
+ ([key, value]) => [`${key}s`, value.map(i => i.default)]);
46
+ const handler = route.file.default[original.method.toLowerCase()];
47
+
48
+ return { body, path, guards, errors, layouts, handler };
49
+ };
50
+ };
@@ -0,0 +1,53 @@
1
+ import log from "#log";
2
+ import cascade from "@rcompat/async/cascade";
3
+ import tryreturn from "@rcompat/async/tryreturn";
4
+ import dim from "@rcompat/cli/color/dim";
5
+ import Router from "@rcompat/fs/router";
6
+ import serve from "@rcompat/http/serve";
7
+ import { INTERNAL_SERVER_ERROR } from "@rcompat/http/status";
8
+ import * as hook from "./exports.js";
9
+
10
+ const post = async app => {
11
+ const types = Object.fromEntries(app.files.types.map(([key, value]) =>
12
+ [key, value.default]));
13
+ let router;
14
+
15
+ try {
16
+ router = await Router.init({
17
+ specials: {
18
+ guard: { recursive: true },
19
+ error: { recursive: false },
20
+ layout: { recursive: true },
21
+ },
22
+ predicate(route, request) {
23
+ return route.default[request.method.toLowerCase()] !== undefined;
24
+ },
25
+ }, app.files.routes);
26
+ } catch {}
27
+
28
+ app.create_csp();
29
+
30
+ const $app = { ...app, types, router };
31
+ $app.route = hook.route($app);
32
+ $app.parse = hook.parse($app);
33
+ const $handle = await hook.handle($app);
34
+
35
+ $app.server = await serve(async request =>
36
+ tryreturn(async _ => $handle(await $app.parse(request)))
37
+ .orelse(error => {
38
+ log.auto(error);
39
+ return new Response(null, { status: INTERNAL_SERVER_ERROR });
40
+ }), $app.get("http"));
41
+ const { host, port } = $app.get("http");
42
+ const address = `http${$app.secure ? "s" : ""}://${host}:${port}`;
43
+ log.system(`started ${dim("->")} ${dim(address)}`);
44
+
45
+ app.set("server", $app.server);
46
+
47
+ return $app;
48
+ };
49
+
50
+ export default async app => {
51
+ log.system("in startup");
52
+ return post(await (await cascade(app.modules.serve))(app));
53
+ };
@@ -0,0 +1,9 @@
1
+ import defaults from "#config";
2
+ import override from "@rcompat/object/override";
3
+ import app from "./app.js";
4
+ import { init, serve } from "./hook/exports.js";
5
+
6
+ export default async (root, { config, ...options }) => serve(
7
+ await init(
8
+ await app(root, { config: override(defaults, config), ...options }),
9
+ ));
@@ -0,0 +1,16 @@
1
+ import * as hooks from "./hook/exports.js";
2
+
3
+ const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
4
+ const load = (modules = []) => modules.map(module =>
5
+ [module, load(module.load?.() ?? [])]).flat();
6
+
7
+ export default async (root, modules) => {
8
+ // collect modules
9
+ const loaded = load(modules).flat(2);
10
+
11
+ return {
12
+ names: loaded.map(module => module.name),
13
+ ...Object.fromEntries([...Object.keys(hooks), "context"]
14
+ .map(hook => [hook, filter(hook, loaded)])),
15
+ };
16
+ };
@@ -0,0 +1 @@
1
+ export default (array, compareFn) => [...array].sort(compareFn);