primate 0.26.3 → 0.27.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/README.md +1 -1
- package/package.json +3 -3
- package/src/app.js +16 -14
- package/src/dispatch.js +3 -1
- package/src/errors.json +1 -11
- package/src/handlers/error.js +2 -3
- package/src/handlers/html.js +16 -17
- package/src/handlers/view.js +1 -1
- package/src/hooks/handle.js +20 -20
- package/src/hooks/parse.js +4 -22
- package/src/hooks/register.js +1 -1
- package/src/hooks/route.js +0 -3
- package/src/hooks/stage.js +1 -1
- package/src/loaders/common.js +1 -1
- package/src/loaders/routes/load.js +1 -1
- package/src/loaders/routes.js +5 -7
- package/src/loaders/types.js +3 -2
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.27.0",
|
|
4
|
+
"description": "Polymorphic development platform",
|
|
5
5
|
"homepage": "https://primatejs.com",
|
|
6
6
|
"bugs": "https://github.com/primatejs/primate/issues",
|
|
7
7
|
"license": "MIT",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"directory": "packages/primate"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"rcompat": "^0.
|
|
21
|
+
"rcompat": "^0.5.0"
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=18"
|
package/src/app.js
CHANGED
|
@@ -2,7 +2,7 @@ import crypto from "rcompat/crypto";
|
|
|
2
2
|
import { tryreturn } from "rcompat/async";
|
|
3
3
|
import { Path } from "rcompat/fs";
|
|
4
4
|
import { is } from "rcompat/invariant";
|
|
5
|
-
import { transform, valmap } from "rcompat/object";
|
|
5
|
+
import { transform, valmap, to } from "rcompat/object";
|
|
6
6
|
import { globify } from "rcompat/string";
|
|
7
7
|
import * as runtime from "rcompat/meta";
|
|
8
8
|
|
|
@@ -43,6 +43,14 @@ const tags = {
|
|
|
43
43
|
},
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
const render_head = (assets, head) =>
|
|
47
|
+
to_sorted(assets, ({ type }) => -1 * (type === "importmap"))
|
|
48
|
+
.map(({ src, code, type, inline, integrity }) =>
|
|
49
|
+
type === "style"
|
|
50
|
+
? tags.style({ inline, code, href: src })
|
|
51
|
+
: tags.script({ inline, code, type, integrity, src }),
|
|
52
|
+
).join("\n").concat("\n", head ?? "");
|
|
53
|
+
|
|
46
54
|
const { name, version } = await new Path(import.meta.url).up(2)
|
|
47
55
|
.join(runtime.manifest).json();
|
|
48
56
|
|
|
@@ -138,19 +146,13 @@ export default async (log, root, config) => {
|
|
|
138
146
|
runpath(...directories) {
|
|
139
147
|
return this.path.build.join(...directories);
|
|
140
148
|
},
|
|
141
|
-
async render({ body
|
|
142
|
-
const {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
(
|
|
148
|
-
.map(({ src, code, type, inline, integrity }) =>
|
|
149
|
-
type === "style"
|
|
150
|
-
? tags.style({ inline, code, href: src })
|
|
151
|
-
: tags.script({ inline, code, type, integrity, src }),
|
|
152
|
-
).join("\n").concat("\n", head);
|
|
153
|
-
return html.replace("%body%", _ => body).replace("%head%", _ => heads);
|
|
149
|
+
async render({ body, head }, page = config.pages.index, placeholders = {}) {
|
|
150
|
+
const { assets, config: { location, pages } } = this;
|
|
151
|
+
|
|
152
|
+
return to({ ...placeholders, body, head: render_head(assets, head) })
|
|
153
|
+
.reduce((html, [key, value]) => html.replace(`%${key}%`, value ?? ""),
|
|
154
|
+
await index(this.runpath(location.pages), page, pages.index))
|
|
155
|
+
.replaceAll(/%.*%/gus, "");
|
|
154
156
|
},
|
|
155
157
|
async inline(code, type) {
|
|
156
158
|
const integrity = await this.hash(code);
|
package/src/dispatch.js
CHANGED
package/src/errors.json
CHANGED
|
@@ -51,11 +51,6 @@
|
|
|
51
51
|
"fix": "use only latin letters and decimal digits in path parameters",
|
|
52
52
|
"level": "Error"
|
|
53
53
|
},
|
|
54
|
-
"InvalidRouteName": {
|
|
55
|
-
"message": "invalid route name {0}",
|
|
56
|
-
"fix": "do not use dots in route names",
|
|
57
|
-
"level": "Error"
|
|
58
|
-
},
|
|
59
54
|
"InvalidTypeExport": {
|
|
60
55
|
"message": "invalid type export at {0}",
|
|
61
56
|
"fix": "export object with a `base` string and a `validate` function",
|
|
@@ -96,14 +91,9 @@
|
|
|
96
91
|
"fix": "add configuration options or remove file",
|
|
97
92
|
"level": "Warn"
|
|
98
93
|
},
|
|
99
|
-
"NoFileForPath": {
|
|
100
|
-
"message": "no file for {0}",
|
|
101
|
-
"fix": "create a file at {1}{0}",
|
|
102
|
-
"level": "Info"
|
|
103
|
-
},
|
|
104
94
|
"NoHandlerForExtension": {
|
|
105
95
|
"message": "no handler for {0} extension",
|
|
106
|
-
"fix": "add handler module for
|
|
96
|
+
"fix": "add handler module for {0} files or remove {1}",
|
|
107
97
|
"level": "Error"
|
|
108
98
|
},
|
|
109
99
|
"NoRouteToPath": {
|
package/src/handlers/error.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Response, Status, MediaType } from "rcompat/http";
|
|
2
2
|
|
|
3
3
|
export default (body = "Not Found", { status = Status.NOT_FOUND, page } = {}) =>
|
|
4
|
-
async app =>
|
|
5
|
-
body,
|
|
6
|
-
page: page ?? app.config.pages.error }), {
|
|
4
|
+
async app =>
|
|
5
|
+
new Response(await app.render({ body }, page ?? app.config.pages.error), {
|
|
7
6
|
status,
|
|
8
7
|
headers: { ...app.headers(), "Content-Type": MediaType.TEXT_HTML },
|
|
9
8
|
});
|
package/src/handlers/html.js
CHANGED
|
@@ -4,26 +4,25 @@ const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
|
|
|
4
4
|
const style_re = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
|
|
5
5
|
const remove = /<(?<tag>script|style)>.*?<\/\k<tag>>/gus;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const render = (body, head, { partial = false, app, page, placeholders }) =>
|
|
8
|
+
partial ? body : app.render({ body, head }, page, placeholders);
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
23
|
|
|
24
|
-
|
|
25
|
-
status,
|
|
24
|
+
return new Response(await render(body, head, { app, ...options }), {
|
|
25
|
+
status: options.status ?? Status.OK,
|
|
26
26
|
headers: { ...app.headers(headers), "Content-Type": MediaType.TEXT_HTML },
|
|
27
27
|
});
|
|
28
|
-
};
|
|
29
28
|
};
|
package/src/handlers/view.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import errors from "../errors.js";
|
|
2
2
|
|
|
3
3
|
export default (name, props, options) => async (app, ...rest) => {
|
|
4
|
-
const extension = name.slice(name.lastIndexOf(".")
|
|
4
|
+
const extension = name.slice(name.lastIndexOf("."));
|
|
5
5
|
return app.handlers[extension]?.(name, props, options)(app, ...rest)
|
|
6
6
|
?? errors.NoHandlerForExtension.throw(extension, name);
|
|
7
7
|
};
|
package/src/hooks/handle.js
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
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 { invalid } from "./route.js";
|
|
5
4
|
import { error as clientError } from "../handlers/exports.js";
|
|
6
|
-
import _errors from "../errors.js";
|
|
7
|
-
const { NoFileForPath } = _errors;
|
|
8
5
|
|
|
9
6
|
const guard_error = Symbol("guard_error");
|
|
10
7
|
const guard = (app, guards) => async (request, next) => {
|
|
11
8
|
// handle guards
|
|
12
9
|
try {
|
|
13
|
-
|
|
14
|
-
const result = guard(request);
|
|
15
|
-
if (result
|
|
16
|
-
|
|
10
|
+
for (const guard of guards) {
|
|
11
|
+
const result = await guard(request);
|
|
12
|
+
if (result !== true) {
|
|
13
|
+
const error = new Error();
|
|
14
|
+
error.result = result;
|
|
15
|
+
error.type = guard_error;
|
|
16
|
+
throw error;
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
error.result = result;
|
|
20
|
-
error.type = guard_error;
|
|
21
|
-
throw error;
|
|
22
|
-
});
|
|
18
|
+
}
|
|
23
19
|
|
|
24
20
|
return next(request);
|
|
25
21
|
} catch (error) {
|
|
@@ -31,18 +27,23 @@ const guard = (app, guards) => async (request, next) => {
|
|
|
31
27
|
}
|
|
32
28
|
};
|
|
33
29
|
|
|
30
|
+
const get_layouts = async (layouts, request) => {
|
|
31
|
+
const stop_at = layouts.findIndex(({ recursive }) => recursive === false);
|
|
32
|
+
return Promise.all(layouts
|
|
33
|
+
.slice(stop_at === -1 ? 0 : stop_at)
|
|
34
|
+
.map(layout => layout.default(request)));
|
|
35
|
+
};
|
|
36
|
+
|
|
34
37
|
export default app => {
|
|
35
38
|
const { config: { http: { static: { root } }, location } } = app;
|
|
39
|
+
const route = request => app.route(request);
|
|
36
40
|
|
|
37
41
|
const as_route = async request => {
|
|
38
|
-
|
|
39
|
-
// if NoFileForPath is thrown, this will remain undefined
|
|
42
|
+
// if tryreturn throws, this will default
|
|
40
43
|
let error_handler = app.error.default;
|
|
41
44
|
|
|
42
45
|
return tryreturn(async _ => {
|
|
43
|
-
const { path, guards, errors, layouts, handler } =
|
|
44
|
-
? NoFileForPath.throw(pathname, location.static)
|
|
45
|
-
: await app.route(request);
|
|
46
|
+
const { path, guards, errors, layouts, handler } = await route(request);
|
|
46
47
|
error_handler = errors?.at(-1);
|
|
47
48
|
|
|
48
49
|
const pathed = { ...request, path };
|
|
@@ -51,9 +52,8 @@ export default app => {
|
|
|
51
52
|
|
|
52
53
|
// handle request
|
|
53
54
|
const response = (await cascade(hooks, handler))(pathed);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}, pathed);
|
|
55
|
+
const $layouts = { layouts: await get_layouts(layouts, request) };
|
|
56
|
+
return (await respond(await response))(app, $layouts, pathed);
|
|
57
57
|
}).orelse(async error => {
|
|
58
58
|
app.log.auto(error);
|
|
59
59
|
|
package/src/hooks/parse.js
CHANGED
|
@@ -1,34 +1,16 @@
|
|
|
1
|
-
import { URL,
|
|
2
|
-
import { tryreturn } from "rcompat/sync";
|
|
3
|
-
import { stringify } from "rcompat/streams";
|
|
1
|
+
import { URL, Body } from "rcompat/http";
|
|
4
2
|
import { from, valmap } from "rcompat/object";
|
|
5
|
-
import errors from "../errors.js";
|
|
6
3
|
|
|
7
|
-
const { APPLICATION_FORM_URLENCODED, APPLICATION_JSON } = MediaType;
|
|
8
|
-
|
|
9
|
-
const { decodeURIComponent: decode } = globalThis;
|
|
10
4
|
const deslash = url => url.replaceAll(/(?<!http:)\/{2,}/gu, _ => "/");
|
|
11
5
|
|
|
12
|
-
const contents = {
|
|
13
|
-
[APPLICATION_FORM_URLENCODED]: body => from(body.split("&")
|
|
14
|
-
.map(part => part.split("=")
|
|
15
|
-
.map(subpart => decode(subpart).replaceAll("+", " ")))),
|
|
16
|
-
[APPLICATION_JSON]: body => JSON.parse(body),
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const content = (type, body) =>
|
|
20
|
-
tryreturn(_ => contents[type?.split(";")[0]]?.(body) ?? body)
|
|
21
|
-
.orelse(_ => errors.CannotParseBody.throw(body, type));
|
|
22
|
-
|
|
23
6
|
export default dispatch => async original => {
|
|
24
|
-
const { headers } = original;
|
|
25
|
-
const url = new URL(deslash(
|
|
26
|
-
const body = await stringify(original.body);
|
|
7
|
+
const { body, headers } = original;
|
|
8
|
+
const url = new URL(deslash(globalThis.decodeURIComponent(original.url)));
|
|
27
9
|
const cookies = headers.get("cookie");
|
|
28
10
|
|
|
29
11
|
return { original, url,
|
|
12
|
+
body: await Body.parse(body, headers.get("content-type")) ?? {},
|
|
30
13
|
...valmap({
|
|
31
|
-
body: [content(headers.get("content-type"), body), body],
|
|
32
14
|
query: [from(url.searchParams), url.search],
|
|
33
15
|
headers: [from(headers), headers, false],
|
|
34
16
|
cookies: [from(cookies?.split(";").map(cookie => cookie.trim().split("="))
|
package/src/hooks/register.js
CHANGED
|
@@ -39,7 +39,7 @@ const post = async app => {
|
|
|
39
39
|
await Promise.all(imports.map(async file => {
|
|
40
40
|
const code = await file.text();
|
|
41
41
|
const src = file.debase(path.static);
|
|
42
|
-
const type = file.extension === "css" ? "style" : "module";
|
|
42
|
+
const type = file.extension === ".css" ? "style" : "module";
|
|
43
43
|
// already copied in `app.stage`
|
|
44
44
|
await app.publish({ src, code, type, copy: false });
|
|
45
45
|
type === "style" && app.export({ type,
|
package/src/hooks/route.js
CHANGED
|
@@ -6,9 +6,6 @@ import validate from "../validate.js";
|
|
|
6
6
|
// insensitive-case equal
|
|
7
7
|
const ieq = (left, right) => left.toLowerCase() === right.toLowerCase();
|
|
8
8
|
|
|
9
|
-
/* routes may not contain dots */
|
|
10
|
-
export const invalid = route => /\./u.test(route);
|
|
11
|
-
|
|
12
9
|
const deroot = pathname => pathname.endsWith("/") && pathname !== "/"
|
|
13
10
|
? pathname.slice(0, -1) : pathname;
|
|
14
11
|
|
package/src/hooks/stage.js
CHANGED
|
@@ -21,7 +21,7 @@ const post = async app => {
|
|
|
21
21
|
await app.runpath(location.routes).create();
|
|
22
22
|
const double = doubled((await app.path.routes.collect())
|
|
23
23
|
.map(path => path.debase(app.path.routes))
|
|
24
|
-
.map(path => `${path}`.slice(1, -path.extension.length
|
|
24
|
+
.map(path => `${path}`.slice(1, -path.extension.length)));
|
|
25
25
|
double && errors.DoubleRoute.throw(double);
|
|
26
26
|
|
|
27
27
|
await app.stage(app.path.routes, location.routes);
|
package/src/loaders/common.js
CHANGED
|
@@ -21,7 +21,7 @@ export default async ({
|
|
|
21
21
|
.filter(filter)
|
|
22
22
|
.map(async path => [
|
|
23
23
|
`${path}`.replace(directory, _ => "").slice(1, -ending.length),
|
|
24
|
-
(await import(path))
|
|
24
|
+
(await import(path)),
|
|
25
25
|
]));
|
|
26
26
|
warn && await Path.exists(directory) && empty(log)(objects, name, directory);
|
|
27
27
|
|
|
@@ -11,7 +11,7 @@ export default type => async (log, directory, load) => {
|
|
|
11
11
|
([a], [b]) => a.length - b.length);
|
|
12
12
|
|
|
13
13
|
const resolve = name => new Path(directory, name, `+${type}.js`);
|
|
14
|
-
objects.some(([name, value]) => typeof value !== "function"
|
|
14
|
+
objects.some(([name, value]) => typeof value.default !== "function"
|
|
15
15
|
&& errors.InvalidDefaultExport.throw(resolve(name)));
|
|
16
16
|
|
|
17
17
|
return objects;
|
package/src/loaders/routes.js
CHANGED
|
@@ -3,7 +3,6 @@ import { from } from "rcompat/object";
|
|
|
3
3
|
import { default as fs, doubled } from "./common.js";
|
|
4
4
|
import * as get from "./routes/exports.js";
|
|
5
5
|
import errors from "../errors.js";
|
|
6
|
-
import { invalid } from "../hooks/route.js";
|
|
7
6
|
|
|
8
7
|
const make = path => {
|
|
9
8
|
const double = doubled(path.split("/")
|
|
@@ -18,8 +17,6 @@ const make = path => {
|
|
|
18
17
|
return `(?<${param}>[^/]{1,}?)`;
|
|
19
18
|
}).orelse(_ => errors.InvalidPathParameter.throw(named, path)));
|
|
20
19
|
|
|
21
|
-
invalid(route) && errors.InvalidRouteName.throw(path);
|
|
22
|
-
|
|
23
20
|
return new RegExp(`^/${route}$`, "u");
|
|
24
21
|
};
|
|
25
22
|
|
|
@@ -31,18 +28,19 @@ export default async (app, load = fs) => {
|
|
|
31
28
|
.map(async extra => [extra, await get[extra](log, directory, load)])));
|
|
32
29
|
|
|
33
30
|
return (await get.routes(log, directory, load)).map(([path, imported]) => {
|
|
34
|
-
|
|
31
|
+
const route = imported.default;
|
|
32
|
+
if (route === undefined || Object.keys(route).length === 0) {
|
|
35
33
|
errors.EmptyRouteFile.warn(log, directory.join(`${path}.js`).path);
|
|
36
34
|
return [];
|
|
37
35
|
}
|
|
38
36
|
const filtered = filter(path);
|
|
39
37
|
|
|
40
|
-
return Object.entries(
|
|
38
|
+
return Object.entries(route).map(([method, handler]) => ({
|
|
41
39
|
method,
|
|
42
40
|
handler,
|
|
43
41
|
pathname: make(path.endsWith("/") ? path.slice(0, -1) : path),
|
|
44
|
-
guards: routes.guards.filter(filtered).map(([, guard]) => guard),
|
|
45
|
-
errors: routes.errors.filter(filtered).map(([, error]) => error),
|
|
42
|
+
guards: routes.guards.filter(filtered).map(([, guard]) => guard.default),
|
|
43
|
+
errors: routes.errors.filter(filtered).map(([, error]) => error.default),
|
|
46
44
|
layouts: routes.layouts.filter(filtered).map(([, layout]) => layout),
|
|
47
45
|
}));
|
|
48
46
|
}).flat();
|
package/src/loaders/types.js
CHANGED
|
@@ -6,8 +6,9 @@ import fs from "./common.js";
|
|
|
6
6
|
|
|
7
7
|
const filter = path => /^[a-z]/u.test(path.name);
|
|
8
8
|
|
|
9
|
-
export default async (log, directory
|
|
10
|
-
const types = await fs({ log, directory, name: "types", filter })
|
|
9
|
+
export default async (log, directory) => {
|
|
10
|
+
const types = (await fs({ log, directory, name: "types", filter }))
|
|
11
|
+
.map(([name, type]) => [name, type.default]);
|
|
11
12
|
|
|
12
13
|
const resolve = name => new Path(directory, name);
|
|
13
14
|
types.every(([name, type]) => tryreturn(_ => {
|