@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,20 @@
1
+ import filter from "@rcompat/object/filter";
2
+ import manifest from "@rcompat/package/manifest";
3
+ import packager from "@rcompat/package/packager";
4
+ import errors from "./errors.js";
5
+
6
+ const { MissingDependencies } = errors;
7
+
8
+ export default async (library_manifest, desired, from) => {
9
+ const app_dependencies = (await manifest()).dependencies;
10
+ const keys = Object.keys(app_dependencies);
11
+ const library_peers = filter(library_manifest.peerDependencies, ([key]) =>
12
+ desired.includes(key));
13
+ const missing = desired.filter(peer => !keys.includes(peer));
14
+
15
+ if (missing.length > 0) {
16
+ const to_install = missing.map(key => `${key}@${library_peers[key]}`);
17
+ const install = `${packager()} install ${to_install.join(" ")}`;
18
+ MissingDependencies.throw(missing.join(", "), from, install);
19
+ }
20
+ };
@@ -0,0 +1,18 @@
1
+ import is from "@rcompat/invariant/is";
2
+
3
+ export default (object, raw, cased = true) => {
4
+ return Object.assign(Object.create(null), {
5
+ get(key) {
6
+ is(key).string();
7
+
8
+ return object[cased ? key : key.toLowerCase()];
9
+ },
10
+ json() {
11
+ return JSON.parse(JSON.stringify(object));
12
+ },
13
+ toString() {
14
+ return JSON.stringify(object);
15
+ },
16
+ raw,
17
+ });
18
+ };
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "bad body returned from route, got {0}",
5
+ fix: "return a proper body from route",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "bad default export at {0}",
5
+ fix: "use only functions for the default export",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "bad path {0}",
5
+ fix: "use only letters, digits, '_', '[', ']' or '=' in path filenames",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "bad type export at {0}",
5
+ fix: "export object with a `base` string and a `validate` function",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "bad type name {0}",
5
+ fix: "use lowercase-first latin letters and decimals in type names",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "double file extension {0}",
5
+ fix: "unload one of the two handlers registering the file extension",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "double module {0} in {1}",
5
+ fix: "load {0} only once",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "double path parameter {0} in route {1}",
5
+ fix: "disambiguate path parameters in route names",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "double route of the form {0}",
5
+ fix: "disambiguate routes",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "empty config file at {0}",
5
+ fix: "add configuration options to the file or remove it",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { warn } from "#error";
2
+
3
+ export default warn(import.meta.url, {
4
+ message: "empty {0} directory",
5
+ fix: "populate {1} or remove it",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "empty path parameter {0} in route {1}",
5
+ fix: "name the parameter or remove it",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { warn } from "#error";
2
+
3
+ export default warn(import.meta.url, {
4
+ message: "empty route file at {0}",
5
+ fix: "add routes to the file or remove it",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "error in config file: {0}",
5
+ fix: "check errors in config file by running {1}",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "{0}: {1}",
5
+ fix: "make sure the body payload corresponds to the used content type",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { info } from "#error";
2
+
3
+ export default info(import.meta.url, {
4
+ message: "mismatched path {0}: {1}",
5
+ fix: "fix the type or the caller",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { info } from "#error";
2
+
3
+ export default info(import.meta.url, {
4
+ message: "mismatched type: {0}",
5
+ fix: "fix the type or the caller",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "module at index {0} has no name",
5
+ fix: "update module at index {0} and inform maintainer",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "the {0} config property must be an array",
5
+ fix: "change {0} to an array in the config or remove this property",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "no handler for {0}",
5
+ fix: "add handler module for this component or remove {0}",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { info } from "#error";
2
+
3
+ export default info(import.meta.url, {
4
+ message: "no {0} route to {1}",
5
+ fix: "create a {0} route function at {2}.js",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "optional route {0} must be a leaf",
5
+ fix: "move route to leaf (last) position in filesystem hierarchy",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "reserved type name {0}",
5
+ fix: "do not use any reserved type names",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { error } from "#error";
2
+
3
+ export default error(import.meta.url, {
4
+ message: "rest route {0} must be a leaf",
5
+ fix: "move route to leaf (last) position in filesystem hierarchy",
6
+ });
@@ -0,0 +1,15 @@
1
+ import log from "@primate/core/log";
2
+ import file from "@rcompat/fs/file";
3
+
4
+ const base = level => (url, args) => (...params) => log[level]({
5
+ params,
6
+ name: file(url).base,
7
+ module: "@primate/core",
8
+ ...args,
9
+ });
10
+
11
+ const info = base("info");
12
+ const warn = base("warn");
13
+ const error = base("error");
14
+
15
+ export { error, info, warn };
@@ -0,0 +1,69 @@
1
+ import loglevel from "#loglevel";
2
+ import mark from "#mark";
3
+ import blue from "@rcompat/cli/color/blue";
4
+ import dim from "@rcompat/cli/color/dim";
5
+ import green from "@rcompat/cli/color/green";
6
+ import red from "@rcompat/cli/color/red";
7
+ import yellow from "@rcompat/cli/color/yellow";
8
+ import print from "@rcompat/cli/print";
9
+
10
+ const url = "https://primatejs.com/errors";
11
+ const slice_length = "@primate/".length;
12
+ const helpat = (name, error) => `${url}/${name.slice(slice_length)}#${error}`;
13
+
14
+ const levels = {
15
+ error: 0,
16
+ warn: 1,
17
+ info: 2,
18
+ };
19
+ const level = levels[loglevel];
20
+
21
+ const make_error = (level, { message, fix, name, params, module }) => ({
22
+ level,
23
+ fix: mark(fix, ...params),
24
+ message: mark(message, ...params),
25
+ name,
26
+ module,
27
+ });
28
+
29
+ const normalize = (level, message) => typeof message === "string"
30
+ ? { message }
31
+ : make_error(level, message);
32
+
33
+ const log = (pre, color, error, override) => {
34
+ const { fix, module, name, message } = { ...error, ...override };
35
+ print(color(pre), `${module !== undefined ? `${color(module)} ` : ""}${message}`, "\n");
36
+ if (fix) {
37
+ print(blue("++"), fix);
38
+ name && print(dim(`\n -> ${helpat(module, name)}`), "\n");
39
+ }
40
+ };
41
+
42
+ export default {
43
+ system(message) {
44
+ log("++", blue, { message });
45
+ },
46
+
47
+ info(error, override) {
48
+ // info prints only on info level
49
+ level === levels.info && log("--", green, normalize("info", error), override);
50
+ },
51
+
52
+ warn(error, override) {
53
+ // warn prints on info and warn levels
54
+ level >= levels.warn && log("??", yellow, normalize("warn", error), override);
55
+ },
56
+
57
+ error(error, override, toss = true) {
58
+ // error always prints
59
+ log("!!", red, normalize("error", error));
60
+ if (toss) {
61
+ error.level = "error";
62
+ throw error;
63
+ }
64
+ },
65
+
66
+ auto(error) {
67
+ Object.keys(levels).includes(error.level) && this[error.level](error, {}, false);
68
+ },
69
+ };
@@ -0,0 +1,9 @@
1
+ import args from "@rcompat/args";
2
+
3
+ const default_level = "warn";
4
+ const flag = "--loglevel=";
5
+ const levels = ["error", "warn", "info"];
6
+
7
+ const loglevel = args.find(arg => arg.startsWith(flag))?.slice(flag.length);
8
+
9
+ export default levels.includes(loglevel) ? loglevel : default_level;
@@ -0,0 +1,5 @@
1
+ import bold from "@rcompat/cli/color/bold";
2
+
3
+ export default (format, ...params) => params.reduce((formatted, param, i) =>
4
+ formatted.replace(`{${i}}`, bold(param)), format);
5
+
@@ -0,0 +1,11 @@
1
+ import is from "@rcompat/invariant/is";
2
+ import maybe from "@rcompat/invariant/maybe";
3
+
4
+ export default (type, value, name) => {
5
+ maybe(type.validate).function();
6
+ if (type.validate) {
7
+ return type.validate(value, name);
8
+ }
9
+ is(type).function();
10
+ return type(value, name);
11
+ };
@@ -0,0 +1,191 @@
1
+ import double_extension from "#error/double-extension";
2
+ import crypto from "@rcompat/crypto";
3
+ import join from "@rcompat/fs/join";
4
+ import { html } from "@rcompat/http/mime";
5
+ import { OK } from "@rcompat/http/status";
6
+ import is from "@rcompat/invariant/is";
7
+ import empty from "@rcompat/object/empty";
8
+ import get from "@rcompat/object/get";
9
+ import valmap from "@rcompat/object/valmap";
10
+ import module_loader from "./module_loader.js";
11
+ import to_sorted from "./to_sorted.js";
12
+
13
+ const to_csp = (config_csp, assets, csp) => config_csp
14
+ // only csp entries in the config will be enriched
15
+ .map(([key, directives]) =>
16
+ // enrich with application assets
17
+ [key, assets[key] ? directives.concat(...assets[key]) : directives])
18
+ .map(([key, directives]) =>
19
+ // enrich with explicit csp
20
+ [key, csp[key] ? directives.concat(...csp[key]) : directives])
21
+ .map(([key, directives]) => `${key} ${directives.join(" ")}`)
22
+ .join(";");
23
+
24
+ const encoder = new TextEncoder();
25
+
26
+ const attribute = attributes => empty(attributes)
27
+ ? ""
28
+ : " ".concat(Object.entries(attributes)
29
+ .map(([key, value]) => `${key}="${value}"`).join(" "))
30
+ ;
31
+ const tag = (name, { attributes = {}, code = "", close = true }) =>
32
+ `<${name}${attribute(attributes)}${close ? `>${code}</${name}>` : "/>"}`;
33
+ const nctag = (name, properties) => tag(name, { ...properties, close: false });
34
+ const tags = {
35
+ // inline: <script type integrity>...</script>
36
+ // outline: <script type integrity src></script>
37
+ script({ inline, code, type, integrity, src }) {
38
+ return inline
39
+ ? tag("script", { attributes: { type, integrity }, code })
40
+ : tag("script", { attributes: { type, integrity, src } });
41
+ },
42
+ // inline: <style>...</style>
43
+ // outline: <link rel="stylesheet" href />
44
+ style({ inline, code, href, rel = "stylesheet" }) {
45
+ return inline
46
+ ? tag("style", { code })
47
+ : nctag("link", { attributes: { rel, href } });
48
+ },
49
+ font({ href, rel = "preload", as = "font", type, crossorigin = true }) {
50
+ return nctag("link", { attributes: { rel, href, as, type, crossorigin } });
51
+ },
52
+ };
53
+
54
+ const render_head = (assets, fonts, head) =>
55
+ to_sorted(assets, ({ type }) => -1 * (type === "importmap"))
56
+ .map(({ src, code, type, inline, integrity }) =>
57
+ type === "style"
58
+ ? tags.style({ inline, code, href: src })
59
+ : tags.script({ inline, code, type, integrity, src }),
60
+ ).join("\n").concat("\n", head ?? "").concat("\n", fonts.map(href =>
61
+ tags.font({ href, type: "font/woff2" }),
62
+ ).join("\n"));
63
+
64
+ export default async (root, { config, assets, files, components, loader, target, mode }) => {
65
+ const { http } = config;
66
+ const secure = http?.ssl !== undefined;
67
+ const path = valmap(config.location, value => root.join(value));
68
+
69
+ // if ssl activated, resolve key and cert early
70
+ if (secure) {
71
+ http.ssl.key = root.join(http.ssl.key);
72
+ http.ssl.cert = root.join(http.ssl.cert);
73
+ }
74
+
75
+ const $components = Object.fromEntries(components ?? []);
76
+ const error = await path.routes.join("+error.js");
77
+
78
+ return {
79
+ secure,
80
+ importmaps: {},
81
+ assets,
82
+ files,
83
+ path,
84
+ root,
85
+ get_component(name) {
86
+ const component = $components[name];
87
+ return component?.default ?? component;
88
+ },
89
+ // pseudostatic thus arrowbound
90
+ get: (config_key, fallback) => get(config, config_key) ?? fallback,
91
+ set: (key, value) => {
92
+ config[key] = value;
93
+ },
94
+ error: {
95
+ default: await error.exists() ? await error.import("default") : undefined,
96
+ },
97
+ handlers: {},
98
+ modules: await module_loader(root, config.modules ?? []),
99
+ fonts: [],
100
+ headers(csp = {}) {
101
+ const http_csp = Object.entries(this.get("http.csp", {}));
102
+
103
+ return {
104
+ ...this.get("http.headers", {}),
105
+ ...http_csp.length === 0 ? {} : {
106
+ "Content-Security-Policy": to_csp(http_csp, this.asset_csp, csp),
107
+ },
108
+ };
109
+ },
110
+ runpath(...directories) {
111
+ return this.root.join(...directories);
112
+ },
113
+ async render(content) {
114
+ const { body, head, partial, placeholders = {}, page } = content;
115
+ ["body", "head"].every(used => is(placeholders[used]).undefined());
116
+
117
+ return partial ? body : Object.entries(placeholders)
118
+ // replace given placeholders, defaulting to ""
119
+ .reduce((html, [key, value]) => html.replace(`%${key}%`, value ?? ""),
120
+ this.loader.page(page))
121
+ // replace non-given placeholders, aside from %body% / %head%
122
+ .replaceAll(/(?<keep>%(?:head|body)%)|%.*?%/gus, "$1")
123
+ // replace body and head
124
+ .replace("%body%", body)
125
+ .replace("%head%", render_head(this.assets, this.fonts, head));
126
+ },
127
+ respond(body, { status = OK, headers = {} } = {}) {
128
+ return new Response(body, { status, headers: {
129
+ "Content-Type": html, ...this.headers(), ...headers },
130
+ });
131
+ },
132
+ async view(options) {
133
+ // split render and respond options
134
+ const { status, headers, ...rest } = options;
135
+ return this.respond(await this.render(rest), { status, headers });
136
+ },
137
+ media(type, { status, headers } = {}) {
138
+ return { status, headers: { ...headers, "Content-Type": type } };
139
+ },
140
+ async inline(code, type) {
141
+ const integrity = await this.hash(code);
142
+ const tag_name = type === "style" ? "style" : "script";
143
+ const head = tags[tag_name]({ code, type, inline: true, integrity });
144
+ return { head, integrity: `'${integrity}'` };
145
+ },
146
+ async publish({ src, code, type = "", inline = false }) {
147
+ if (inline || type === "style") {
148
+ this.assets.push({
149
+ src: join(http.static.root, src ?? "").path,
150
+ code: inline ? code : "",
151
+ type,
152
+ inline,
153
+ integrity: await this.hash(code),
154
+ });
155
+ }
156
+
157
+ // rehash assets_csp
158
+ this.asset_csp = this.assets.map(({ type: directive, integrity }) => [
159
+ `${directive === "style" ? "style" : "script"}-src`, integrity])
160
+ .reduce((csp, [directive, hash]) =>
161
+ ({ ...csp, [directive]: csp[directive].concat(`'${hash}'`) } ),
162
+ { "style-src": [], "script-src": [] },
163
+ );
164
+ },
165
+ create_csp() {
166
+ this.asset_csp = this.assets.map(({ type: directive, integrity }) => [
167
+ `${directive === "style" ? "style" : "script"}-src`, integrity])
168
+ .reduce((csp, [directive, hash]) =>
169
+ ({ ...csp, [directive]: csp[directive].concat(`'${hash}'`) } ),
170
+ { "style-src": [], "script-src": [] },
171
+ );
172
+ },
173
+ register(extension, handle) {
174
+ this.handlers[extension] !== undefined && double_extension(extension);
175
+ this.handlers[extension] = handle;
176
+ },
177
+ async hash(data, algorithm = "sha-384") {
178
+ const bytes = await crypto.subtle.digest(algorithm, encoder.encode(data));
179
+ const prefix = algorithm.replace("-", _ => "");
180
+ return `${prefix}-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}`;
181
+ },
182
+ // noop
183
+ target(name, handler) {},
184
+ build_target: target,
185
+ loader,
186
+ stop() {
187
+ this.get("server").stop();
188
+ },
189
+ mode,
190
+ };
191
+ };
@@ -0,0 +1,10 @@
1
+ import { NOT_FOUND } from "@rcompat/http/status";
2
+
3
+ /**
4
+ * Render an error page
5
+ * @param {string} body replacement for %body%
6
+ * @param {ErrorOptions} options rendering options
7
+ * @return {ResponseFn}
8
+ */
9
+ export default (body = "Not Found", { status = NOT_FOUND, page } = {}) =>
10
+ app => app.view({ body, status, page: page ?? app.get("pages.error") });
@@ -0,0 +1,10 @@
1
+ import { json } from "@rcompat/http/mime";
2
+ import base from "./shared/base.js";
3
+
4
+ /**
5
+ * Issue a JSON response
6
+ * @param {object} body object
7
+ * @param {MinOptions} options rendering options
8
+ * @return {ResponseFn}
9
+ */
10
+ export default base(json, JSON.stringify);
@@ -0,0 +1,11 @@
1
+ import { FOUND } from "@rcompat/http/status";
2
+
3
+ /**
4
+ * Redirect request
5
+ * @param {string} Location location to redirect to
6
+ * @param {MinOptions} options handler options
7
+ * @return {ResponseFn}
8
+ */
9
+ export default (Location, { status = FOUND } = {}) => app =>
10
+ // no body
11
+ app.respond(null, { status, headers: { Location } });
@@ -0,0 +1,4 @@
1
+ import identity from "@rcompat/function/identity";
2
+
3
+ export default (mediatype, mapper = identity) => (body, options) => app =>
4
+ app.respond(mapper(body), app.media(mediatype, options));
@@ -0,0 +1,24 @@
1
+ import { sse } from "@rcompat/http/mime";
2
+ import base from "./shared/base.js";
3
+
4
+ /**
5
+ * Open a server-sent event stream
6
+ * @param {object} body docs1
7
+ * @param {object} options docs2
8
+ * @return {ResponseFn}
9
+ */
10
+ export default base(sse, implementation =>
11
+ new ReadableStream({
12
+ start(controller) {
13
+ implementation.open({
14
+ send(name, data) {
15
+ const event = data === undefined ? "" : `event: ${name}\n`;
16
+ const $data = data === undefined ? name : data;
17
+ controller.enqueue(`${event}data:${JSON.stringify($data)}\n\n`);
18
+ },
19
+ });
20
+ },
21
+ cancel() {
22
+ implementation.close?.();
23
+ },
24
+ }));
@@ -0,0 +1,4 @@
1
+ import { bin } from "@rcompat/http/mime";
2
+ import base from "./shared/base.js";
3
+
4
+ export default base(bin);
@@ -0,0 +1,10 @@
1
+ import { txt } from "@rcompat/http/mime";
2
+ import base from "./shared/base.js";
3
+
4
+ /**
5
+ * Issue a plaintext response
6
+ * @param {string} body plaintext
7
+ * @param {MinOptions} options rendering options
8
+ * @return {ResponseFn}
9
+ */
10
+ export default base(txt);
@@ -0,0 +1,15 @@
1
+ import no_handler from "#error/no-handler";
2
+ import file from "@rcompat/fs/file";
3
+
4
+ const extensions = ["extension", "fullExtension"];
5
+ /**
6
+ * Render a component using handler for the given filename extension
7
+ * @param {string} name component filename
8
+ * @param {object} props props passed to component
9
+ * @param {object} options rendering options
10
+ * @return {ResponseFn}
11
+ */
12
+ export default (name, props, options) => (app, ...rest) => extensions
13
+ .map(extension => app.handlers[file(name)[extension]])
14
+ .find(extension => extension !== undefined)
15
+ ?.(name, props, options)(app, ...rest) ?? no_handler(name);
@@ -0,0 +1,2 @@
1
+ export default implementation => ({ server }, _, { original }) =>
2
+ server.upgrade(original, implementation);
@@ -0,0 +1,5 @@
1
+ export { default as handle } from "./handle.js";
2
+ export { default as init } from "./init.js";
3
+ export { default as parse } from "./parse.js";
4
+ export { default as route } from "./route.js";
5
+ export { default as serve } from "./serve.js";