primate 0.28.0 → 0.29.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/package.json +1 -1
- package/src/app.js +30 -16
- package/src/exports.js +1 -1
- package/src/handlers.js +77 -0
- package/src/hooks/exports.js +0 -1
- package/src/hooks/handle.js +3 -1
- package/src/hooks/respond/respond.js +1 -1
- package/src/hooks/route.js +1 -2
- package/src/run.js +1 -1
- package/src/start.js +2 -5
- package/types/index.d.ts +6 -1
- package/src/handlers/error.js +0 -8
- package/src/handlers/exports.js +0 -7
- package/src/handlers/html.js +0 -28
- package/src/handlers/json.js +0 -7
- package/src/handlers/redirect.js +0 -8
- package/src/handlers/stream.js +0 -8
- package/src/handlers/text.js +0 -7
- package/src/handlers/view.js +0 -7
- package/src/hooks/serve.js +0 -6
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -5,16 +5,17 @@ import { is } from "rcompat/invariant";
|
|
|
5
5
|
import { transform, valmap, to } from "rcompat/object";
|
|
6
6
|
import { globify } from "rcompat/string";
|
|
7
7
|
import * as runtime from "rcompat/meta";
|
|
8
|
+
import { Response, Status, MediaType } from "rcompat/http";
|
|
8
9
|
|
|
9
10
|
import errors from "./errors.js";
|
|
10
11
|
import to_sorted from "./to_sorted.js";
|
|
11
|
-
import * as handlers from "./handlers
|
|
12
|
+
import * as handlers from "./handlers.js";
|
|
12
13
|
import * as loaders from "./loaders/exports.js";
|
|
13
14
|
|
|
14
15
|
const { DoubleFileExtension } = errors;
|
|
15
16
|
|
|
16
17
|
// use user-provided file or fall back to default
|
|
17
|
-
const
|
|
18
|
+
const get_index = (base, page, fallback) =>
|
|
18
19
|
tryreturn(_ => File.text(`${base.join(page)}`))
|
|
19
20
|
.orelse(_ => File.text(`${base.join(fallback)}`));
|
|
20
21
|
|
|
@@ -112,7 +113,7 @@ export default async (log, root, config) => {
|
|
|
112
113
|
const { location: { server, client, components } } = this.config;
|
|
113
114
|
|
|
114
115
|
const source = this.path.components;
|
|
115
|
-
const compile = this.extensions[component.
|
|
116
|
+
const compile = this.extensions[component.fullExtension]?.compile;
|
|
116
117
|
if (compile === undefined) {
|
|
117
118
|
const debased = `${component.path}`.replace(source, "");
|
|
118
119
|
|
|
@@ -134,15 +135,13 @@ export default async (log, root, config) => {
|
|
|
134
135
|
headers({ script = "", style = "" } = {}) {
|
|
135
136
|
const csp = Object.keys(http.csp).reduce((policy, key) =>
|
|
136
137
|
`${policy}${key} ${http.csp[key]};`, "")
|
|
137
|
-
.replace("script-src 'self'", `script-src 'self' ${script} ${
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.map(asset => `'${asset.integrity}'`).join(" ")
|
|
138
|
+
.replace("script-src 'self'", `script-src 'self' ${script} ${this.assets
|
|
139
|
+
.filter(({ type }) => type !== "style")
|
|
140
|
+
.map(asset => `'${asset.integrity}'`).join(" ")
|
|
141
141
|
}`)
|
|
142
|
-
.replace("style-src 'self'", `style-src 'self' ${style} ${
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
.map(asset => `'${asset.integrity}'`).join(" ")
|
|
142
|
+
.replace("style-src 'self'", `style-src 'self' ${style} ${this.assets
|
|
143
|
+
.filter(({ type }) => type === "style")
|
|
144
|
+
.map(asset => `'${asset.integrity}'`).join(" ")
|
|
146
145
|
}`);
|
|
147
146
|
|
|
148
147
|
return { "Content-Security-Policy": csp, "Referrer-Policy": "same-origin" };
|
|
@@ -150,18 +149,33 @@ export default async (log, root, config) => {
|
|
|
150
149
|
runpath(...directories) {
|
|
151
150
|
return this.path.build.join(...directories);
|
|
152
151
|
},
|
|
153
|
-
async render(
|
|
152
|
+
async render(content) {
|
|
153
|
+
const { assets, config: { location, pages: { index } } } = this;
|
|
154
|
+
const { body, head, partial, placeholders = {}, page = index } = content;
|
|
154
155
|
["body", "head"].every(used => is(placeholders[used]).undefined());
|
|
155
|
-
const { assets, config: { location, pages } } = this;
|
|
156
156
|
|
|
157
|
-
return to(placeholders)
|
|
157
|
+
return partial ? body : to(placeholders)
|
|
158
158
|
// replace given placeholders, defaulting to ""
|
|
159
159
|
.reduce((html, [key, value]) => html.replace(`%${key}%`, value ?? ""),
|
|
160
|
-
await
|
|
160
|
+
await get_index(this.runpath(location.pages), page, index))
|
|
161
161
|
// replace non-given placeholders, aside from %body% / %head%
|
|
162
162
|
.replaceAll(/(?<keep>%(?:head|body)%)|%.*?%/gus, "$1")
|
|
163
163
|
// replace body and head
|
|
164
|
-
.replace("%body%", body)
|
|
164
|
+
.replace("%body%", body)
|
|
165
|
+
.replace("%head%", render_head(assets, head));
|
|
166
|
+
},
|
|
167
|
+
respond(body, { status = Status.OK, headers = {} } = {}) {
|
|
168
|
+
return new Response(body, { status, headers: {
|
|
169
|
+
...this.headers(), "Content-Type": MediaType.TEXT_HTML, ...headers },
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
async view(options) {
|
|
173
|
+
// split render and respond options
|
|
174
|
+
const { status, headers, ...rest } = options;
|
|
175
|
+
return this.respond(await this.render(rest), { status, headers });
|
|
176
|
+
},
|
|
177
|
+
media(type, { status, headers } = {}) {
|
|
178
|
+
return { status, headers: { ...headers, "Content-Type": type } };
|
|
165
179
|
},
|
|
166
180
|
async inline(code, type) {
|
|
167
181
|
const integrity = await this.hash(code);
|
package/src/exports.js
CHANGED
package/src/handlers.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { File } from "rcompat/fs";
|
|
2
|
+
import { MediaType, Status } from "rcompat/http";
|
|
3
|
+
import { identity } from "rcompat/function";
|
|
4
|
+
import errors from "./errors.js";
|
|
5
|
+
|
|
6
|
+
const handle = (mediatype, mapper = identity) => (body, options) => app =>
|
|
7
|
+
app.respond(mapper(body), app.media(mediatype, options));
|
|
8
|
+
|
|
9
|
+
// {{{ text
|
|
10
|
+
const text = handle(MediaType.TEXT_PLAIN);
|
|
11
|
+
// }}}
|
|
12
|
+
// {{{ json
|
|
13
|
+
const json = handle(MediaType.APPLICATION_JSON, JSON.stringify);
|
|
14
|
+
// }}}
|
|
15
|
+
// {{{ stream
|
|
16
|
+
const stream = handle(MediaType.APPLICATION_OCTET_STREAM);
|
|
17
|
+
// }}}
|
|
18
|
+
// {{{ ws
|
|
19
|
+
const ws = implementation => ({ server }, _, { original }) =>
|
|
20
|
+
server.upgrade(original, implementation);
|
|
21
|
+
// }}}
|
|
22
|
+
// {{{ sse
|
|
23
|
+
const sse = handle(MediaType.TEXT_EVENT_STREAM, implementation =>
|
|
24
|
+
new ReadableStream({
|
|
25
|
+
start(controller) {
|
|
26
|
+
implementation.open({
|
|
27
|
+
send(name, data) {
|
|
28
|
+
const event = data === undefined ? "" : `event: ${name}\n`;
|
|
29
|
+
const $data = data === undefined ? name : data;
|
|
30
|
+
controller.enqueue(`${event}data:${JSON.stringify($data)}\n\n`);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
cancel() {
|
|
35
|
+
implementation.close?.();
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
// }}}
|
|
39
|
+
// {{{ redirect
|
|
40
|
+
const redirect = (Location, { status = Status.FOUND } = {}) => app =>
|
|
41
|
+
/* no body */
|
|
42
|
+
app.respond(null, { status, headers: { Location } });
|
|
43
|
+
// }}}
|
|
44
|
+
// {{{ error
|
|
45
|
+
const error = (body = "Not Found", { status = Status.NOT_FOUND, page } = {}) =>
|
|
46
|
+
app => app.view({ body, status, page: page ?? app.config.pages.error });
|
|
47
|
+
// }}}
|
|
48
|
+
// {{{ html
|
|
49
|
+
const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
|
|
50
|
+
const style_re = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
|
|
51
|
+
const remove = /<(?<tag>script|style)>.*?<\/\k<tag>>/gus;
|
|
52
|
+
const html = (name, options) => async app => {
|
|
53
|
+
const component = await app.path.components.join(name).text();
|
|
54
|
+
const scripts = await Promise.all([...component.matchAll(script_re)]
|
|
55
|
+
.map(({ groups: { code } }) => app.inline(code, "module")));
|
|
56
|
+
const styles = await Promise.all([...component.matchAll(style_re)]
|
|
57
|
+
.map(({ groups: { code } }) => app.inline(code, "style")));
|
|
58
|
+
const assets = [...scripts, ...styles];
|
|
59
|
+
|
|
60
|
+
const body = component.replaceAll(remove, _ => "");
|
|
61
|
+
const head = assets.map(asset => asset.head).join("\n");
|
|
62
|
+
const script = scripts.map(asset => asset.csp).join(" ");
|
|
63
|
+
const style = styles.map(asset => asset.csp).join(" ");
|
|
64
|
+
const headers = app.headers({ script, style });
|
|
65
|
+
|
|
66
|
+
return app.view({ body, head, headers, ...options });
|
|
67
|
+
};
|
|
68
|
+
// }}}
|
|
69
|
+
// {{{ view
|
|
70
|
+
const view = (name, props, options) => async (app, ...rest) => {
|
|
71
|
+
const { fullExtension: extension } = new File(name);
|
|
72
|
+
return app.extensions[extension]?.handle(name, props, options)(app, ...rest)
|
|
73
|
+
?? errors.NoHandlerForExtension.throw(extension, name);
|
|
74
|
+
};
|
|
75
|
+
// }}}
|
|
76
|
+
|
|
77
|
+
export { text, json, stream, redirect, error, html, view, ws, sse };
|
package/src/hooks/exports.js
CHANGED
package/src/hooks/handle.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Response, Status, MediaType } from "rcompat/http";
|
|
2
2
|
import { cascade, tryreturn } from "rcompat/async";
|
|
3
3
|
import { respond } from "./respond/exports.js";
|
|
4
|
-
import { error as clientError } from "../handlers
|
|
4
|
+
import { error as clientError } from "../handlers.js";
|
|
5
5
|
|
|
6
6
|
const guard_error = Symbol("guard_error");
|
|
7
7
|
const guard = (app, guards) => async (request, next) => {
|
|
@@ -44,6 +44,7 @@ export default app => {
|
|
|
44
44
|
|
|
45
45
|
return tryreturn(async _ => {
|
|
46
46
|
const { path, guards, errors, layouts, handler } = await route(request);
|
|
47
|
+
|
|
47
48
|
error_handler = errors?.at(-1);
|
|
48
49
|
|
|
49
50
|
const pathed = { ...request, path };
|
|
@@ -52,6 +53,7 @@ export default app => {
|
|
|
52
53
|
|
|
53
54
|
// handle request
|
|
54
55
|
const response = await (await cascade(hooks, handler))(pathed);
|
|
56
|
+
|
|
55
57
|
const $layouts = { layouts: await get_layouts(layouts, request) };
|
|
56
58
|
return (await respond(response))(app, $layouts, pathed);
|
|
57
59
|
}).orelse(async error => {
|
|
@@ -18,7 +18,7 @@ const is_response = value => is_response_duck(value)
|
|
|
18
18
|
const is_stream = value => value instanceof ReadableStream
|
|
19
19
|
? stream(value) : is_response(value);
|
|
20
20
|
const is_blob = value => value instanceof Blob
|
|
21
|
-
|
|
21
|
+
? stream(value.stream()) : is_stream(value);
|
|
22
22
|
const is_URL = value => value instanceof URL
|
|
23
23
|
? redirect(value.href) : is_blob(value);
|
|
24
24
|
const guess = value => is_URL(value);
|
package/src/hooks/route.js
CHANGED
|
@@ -21,8 +21,7 @@ export default app => {
|
|
|
21
21
|
[name, type === undefined ? value : validate(types[type], value, name)],
|
|
22
22
|
)));
|
|
23
23
|
|
|
24
|
-
const is_type = (groups, pathname) => Object
|
|
25
|
-
.entries(groups ?? {})
|
|
24
|
+
const is_type = (groups, pathname) => Object.entries(groups ?? {})
|
|
26
25
|
.map(([name, value]) =>
|
|
27
26
|
[types[name] === undefined || explicit ? name : `${name}$${name}`, value])
|
|
28
27
|
.filter(([name]) => name.includes("$"))
|
package/src/run.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { tryreturn } from "rcompat/async";
|
|
2
2
|
import { File } from "rcompat/fs";
|
|
3
3
|
import { extend } from "rcompat/object";
|
|
4
|
+
import { runtime } from "rcompat/meta";
|
|
4
5
|
import app from "./app.js";
|
|
5
6
|
import { default as Logger, bye } from "./Logger.js";
|
|
6
7
|
import errors from "./errors.js";
|
|
@@ -8,7 +9,6 @@ import command from "./commands/exports.js";
|
|
|
8
9
|
import defaults from "./defaults/primate.config.js";
|
|
9
10
|
|
|
10
11
|
let logger = new Logger({ level: Logger.Warn });
|
|
11
|
-
const { runtime = "node" } = import.meta;
|
|
12
12
|
|
|
13
13
|
const get_config = async root => {
|
|
14
14
|
const name = "primate.config.js";
|
package/src/start.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { serve, Response, Status } from "rcompat/http";
|
|
2
|
-
import {
|
|
2
|
+
import { tryreturn } from "rcompat/async";
|
|
3
3
|
import { bold, blue } from "rcompat/colors";
|
|
4
4
|
import * as hooks from "./hooks/exports.js";
|
|
5
5
|
import { print } from "./Logger.js";
|
|
@@ -24,14 +24,11 @@ export default async (app$, mode = "development") => {
|
|
|
24
24
|
|
|
25
25
|
app.route = hooks.route(app);
|
|
26
26
|
app.parse = hooks.parse(app.dispatch);
|
|
27
|
-
|
|
28
|
-
const server = await serve(async request =>
|
|
27
|
+
app.server = await serve(async request =>
|
|
29
28
|
tryreturn(async _ => (await hooks.handle(app))(await app.parse(request)))
|
|
30
29
|
.orelse(error => {
|
|
31
30
|
app.log.auto(error);
|
|
32
31
|
return new Response(null, { status: Status.INTERNAL_SERVER_ERROR });
|
|
33
32
|
}),
|
|
34
33
|
app.config.http);
|
|
35
|
-
|
|
36
|
-
await (await cascade(app.modules.serve))({ ...app, server });
|
|
37
34
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ declare module "primate" {
|
|
|
3
3
|
|
|
4
4
|
interface MinOptions {
|
|
5
5
|
status: number,
|
|
6
|
+
headers: Headers | {},
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
interface ErrorOptions extends MinOptions {
|
|
@@ -11,7 +12,6 @@ declare module "primate" {
|
|
|
11
12
|
|
|
12
13
|
interface Options extends ErrorOptions {
|
|
13
14
|
placeholders: {},
|
|
14
|
-
headers: Headers | {},
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
type Dispatcher = {
|
|
@@ -61,4 +61,9 @@ declare module "primate" {
|
|
|
61
61
|
export function view(name: string, props: {}, options?: Options): ResponseFn;
|
|
62
62
|
|
|
63
63
|
export function error(body: string, options?: ErrorOptions): ResponseFn;
|
|
64
|
+
|
|
65
|
+
export function sse(implementation: {
|
|
66
|
+
open?: () => void,
|
|
67
|
+
close?: () => void,
|
|
68
|
+
}, options?: MinOptions): ResponseFn;
|
|
64
69
|
}
|
package/src/handlers/error.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { Response, Status, MediaType } from "rcompat/http";
|
|
2
|
-
|
|
3
|
-
export default (body = "Not Found", { status = Status.NOT_FOUND, page } = {}) =>
|
|
4
|
-
async app =>
|
|
5
|
-
new Response(await app.render({ body }, page ?? app.config.pages.error), {
|
|
6
|
-
status,
|
|
7
|
-
headers: { ...app.headers(), "Content-Type": MediaType.TEXT_HTML },
|
|
8
|
-
});
|
package/src/handlers/exports.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { default as text } from "./text.js";
|
|
2
|
-
export { default as json } from "./json.js";
|
|
3
|
-
export { default as stream } from "./stream.js";
|
|
4
|
-
export { default as redirect } from "./redirect.js";
|
|
5
|
-
export { default as html } from "./html.js";
|
|
6
|
-
export { default as view } from "./view.js";
|
|
7
|
-
export { default as error } from "./error.js";
|
package/src/handlers/html.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { Response, Status, MediaType } from "rcompat/http";
|
|
2
|
-
|
|
3
|
-
const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
|
|
4
|
-
const style_re = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
|
|
5
|
-
const remove = /<(?<tag>script|style)>.*?<\/\k<tag>>/gus;
|
|
6
|
-
|
|
7
|
-
const render = (body, head, { partial = false, app, page, placeholders }) =>
|
|
8
|
-
partial ? body : app.render({ body, head }, page, placeholders);
|
|
9
|
-
|
|
10
|
-
export default (name, options = {}) => async app => {
|
|
11
|
-
const component = await app.path.components.join(name).text();
|
|
12
|
-
const scripts = await Promise.all([...component.matchAll(script_re)]
|
|
13
|
-
.map(({ groups: { code } }) => app.inline(code, "module")));
|
|
14
|
-
const styles = await Promise.all([...component.matchAll(style_re)]
|
|
15
|
-
.map(({ groups: { code } }) => app.inline(code, "style")));
|
|
16
|
-
const assets = [...scripts, ...styles];
|
|
17
|
-
|
|
18
|
-
const body = component.replaceAll(remove, _ => "");
|
|
19
|
-
const head = assets.map(asset => asset.head).join("\n");
|
|
20
|
-
const script = scripts.map(asset => asset.csp).join(" ");
|
|
21
|
-
const style = styles.map(asset => asset.csp).join(" ");
|
|
22
|
-
const headers = { script, style };
|
|
23
|
-
|
|
24
|
-
return new Response(await render(body, head, { app, ...options }), {
|
|
25
|
-
status: options.status ?? Status.OK,
|
|
26
|
-
headers: { ...app.headers(headers), "Content-Type": MediaType.TEXT_HTML },
|
|
27
|
-
});
|
|
28
|
-
};
|
package/src/handlers/json.js
DELETED
package/src/handlers/redirect.js
DELETED
package/src/handlers/stream.js
DELETED
package/src/handlers/text.js
DELETED
package/src/handlers/view.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import errors from "../errors.js";
|
|
2
|
-
|
|
3
|
-
export default (name, props, options) => async (app, ...rest) => {
|
|
4
|
-
const extension = name.slice(name.lastIndexOf("."));
|
|
5
|
-
return app.extensions[extension]?.handle(name, props, options)(app, ...rest)
|
|
6
|
-
?? errors.NoHandlerForExtension.throw(extension, name);
|
|
7
|
-
};
|