primate 0.27.6 → 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 +5 -3
- package/src/app.js +40 -26
- package/src/cwd.js +2 -2
- package/src/errors.js +2 -2
- package/src/exports.js +1 -1
- package/src/handlers.js +77 -0
- package/src/hooks/copy_includes.js +2 -2
- package/src/hooks/exports.js +0 -1
- package/src/hooks/handle.js +3 -1
- package/src/hooks/parse.js +3 -3
- package/src/hooks/publish.js +2 -2
- package/src/hooks/register.js +6 -6
- package/src/hooks/respond/respond.js +2 -2
- package/src/hooks/route.js +1 -2
- package/src/loaders/common.js +4 -4
- package/src/loaders/routes/load.js +2 -2
- package/src/loaders/types.js +2 -2
- package/src/run.js +3 -3
- package/src/start.js +2 -5
- package/types/index.d.ts +69 -0
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Polymorphic development platform",
|
|
5
5
|
"homepage": "https://primatejs.com",
|
|
6
6
|
"bugs": "https://github.com/primatejs/primate/issues",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"src/**/*.js",
|
|
10
10
|
"src/errors.json",
|
|
11
11
|
"src/defaults/*.html",
|
|
12
|
-
"!src/**/*.spec.js"
|
|
12
|
+
"!src/**/*.spec.js",
|
|
13
|
+
"types/*.ts"
|
|
13
14
|
],
|
|
14
15
|
"bin": "src/bin.js",
|
|
15
16
|
"repository": {
|
|
@@ -18,11 +19,12 @@
|
|
|
18
19
|
"directory": "packages/primate"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
|
-
"rcompat": "^0.
|
|
22
|
+
"rcompat": "^0.7.2"
|
|
22
23
|
},
|
|
23
24
|
"engines": {
|
|
24
25
|
"node": ">=18"
|
|
25
26
|
},
|
|
26
27
|
"type": "module",
|
|
28
|
+
"types": "./types/index.d.ts",
|
|
27
29
|
"exports": "./src/exports.js"
|
|
28
30
|
}
|
package/src/app.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import crypto from "rcompat/crypto";
|
|
2
2
|
import { tryreturn } from "rcompat/async";
|
|
3
|
-
import {
|
|
3
|
+
import { File } from "rcompat/fs";
|
|
4
4
|
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
|
-
tryreturn(_ =>
|
|
19
|
-
.orelse(_ =>
|
|
18
|
+
const get_index = (base, page, fallback) =>
|
|
19
|
+
tryreturn(_ => File.text(`${base.join(page)}`))
|
|
20
|
+
.orelse(_ => File.text(`${base.join(fallback)}`));
|
|
20
21
|
|
|
21
22
|
const encoder = new TextEncoder();
|
|
22
23
|
|
|
@@ -51,7 +52,7 @@ const render_head = (assets, head) =>
|
|
|
51
52
|
: tags.script({ inline, code, type, integrity, src }),
|
|
52
53
|
).join("\n").concat("\n", head ?? "");
|
|
53
54
|
|
|
54
|
-
const { name, version } = await new
|
|
55
|
+
const { name, version } = await new File(import.meta.url).up(2)
|
|
55
56
|
.join(runtime.manifest).json();
|
|
56
57
|
|
|
57
58
|
export default async (log, root, config) => {
|
|
@@ -100,7 +101,7 @@ export default async (log, root, config) => {
|
|
|
100
101
|
|
|
101
102
|
await Promise.all((await source.collect(filter)).map(async path => {
|
|
102
103
|
const debased = path.debase(this.root).path.slice(1);
|
|
103
|
-
const filename =
|
|
104
|
+
const filename = File.join(directory, path.debase(source));
|
|
104
105
|
const target = await target_base.join(filename.debase(directory));
|
|
105
106
|
await target.directory.create();
|
|
106
107
|
await (regexs.some(regex => regex.test(debased))
|
|
@@ -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);
|
|
@@ -177,7 +191,7 @@ export default async (log, root, config) => {
|
|
|
177
191
|
}
|
|
178
192
|
if (inline || type === "style") {
|
|
179
193
|
this.assets.push({
|
|
180
|
-
src:
|
|
194
|
+
src: File.join(http.static.root, src ?? "").path,
|
|
181
195
|
code: inline ? code : "",
|
|
182
196
|
type,
|
|
183
197
|
inline,
|
|
@@ -203,7 +217,7 @@ export default async (log, root, config) => {
|
|
|
203
217
|
|
|
204
218
|
const parts = module.split("/");
|
|
205
219
|
const path = [this.library, ...parts];
|
|
206
|
-
const pkg = await
|
|
220
|
+
const pkg = await File.resolve().join(...path, this.manifest).json();
|
|
207
221
|
const exports = pkg.exports === undefined
|
|
208
222
|
? { [module]: `/${module}/${pkg.main}` }
|
|
209
223
|
: transform(pkg.exports, entry => entry
|
|
@@ -220,11 +234,11 @@ export default async (log, root, config) => {
|
|
|
220
234
|
?? value.default?.replace(".", `./${module}`)
|
|
221
235
|
?? value.import?.replace(".", `./${module}`),
|
|
222
236
|
]));
|
|
223
|
-
const dependency =
|
|
224
|
-
const target =
|
|
237
|
+
const dependency = File.resolve().join(...path);
|
|
238
|
+
const target = File.join(this.runpath(client), this.library, ...parts);
|
|
225
239
|
await dependency.copy(target);
|
|
226
240
|
this.importmaps = {
|
|
227
|
-
...valmap(exports, value =>
|
|
241
|
+
...valmap(exports, value => File.join(root, this.library, value).path),
|
|
228
242
|
...this.importmaps };
|
|
229
243
|
},
|
|
230
244
|
};
|
package/src/cwd.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { File } from "rcompat/fs";
|
|
2
2
|
|
|
3
|
-
export default (meta, up = 1) => new
|
|
3
|
+
export default (meta, up = 1) => new File(meta.url).up(up);
|
package/src/errors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { File } from "rcompat/fs";
|
|
2
2
|
import Logger from "./Logger.js";
|
|
3
3
|
|
|
4
|
-
const json = await new
|
|
4
|
+
const json = await new File(import.meta.url).up(1).join("errors.json").json();
|
|
5
5
|
|
|
6
6
|
const errors = Logger.err(json.errors, json.module);
|
|
7
7
|
|
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 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { File } from "rcompat/fs";
|
|
2
2
|
|
|
3
3
|
export default async (app, type, post = () => undefined) => {
|
|
4
4
|
const { config } = app;
|
|
@@ -14,7 +14,7 @@ export default async (app, type, post = () => undefined) => {
|
|
|
14
14
|
.map(async include => {
|
|
15
15
|
const path = app.root.join(include);
|
|
16
16
|
if (await path.exists()) {
|
|
17
|
-
const target =
|
|
17
|
+
const target = File.join(type, include);
|
|
18
18
|
await app.stage(path, target);
|
|
19
19
|
await post(target);
|
|
20
20
|
}
|
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 => {
|
package/src/hooks/parse.js
CHANGED
|
@@ -5,15 +5,15 @@ import errors from "../errors.js";
|
|
|
5
5
|
|
|
6
6
|
const deslash = url => url.replaceAll(/(?<!http:)\/{2,}/gu, _ => "/");
|
|
7
7
|
|
|
8
|
-
const parse_body = (
|
|
9
|
-
tryreturn(async _ => Body.parse(
|
|
8
|
+
const parse_body = (request, url) =>
|
|
9
|
+
tryreturn(async _ => await Body.parse(request) ?? {})
|
|
10
10
|
.orelse(error => errors.MismatchedBody.throw(url.pathname, error.message));
|
|
11
11
|
|
|
12
12
|
export default dispatch => async original => {
|
|
13
13
|
const { headers } = original;
|
|
14
14
|
const url = new URL(deslash(globalThis.decodeURIComponent(original.url)));
|
|
15
15
|
const cookies = headers.get("cookie");
|
|
16
|
-
const body = await parse_body(original
|
|
16
|
+
const body = await parse_body(original, url);
|
|
17
17
|
|
|
18
18
|
return { original, url, body, ...valmap({
|
|
19
19
|
query: [from(url.searchParams), url.search],
|
package/src/hooks/publish.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { File } from "rcompat/fs";
|
|
2
2
|
import { cascade } from "rcompat/async";
|
|
3
3
|
import { stringify } from "rcompat/object";
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ const post = async app => {
|
|
|
7
7
|
|
|
8
8
|
{
|
|
9
9
|
// after hook, publish a zero assumptions app.js (no css imports)
|
|
10
|
-
const src =
|
|
10
|
+
const src = File.join(root, app.config.build.index);
|
|
11
11
|
|
|
12
12
|
await app.publish({
|
|
13
13
|
code: app.exports.filter(({ type }) => type === "script")
|
package/src/hooks/register.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { File } from "rcompat/fs";
|
|
2
2
|
import { cascade } from "rcompat/async";
|
|
3
3
|
import cwd from "../cwd.js";
|
|
4
4
|
import copy_includes from "./copy_includes.js";
|
|
@@ -17,7 +17,7 @@ const pre = async app => {
|
|
|
17
17
|
if (await path.components.exists()) {
|
|
18
18
|
// copy .js files from components to build/client/components, since
|
|
19
19
|
// frontend frameworks handle non-js files
|
|
20
|
-
const target =
|
|
20
|
+
const target = File.join(client, components);
|
|
21
21
|
await app.stage(path.components, target, /^.*.js$/u);
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -29,13 +29,13 @@ const post = async app => {
|
|
|
29
29
|
|
|
30
30
|
if (await path.static.exists()) {
|
|
31
31
|
// copy static files to build/server/static
|
|
32
|
-
await app.stage(path.static,
|
|
32
|
+
await app.stage(path.static, File.join(location.server, location.static));
|
|
33
33
|
|
|
34
34
|
// copy static files to build/client/static
|
|
35
|
-
await app.stage(path.static,
|
|
35
|
+
await app.stage(path.static, File.join(location.client, location.static));
|
|
36
36
|
|
|
37
37
|
// publish JavaScript and CSS files
|
|
38
|
-
const imports = await
|
|
38
|
+
const imports = await File.collect(path.static, /\.(?:js|css)$/u);
|
|
39
39
|
await Promise.all(imports.map(async file => {
|
|
40
40
|
const code = await file.text();
|
|
41
41
|
const src = file.debase(path.static);
|
|
@@ -54,7 +54,7 @@ const post = async app => {
|
|
|
54
54
|
const client = app.runpath(location.client);
|
|
55
55
|
await copy_includes(app, location.client, async to =>
|
|
56
56
|
Promise.all((await to.collect(/\.js$/u)).map(async script => {
|
|
57
|
-
const src =
|
|
57
|
+
const src = File.join(root, script.path.replace(client, _ => ""));
|
|
58
58
|
await app.publish({ src, code: await script.text(), type: "module" });
|
|
59
59
|
})),
|
|
60
60
|
);
|
|
@@ -15,10 +15,10 @@ const is_object = value => is_non_null_object(value)
|
|
|
15
15
|
? json(value) : is_text(value);
|
|
16
16
|
const is_response = value => is_response_duck(value)
|
|
17
17
|
? _ => value : is_object(value);
|
|
18
|
-
const
|
|
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
|
-
? stream(value) :
|
|
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/loaders/common.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { File } from "rcompat/fs";
|
|
2
2
|
import { identity } from "rcompat/function";
|
|
3
3
|
import errors from "../errors.js";
|
|
4
4
|
|
|
@@ -17,13 +17,13 @@ export default async ({
|
|
|
17
17
|
warn = true,
|
|
18
18
|
} = {}) => {
|
|
19
19
|
const objects = directory === undefined ? [] : await Promise.all(
|
|
20
|
-
(await
|
|
20
|
+
(await File.collect(directory, /^.*.js$/u, { recursive }))
|
|
21
21
|
.filter(filter)
|
|
22
22
|
.map(async path => [
|
|
23
23
|
`${path}`.replace(directory, _ => "").slice(1, -ending.length),
|
|
24
|
-
|
|
24
|
+
await import(path),
|
|
25
25
|
]));
|
|
26
|
-
warn && await
|
|
26
|
+
warn && await directory.exists() && empty(log)(objects, name, directory);
|
|
27
27
|
|
|
28
28
|
return objects;
|
|
29
29
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { File } from "rcompat/fs";
|
|
2
2
|
import errors from "../../errors.js";
|
|
3
3
|
import to_sorted from "../../to_sorted.js";
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ export default type => async (log, directory, load) => {
|
|
|
10
10
|
.map(([name, object]) => [name.replace(replace, () => ""), object]),
|
|
11
11
|
([a], [b]) => a.length - b.length);
|
|
12
12
|
|
|
13
|
-
const resolve = name =>
|
|
13
|
+
const resolve = name => File.join(directory, name, `+${type}.js`);
|
|
14
14
|
objects.some(([name, value]) => typeof value.default !== "function"
|
|
15
15
|
&& errors.InvalidDefaultExport.throw(resolve(name)));
|
|
16
16
|
|
package/src/loaders/types.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { File } from "rcompat/fs";
|
|
2
2
|
import { is } from "rcompat/invariant";
|
|
3
3
|
import { tryreturn } from "rcompat/sync";
|
|
4
4
|
import errors from "../errors.js";
|
|
@@ -10,7 +10,7 @@ export default async (log, directory, load = fs) => {
|
|
|
10
10
|
const types = (await load({ log, directory, name: "types", filter }))
|
|
11
11
|
.map(([name, type]) => [name, type.default]);
|
|
12
12
|
|
|
13
|
-
const resolve = name =>
|
|
13
|
+
const resolve = name => File.join(directory, name);
|
|
14
14
|
types.every(([name, type]) => tryreturn(_ => {
|
|
15
15
|
is(type).object();
|
|
16
16
|
is(type.base).string();
|
package/src/run.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { tryreturn } from "rcompat/async";
|
|
2
|
-
import {
|
|
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";
|
|
@@ -28,7 +28,7 @@ const get_config = async root => {
|
|
|
28
28
|
|
|
29
29
|
export default async name => tryreturn(async _ => {
|
|
30
30
|
// use module root if possible, fall back to current directory
|
|
31
|
-
const root = await tryreturn(_ =>
|
|
31
|
+
const root = await tryreturn(_ => File.root()).orelse(_ => File.resolve());
|
|
32
32
|
const config = await get_config(root);
|
|
33
33
|
logger = new Logger(config.logger);
|
|
34
34
|
await command(name)(await app(logger, root, config));
|
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
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
declare module "primate" {
|
|
2
|
+
type App = any;
|
|
3
|
+
|
|
4
|
+
interface MinOptions {
|
|
5
|
+
status: number,
|
|
6
|
+
headers: Headers | {},
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ErrorOptions extends MinOptions {
|
|
10
|
+
page: string,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Options extends ErrorOptions {
|
|
14
|
+
placeholders: {},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Dispatcher = {
|
|
18
|
+
get(property: string): string,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type RequestFacade = {
|
|
22
|
+
body: {}
|
|
23
|
+
path: Dispatcher,
|
|
24
|
+
query: Dispatcher,
|
|
25
|
+
cookies: Dispatcher,
|
|
26
|
+
headers: Dispatcher,
|
|
27
|
+
original: Request,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type ResponseFn = (app: App, ...rest: any) => Response;
|
|
31
|
+
type ResponseFacade =
|
|
32
|
+
string
|
|
33
|
+
| object
|
|
34
|
+
| URL
|
|
35
|
+
| Blob
|
|
36
|
+
| ReadableStream
|
|
37
|
+
| Response
|
|
38
|
+
| ResponseFn;
|
|
39
|
+
|
|
40
|
+
type RouteFunction = (request?: RequestFacade) => ResponseFacade;
|
|
41
|
+
|
|
42
|
+
type Streamable = ReadableStream | Blob;
|
|
43
|
+
|
|
44
|
+
export type Route = {
|
|
45
|
+
get?: RouteFunction,
|
|
46
|
+
post?: RouteFunction,
|
|
47
|
+
put?: RouteFunction,
|
|
48
|
+
delete?: RouteFunction,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function text(body: string, options?: MinOptions): ResponseFn;
|
|
52
|
+
|
|
53
|
+
export function json(body: {}, options?: MinOptions): ResponseFn;
|
|
54
|
+
|
|
55
|
+
export function stream(body: Streamable, options?: MinOptions): ResponseFn;
|
|
56
|
+
|
|
57
|
+
export function redirect(location: string, options?: MinOptions): ResponseFn;
|
|
58
|
+
|
|
59
|
+
export function html(name: string, options?: MinOptions): ResponseFn;
|
|
60
|
+
|
|
61
|
+
export function view(name: string, props: {}, options?: Options): ResponseFn;
|
|
62
|
+
|
|
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;
|
|
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
|
-
};
|