@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.
- package/LICENSE +19 -0
- package/package.json +53 -0
- package/src/build/app.js +105 -0
- package/src/build/defaults/app.html +9 -0
- package/src/build/defaults/error.html +14 -0
- package/src/build/hook/build.js +179 -0
- package/src/build/hook/copy_includes.js +20 -0
- package/src/build/hook/exports.js +2 -0
- package/src/build/hook/init.js +3 -0
- package/src/build/hook/router.js +32 -0
- package/src/build/index.js +41 -0
- package/src/build/module_loader.js +30 -0
- package/src/build/targets/exports.js +1 -0
- package/src/build/targets/web.js +81 -0
- package/src/dispatch/index.js +1 -0
- package/src/log/index.js +1 -0
- package/src/private/bye.js +5 -0
- package/src/private/config-filename.js +1 -0
- package/src/private/config.js +48 -0
- package/src/private/depend.js +20 -0
- package/src/private/dispatch.js +18 -0
- package/src/private/error/bad-body.js +6 -0
- package/src/private/error/bad-default-export.js +6 -0
- package/src/private/error/bad-path.js +6 -0
- package/src/private/error/bad-type-export.js +6 -0
- package/src/private/error/bad-type-name.js +6 -0
- package/src/private/error/double-extension.js +6 -0
- package/src/private/error/double-module.js +6 -0
- package/src/private/error/double-path-parameter.js +6 -0
- package/src/private/error/double-route.js +6 -0
- package/src/private/error/empty-config-file.js +6 -0
- package/src/private/error/empty-directory.js +6 -0
- package/src/private/error/empty-path-parameter.js +6 -0
- package/src/private/error/empty-route-file.js +6 -0
- package/src/private/error/error-in-config-file.js +6 -0
- package/src/private/error/mismatched-body.js +6 -0
- package/src/private/error/mismatched-path.js +6 -0
- package/src/private/error/mismatched-type.js +6 -0
- package/src/private/error/module-no-name.js +6 -0
- package/src/private/error/modules-array.js +6 -0
- package/src/private/error/no-handler.js +6 -0
- package/src/private/error/no-route-to-path.js +6 -0
- package/src/private/error/optional-route.js +6 -0
- package/src/private/error/reserved-type-name.js +6 -0
- package/src/private/error/rest-route.js +6 -0
- package/src/private/error.js +15 -0
- package/src/private/log.js +69 -0
- package/src/private/loglevel.js +9 -0
- package/src/private/mark.js +5 -0
- package/src/private/validate.js +11 -0
- package/src/serve/app.js +191 -0
- package/src/serve/handler/error.js +10 -0
- package/src/serve/handler/json.js +10 -0
- package/src/serve/handler/redirect.js +11 -0
- package/src/serve/handler/shared/base.js +4 -0
- package/src/serve/handler/sse.js +24 -0
- package/src/serve/handler/stream.js +4 -0
- package/src/serve/handler/text.js +10 -0
- package/src/serve/handler/view.js +15 -0
- package/src/serve/handler/ws.js +2 -0
- package/src/serve/hook/exports.js +5 -0
- package/src/serve/hook/handle.js +120 -0
- package/src/serve/hook/init.js +3 -0
- package/src/serve/hook/parse.js +17 -0
- package/src/serve/hook/respond.js +28 -0
- package/src/serve/hook/route.js +50 -0
- package/src/serve/hook/serve.js +53 -0
- package/src/serve/index.js +9 -0
- package/src/serve/module_loader.js +16 -0
- 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,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,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
|
+
};
|
package/src/serve/app.js
ADDED
|
@@ -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,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,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);
|