primate 0.30.2 → 0.31.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 +2 -2
- package/src/app.js +6 -42
- package/src/defaults/primate.config.js +2 -1
- package/src/errors.js +2 -2
- package/src/errors.json +10 -0
- package/src/handlers.js +25 -10
- package/src/hooks/copy_includes.js +2 -2
- package/src/hooks/exports.js +0 -1
- package/src/hooks/handle.js +18 -4
- package/src/hooks/parse.js +1 -8
- package/src/hooks/publish.js +1 -22
- package/src/hooks/register.js +6 -32
- package/src/hooks/respond.js +4 -4
- package/src/hooks/route.js +33 -32
- package/src/hooks/stage.js +30 -17
- package/src/loaders/common.js +3 -3
- package/src/loaders/exports.js +0 -1
- package/src/loaders/types.js +2 -2
- package/src/run.js +3 -2
- package/src/start.js +39 -7
- package/src/hooks/bundle.js +0 -3
- package/src/loaders/routes/exports.js +0 -6
- package/src/loaders/routes/load.js +0 -18
- package/src/loaders/routes/routes.js +0 -25
- package/src/loaders/routes.js +0 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Polymorphic development platform",
|
|
5
5
|
"homepage": "https://primatejs.com",
|
|
6
6
|
"bugs": "https://github.com/primatejs/primate/issues",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"directory": "packages/primate"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"rcompat": "^0.
|
|
22
|
+
"rcompat": "^0.11.0"
|
|
23
23
|
},
|
|
24
24
|
"engines": {
|
|
25
25
|
"node": ">=18"
|
package/src/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "rcompat/crypto";
|
|
2
2
|
import { tryreturn } from "rcompat/async";
|
|
3
|
-
import
|
|
3
|
+
import FS from "rcompat/fs";
|
|
4
4
|
import { is } from "rcompat/invariant";
|
|
5
5
|
import o from "rcompat/object";
|
|
6
6
|
import { globify } from "rcompat/string";
|
|
@@ -28,8 +28,8 @@ const to_csp = (config_csp, assets, csp) => config_csp
|
|
|
28
28
|
|
|
29
29
|
// use user-provided file or fall back to default
|
|
30
30
|
const load = (base, page, fallback) =>
|
|
31
|
-
tryreturn(_ => File.text(`${base.join(page)}`))
|
|
32
|
-
.orelse(_ => File.text(`${base.join(fallback)}`));
|
|
31
|
+
tryreturn(_ => FS.File.text(`${base.join(page)}`))
|
|
32
|
+
.orelse(_ => FS.File.text(`${base.join(fallback)}`));
|
|
33
33
|
|
|
34
34
|
const encoder = new TextEncoder();
|
|
35
35
|
|
|
@@ -81,7 +81,6 @@ export default async (log, root, config) => {
|
|
|
81
81
|
secure,
|
|
82
82
|
importmaps: {},
|
|
83
83
|
assets: [],
|
|
84
|
-
exports: [],
|
|
85
84
|
path,
|
|
86
85
|
root,
|
|
87
86
|
log,
|
|
@@ -109,7 +108,7 @@ export default async (log, root, config) => {
|
|
|
109
108
|
|
|
110
109
|
await Promise.all((await source.collect(filter)).map(async path => {
|
|
111
110
|
const debased = path.debase(this.root).path.slice(1);
|
|
112
|
-
const filename = File.join(directory, path.debase(source));
|
|
111
|
+
const filename = FS.File.join(directory, path.debase(source));
|
|
113
112
|
const target = await target_base.join(filename.debase(directory));
|
|
114
113
|
await target.directory.create();
|
|
115
114
|
await (regexs.some(regex => regex.test(debased))
|
|
@@ -188,15 +187,10 @@ export default async (log, root, config) => {
|
|
|
188
187
|
const head = tags[tag_name]({ code, type, inline: true, integrity });
|
|
189
188
|
return { head, integrity: `'${integrity}'` };
|
|
190
189
|
},
|
|
191
|
-
async publish({ src, code, type = "", inline = false
|
|
192
|
-
if (!inline && copy) {
|
|
193
|
-
const base = this.runpath(this.get("location.client")).join(src);
|
|
194
|
-
await base.directory.create();
|
|
195
|
-
await base.write(code);
|
|
196
|
-
}
|
|
190
|
+
async publish({ src, code, type = "", inline = false }) {
|
|
197
191
|
if (inline || type === "style") {
|
|
198
192
|
this.assets.push({
|
|
199
|
-
src: File.join(http.static.root, src ?? "").path,
|
|
193
|
+
src: FS.File.join(http.static.root, src ?? "").path,
|
|
200
194
|
code: inline ? code : "",
|
|
201
195
|
type,
|
|
202
196
|
inline,
|
|
@@ -211,9 +205,6 @@ export default async (log, root, config) => {
|
|
|
211
205
|
{ "style-src": [], "script-src": [] },
|
|
212
206
|
);
|
|
213
207
|
},
|
|
214
|
-
export({ type, code }) {
|
|
215
|
-
this.exports.push({ type, code });
|
|
216
|
-
},
|
|
217
208
|
register(extension, operations) {
|
|
218
209
|
is(this.handlers[extension]).undefined(DoubleFileExtension.new(extension));
|
|
219
210
|
this.handlers[extension] = operations.handle;
|
|
@@ -224,32 +215,5 @@ export default async (log, root, config) => {
|
|
|
224
215
|
const prefix = algorithm.replace("-", _ => "");
|
|
225
216
|
return `${prefix}-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}`;
|
|
226
217
|
},
|
|
227
|
-
async import(module, deep_import) {
|
|
228
|
-
const parts = module.split("/");
|
|
229
|
-
const path = [this.library, ...parts];
|
|
230
|
-
const pkg = await File.resolve().join(...path, this.manifest).json();
|
|
231
|
-
const exports = pkg.exports === undefined
|
|
232
|
-
? { [module]: `/${module}/${pkg.main}` }
|
|
233
|
-
: o.transform(pkg.exports, entry => entry
|
|
234
|
-
.filter(([, export$]) =>
|
|
235
|
-
export$.browser?.[deep_import] !== undefined
|
|
236
|
-
|| export$.browser?.default !== undefined
|
|
237
|
-
|| export$.import !== undefined
|
|
238
|
-
|| export$.default !== undefined)
|
|
239
|
-
.map(([key, value]) => [
|
|
240
|
-
key.replace(".", deep_import === undefined
|
|
241
|
-
? module : `${module}/${deep_import}`),
|
|
242
|
-
value.browser?.[deep_import]?.replace(".", `./${module}`)
|
|
243
|
-
?? value.browser?.default.replace(".", `./${module}`)
|
|
244
|
-
?? value.default?.replace(".", `./${module}`)
|
|
245
|
-
?? value.import?.replace(".", `./${module}`),
|
|
246
|
-
]));
|
|
247
|
-
const dependency = File.resolve().join(...path);
|
|
248
|
-
const target = this.runpath(this.get("location.client")).join(...path);
|
|
249
|
-
await dependency.copy(target);
|
|
250
|
-
this.importmaps = { ...o.valmap(exports, value =>
|
|
251
|
-
File.join(this.get("http.static.root"), this.library, value).webpath()),
|
|
252
|
-
...this.importmaps };
|
|
253
|
-
},
|
|
254
218
|
};
|
|
255
219
|
};
|
package/src/errors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import FS from "rcompat/fs";
|
|
2
2
|
import Logger from "./Logger.js";
|
|
3
3
|
|
|
4
|
-
const json = await new File(import.meta.url).up(1).join("errors.json").json();
|
|
4
|
+
const json = await new FS.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/errors.json
CHANGED
|
@@ -111,10 +111,20 @@
|
|
|
111
111
|
"fix": "create a {0} route function at {2}.js",
|
|
112
112
|
"level": "Info"
|
|
113
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
|
+
},
|
|
114
119
|
"ReservedTypeName": {
|
|
115
120
|
"message": "reserved type name {0}",
|
|
116
121
|
"fix": "do not use any reserved type names",
|
|
117
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"
|
|
118
128
|
}
|
|
119
129
|
}
|
|
120
130
|
}
|
package/src/handlers.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import FS from "rcompat/fs";
|
|
2
2
|
import { MediaType, Status } from "rcompat/http";
|
|
3
3
|
import { identity } from "rcompat/function";
|
|
4
|
+
import { HTML } from "rcompat/string";
|
|
4
5
|
import errors from "./errors.js";
|
|
5
6
|
|
|
6
7
|
const handle = (mediatype, mapper = identity) => (body, options) => app =>
|
|
@@ -49,27 +50,41 @@ const error = (body = "Not Found", { status = Status.NOT_FOUND, page } = {}) =>
|
|
|
49
50
|
const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
|
|
50
51
|
const style_re = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
|
|
51
52
|
const remove = /<(?<tag>script|style)>.*?<\/\k<tag>>/gus;
|
|
52
|
-
const
|
|
53
|
-
const
|
|
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;
|
|
54
65
|
const scripts = await Promise.all([...component.matchAll(script_re)]
|
|
55
66
|
.map(({ groups: { code } }) => app.inline(code, "module")));
|
|
56
67
|
const styles = await Promise.all([...component.matchAll(style_re)]
|
|
57
68
|
.map(({ groups: { code } }) => app.inline(code, "style")));
|
|
58
|
-
const style_src = styles.map(asset => asset.integrity);
|
|
59
|
-
const script_src = scripts.map(asset => asset.integrity);
|
|
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);
|
|
60
72
|
|
|
61
73
|
return app.view({
|
|
62
|
-
body: component.replaceAll(remove, _ => ""),
|
|
63
|
-
head: [...
|
|
64
|
-
headers:
|
|
65
|
-
|
|
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,
|
|
66
81
|
});
|
|
67
82
|
};
|
|
68
83
|
// }}}
|
|
69
84
|
// {{{ view
|
|
70
85
|
const extensions = ["fullExtension", "extension"];
|
|
71
86
|
const view = (name, props, options) => (app, ...rest) => extensions
|
|
72
|
-
.map(extension => app.extensions[new File(name)[extension]])
|
|
87
|
+
.map(extension => app.extensions[new FS.File(name)[extension]])
|
|
73
88
|
.find(extension => extension?.handle)
|
|
74
89
|
?.handle(name, props, options)(app, ...rest)
|
|
75
90
|
?? errors.NoHandlerForComponent.throw(name);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import FS from "rcompat/fs";
|
|
2
2
|
|
|
3
3
|
export default async (app, type, post = () => undefined) => {
|
|
4
4
|
const includes = app.get("build.includes");
|
|
@@ -11,7 +11,7 @@ export default async (app, type, post = () => undefined) => {
|
|
|
11
11
|
.map(async include => {
|
|
12
12
|
const path = app.root.join(include);
|
|
13
13
|
if (await path.exists()) {
|
|
14
|
-
const target = File.join(type, include);
|
|
14
|
+
const target = FS.File.join(type, include);
|
|
15
15
|
await app.stage(path, target);
|
|
16
16
|
await post(target);
|
|
17
17
|
}
|
package/src/hooks/exports.js
CHANGED
|
@@ -2,7 +2,6 @@ export { default as init } from "./init.js";
|
|
|
2
2
|
export { default as stage } from "./stage.js";
|
|
3
3
|
export { default as register } from "./register.js";
|
|
4
4
|
export { default as publish } from "./publish.js";
|
|
5
|
-
export { default as bundle } from "./bundle.js";
|
|
6
5
|
export { default as route } from "./route.js";
|
|
7
6
|
export { default as handle } from "./handle.js";
|
|
8
7
|
export { default as parse } from "./parse.js";
|
package/src/hooks/handle.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Response, Status, MediaType } from "rcompat/http";
|
|
1
|
+
import { Response, Status, MediaType, fetch } from "rcompat/http";
|
|
2
2
|
import { cascade, tryreturn } from "rcompat/async";
|
|
3
3
|
import respond from "./respond.js";
|
|
4
4
|
import { error as clientError } from "../handlers.js";
|
|
@@ -47,14 +47,15 @@ export default app => {
|
|
|
47
47
|
let error_handler = app.error.default;
|
|
48
48
|
|
|
49
49
|
return tryreturn(async _ => {
|
|
50
|
-
const { path, guards, errors, layouts, handler } =
|
|
50
|
+
const { body, path, guards, errors, layouts, handler } =
|
|
51
|
+
await route(request);
|
|
51
52
|
|
|
52
53
|
error_handler = errors?.at(-1);
|
|
53
54
|
|
|
54
55
|
const hooks = [...app.modules.route, guard(app, guards), last(handler)];
|
|
55
56
|
|
|
56
57
|
// handle request
|
|
57
|
-
const routed = await (await cascade(hooks))({ ...request, path });
|
|
58
|
+
const routed = await (await cascade(hooks))({ ...request, body, path });
|
|
58
59
|
|
|
59
60
|
const $layouts = { layouts: await get_layouts(layouts, routed.request) };
|
|
60
61
|
return respond(routed.response)(app, $layouts, routed.request);
|
|
@@ -94,6 +95,19 @@ export default app => {
|
|
|
94
95
|
}
|
|
95
96
|
return as_route(request);
|
|
96
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}`;
|
|
97
104
|
|
|
98
|
-
|
|
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);
|
|
99
113
|
};
|
package/src/hooks/parse.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { URL
|
|
1
|
+
import { URL } from "rcompat/http";
|
|
2
2
|
import o from "rcompat/object";
|
|
3
|
-
import { tryreturn } from "rcompat/async";
|
|
4
|
-
import errors from "../errors.js";
|
|
5
|
-
|
|
6
|
-
const get_body = (request, url) =>
|
|
7
|
-
tryreturn(async _ => await Body.parse(request) ?? {})
|
|
8
|
-
.orelse(error => errors.MismatchedBody.throw(url.pathname, error.message));
|
|
9
3
|
|
|
10
4
|
export default app => async original => {
|
|
11
5
|
const { headers } = original;
|
|
@@ -14,7 +8,6 @@ export default app => async original => {
|
|
|
14
8
|
const cookies = headers.get("cookie");
|
|
15
9
|
|
|
16
10
|
return { original, url,
|
|
17
|
-
body: app.get("request.body.parse") ? await get_body(original, url) : {},
|
|
18
11
|
...o.valmap({
|
|
19
12
|
query: [o.from(url.searchParams), url.search],
|
|
20
13
|
headers: [o.from(headers), headers, false],
|
package/src/hooks/publish.js
CHANGED
|
@@ -1,24 +1,3 @@
|
|
|
1
|
-
import { File } from "rcompat/fs";
|
|
2
1
|
import { cascade } from "rcompat/async";
|
|
3
|
-
import o from "rcompat/object";
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
// after hook, publish a zero assumptions app.js (no css imports)
|
|
7
|
-
const src = File.join(app.get("http.static.root"), app.get("build.index"));
|
|
8
|
-
|
|
9
|
-
await app.publish({
|
|
10
|
-
code: app.exports.filter(({ type }) => type === "script")
|
|
11
|
-
.map(({ code }) => code).join(""),
|
|
12
|
-
src,
|
|
13
|
-
type: "module",
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const imports = { ...app.importmaps, app: src.webpath() };
|
|
17
|
-
const type = "importmap";
|
|
18
|
-
await app.publish({ inline: true, code: o.stringify({ imports }), type });
|
|
19
|
-
|
|
20
|
-
return app;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export default async app =>
|
|
24
|
-
post(await (await cascade(app.modules.publish))(app));
|
|
3
|
+
export default async app => (await cascade(app.modules.publish))(app);
|
package/src/hooks/register.js
CHANGED
|
@@ -1,26 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import FS from "rcompat/fs";
|
|
2
2
|
import { cascade } from "rcompat/async";
|
|
3
3
|
import copy_includes from "./copy_includes.js";
|
|
4
4
|
|
|
5
5
|
const html = /^.*.html$/u;
|
|
6
|
-
const defaults = new File(import.meta.url).up(2).join("defaults");
|
|
6
|
+
const defaults = new FS.File(import.meta.url).up(2).join("defaults");
|
|
7
7
|
|
|
8
8
|
const pre = async app => {
|
|
9
|
-
const
|
|
10
|
-
const { pages, client, components } = location;
|
|
9
|
+
const pages = app.get("location.pages");
|
|
11
10
|
|
|
12
11
|
// copy framework pages
|
|
13
12
|
await app.stage(defaults, pages, html);
|
|
14
13
|
// overwrite transformed pages to build
|
|
15
14
|
await app.path.pages.exists() && await app.stage(app.path.pages, pages, html);
|
|
16
15
|
|
|
17
|
-
if (await app.path.components.exists()) {
|
|
18
|
-
// copy .js files from components to build/client/components, since
|
|
19
|
-
// frontend frameworks handle non-js files
|
|
20
|
-
const target = File.join(client, components);
|
|
21
|
-
await app.stage(app.path.components, target, /^.*.js$/u);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
16
|
return app;
|
|
25
17
|
};
|
|
26
18
|
|
|
@@ -30,37 +22,19 @@ const post = async app => {
|
|
|
30
22
|
|
|
31
23
|
if (await _static.exists()) {
|
|
32
24
|
// copy static files to build/server/static
|
|
33
|
-
await app.stage(_static, File.join(location.server, location.static));
|
|
34
|
-
|
|
35
|
-
// copy static files to build/client/static
|
|
36
|
-
await app.stage(_static, File.join(location.client, location.static));
|
|
25
|
+
await app.stage(_static, FS.File.join(location.server, location.static));
|
|
37
26
|
|
|
38
27
|
// publish JavaScript and CSS files
|
|
39
|
-
const imports = await File.collect(_static, /\.(?:
|
|
28
|
+
const imports = await FS.File.collect(_static, /\.(?:css)$/u);
|
|
40
29
|
await Promise.all(imports.map(async file => {
|
|
41
|
-
const code = await file.text();
|
|
42
30
|
const src = file.debase(_static);
|
|
43
|
-
|
|
44
|
-
// already copied in `app.stage`
|
|
45
|
-
await app.publish({ src, code, type, copy: false });
|
|
46
|
-
type === "style" && app.export({ type,
|
|
47
|
-
code: `import "./${location.static}${src}";` });
|
|
31
|
+
app.build.export(`import "./${location.static}${src}";`);
|
|
48
32
|
}));
|
|
49
33
|
}
|
|
50
34
|
|
|
51
35
|
// copy additional subdirectories to build/server
|
|
52
36
|
await copy_includes(app, location.server);
|
|
53
37
|
|
|
54
|
-
// copy additional subdirectories to build/client
|
|
55
|
-
const client = app.runpath(location.client);
|
|
56
|
-
const root = app.get("http.static.root");
|
|
57
|
-
await copy_includes(app, location.client, async to =>
|
|
58
|
-
Promise.all((await to.collect(/\.js$/u)).map(async script => {
|
|
59
|
-
const src = File.join(root, script.path.replace(client, _ => ""));
|
|
60
|
-
await app.publish({ src, code: await script.text(), type: "module" });
|
|
61
|
-
})),
|
|
62
|
-
);
|
|
63
|
-
|
|
64
38
|
const components = await app.path.components.collect();
|
|
65
39
|
|
|
66
40
|
// from the build directory, compile to server and client
|
package/src/hooks/respond.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import FS from "rcompat/fs";
|
|
2
2
|
import { URL, Response } from "rcompat/http";
|
|
3
3
|
import { identity } from "rcompat/function";
|
|
4
|
+
import o from "rcompat/object";
|
|
4
5
|
import { text, json, stream, redirect } from "primate";
|
|
5
6
|
import errors from "../errors.js";
|
|
6
7
|
|
|
7
8
|
const not_found = value => errors.InvalidBodyReturned.throw(value);
|
|
8
9
|
const is_text = value => typeof value === "string";
|
|
9
|
-
const is_non_null_object = value => typeof value === "object" && value !== null;
|
|
10
10
|
const is_instance = of => value => value instanceof of;
|
|
11
11
|
const is_response = is_instance(globalThis.Response);
|
|
12
12
|
const is_fake_response = is_instance(Response);
|
|
13
13
|
const is_streamable =
|
|
14
|
-
value => value instanceof Blob || value?.streamable === s_streamable;
|
|
14
|
+
value => value instanceof FS.Blob || value?.streamable === FS.s_streamable;
|
|
15
15
|
|
|
16
16
|
// [if, then]
|
|
17
17
|
const guesses = [
|
|
@@ -19,7 +19,7 @@ const guesses = [
|
|
|
19
19
|
[is_streamable, value => stream(value.stream())],
|
|
20
20
|
[is_instance(ReadableStream), stream],
|
|
21
21
|
[value => is_response(value) || is_fake_response(value), value => _ => value],
|
|
22
|
-
[
|
|
22
|
+
[o.proper, json],
|
|
23
23
|
[is_text, text],
|
|
24
24
|
[not_found, identity],
|
|
25
25
|
];
|
package/src/hooks/route.js
CHANGED
|
@@ -1,48 +1,49 @@
|
|
|
1
1
|
import o from "rcompat/object";
|
|
2
2
|
import { tryreturn } from "rcompat/sync";
|
|
3
|
-
import
|
|
3
|
+
import { Body } from "rcompat/http";
|
|
4
|
+
import $errors from "../errors.js";
|
|
4
5
|
import validate from "../validate.js";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
const ieq = (left, right) => left.toLowerCase() === right.toLowerCase();
|
|
7
|
+
const { MismatchedBody, MismatchedPath, NoRouteToPath } = $errors;
|
|
8
8
|
|
|
9
9
|
const deroot = pathname => pathname.endsWith("/") && pathname !== "/"
|
|
10
10
|
? pathname.slice(0, -1) : pathname;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const to_path = (route, pathname) => app.dispatch(o.from(Object
|
|
17
|
-
.entries(route.pathname.exec(pathname)?.groups ?? {})
|
|
18
|
-
.map(([name, value]) => [name.split("$"), value])
|
|
19
|
-
.map(([[name, type], value]) =>
|
|
20
|
-
[name, type === undefined ? value : validate(types[type], value, name)],
|
|
21
|
-
)));
|
|
12
|
+
const parse_body = (request, url) =>
|
|
13
|
+
tryreturn(async _ => await Body.parse(request) ?? {})
|
|
14
|
+
.orelse(error => MismatchedBody.throw(url.pathname, error.message));
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.map(([[name, type], value]) =>
|
|
27
|
-
tryreturn(_ => [name, validate(types[type], value, name)])
|
|
28
|
-
.orelse(({ message }) => errors.MismatchedPath.throw(pathname, message)));
|
|
29
|
-
const is_path = ({ route, pathname }) => {
|
|
30
|
-
const result = route.pathname.exec(pathname);
|
|
31
|
-
return result === null ? false : is_type(result.groups, pathname);
|
|
32
|
-
};
|
|
33
|
-
const is_method = ({ route, method, pathname }) => ieq(route.method, method)
|
|
34
|
-
&& is_path({ route, pathname });
|
|
35
|
-
const find = (method, pathname) => routes.find(route =>
|
|
36
|
-
is_method({ route, method, pathname }));
|
|
16
|
+
export default app => {
|
|
17
|
+
const $request_body_parse = app.get("request.body.parse");
|
|
18
|
+
const $location = app.get("location");
|
|
37
19
|
|
|
38
|
-
const index = path => `${location.routes}${path === "/" ? "/index" : path}`;
|
|
20
|
+
const index = path => `${$location.routes}${path === "/" ? "/index" : path}`;
|
|
39
21
|
// remove excess slashes
|
|
40
22
|
const deslash = url => url.replaceAll(/\/{2,}/gu, _ => "/");
|
|
41
23
|
|
|
42
|
-
return ({ original
|
|
24
|
+
return async ({ original, url }) => {
|
|
43
25
|
const pathname = deroot(deslash(url.pathname));
|
|
44
|
-
const route =
|
|
45
|
-
.throw(method.toLowerCase(), pathname, index(pathname));
|
|
46
|
-
|
|
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.default]);
|
|
45
|
+
const handler = route.file.default[original.method.toLowerCase()];
|
|
46
|
+
|
|
47
|
+
return { body, path, guards, errors, layouts, handler };
|
|
47
48
|
};
|
|
48
49
|
};
|
package/src/hooks/stage.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import FS from "rcompat/fs";
|
|
1
2
|
import { cascade } from "rcompat/async";
|
|
2
3
|
import dispatch from "../dispatch.js";
|
|
3
4
|
import * as loaders from "../loaders/exports.js";
|
|
@@ -15,34 +16,46 @@ const pre = async app => {
|
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
const post = async app => {
|
|
18
|
-
const location = app.get("location");
|
|
19
|
+
const $location = app.get("location");
|
|
19
20
|
|
|
20
21
|
// stage routes
|
|
21
|
-
const double = doubled((await app.path.routes.collect())
|
|
22
|
-
.map(path => path.debase(app.path.routes))
|
|
23
|
-
.map(path => `${path}`.slice(1, -path.extension.length)));
|
|
24
|
-
double && errors.DoubleRoute.throw(double);
|
|
25
|
-
|
|
26
22
|
if (await app.path.routes.exists()) {
|
|
27
|
-
await app.stage(app.path.routes, location.routes);
|
|
23
|
+
await app.stage(app.path.routes, $location.routes);
|
|
28
24
|
}
|
|
29
25
|
if (await app.path.types.exists()) {
|
|
30
|
-
await app.stage(app.path.types, location.types);
|
|
26
|
+
await app.stage(app.path.types, $location.types);
|
|
31
27
|
}
|
|
32
|
-
const user_types = await loaders.types(app.log, app.runpath(location.types));
|
|
28
|
+
const user_types = await loaders.types(app.log, app.runpath($location.types));
|
|
33
29
|
const types = { ...app.types, ...user_types };
|
|
34
30
|
|
|
35
|
-
const
|
|
36
|
-
for (const path of await
|
|
31
|
+
const directory = app.runpath($location.routes);
|
|
32
|
+
for (const path of await directory.collect()) {
|
|
37
33
|
await app.extensions[path.extension]
|
|
38
|
-
?.route(
|
|
34
|
+
?.route(directory, path.debase(`${directory}/`), types);
|
|
39
35
|
}
|
|
40
|
-
const routes = await loaders.routes(app);
|
|
41
|
-
const layout = {
|
|
42
|
-
depth: Math.max(...routes.map(({ layouts }) => layouts.length)) + 1,
|
|
43
|
-
};
|
|
44
36
|
|
|
45
|
-
|
|
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
|
+
}
|
|
57
|
+
const layout = { depth: router.depth("layout") };
|
|
58
|
+
return { ...app, types, dispatch: dispatch(types), layout, router };
|
|
46
59
|
};
|
|
47
60
|
|
|
48
61
|
export default async app =>
|
package/src/loaders/common.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import FS from "rcompat/fs";
|
|
2
2
|
import { identity } from "rcompat/function";
|
|
3
3
|
import errors from "../errors.js";
|
|
4
4
|
|
|
@@ -11,13 +11,13 @@ const empty = log => (objects, name, path) =>
|
|
|
11
11
|
export default async ({
|
|
12
12
|
log,
|
|
13
13
|
directory,
|
|
14
|
-
filter = identity,
|
|
15
14
|
name = "routes",
|
|
15
|
+
filter = identity,
|
|
16
16
|
recursive = true,
|
|
17
17
|
warn = true,
|
|
18
18
|
} = {}) => {
|
|
19
19
|
const objects = directory === undefined ? [] : await Promise.all(
|
|
20
|
-
(await File.collect(directory, /^.*.js$/u, { recursive }))
|
|
20
|
+
(await FS.File.collect(directory, /^.*.js$/u, { recursive }))
|
|
21
21
|
.filter(filter)
|
|
22
22
|
.map(async file => [
|
|
23
23
|
`${file}`.replace(directory, _ => "").slice(1, -ending.length),
|
package/src/loaders/exports.js
CHANGED
package/src/loaders/types.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import FS 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 => File.join(directory, name);
|
|
13
|
+
const resolve = name => FS.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,5 +1,5 @@
|
|
|
1
1
|
import { tryreturn } from "rcompat/async";
|
|
2
|
-
import
|
|
2
|
+
import FS from "rcompat/fs";
|
|
3
3
|
import o from "rcompat/object";
|
|
4
4
|
import { runtime } from "rcompat/meta";
|
|
5
5
|
import app from "./app.js";
|
|
@@ -28,7 +28,8 @@ 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(_ => File.root())
|
|
31
|
+
const root = await tryreturn(_ => FS.File.root())
|
|
32
|
+
.orelse(_ => FS.File.resolve());
|
|
32
33
|
const config = await get_config(root);
|
|
33
34
|
logger = new Logger(config.logger);
|
|
34
35
|
await command(name)(await app(logger, root, config));
|
package/src/start.js
CHANGED
|
@@ -2,15 +2,44 @@ import { serve, Response, Status } from "rcompat/http";
|
|
|
2
2
|
import { tryreturn } from "rcompat/async";
|
|
3
3
|
import { bold, blue, dim } from "rcompat/colors";
|
|
4
4
|
import { resolve } from "rcompat/package";
|
|
5
|
+
import o from "rcompat/object";
|
|
6
|
+
import Build from "rcompat/build";
|
|
7
|
+
import FS from "rcompat/fs";
|
|
5
8
|
import * as hooks from "./hooks/exports.js";
|
|
6
9
|
import { print } from "./Logger.js";
|
|
7
10
|
|
|
8
|
-
const base_hooks = ["init", "stage", "register", "publish"
|
|
11
|
+
const base_hooks = ["init", "stage", "register", "publish"];
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const publish = async app => {
|
|
14
|
+
const location = app.get("location");
|
|
15
|
+
const http = app.get("http");
|
|
16
|
+
const client = app.runpath(app.get("location.client"));
|
|
17
|
+
|
|
18
|
+
const re = new RegExp(`${location.client}/app..*(?:js|css)$`, "u");
|
|
19
|
+
for (const path of await client.collect(re, { recursive: false })) {
|
|
20
|
+
const src = path.name;
|
|
21
|
+
const type = path.extension === ".css" ? "style" : "module";
|
|
22
|
+
await app.publish({ src, type });
|
|
23
|
+
if (path.extension === ".js") {
|
|
24
|
+
const imports = { app: FS.File.join(http.static.root, src).path };
|
|
25
|
+
await app.publish({
|
|
26
|
+
inline: true,
|
|
27
|
+
code: JSON.stringify({ imports }, null, 2),
|
|
28
|
+
type: "importmap",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default async ($app, mode = "development") => {
|
|
35
|
+
let app = $app;
|
|
36
|
+
|
|
37
|
+
app.mode = mode;
|
|
38
|
+
app.build = new Build({
|
|
39
|
+
...o.exclude(app.get("build"), ["includes", "index", "transform"]),
|
|
40
|
+
outdir: app.runpath(app.get("location.client")).path,
|
|
41
|
+
resolveDir: app.root.path,
|
|
42
|
+
}, mode);
|
|
14
43
|
|
|
15
44
|
const primate = await resolve(import.meta.url);
|
|
16
45
|
print(blue(bold(primate.name)), blue(primate.version), "in startup\n");
|
|
@@ -19,11 +48,14 @@ export default async (app$, mode = "development") => {
|
|
|
19
48
|
app.log.info(`running ${dim(hook)} hooks`, { module: "primate" });
|
|
20
49
|
app = await hooks[hook](app);
|
|
21
50
|
}
|
|
22
|
-
|
|
51
|
+
// start the build
|
|
52
|
+
await app.build.start();
|
|
53
|
+
await publish(app);
|
|
23
54
|
app.route = hooks.route(app);
|
|
24
55
|
app.parse = hooks.parse(app);
|
|
56
|
+
const handle = await hooks.handle(app);
|
|
25
57
|
app.server = await serve(async request =>
|
|
26
|
-
tryreturn(async _ =>
|
|
58
|
+
tryreturn(async _ => handle(await app.parse(request)))
|
|
27
59
|
.orelse(error => {
|
|
28
60
|
app.log.auto(error);
|
|
29
61
|
return new Response(null, { status: Status.INTERNAL_SERVER_ERROR });
|
package/src/hooks/bundle.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { File } from "rcompat/fs";
|
|
2
|
-
import errors from "../../errors.js";
|
|
3
|
-
import to_sorted from "../../to_sorted.js";
|
|
4
|
-
|
|
5
|
-
export default type => async (log, directory, load) => {
|
|
6
|
-
const filter = path => new RegExp(`^\\+${type}.js$`, "u").test(path.name);
|
|
7
|
-
|
|
8
|
-
const replace = new RegExp(`\\+${type}`, "u");
|
|
9
|
-
const objects = to_sorted((await load({ log, directory, filter, warn: false }))
|
|
10
|
-
.map(([name, object]) => [name.replace(replace, () => ""), object]),
|
|
11
|
-
([a], [b]) => a.length - b.length);
|
|
12
|
-
|
|
13
|
-
const resolve = name => File.join(directory, name, `+${type}.js`);
|
|
14
|
-
objects.some(([name, value]) => typeof value.default !== "function"
|
|
15
|
-
&& errors.InvalidDefaultExport.throw(resolve(name)));
|
|
16
|
-
|
|
17
|
-
return objects;
|
|
18
|
-
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { doubled } from "../common.js";
|
|
2
|
-
import errors from "../../errors.js";
|
|
3
|
-
|
|
4
|
-
const normalize = route => {
|
|
5
|
-
let i = 0;
|
|
6
|
-
// user/ -> user
|
|
7
|
-
// user/{id=number}/{id2=number} -> user/{0}/{1}
|
|
8
|
-
return (route.endsWith("/") ? route.slice(0, -1) : route)
|
|
9
|
-
.replaceAll(/\{(?:\w*)(?:=\w+)?\}?/gu, _ => `{${i++}}`);
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// index -> ""
|
|
13
|
-
const deindex = path => path.endsWith("index") ?
|
|
14
|
-
path.replace("index", "") : path;
|
|
15
|
-
|
|
16
|
-
export default async (log, directory, load) => {
|
|
17
|
-
const filter = path => /^[^+].*.js$/u.test(path.name);
|
|
18
|
-
const routes = (await load({ log, directory, filter }))
|
|
19
|
-
.map(([path, handler]) => [deindex(path), handler]);
|
|
20
|
-
|
|
21
|
-
const double = doubled(routes.map(([route]) => normalize(route)));
|
|
22
|
-
double && errors.DoubleRoute.throw(double);
|
|
23
|
-
|
|
24
|
-
return routes;
|
|
25
|
-
};
|
package/src/loaders/routes.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { tryreturn } from "rcompat/sync";
|
|
2
|
-
import o from "rcompat/object";
|
|
3
|
-
import { File } from "rcompat/fs";
|
|
4
|
-
import { default as fs, doubled } from "./common.js";
|
|
5
|
-
import * as get from "./routes/exports.js";
|
|
6
|
-
import errors from "../errors.js";
|
|
7
|
-
|
|
8
|
-
const { separator } = File;
|
|
9
|
-
|
|
10
|
-
const valid_route = /^[\w\-[\]=/.]*$/u;
|
|
11
|
-
|
|
12
|
-
const make = path => {
|
|
13
|
-
!valid_route.test(new File(path).webpath()) && errors.InvalidPath.throw(path);
|
|
14
|
-
|
|
15
|
-
const double = doubled(path.split(separator)
|
|
16
|
-
.filter(part => part.startsWith("[") && part.endsWith("]"))
|
|
17
|
-
.map(part => part.slice(1, part.indexOf("="))));
|
|
18
|
-
double && errors.DoublePathParameter.throw(double, path);
|
|
19
|
-
|
|
20
|
-
const route = path.replaceAll(/\[(?<named>.*?)\]/gu, (_, named) =>
|
|
21
|
-
tryreturn(_ => {
|
|
22
|
-
const { name, type } = /^(?<name>\w+)(?<type>=\w+)?$/u.exec(named).groups;
|
|
23
|
-
const param = type === undefined ? name : `${name}$${type.slice(1)}`;
|
|
24
|
-
return `(?<${param}>[^/]{1,}?)`;
|
|
25
|
-
}).orelse(_ => errors.EmptyPathParameter.throw(named, path)));
|
|
26
|
-
|
|
27
|
-
// normalize to unix
|
|
28
|
-
return new RegExp(`^/${route.replaceAll(separator, "/")}$`, "u");
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export default async (app, load = fs) => {
|
|
32
|
-
const { log } = app;
|
|
33
|
-
const directory = app.runpath(app.get("location.routes"));
|
|
34
|
-
const filter = path => ([name]) => path.includes(name);
|
|
35
|
-
const routes = o.from(await Promise.all(["guards", "errors", "layouts"]
|
|
36
|
-
.map(async extra => [extra, await get[extra](log, directory, load)])));
|
|
37
|
-
|
|
38
|
-
return (await get.routes(log, directory, load)).map(([path, imported]) => {
|
|
39
|
-
const route = imported.default;
|
|
40
|
-
if (route === undefined || Object.keys(route).length === 0) {
|
|
41
|
-
errors.EmptyRouteFile.warn(log, directory.join(`${path}.js`).path);
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
|
-
const filtered = filter(path);
|
|
45
|
-
|
|
46
|
-
return Object.entries(route).map(([method, handler]) => ({
|
|
47
|
-
method,
|
|
48
|
-
handler,
|
|
49
|
-
pathname: make(path.endsWith(separator) ? path.slice(0, -1) : path),
|
|
50
|
-
guards: routes.guards.filter(filtered).map(([, guard]) => guard.default),
|
|
51
|
-
errors: routes.errors.filter(filtered).map(([, error]) => error.default),
|
|
52
|
-
layouts: routes.layouts.filter(filtered).map(([, layout]) => layout),
|
|
53
|
-
}));
|
|
54
|
-
}).flat();
|
|
55
|
-
};
|