primate 0.31.13 → 0.32.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 +13 -6
- package/src/bin.js +3 -3
- package/src/commands/build.js +4 -0
- package/src/commands/dev.js +7 -2
- package/src/commands/exports.js +2 -1
- package/src/commands/serve.js +7 -2
- package/src/handlers/error.js +1 -0
- package/src/handlers/json.js +1 -0
- package/src/handlers/redirect.js +1 -0
- package/src/handlers/sse.js +1 -0
- package/src/handlers/stream.js +1 -0
- package/src/handlers/text.js +1 -0
- package/src/handlers/view.js +1 -0
- package/src/handlers/ws.js +1 -0
- package/src/init.js +12 -0
- package/README.md +0 -14
- package/src/Logger.js +0 -111
- package/src/app.js +0 -220
- package/src/defaults/app.html +0 -9
- package/src/defaults/error.html +0 -14
- package/src/defaults/primate.config.js +0 -55
- package/src/dispatch.js +0 -28
- package/src/errors.js +0 -8
- package/src/errors.json +0 -130
- package/src/exports.js +0 -9
- package/src/handlers.js +0 -93
- package/src/hooks/copy_includes.js +0 -20
- package/src/hooks/exports.js +0 -7
- package/src/hooks/handle.js +0 -113
- package/src/hooks/init.js +0 -3
- package/src/hooks/parse.js +0 -17
- package/src/hooks/publish.js +0 -3
- package/src/hooks/register.js +0 -60
- package/src/hooks/respond.js +0 -29
- package/src/hooks/route.js +0 -49
- package/src/hooks/stage.js +0 -64
- package/src/loaders/common.js +0 -32
- package/src/loaders/exports.js +0 -2
- package/src/loaders/modules.js +0 -33
- package/src/loaders/types.js +0 -29
- package/src/run.js +0 -42
- package/src/start.js +0 -66
- package/src/to_sorted.js +0 -1
- package/src/validate.js +0 -10
- package/types/index.d.ts +0 -69
package/src/errors.json
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"module": "primate",
|
|
3
|
-
"errors": {
|
|
4
|
-
"DoubleFileExtension": {
|
|
5
|
-
"message": "double file extension {0}",
|
|
6
|
-
"fix": "unload one of the two handlers registering the file extension",
|
|
7
|
-
"level": "Error"
|
|
8
|
-
},
|
|
9
|
-
"DoubleModule": {
|
|
10
|
-
"message": "double module {0} in {1}",
|
|
11
|
-
"fix": "load {0} only once",
|
|
12
|
-
"level": "Error"
|
|
13
|
-
},
|
|
14
|
-
"DoublePathParameter": {
|
|
15
|
-
"message": "double path parameter {0} in route {1}",
|
|
16
|
-
"fix": "disambiguate path parameters in route names",
|
|
17
|
-
"level": "Error"
|
|
18
|
-
},
|
|
19
|
-
"DoubleRoute": {
|
|
20
|
-
"message": "double route of the form {0}",
|
|
21
|
-
"fix": "disambiguate routes",
|
|
22
|
-
"level": "Error"
|
|
23
|
-
},
|
|
24
|
-
"EmptyConfigFile": {
|
|
25
|
-
"message": "empty config file at {0}",
|
|
26
|
-
"fix": "add configuration options to the file or remove it",
|
|
27
|
-
"level": "Warn"
|
|
28
|
-
},
|
|
29
|
-
"EmptyPathParameter": {
|
|
30
|
-
"message": "empty path parameter {0} in route {1}",
|
|
31
|
-
"fix": "name the parameter or remove it",
|
|
32
|
-
"level": "Error"
|
|
33
|
-
},
|
|
34
|
-
"EmptyRouteFile": {
|
|
35
|
-
"message": "empty route file at {0}",
|
|
36
|
-
"fix": "add routes to the file or remove it",
|
|
37
|
-
"level": "Warn"
|
|
38
|
-
},
|
|
39
|
-
"EmptyDirectory": {
|
|
40
|
-
"message": "empty {0} directory",
|
|
41
|
-
"fix": "populate {1} or remove it",
|
|
42
|
-
"level": "Warn"
|
|
43
|
-
},
|
|
44
|
-
"ErrorInConfigFile": {
|
|
45
|
-
"message": "error in config file: {0}",
|
|
46
|
-
"fix": "check errors in config file by running {1}",
|
|
47
|
-
"level": "Error"
|
|
48
|
-
},
|
|
49
|
-
"InvalidBodyReturned": {
|
|
50
|
-
"message": "invalid body returned from route, got {0}",
|
|
51
|
-
"fix": "return a proper body from route",
|
|
52
|
-
"level": "Error"
|
|
53
|
-
},
|
|
54
|
-
"InvalidDefaultExport": {
|
|
55
|
-
"message": "invalid default export at {0}",
|
|
56
|
-
"fix": "use only functions for the default export",
|
|
57
|
-
"level": "Error"
|
|
58
|
-
},
|
|
59
|
-
"InvalidPath": {
|
|
60
|
-
"message": "invalid path {0}",
|
|
61
|
-
"fix": "use only letters, digits, '_', '[', ']' or '=' in path filenames",
|
|
62
|
-
"level": "Error"
|
|
63
|
-
},
|
|
64
|
-
"InvalidTypeExport": {
|
|
65
|
-
"message": "invalid type export at {0}",
|
|
66
|
-
"fix": "export object with a `base` string and a `validate` function",
|
|
67
|
-
"level": "Error"
|
|
68
|
-
},
|
|
69
|
-
"InvalidTypeName": {
|
|
70
|
-
"message": "invalid type name {0}",
|
|
71
|
-
"fix": "use lowercase-first latin letters and decimals in type names",
|
|
72
|
-
"level": "Error"
|
|
73
|
-
},
|
|
74
|
-
"MismatchedBody": {
|
|
75
|
-
"message": "{0}: {1}",
|
|
76
|
-
"fix": "make sure the body payload corresponds to the used content type",
|
|
77
|
-
"level": "Error"
|
|
78
|
-
},
|
|
79
|
-
"MismatchedPath": {
|
|
80
|
-
"message": "mismatched path {0}: {1}",
|
|
81
|
-
"fix": "fix the type or the caller",
|
|
82
|
-
"level": "Info"
|
|
83
|
-
},
|
|
84
|
-
"MismatchedType": {
|
|
85
|
-
"message": "mismatched type: {0}",
|
|
86
|
-
"fix": "fix the type or the caller",
|
|
87
|
-
"level": "Info"
|
|
88
|
-
},
|
|
89
|
-
"ModuleHasNoHooks": {
|
|
90
|
-
"message": "module {0} has no hooks",
|
|
91
|
-
"fix": "ensure every module uses at least one hook or deactivate it",
|
|
92
|
-
"level": "Warn"
|
|
93
|
-
},
|
|
94
|
-
"ModuleHasNoName": {
|
|
95
|
-
"message": "module at index {0} has no name",
|
|
96
|
-
"fix": "update module at index {0} and inform maintainer",
|
|
97
|
-
"level": "Error"
|
|
98
|
-
},
|
|
99
|
-
"ModulesMustBeArray" :{
|
|
100
|
-
"message": "the {0} config property must be an array",
|
|
101
|
-
"fix": "change {0} to an array in the config or remove this property",
|
|
102
|
-
"level": "Error"
|
|
103
|
-
},
|
|
104
|
-
"NoHandlerForComponent": {
|
|
105
|
-
"message": "no handler for {0}",
|
|
106
|
-
"fix": "add handler module for this component or remove {0}",
|
|
107
|
-
"level": "Error"
|
|
108
|
-
},
|
|
109
|
-
"NoRouteToPath": {
|
|
110
|
-
"message": "no {0} route to {1}",
|
|
111
|
-
"fix": "create a {0} route function at {2}.js",
|
|
112
|
-
"level": "Info"
|
|
113
|
-
},
|
|
114
|
-
"OptionalRoute": {
|
|
115
|
-
"message": "optional route {0} must be a leaf",
|
|
116
|
-
"fix": "move route to leaf (last) position in filesystem hierarchy",
|
|
117
|
-
"level": "Error"
|
|
118
|
-
},
|
|
119
|
-
"ReservedTypeName": {
|
|
120
|
-
"message": "reserved type name {0}",
|
|
121
|
-
"fix": "do not use any reserved type names",
|
|
122
|
-
"level": "Error"
|
|
123
|
-
},
|
|
124
|
-
"RestRoute": {
|
|
125
|
-
"message": "rest route {0} must be a leaf",
|
|
126
|
-
"fix": "move route to leaf (last) position in filesystem hierarchy",
|
|
127
|
-
"level": "Error"
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
package/src/exports.js
DELETED
package/src/handlers.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import FS from "rcompat/fs";
|
|
2
|
-
import { MediaType, Status } from "rcompat/http";
|
|
3
|
-
import { identity } from "rcompat/function";
|
|
4
|
-
import { HTML } from "rcompat/string";
|
|
5
|
-
import errors from "./errors.js";
|
|
6
|
-
|
|
7
|
-
const handle = (mediatype, mapper = identity) => (body, options) => app =>
|
|
8
|
-
app.respond(mapper(body), app.media(mediatype, options));
|
|
9
|
-
|
|
10
|
-
// {{{ text
|
|
11
|
-
const text = handle(MediaType.TEXT_PLAIN);
|
|
12
|
-
// }}}
|
|
13
|
-
// {{{ json
|
|
14
|
-
const json = handle(MediaType.APPLICATION_JSON, JSON.stringify);
|
|
15
|
-
// }}}
|
|
16
|
-
// {{{ stream
|
|
17
|
-
const stream = handle(MediaType.APPLICATION_OCTET_STREAM);
|
|
18
|
-
// }}}
|
|
19
|
-
// {{{ ws
|
|
20
|
-
const ws = implementation => ({ server }, _, { original }) =>
|
|
21
|
-
server.upgrade(original, implementation);
|
|
22
|
-
// }}}
|
|
23
|
-
// {{{ sse
|
|
24
|
-
const sse = handle(MediaType.TEXT_EVENT_STREAM, implementation =>
|
|
25
|
-
new ReadableStream({
|
|
26
|
-
start(controller) {
|
|
27
|
-
implementation.open({
|
|
28
|
-
send(name, data) {
|
|
29
|
-
const event = data === undefined ? "" : `event: ${name}\n`;
|
|
30
|
-
const $data = data === undefined ? name : data;
|
|
31
|
-
controller.enqueue(`${event}data:${JSON.stringify($data)}\n\n`);
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
},
|
|
35
|
-
cancel() {
|
|
36
|
-
implementation.close?.();
|
|
37
|
-
},
|
|
38
|
-
}));
|
|
39
|
-
// }}}
|
|
40
|
-
// {{{ redirect
|
|
41
|
-
const redirect = (Location, { status = Status.FOUND } = {}) => app =>
|
|
42
|
-
/* no body */
|
|
43
|
-
app.respond(null, { status, headers: { Location } });
|
|
44
|
-
// }}}
|
|
45
|
-
// {{{ error
|
|
46
|
-
const error = (body = "Not Found", { status = Status.NOT_FOUND, page } = {}) =>
|
|
47
|
-
app => app.view({ body, status, page: page ?? app.get("pages.error") });
|
|
48
|
-
// }}}
|
|
49
|
-
// {{{ html
|
|
50
|
-
const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
|
|
51
|
-
const style_re = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
|
|
52
|
-
const remove = /<(?<tag>script|style)>.*?<\/\k<tag>>/gus;
|
|
53
|
-
const render = (component, props = {}) => {
|
|
54
|
-
const encoded = JSON.parse(HTML.escape(JSON.stringify(props)));
|
|
55
|
-
const keys = Object.keys(encoded);
|
|
56
|
-
const values = Object.values(encoded);
|
|
57
|
-
return new Function(...keys, `return \`${component}\`;`)(...values);
|
|
58
|
-
};
|
|
59
|
-
const html = (name, props, options = {}) => async app => {
|
|
60
|
-
const location = app.get("location");
|
|
61
|
-
const components = app.runpath(location.server, location.components);
|
|
62
|
-
const component = await components.join(name).text();
|
|
63
|
-
const { head: xhead = [], csp = {}, headers, ...rest } = options;
|
|
64
|
-
const { script_src: xscript_src = [], style_src: xstyle_src = [] } = csp;
|
|
65
|
-
const scripts = await Promise.all([...component.matchAll(script_re)]
|
|
66
|
-
.map(({ groups: { code } }) => app.inline(code, "module")));
|
|
67
|
-
const styles = await Promise.all([...component.matchAll(style_re)]
|
|
68
|
-
.map(({ groups: { code } }) => app.inline(code, "style")));
|
|
69
|
-
const style_src = styles.map(asset => asset.integrity).concat(xstyle_src);
|
|
70
|
-
const script_src = scripts.map(asset => asset.integrity).concat(xscript_src);
|
|
71
|
-
const head = [...scripts, ...styles].map(asset => asset.head);
|
|
72
|
-
|
|
73
|
-
return app.view({
|
|
74
|
-
body: render(component.replaceAll(remove, _ => ""), props),
|
|
75
|
-
head: [...head, ...xhead].join("\n"),
|
|
76
|
-
headers: {
|
|
77
|
-
...app.headers({ "style-src": style_src, "script-src": script_src }),
|
|
78
|
-
...headers,
|
|
79
|
-
},
|
|
80
|
-
...rest,
|
|
81
|
-
});
|
|
82
|
-
};
|
|
83
|
-
// }}}
|
|
84
|
-
// {{{ view
|
|
85
|
-
const extensions = ["fullExtension", "extension"];
|
|
86
|
-
const view = (name, props, options) => (app, ...rest) => extensions
|
|
87
|
-
.map(extension => app.extensions[new FS.File(name)[extension]])
|
|
88
|
-
.find(extension => extension?.handle)
|
|
89
|
-
?.handle(name, props, options)(app, ...rest)
|
|
90
|
-
?? errors.NoHandlerForComponent.throw(name);
|
|
91
|
-
// }}}
|
|
92
|
-
|
|
93
|
-
export { text, json, stream, redirect, error, html, view, ws, sse };
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import FS from "rcompat/fs";
|
|
2
|
-
|
|
3
|
-
export default async (app, type, post = () => undefined) => {
|
|
4
|
-
const includes = app.get("build.includes");
|
|
5
|
-
const reserved = Object.values(app.get("location"));
|
|
6
|
-
|
|
7
|
-
if (Array.isArray(includes)) {
|
|
8
|
-
await Promise.all(includes
|
|
9
|
-
.filter(include => !reserved.includes(include))
|
|
10
|
-
.filter(include => /^[^/]*$/u.test(include))
|
|
11
|
-
.map(async include => {
|
|
12
|
-
const path = app.root.join(include);
|
|
13
|
-
if (await path.exists()) {
|
|
14
|
-
const target = FS.File.join(type, include);
|
|
15
|
-
await app.stage(path, target);
|
|
16
|
-
await post(target);
|
|
17
|
-
}
|
|
18
|
-
}));
|
|
19
|
-
}
|
|
20
|
-
};
|
package/src/hooks/exports.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { default as init } from "./init.js";
|
|
2
|
-
export { default as stage } from "./stage.js";
|
|
3
|
-
export { default as register } from "./register.js";
|
|
4
|
-
export { default as publish } from "./publish.js";
|
|
5
|
-
export { default as route } from "./route.js";
|
|
6
|
-
export { default as handle } from "./handle.js";
|
|
7
|
-
export { default as parse } from "./parse.js";
|
package/src/hooks/handle.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { Response, Status, MediaType, fetch } from "rcompat/http";
|
|
2
|
-
import { cascade, tryreturn } from "rcompat/async";
|
|
3
|
-
import respond from "./respond.js";
|
|
4
|
-
import { error as clientError } from "../handlers.js";
|
|
5
|
-
|
|
6
|
-
const guard_error = Symbol("guard_error");
|
|
7
|
-
const guard = (app, guards) => async (request, next) => {
|
|
8
|
-
// handle guards
|
|
9
|
-
try {
|
|
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
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return next(request);
|
|
21
|
-
} catch (error) {
|
|
22
|
-
if (error.type === guard_error) {
|
|
23
|
-
return { request, response: respond(error.result)(app) };
|
|
24
|
-
}
|
|
25
|
-
// rethrow if not guard error
|
|
26
|
-
throw error;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
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(request)));
|
|
35
|
-
};
|
|
36
|
-
// last handler, preserve final request form
|
|
37
|
-
const last = handler => async request => {
|
|
38
|
-
const response = await handler(request);
|
|
39
|
-
return { request, response };
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export default app => {
|
|
43
|
-
const route = request => app.route(request);
|
|
44
|
-
|
|
45
|
-
const as_route = async request => {
|
|
46
|
-
// if tryreturn throws, this will default
|
|
47
|
-
let error_handler = app.error.default;
|
|
48
|
-
|
|
49
|
-
return tryreturn(async _ => {
|
|
50
|
-
const { body, path, guards, errors, layouts, handler } =
|
|
51
|
-
await route(request);
|
|
52
|
-
|
|
53
|
-
error_handler = errors?.at(-1);
|
|
54
|
-
|
|
55
|
-
const hooks = [...app.modules.route, guard(app, guards), last(handler)];
|
|
56
|
-
|
|
57
|
-
// handle request
|
|
58
|
-
const routed = await (await cascade(hooks))({ ...request, body, path });
|
|
59
|
-
|
|
60
|
-
const $layouts = { layouts: await get_layouts(layouts, routed.request) };
|
|
61
|
-
return respond(routed.response)(app, $layouts, routed.request);
|
|
62
|
-
}).orelse(async error => {
|
|
63
|
-
app.log.auto(error);
|
|
64
|
-
|
|
65
|
-
// the +error.js page itself could fail
|
|
66
|
-
return tryreturn(_ => respond(error_handler(request))(app, {}, request))
|
|
67
|
-
.orelse(_ => clientError()(app, {}, request));
|
|
68
|
-
});
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const as_asset = async path => new Response(path.stream(), {
|
|
72
|
-
status: Status.OK,
|
|
73
|
-
headers: {
|
|
74
|
-
"Content-Type": MediaType.resolve(path.name),
|
|
75
|
-
Etag: await path.modified(),
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const location = app.get("location");
|
|
80
|
-
const root = app.get("http.static.root");
|
|
81
|
-
const client = app.runpath(location.client);
|
|
82
|
-
const handle = async request => {
|
|
83
|
-
const { pathname } = request.url;
|
|
84
|
-
if (pathname.startsWith(root)) {
|
|
85
|
-
const debased = pathname.replace(root, _ => "");
|
|
86
|
-
// try static first
|
|
87
|
-
const asset = client.join(location.static, debased);
|
|
88
|
-
if (await asset.isFile) {
|
|
89
|
-
return as_asset(asset);
|
|
90
|
-
}
|
|
91
|
-
const path = client.join(debased);
|
|
92
|
-
if (await path.isFile) {
|
|
93
|
-
return as_asset(path);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return as_route(request);
|
|
97
|
-
};
|
|
98
|
-
// first hook
|
|
99
|
-
const pass = (request, next) => next({
|
|
100
|
-
...request,
|
|
101
|
-
pass(to) {
|
|
102
|
-
const { method, headers, body } = request.original;
|
|
103
|
-
const input = `${to}${request.url.pathname}`;
|
|
104
|
-
|
|
105
|
-
return fetch(input, { headers, method, body, duplex: "half" });
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
const hotreload = (request, next) => app.mode === "development"
|
|
109
|
-
? app.build.proxy(request, next)
|
|
110
|
-
: next(request);
|
|
111
|
-
|
|
112
|
-
return cascade([pass, hotreload, ...app.modules.handle], handle);
|
|
113
|
-
};
|
package/src/hooks/init.js
DELETED
package/src/hooks/parse.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { URL } from "rcompat/http";
|
|
2
|
-
import o from "rcompat/object";
|
|
3
|
-
|
|
4
|
-
export default app => async original => {
|
|
5
|
-
const { headers } = original;
|
|
6
|
-
|
|
7
|
-
const url = new URL(original.url);
|
|
8
|
-
const cookies = headers.get("cookie");
|
|
9
|
-
|
|
10
|
-
return { original, url,
|
|
11
|
-
...o.valmap({
|
|
12
|
-
query: [o.from(url.searchParams), url.search],
|
|
13
|
-
headers: [o.from(headers), headers, false],
|
|
14
|
-
cookies: [o.from(cookies?.split(";").map(cookie =>
|
|
15
|
-
cookie.trim().split("=")) ?? []), cookies],
|
|
16
|
-
}, value => app.dispatch(...value)) };
|
|
17
|
-
};
|
package/src/hooks/publish.js
DELETED
package/src/hooks/register.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import FS from "rcompat/fs";
|
|
2
|
-
import { cascade } from "rcompat/async";
|
|
3
|
-
import copy_includes from "./copy_includes.js";
|
|
4
|
-
|
|
5
|
-
const html = /^.*.html$/u;
|
|
6
|
-
const defaults = new FS.File(import.meta.url).up(2).join("defaults");
|
|
7
|
-
|
|
8
|
-
const pre = async app => {
|
|
9
|
-
const pages = app.get("location.pages");
|
|
10
|
-
|
|
11
|
-
// copy framework pages
|
|
12
|
-
await app.stage(defaults, pages, html);
|
|
13
|
-
// overwrite transformed pages to build
|
|
14
|
-
await app.path.pages.exists() && await app.stage(app.path.pages, pages, html);
|
|
15
|
-
|
|
16
|
-
return app;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const post = async app => {
|
|
20
|
-
const _static = app.path.static;
|
|
21
|
-
const location = app.get("location");
|
|
22
|
-
|
|
23
|
-
if (await _static.exists()) {
|
|
24
|
-
// copy static files to build/server/static
|
|
25
|
-
await app.stage(_static, FS.File.join(location.server, location.static));
|
|
26
|
-
|
|
27
|
-
// copy static files to build/client/static
|
|
28
|
-
await app.stage(_static, FS.File.join(location.client, location.static));
|
|
29
|
-
|
|
30
|
-
// copy static files to build/static
|
|
31
|
-
await app.stage(_static, FS.File.join(location.static));
|
|
32
|
-
|
|
33
|
-
// publish JavaScript and CSS files
|
|
34
|
-
const imports = await FS.File.collect(_static, /\.(?:css)$/u);
|
|
35
|
-
await Promise.all(imports.map(async file => {
|
|
36
|
-
const src = file.debase(_static);
|
|
37
|
-
app.build.export(`import "./${location.static}${src}";`);
|
|
38
|
-
}));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// copy additional subdirectories to build/server
|
|
42
|
-
await copy_includes(app, location.server);
|
|
43
|
-
// copy additional subdirectories to build
|
|
44
|
-
await copy_includes(app, "");
|
|
45
|
-
|
|
46
|
-
if (await app.path.components.exists()) {
|
|
47
|
-
// copy components to build/components
|
|
48
|
-
await app.stage(app.path.components, FS.File.join(location.components));
|
|
49
|
-
|
|
50
|
-
const components = await app.runpath(location.components).collect();
|
|
51
|
-
|
|
52
|
-
// from the build directory, compile to server and client
|
|
53
|
-
await Promise.all(components.map(component => app.compile(component)));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return app;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export default async app =>
|
|
60
|
-
post(await (await cascade(app.modules.register))(await pre(app)));
|
package/src/hooks/respond.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import FS from "rcompat/fs";
|
|
2
|
-
import { URL, Response } from "rcompat/http";
|
|
3
|
-
import { identity } from "rcompat/function";
|
|
4
|
-
import o from "rcompat/object";
|
|
5
|
-
import { text, json, stream, redirect } from "primate";
|
|
6
|
-
import errors from "../errors.js";
|
|
7
|
-
|
|
8
|
-
const not_found = value => errors.InvalidBodyReturned.throw(value);
|
|
9
|
-
const is_text = value => typeof value === "string";
|
|
10
|
-
const is_instance = of => value => value instanceof of;
|
|
11
|
-
const is_response = is_instance(globalThis.Response);
|
|
12
|
-
const is_fake_response = is_instance(Response);
|
|
13
|
-
const is_streamable =
|
|
14
|
-
value => value instanceof FS.Blob || value?.streamable === FS.s_streamable;
|
|
15
|
-
|
|
16
|
-
// [if, then]
|
|
17
|
-
const guesses = [
|
|
18
|
-
[is_instance(URL), redirect],
|
|
19
|
-
[is_streamable, value => stream(value.stream())],
|
|
20
|
-
[is_instance(ReadableStream), stream],
|
|
21
|
-
[value => is_response(value) || is_fake_response(value), value => _ => value],
|
|
22
|
-
[o.proper, json],
|
|
23
|
-
[is_text, text],
|
|
24
|
-
[not_found, identity],
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
const guess = value => guesses.find(([check]) => check(value))?.[1](value);
|
|
28
|
-
|
|
29
|
-
export default result => typeof result === "function" ? result : guess(result);
|
package/src/hooks/route.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import o from "rcompat/object";
|
|
2
|
-
import { tryreturn } from "rcompat/sync";
|
|
3
|
-
import { Body } from "rcompat/http";
|
|
4
|
-
import $errors from "../errors.js";
|
|
5
|
-
import validate from "../validate.js";
|
|
6
|
-
|
|
7
|
-
const { MismatchedBody, MismatchedPath, NoRouteToPath } = $errors;
|
|
8
|
-
|
|
9
|
-
const deroot = pathname => pathname.endsWith("/") && pathname !== "/"
|
|
10
|
-
? pathname.slice(0, -1) : pathname;
|
|
11
|
-
|
|
12
|
-
const parse_body = (request, url) =>
|
|
13
|
-
tryreturn(async _ => await Body.parse(request) ?? {})
|
|
14
|
-
.orelse(error => MismatchedBody.throw(url.pathname, error.message));
|
|
15
|
-
|
|
16
|
-
export default app => {
|
|
17
|
-
const $request_body_parse = app.get("request.body.parse");
|
|
18
|
-
const $location = app.get("location");
|
|
19
|
-
|
|
20
|
-
const index = path => `${$location.routes}${path === "/" ? "/index" : path}`;
|
|
21
|
-
// remove excess slashes
|
|
22
|
-
const deslash = url => url.replaceAll(/\/{2,}/gu, _ => "/");
|
|
23
|
-
|
|
24
|
-
return async ({ original, url }) => {
|
|
25
|
-
const pathname = deroot(deslash(url.pathname));
|
|
26
|
-
const route = await app.router.match(original) ?? NoRouteToPath
|
|
27
|
-
.throw(original.method.toLowerCase(), pathname, index(pathname));
|
|
28
|
-
const { params } = route;
|
|
29
|
-
const untyped_path = Object.fromEntries(Object.entries(params)
|
|
30
|
-
.filter(([name]) => !name.includes("="))
|
|
31
|
-
.map(([key, value]) => [key, value]));
|
|
32
|
-
const typed_path = Object.fromEntries(Object.entries(params)
|
|
33
|
-
.filter(([name]) => name.includes("="))
|
|
34
|
-
.map(([name, value]) => [name.split("="), value])
|
|
35
|
-
.map(([[name, type], value]) =>
|
|
36
|
-
tryreturn(_ => {
|
|
37
|
-
validate(app.types[type], value, name);
|
|
38
|
-
return [name, value];
|
|
39
|
-
}).orelse(({ message }) => MismatchedPath.throw(pathname, message))));
|
|
40
|
-
const path = app.dispatch({ ...untyped_path, ...typed_path });
|
|
41
|
-
const local_parse_body = route.file.body?.parse ?? $request_body_parse;
|
|
42
|
-
const body = local_parse_body ? await parse_body(original, url) : null;
|
|
43
|
-
const { guards = [], errors = [], layouts = [] } = o.map(route.specials,
|
|
44
|
-
([key, value]) => [`${key}s`, value.map(i => i.default)]);
|
|
45
|
-
const handler = route.file.default[original.method.toLowerCase()];
|
|
46
|
-
|
|
47
|
-
return { body, path, guards, errors, layouts, handler };
|
|
48
|
-
};
|
|
49
|
-
};
|
package/src/hooks/stage.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import FS from "rcompat/fs";
|
|
2
|
-
import { cascade } from "rcompat/async";
|
|
3
|
-
import dispatch from "../dispatch.js";
|
|
4
|
-
import * as loaders from "../loaders/exports.js";
|
|
5
|
-
import { doubled } from "../loaders/common.js";
|
|
6
|
-
import errors from "../errors.js";
|
|
7
|
-
|
|
8
|
-
const pre = async app => {
|
|
9
|
-
// remove build directory in case exists
|
|
10
|
-
await app.path.build.remove();
|
|
11
|
-
await app.path.build.create();
|
|
12
|
-
await Promise.all(["server", "client", "pages", "components"]
|
|
13
|
-
.map(directory => app.runpath(directory).create()));
|
|
14
|
-
|
|
15
|
-
return { ...app };
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const post = async app => {
|
|
19
|
-
const $location = app.get("location");
|
|
20
|
-
|
|
21
|
-
// stage routes
|
|
22
|
-
if (await app.path.routes.exists()) {
|
|
23
|
-
await app.stage(app.path.routes, $location.routes);
|
|
24
|
-
}
|
|
25
|
-
if (await app.path.types.exists()) {
|
|
26
|
-
await app.stage(app.path.types, $location.types);
|
|
27
|
-
}
|
|
28
|
-
const user_types = await loaders.types(app.log, app.runpath($location.types));
|
|
29
|
-
const types = { ...app.types, ...user_types };
|
|
30
|
-
|
|
31
|
-
const directory = app.runpath($location.routes);
|
|
32
|
-
for (const path of await directory.collect()) {
|
|
33
|
-
await app.extensions[path.extension]
|
|
34
|
-
?.route(directory, path.debase(`${directory}/`), types);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let router;
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
router = await FS.Router.load({
|
|
41
|
-
directory,
|
|
42
|
-
specials: {
|
|
43
|
-
guard: { recursive: true },
|
|
44
|
-
error: { recursive: false },
|
|
45
|
-
layout: { recursive: true },
|
|
46
|
-
},
|
|
47
|
-
predicate(route, request) {
|
|
48
|
-
return route.default[request.method.toLowerCase()] !== undefined;
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
} catch (error) {
|
|
52
|
-
const { DoubleRoute, OptionalRoute, RestRoute } = FS.Router.Error;
|
|
53
|
-
error instanceof DoubleRoute && errors.DoubleRoute.throw(error.route);
|
|
54
|
-
error instanceof OptionalRoute && errors.OptionalRoute.throw(error.route);
|
|
55
|
-
error instanceof RestRoute && errors.RestRoute.throw(error.route);
|
|
56
|
-
// rethrow original error
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
const layout = { depth: router.depth("layout") };
|
|
60
|
-
return { ...app, types, dispatch: dispatch(types), layout, router };
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export default async app =>
|
|
64
|
-
post(await (await cascade(app.modules.stage))(await pre(app)));
|
package/src/loaders/common.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import FS from "rcompat/fs";
|
|
2
|
-
import { identity } from "rcompat/function";
|
|
3
|
-
import errors from "../errors.js";
|
|
4
|
-
|
|
5
|
-
const ending = ".js";
|
|
6
|
-
|
|
7
|
-
const empty = log => (objects, name, path) =>
|
|
8
|
-
Object.keys(objects).length === 0
|
|
9
|
-
&& errors.EmptyDirectory.warn(log, name, path);
|
|
10
|
-
|
|
11
|
-
export default async ({
|
|
12
|
-
log,
|
|
13
|
-
directory,
|
|
14
|
-
name = "routes",
|
|
15
|
-
filter = identity,
|
|
16
|
-
recursive = true,
|
|
17
|
-
warn = true,
|
|
18
|
-
} = {}) => {
|
|
19
|
-
const objects = directory === undefined ? [] : await Promise.all(
|
|
20
|
-
(await FS.File.collect(directory, /^.*.js$/u, { recursive }))
|
|
21
|
-
.filter(filter)
|
|
22
|
-
.map(async file => [
|
|
23
|
-
`${file}`.replace(directory, _ => "").slice(1, -ending.length),
|
|
24
|
-
await file.import(),
|
|
25
|
-
]));
|
|
26
|
-
warn && await directory.exists() && empty(log)(objects, name, directory);
|
|
27
|
-
|
|
28
|
-
return objects;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export const doubled = set => set.find((part, i, array) =>
|
|
32
|
-
array.filter((_, j) => i !== j).includes(part));
|
package/src/loaders/exports.js
DELETED
package/src/loaders/modules.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import * as hooks from "../hooks/exports.js";
|
|
2
|
-
import { doubled } from "./common.js";
|
|
3
|
-
import errors from "../errors.js";
|
|
4
|
-
|
|
5
|
-
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
6
|
-
|
|
7
|
-
const load = (modules = []) => modules.map(module =>
|
|
8
|
-
[module, load(module.load?.() ?? [])]).flat();
|
|
9
|
-
|
|
10
|
-
export default async (log, root, modules) => {
|
|
11
|
-
Array.isArray(modules) || errors.ModulesMustBeArray.throw("modules");
|
|
12
|
-
|
|
13
|
-
modules.some(({ name }, n) => name === undefined &&
|
|
14
|
-
errors.ModuleHasNoName.throw(n));
|
|
15
|
-
|
|
16
|
-
const names = modules.map(({ name }) => name);
|
|
17
|
-
new Set(names).size !== modules.length &&
|
|
18
|
-
errors.DoubleModule.throw(doubled(names), root.join("primate.config.js"));
|
|
19
|
-
|
|
20
|
-
const hookless = modules.filter(module => !Object.keys(module).some(key =>
|
|
21
|
-
[...Object.keys(hooks), "load", "context"].includes(key)));
|
|
22
|
-
hookless.length > 0 && errors.ModuleHasNoHooks.warn(log,
|
|
23
|
-
hookless.map(({ name }) => name).join(", "));
|
|
24
|
-
|
|
25
|
-
// collect modules
|
|
26
|
-
const loaded = load(modules).flat(2);
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
names: loaded.map(module => module.name),
|
|
30
|
-
...Object.fromEntries([...Object.keys(hooks), "context"]
|
|
31
|
-
.map(hook => [hook, filter(hook, loaded)])),
|
|
32
|
-
};
|
|
33
|
-
};
|