primate 0.29.7 → 0.30.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/Logger.js +4 -4
- package/src/app.js +46 -37
- package/src/defaults/primate.config.js +6 -12
- package/src/dispatch.js +7 -7
- package/src/errors.json +24 -14
- package/src/handlers.js +9 -9
- package/src/hooks/copy_includes.js +2 -5
- package/src/hooks/handle.js +11 -7
- package/src/hooks/parse.js +10 -9
- package/src/hooks/publish.js +12 -16
- package/src/hooks/register.js +14 -12
- package/src/hooks/respond.js +2 -3
- package/src/hooks/route.js +4 -7
- package/src/hooks/stage.js +4 -3
- package/src/loaders/modules.js +1 -3
- package/src/loaders/routes.js +10 -6
- package/src/run.js +2 -2
- package/src/start.js +11 -10
- package/src/cwd.js +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.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.9.4"
|
|
23
23
|
},
|
|
24
24
|
"engines": {
|
|
25
25
|
"node": ">=18"
|
package/src/Logger.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { assert, is } from "rcompat/invariant";
|
|
2
2
|
import { blue, bold, green, red, yellow, dim } from "rcompat/colors";
|
|
3
|
-
import
|
|
3
|
+
import o from "rcompat/object";
|
|
4
4
|
import console from "rcompat/console";
|
|
5
5
|
import { stdout } from "rcompat/stdio";
|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ const mark = (format, ...params) => params.reduce((formatted, param, i) =>
|
|
|
16
16
|
formatted.replace(`{${i}}`, bold(param)), format);
|
|
17
17
|
|
|
18
18
|
const reference = (module, error) => {
|
|
19
|
-
const base = module ? `modules/${module}
|
|
19
|
+
const base = module === "primate" ? "guide/logging" : `modules/${module}`;
|
|
20
20
|
return `https://primatejs.com/${base}#${hyphenate(error)}`;
|
|
21
21
|
};
|
|
22
22
|
|
|
@@ -49,7 +49,7 @@ const Logger = class Logger {
|
|
|
49
49
|
#level; #trace;
|
|
50
50
|
|
|
51
51
|
static err(errors, module) {
|
|
52
|
-
return map(errors, ([key, value]) => [key, throwable(value, key, module)]);
|
|
52
|
+
return o.map(errors, ([key, value]) => [key, throwable(value, key, module)]);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
constructor({ level = levels.Error, trace = false } = {}) {
|
|
@@ -101,7 +101,7 @@ const Logger = class Logger {
|
|
|
101
101
|
|
|
102
102
|
auto(error) {
|
|
103
103
|
const { message } = error;
|
|
104
|
-
const matches = map(levels, ([
|
|
104
|
+
const matches = o.map(levels, ([key, level]) => [level, key.toLowerCase()]);
|
|
105
105
|
return this[matches[error.level] ?? "error"](message, error);
|
|
106
106
|
}
|
|
107
107
|
};
|
package/src/app.js
CHANGED
|
@@ -2,9 +2,10 @@ import crypto from "rcompat/crypto";
|
|
|
2
2
|
import { tryreturn } from "rcompat/async";
|
|
3
3
|
import { File } from "rcompat/fs";
|
|
4
4
|
import { is } from "rcompat/invariant";
|
|
5
|
-
import
|
|
5
|
+
import o from "rcompat/object";
|
|
6
6
|
import { globify } from "rcompat/string";
|
|
7
7
|
import * as runtime from "rcompat/meta";
|
|
8
|
+
import { identity } from "rcompat/function";
|
|
8
9
|
import { Response, Status, MediaType } from "rcompat/http";
|
|
9
10
|
|
|
10
11
|
import errors from "./errors.js";
|
|
@@ -14,8 +15,19 @@ import * as loaders from "./loaders/exports.js";
|
|
|
14
15
|
|
|
15
16
|
const { DoubleFileExtension } = errors;
|
|
16
17
|
|
|
18
|
+
const to_csp = (config_csp, assets, csp) => config_csp
|
|
19
|
+
// only csp entries in the config will be enriched
|
|
20
|
+
.map(([key, directives]) =>
|
|
21
|
+
// enrich with application assets
|
|
22
|
+
[key, assets[key] ? directives.concat(...assets[key]) : directives])
|
|
23
|
+
.map(([key, directives]) =>
|
|
24
|
+
// enrich with explicit csp
|
|
25
|
+
[key, csp[key] ? directives.concat(...csp[key]) : directives])
|
|
26
|
+
.map(([key, directives]) => `${key} ${directives.join(" ")}`)
|
|
27
|
+
.join(";");
|
|
28
|
+
|
|
17
29
|
// use user-provided file or fall back to default
|
|
18
|
-
const
|
|
30
|
+
const load = (base, page, fallback) =>
|
|
19
31
|
tryreturn(_ => File.text(`${base.join(page)}`))
|
|
20
32
|
.orelse(_ => File.text(`${base.join(fallback)}`));
|
|
21
33
|
|
|
@@ -52,13 +64,10 @@ const render_head = (assets, head) =>
|
|
|
52
64
|
: tags.script({ inline, code, type, integrity, src }),
|
|
53
65
|
).join("\n").concat("\n", head ?? "");
|
|
54
66
|
|
|
55
|
-
const { name, version } = await new File(import.meta.url).up(2)
|
|
56
|
-
.join(runtime.manifest).json();
|
|
57
|
-
|
|
58
67
|
export default async (log, root, config) => {
|
|
59
68
|
const { http } = config;
|
|
60
69
|
const secure = http?.ssl !== undefined;
|
|
61
|
-
const path = valmap(config.location, value => root.join(value));
|
|
70
|
+
const path = o.valmap(config.location, value => root.join(value));
|
|
62
71
|
|
|
63
72
|
// if ssl activated, resolve key and cert early
|
|
64
73
|
if (secure) {
|
|
@@ -69,16 +78,15 @@ export default async (log, root, config) => {
|
|
|
69
78
|
const error = await path.routes.join("+error.js");
|
|
70
79
|
|
|
71
80
|
return {
|
|
72
|
-
config,
|
|
73
81
|
secure,
|
|
74
|
-
name,
|
|
75
|
-
version,
|
|
76
82
|
importmaps: {},
|
|
77
83
|
assets: [],
|
|
78
84
|
exports: [],
|
|
79
85
|
path,
|
|
80
86
|
root,
|
|
81
87
|
log,
|
|
88
|
+
// pseudostatic thus arrowbound
|
|
89
|
+
get: (config_key, fallback) => o.get(config, config_key) ?? fallback,
|
|
82
90
|
error: {
|
|
83
91
|
default: await error.exists() ? await error.import("default") : undefined,
|
|
84
92
|
},
|
|
@@ -88,11 +96,11 @@ export default async (log, root, config) => {
|
|
|
88
96
|
handle: handlers.html,
|
|
89
97
|
},
|
|
90
98
|
},
|
|
91
|
-
modules: await loaders.modules(log, root, config),
|
|
99
|
+
modules: await loaders.modules(log, root, config.modules ?? []),
|
|
92
100
|
...runtime,
|
|
93
101
|
// copy files to build folder, potentially transforming them
|
|
94
102
|
async stage(source, directory, filter) {
|
|
95
|
-
const { paths, mapper } = this.
|
|
103
|
+
const { paths = [], mapper = identity } = this.get("build.transform", {});
|
|
96
104
|
is(paths).array();
|
|
97
105
|
is(mapper).function();
|
|
98
106
|
|
|
@@ -110,7 +118,7 @@ export default async (log, root, config) => {
|
|
|
110
118
|
}));
|
|
111
119
|
},
|
|
112
120
|
async compile(component) {
|
|
113
|
-
const {
|
|
121
|
+
const { server, client, components } = this.get("location");
|
|
114
122
|
|
|
115
123
|
const source = this.path.components;
|
|
116
124
|
const compile = this.extensions[component.fullExtension]?.compile
|
|
@@ -133,41 +141,37 @@ export default async (log, root, config) => {
|
|
|
133
141
|
await compile.client(component);
|
|
134
142
|
}
|
|
135
143
|
},
|
|
136
|
-
headers(
|
|
137
|
-
const
|
|
138
|
-
`${policy}${key} ${http.csp[key]};`, "")
|
|
139
|
-
.replace("script-src 'self'", `script-src 'self' ${script} ${this.assets
|
|
140
|
-
.filter(({ type }) => type !== "style")
|
|
141
|
-
.map(asset => `'${asset.integrity}'`).join(" ")
|
|
142
|
-
}`)
|
|
143
|
-
.replace("style-src 'self'", `style-src 'self' ${style} ${this.assets
|
|
144
|
-
.filter(({ type }) => type === "style")
|
|
145
|
-
.map(asset => `'${asset.integrity}'`).join(" ")
|
|
146
|
-
}`);
|
|
144
|
+
headers(csp = {}) {
|
|
145
|
+
const http_csp = Object.entries(this.get("http.csp", {}));
|
|
147
146
|
|
|
148
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
...this.get("http.headers", {}),
|
|
149
|
+
...http_csp.length === 0 ? {} : {
|
|
150
|
+
"Content-Security-Policy": to_csp(http_csp, this.asset_csp, csp),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
149
153
|
},
|
|
150
154
|
runpath(...directories) {
|
|
151
155
|
return this.path.build.join(...directories);
|
|
152
156
|
},
|
|
153
157
|
async render(content) {
|
|
154
|
-
const
|
|
158
|
+
const index = this.get("pages.index");
|
|
155
159
|
const { body, head, partial, placeholders = {}, page = index } = content;
|
|
156
160
|
["body", "head"].every(used => is(placeholders[used]).undefined());
|
|
157
161
|
|
|
158
|
-
return partial ? body :
|
|
162
|
+
return partial ? body : Object.entries(placeholders)
|
|
159
163
|
// replace given placeholders, defaulting to ""
|
|
160
164
|
.reduce((html, [key, value]) => html.replace(`%${key}%`, value ?? ""),
|
|
161
|
-
await
|
|
165
|
+
await load(this.runpath(this.get("location.pages")), page, index))
|
|
162
166
|
// replace non-given placeholders, aside from %body% / %head%
|
|
163
167
|
.replaceAll(/(?<keep>%(?:head|body)%)|%.*?%/gus, "$1")
|
|
164
168
|
// replace body and head
|
|
165
169
|
.replace("%body%", body)
|
|
166
|
-
.replace("%head%", render_head(assets, head));
|
|
170
|
+
.replace("%head%", render_head(this.assets, head));
|
|
167
171
|
},
|
|
168
172
|
respond(body, { status = Status.OK, headers = {} } = {}) {
|
|
169
173
|
return new Response(body, { status, headers: {
|
|
170
|
-
|
|
174
|
+
"Content-Type": MediaType.TEXT_HTML, ...this.headers(), ...headers },
|
|
171
175
|
});
|
|
172
176
|
},
|
|
173
177
|
async view(options) {
|
|
@@ -182,11 +186,11 @@ export default async (log, root, config) => {
|
|
|
182
186
|
const integrity = await this.hash(code);
|
|
183
187
|
const tag_name = type === "style" ? "style" : "script";
|
|
184
188
|
const head = tags[tag_name]({ code, type, inline: true, integrity });
|
|
185
|
-
return { head,
|
|
189
|
+
return { head, integrity: `'${integrity}'` };
|
|
186
190
|
},
|
|
187
191
|
async publish({ src, code, type = "", inline = false, copy = true }) {
|
|
188
192
|
if (!inline && copy) {
|
|
189
|
-
const base = this.runpath(this.
|
|
193
|
+
const base = this.runpath(this.get("location.client")).join(src);
|
|
190
194
|
await base.directory.create();
|
|
191
195
|
await base.write(code);
|
|
192
196
|
}
|
|
@@ -199,6 +203,13 @@ export default async (log, root, config) => {
|
|
|
199
203
|
integrity: await this.hash(code),
|
|
200
204
|
});
|
|
201
205
|
}
|
|
206
|
+
// rehash assets_csp
|
|
207
|
+
this.asset_csp = this.assets.map(({ type: directive, integrity }) => [
|
|
208
|
+
`${directive === "style" ? "style" : "script"}-src`, integrity])
|
|
209
|
+
.reduce((csp, [directive, hash]) =>
|
|
210
|
+
({ ...csp, [directive]: csp[directive].concat(`'${hash}'`) } ),
|
|
211
|
+
{ "style-src": [], "script-src": [] },
|
|
212
|
+
);
|
|
202
213
|
},
|
|
203
214
|
export({ type, code }) {
|
|
204
215
|
this.exports.push({ type, code });
|
|
@@ -214,14 +225,12 @@ export default async (log, root, config) => {
|
|
|
214
225
|
return `${prefix}-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}`;
|
|
215
226
|
},
|
|
216
227
|
async import(module, deep_import) {
|
|
217
|
-
const { http: { static: { root } }, location: { client } } = this.config;
|
|
218
|
-
|
|
219
228
|
const parts = module.split("/");
|
|
220
229
|
const path = [this.library, ...parts];
|
|
221
230
|
const pkg = await File.resolve().join(...path, this.manifest).json();
|
|
222
231
|
const exports = pkg.exports === undefined
|
|
223
232
|
? { [module]: `/${module}/${pkg.main}` }
|
|
224
|
-
: transform(pkg.exports, entry => entry
|
|
233
|
+
: o.transform(pkg.exports, entry => entry
|
|
225
234
|
.filter(([, export$]) =>
|
|
226
235
|
export$.browser?.[deep_import] !== undefined
|
|
227
236
|
|| export$.browser?.default !== undefined
|
|
@@ -236,10 +245,10 @@ export default async (log, root, config) => {
|
|
|
236
245
|
?? value.import?.replace(".", `./${module}`),
|
|
237
246
|
]));
|
|
238
247
|
const dependency = File.resolve().join(...path);
|
|
239
|
-
const target =
|
|
248
|
+
const target = this.runpath(this.get("location.client")).join(...path);
|
|
240
249
|
await dependency.copy(target);
|
|
241
|
-
this.importmaps = { ...valmap(exports, value =>
|
|
242
|
-
File.join(root, this.library, value).
|
|
250
|
+
this.importmaps = { ...o.valmap(exports, value =>
|
|
251
|
+
File.join(this.get("http.static.root"), this.library, value).webpath()),
|
|
243
252
|
...this.importmaps };
|
|
244
253
|
},
|
|
245
254
|
};
|
|
@@ -15,19 +15,16 @@ export default {
|
|
|
15
15
|
http: {
|
|
16
16
|
host: "localhost",
|
|
17
17
|
port: 6161,
|
|
18
|
-
csp: {
|
|
19
|
-
"default-src": "'self'",
|
|
20
|
-
"style-src": "'self'",
|
|
21
|
-
"script-src": "'self'",
|
|
22
|
-
"object-src": "'none'",
|
|
23
|
-
"frame-ancestors": "'none'",
|
|
24
|
-
"form-action": "'self'",
|
|
25
|
-
"base-uri": "'self'",
|
|
26
|
-
},
|
|
18
|
+
csp: {},
|
|
27
19
|
static: {
|
|
28
20
|
root: "/",
|
|
29
21
|
},
|
|
30
22
|
},
|
|
23
|
+
request: {
|
|
24
|
+
body: {
|
|
25
|
+
parse: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
31
28
|
location: {
|
|
32
29
|
// renderable components
|
|
33
30
|
components: "components",
|
|
@@ -54,7 +51,4 @@ export default {
|
|
|
54
51
|
mapper: identity,
|
|
55
52
|
},
|
|
56
53
|
},
|
|
57
|
-
types: {
|
|
58
|
-
explicit: false,
|
|
59
|
-
},
|
|
60
54
|
};
|
package/src/dispatch.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { is } from "rcompat/invariant";
|
|
2
2
|
import { tryreturn } from "rcompat/sync";
|
|
3
|
-
import
|
|
3
|
+
import o from "rcompat/object";
|
|
4
4
|
import { camelcased } from "rcompat/string";
|
|
5
5
|
import errors from "./errors.js";
|
|
6
6
|
import validate from "./validate.js";
|
|
7
7
|
|
|
8
8
|
export default (patches = {}) => (object, raw, cased = true) => {
|
|
9
9
|
return Object.assign(Object.create(null), {
|
|
10
|
-
...map(patches, ([name, patch]) => [`get${camelcased(name)}`,
|
|
11
|
-
is(
|
|
12
|
-
return tryreturn(_ => validate(patch, object[
|
|
10
|
+
...o.map(patches, ([name, patch]) => [`get${camelcased(name)}`, key => {
|
|
11
|
+
is(key).defined(`\`${name}\` called without key`);
|
|
12
|
+
return tryreturn(_ => validate(patch, object[key], key))
|
|
13
13
|
.orelse(({ message }) => errors.MismatchedType.throw(message));
|
|
14
14
|
}]),
|
|
15
|
-
get(
|
|
16
|
-
is(
|
|
15
|
+
get(key) {
|
|
16
|
+
is(key).string();
|
|
17
17
|
|
|
18
|
-
return object[cased ?
|
|
18
|
+
return object[cased ? key : key.toLowerCase()];
|
|
19
19
|
},
|
|
20
20
|
json() {
|
|
21
21
|
return JSON.parse(JSON.stringify(object));
|
package/src/errors.json
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"module": "primate",
|
|
3
3
|
"errors": {
|
|
4
|
-
"MismatchedBody": {
|
|
5
|
-
"message": "{0}: {1}",
|
|
6
|
-
"fix": "make sure the body payload corresponds to the used content type",
|
|
7
|
-
"level": "Error"
|
|
8
|
-
},
|
|
9
4
|
"DoubleFileExtension": {
|
|
10
5
|
"message": "double file extension {0}",
|
|
11
6
|
"fix": "unload one of the two handlers registering the file extension",
|
|
@@ -26,9 +21,19 @@
|
|
|
26
21
|
"fix": "disambiguate routes",
|
|
27
22
|
"level": "Error"
|
|
28
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
|
+
},
|
|
29
34
|
"EmptyRouteFile": {
|
|
30
35
|
"message": "empty route file at {0}",
|
|
31
|
-
"fix": "add routes or remove
|
|
36
|
+
"fix": "add routes to the file or remove it",
|
|
32
37
|
"level": "Warn"
|
|
33
38
|
},
|
|
34
39
|
"EmptyDirectory": {
|
|
@@ -41,14 +46,19 @@
|
|
|
41
46
|
"fix": "check errors in config file by running {1}",
|
|
42
47
|
"level": "Error"
|
|
43
48
|
},
|
|
49
|
+
"InvalidBodyReturned": {
|
|
50
|
+
"message": "invalid body returned from route, got {0}",
|
|
51
|
+
"fix": "return a proper body from route",
|
|
52
|
+
"level": "Error"
|
|
53
|
+
},
|
|
44
54
|
"InvalidDefaultExport": {
|
|
45
55
|
"message": "invalid default export at {0}",
|
|
46
56
|
"fix": "use only functions for the default export",
|
|
47
57
|
"level": "Error"
|
|
48
58
|
},
|
|
49
|
-
"
|
|
50
|
-
"message": "invalid path
|
|
51
|
-
"fix": "use only
|
|
59
|
+
"InvalidPath": {
|
|
60
|
+
"message": "invalid path {0}",
|
|
61
|
+
"fix": "use only letters, digits, '_', '[', ']' or '=' in path filenames",
|
|
52
62
|
"level": "Error"
|
|
53
63
|
},
|
|
54
64
|
"InvalidTypeExport": {
|
|
@@ -61,6 +71,11 @@
|
|
|
61
71
|
"fix": "use lowercase-first latin letters and decimals in type names",
|
|
62
72
|
"level": "Error"
|
|
63
73
|
},
|
|
74
|
+
"MismatchedBody": {
|
|
75
|
+
"message": "{0}: {1}",
|
|
76
|
+
"fix": "make sure the body payload corresponds to the used content type",
|
|
77
|
+
"level": "Error"
|
|
78
|
+
},
|
|
64
79
|
"MismatchedPath": {
|
|
65
80
|
"message": "mismatched path {0}: {1}",
|
|
66
81
|
"fix": "fix the type or the caller",
|
|
@@ -86,11 +101,6 @@
|
|
|
86
101
|
"fix": "change {0} to an array in the config or remove this property",
|
|
87
102
|
"level": "Error"
|
|
88
103
|
},
|
|
89
|
-
"EmptyConfigFile": {
|
|
90
|
-
"message": "empty config file at {0}",
|
|
91
|
-
"fix": "add configuration options or remove file",
|
|
92
|
-
"level": "Warn"
|
|
93
|
-
},
|
|
94
104
|
"NoHandlerForComponent": {
|
|
95
105
|
"message": "no handler for {0}",
|
|
96
106
|
"fix": "add handler module for this component or remove {0}",
|
package/src/handlers.js
CHANGED
|
@@ -43,7 +43,7 @@ const redirect = (Location, { status = Status.FOUND } = {}) => app =>
|
|
|
43
43
|
// }}}
|
|
44
44
|
// {{{ error
|
|
45
45
|
const error = (body = "Not Found", { status = Status.NOT_FOUND, page } = {}) =>
|
|
46
|
-
app => app.view({ body, status, page: page ?? app.
|
|
46
|
+
app => app.view({ body, status, page: page ?? app.get("pages.error") });
|
|
47
47
|
// }}}
|
|
48
48
|
// {{{ html
|
|
49
49
|
const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
|
|
@@ -55,15 +55,15 @@ const html = (name, options) => async app => {
|
|
|
55
55
|
.map(({ groups: { code } }) => app.inline(code, "module")));
|
|
56
56
|
const styles = await Promise.all([...component.matchAll(style_re)]
|
|
57
57
|
.map(({ groups: { code } }) => app.inline(code, "style")));
|
|
58
|
-
const
|
|
58
|
+
const style_src = styles.map(asset => asset.integrity);
|
|
59
|
+
const script_src = scripts.map(asset => asset.integrity);
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return app.view({ body, head, headers, ...options });
|
|
61
|
+
return app.view({
|
|
62
|
+
body: component.replaceAll(remove, _ => ""),
|
|
63
|
+
head: [...scripts, ...styles].map(asset => asset.head).join("\n"),
|
|
64
|
+
headers: app.headers({ "style-src": style_src, "script-src": script_src }),
|
|
65
|
+
...options,
|
|
66
|
+
});
|
|
67
67
|
};
|
|
68
68
|
// }}}
|
|
69
69
|
// {{{ view
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { File } from "rcompat/fs";
|
|
2
2
|
|
|
3
3
|
export default async (app, type, post = () => undefined) => {
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const { includes } = build;
|
|
7
|
-
|
|
8
|
-
const reserved = Object.values(app.config.location);
|
|
4
|
+
const includes = app.get("build.includes");
|
|
5
|
+
const reserved = Object.values(app.get("location"));
|
|
9
6
|
|
|
10
7
|
if (Array.isArray(includes)) {
|
|
11
8
|
await Promise.all(includes
|
package/src/hooks/handle.js
CHANGED
|
@@ -33,9 +33,13 @@ const get_layouts = async (layouts, request) => {
|
|
|
33
33
|
.slice(stop_at === -1 ? 0 : stop_at)
|
|
34
34
|
.map(layout => layout.default(request)));
|
|
35
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
|
+
};
|
|
36
41
|
|
|
37
42
|
export default app => {
|
|
38
|
-
const { config: { http: { static: { root } }, location } } = app;
|
|
39
43
|
const route = request => app.route(request);
|
|
40
44
|
|
|
41
45
|
const as_route = async request => {
|
|
@@ -47,15 +51,13 @@ export default app => {
|
|
|
47
51
|
|
|
48
52
|
error_handler = errors?.at(-1);
|
|
49
53
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const hooks = [...app.modules.route, guard(app, guards)];
|
|
54
|
+
const hooks = [...app.modules.route, guard(app, guards), last(handler)];
|
|
53
55
|
|
|
54
56
|
// handle request
|
|
55
|
-
const
|
|
57
|
+
const routed = await (await cascade(hooks))({ ...request, path });
|
|
56
58
|
|
|
57
|
-
const $layouts = { layouts: await get_layouts(layouts, request) };
|
|
58
|
-
return (await respond(response))(app, $layouts,
|
|
59
|
+
const $layouts = { layouts: await get_layouts(layouts, routed.request) };
|
|
60
|
+
return (await respond(routed.response))(app, $layouts, routed.request);
|
|
59
61
|
}).orelse(async error => {
|
|
60
62
|
app.log.auto(error);
|
|
61
63
|
|
|
@@ -73,6 +75,8 @@ export default app => {
|
|
|
73
75
|
},
|
|
74
76
|
});
|
|
75
77
|
|
|
78
|
+
const location = app.get("location");
|
|
79
|
+
const root = app.get("http.static.root");
|
|
76
80
|
const client = app.runpath(location.client);
|
|
77
81
|
const handle = async request => {
|
|
78
82
|
const { pathname } = request.url;
|
package/src/hooks/parse.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { URL, Body } from "rcompat/http";
|
|
2
|
-
import
|
|
2
|
+
import o from "rcompat/object";
|
|
3
3
|
import { tryreturn } from "rcompat/async";
|
|
4
4
|
import errors from "../errors.js";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const get_body = (request, url) =>
|
|
7
7
|
tryreturn(async _ => await Body.parse(request) ?? {})
|
|
8
8
|
.orelse(error => errors.MismatchedBody.throw(url.pathname, error.message));
|
|
9
9
|
|
|
10
|
-
export default
|
|
10
|
+
export default app => async original => {
|
|
11
11
|
const { headers } = original;
|
|
12
12
|
|
|
13
13
|
const url = new URL(globalThis.decodeURIComponent(original.url));
|
|
14
14
|
const cookies = headers.get("cookie");
|
|
15
|
-
const body = await parse_body(original, url);
|
|
16
15
|
|
|
17
|
-
return { original, url,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
return { original, url,
|
|
17
|
+
body: app.get("request.body.parse") ? await get_body(original, url) : {},
|
|
18
|
+
...o.valmap({
|
|
19
|
+
query: [o.from(url.searchParams), url.search],
|
|
20
|
+
headers: [o.from(headers), headers, false],
|
|
21
|
+
cookies: [o.from(cookies?.split(";").map(cookie =>
|
|
21
22
|
cookie.trim().split("=")) ?? []), cookies],
|
|
22
|
-
}, value => dispatch(...value)) };
|
|
23
|
+
}, value => app.dispatch(...value)) };
|
|
23
24
|
};
|
package/src/hooks/publish.js
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
import { File } from "rcompat/fs";
|
|
2
2
|
import { cascade } from "rcompat/async";
|
|
3
|
-
import
|
|
3
|
+
import o from "rcompat/object";
|
|
4
4
|
|
|
5
5
|
const post = async app => {
|
|
6
|
-
|
|
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"));
|
|
7
8
|
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
await app.publish({
|
|
10
|
+
code: app.exports.filter(({ type }) => type === "script")
|
|
11
|
+
.map(({ code }) => code).join(""),
|
|
12
|
+
src,
|
|
13
|
+
type: "module",
|
|
14
|
+
});
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
src,
|
|
16
|
-
type: "module",
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const imports = { ...app.importmaps, app: src.normalize() };
|
|
20
|
-
const type = "importmap";
|
|
21
|
-
await app.publish({ inline: true, code: stringify({ imports }), type });
|
|
22
|
-
}
|
|
16
|
+
const imports = { ...app.importmaps, app: src.webpath() };
|
|
17
|
+
const type = "importmap";
|
|
18
|
+
await app.publish({ inline: true, code: o.stringify({ imports }), type });
|
|
23
19
|
|
|
24
20
|
return app;
|
|
25
21
|
};
|
package/src/hooks/register.js
CHANGED
|
@@ -1,44 +1,45 @@
|
|
|
1
1
|
import { File } from "rcompat/fs";
|
|
2
2
|
import { cascade } from "rcompat/async";
|
|
3
|
-
import cwd from "../cwd.js";
|
|
4
3
|
import copy_includes from "./copy_includes.js";
|
|
5
4
|
|
|
6
5
|
const html = /^.*.html$/u;
|
|
7
|
-
const defaults =
|
|
6
|
+
const defaults = new File(import.meta.url).up(2).join("defaults");
|
|
8
7
|
|
|
9
8
|
const pre = async app => {
|
|
10
|
-
const
|
|
9
|
+
const location = app.get("location");
|
|
10
|
+
const { pages, client, components } = location;
|
|
11
11
|
|
|
12
12
|
// copy framework pages
|
|
13
13
|
await app.stage(defaults, pages, html);
|
|
14
14
|
// overwrite transformed pages to build
|
|
15
|
-
await path.pages.exists() && await app.stage(path.pages, pages, html);
|
|
15
|
+
await app.path.pages.exists() && await app.stage(app.path.pages, pages, html);
|
|
16
16
|
|
|
17
|
-
if (await path.components.exists()) {
|
|
17
|
+
if (await app.path.components.exists()) {
|
|
18
18
|
// copy .js files from components to build/client/components, since
|
|
19
19
|
// frontend frameworks handle non-js files
|
|
20
20
|
const target = File.join(client, components);
|
|
21
|
-
await app.stage(path.components, target, /^.*.js$/u);
|
|
21
|
+
await app.stage(app.path.components, target, /^.*.js$/u);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
return app;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const post = async app => {
|
|
28
|
-
const
|
|
28
|
+
const _static = app.path.static;
|
|
29
|
+
const location = app.get("location");
|
|
29
30
|
|
|
30
|
-
if (await
|
|
31
|
+
if (await _static.exists()) {
|
|
31
32
|
// copy static files to build/server/static
|
|
32
|
-
await app.stage(
|
|
33
|
+
await app.stage(_static, File.join(location.server, location.static));
|
|
33
34
|
|
|
34
35
|
// copy static files to build/client/static
|
|
35
|
-
await app.stage(
|
|
36
|
+
await app.stage(_static, File.join(location.client, location.static));
|
|
36
37
|
|
|
37
38
|
// publish JavaScript and CSS files
|
|
38
|
-
const imports = await File.collect(
|
|
39
|
+
const imports = await File.collect(_static, /\.(?:js|css)$/u);
|
|
39
40
|
await Promise.all(imports.map(async file => {
|
|
40
41
|
const code = await file.text();
|
|
41
|
-
const src = file.debase(
|
|
42
|
+
const src = file.debase(_static);
|
|
42
43
|
const type = file.extension === ".css" ? "style" : "module";
|
|
43
44
|
// already copied in `app.stage`
|
|
44
45
|
await app.publish({ src, code, type, copy: false });
|
|
@@ -52,6 +53,7 @@ const post = async app => {
|
|
|
52
53
|
|
|
53
54
|
// copy additional subdirectories to build/client
|
|
54
55
|
const client = app.runpath(location.client);
|
|
56
|
+
const root = app.get("http.static.root");
|
|
55
57
|
await copy_includes(app, location.client, async to =>
|
|
56
58
|
Promise.all((await to.collect(/\.js$/u)).map(async script => {
|
|
57
59
|
const src = File.join(root, script.path.replace(client, _ => ""));
|
package/src/hooks/respond.js
CHANGED
|
@@ -2,10 +2,9 @@ import { Blob, s_streamable } from "rcompat/fs";
|
|
|
2
2
|
import { URL, Response } from "rcompat/http";
|
|
3
3
|
import { identity } from "rcompat/function";
|
|
4
4
|
import { text, json, stream, redirect } from "primate";
|
|
5
|
+
import errors from "../errors.js";
|
|
5
6
|
|
|
6
|
-
const not_found = value =>
|
|
7
|
-
throw new Error(`no handler found for ${value}`);
|
|
8
|
-
};
|
|
7
|
+
const not_found = value => errors.InvalidBodyReturned.throw(value);
|
|
9
8
|
const is_text = value => typeof value === "string";
|
|
10
9
|
const is_non_null_object = value => typeof value === "object" && value !== null;
|
|
11
10
|
const is_instance = of => value => value instanceof of;
|
package/src/hooks/route.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import o from "rcompat/object";
|
|
2
2
|
import { tryreturn } from "rcompat/sync";
|
|
3
3
|
import errors from "../errors.js";
|
|
4
4
|
import validate from "../validate.js";
|
|
@@ -10,20 +10,17 @@ const deroot = pathname => pathname.endsWith("/") && pathname !== "/"
|
|
|
10
10
|
? pathname.slice(0, -1) : pathname;
|
|
11
11
|
|
|
12
12
|
export default app => {
|
|
13
|
-
const { types, routes
|
|
13
|
+
const { types, routes } = app;
|
|
14
|
+
const location = app.get("location");
|
|
14
15
|
|
|
15
|
-
const to_path = (route, pathname) => app.dispatch(from(Object
|
|
16
|
+
const to_path = (route, pathname) => app.dispatch(o.from(Object
|
|
16
17
|
.entries(route.pathname.exec(pathname)?.groups ?? {})
|
|
17
|
-
.map(([name, value]) =>
|
|
18
|
-
[types[name] === undefined || explicit ? name : `${name}$${name}`, value])
|
|
19
18
|
.map(([name, value]) => [name.split("$"), value])
|
|
20
19
|
.map(([[name, type], value]) =>
|
|
21
20
|
[name, type === undefined ? value : validate(types[type], value, name)],
|
|
22
21
|
)));
|
|
23
22
|
|
|
24
23
|
const is_type = (groups, pathname) => Object.entries(groups ?? {})
|
|
25
|
-
.map(([name, value]) =>
|
|
26
|
-
[types[name] === undefined || explicit ? name : `${name}$${name}`, value])
|
|
27
24
|
.filter(([name]) => name.includes("$"))
|
|
28
25
|
.map(([name, value]) => [name.split("$"), value])
|
|
29
26
|
.map(([[name, type], value]) =>
|
package/src/hooks/stage.js
CHANGED
|
@@ -15,16 +15,17 @@ const pre = async app => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
const post = async app => {
|
|
18
|
-
const
|
|
18
|
+
const location = app.get("location");
|
|
19
19
|
|
|
20
20
|
// stage routes
|
|
21
|
-
await app.runpath(location.routes).create();
|
|
22
21
|
const double = doubled((await app.path.routes.collect())
|
|
23
22
|
.map(path => path.debase(app.path.routes))
|
|
24
23
|
.map(path => `${path}`.slice(1, -path.extension.length)));
|
|
25
24
|
double && errors.DoubleRoute.throw(double);
|
|
26
25
|
|
|
27
|
-
await app.
|
|
26
|
+
if (await app.path.routes.exists()) {
|
|
27
|
+
await app.stage(app.path.routes, location.routes);
|
|
28
|
+
}
|
|
28
29
|
if (await app.path.types.exists()) {
|
|
29
30
|
await app.stage(app.path.types, location.types);
|
|
30
31
|
}
|
package/src/loaders/modules.js
CHANGED
|
@@ -7,9 +7,7 @@ const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
|
7
7
|
const load = (modules = []) => modules.map(module =>
|
|
8
8
|
[module, load(module.load?.() ?? [])]).flat();
|
|
9
9
|
|
|
10
|
-
export default async (log, root,
|
|
11
|
-
const modules = config.modules ?? [];
|
|
12
|
-
|
|
10
|
+
export default async (log, root, modules) => {
|
|
13
11
|
Array.isArray(modules) || errors.ModulesMustBeArray.throw("modules");
|
|
14
12
|
|
|
15
13
|
modules.some(({ name }, n) => name === undefined &&
|
package/src/loaders/routes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tryreturn } from "rcompat/sync";
|
|
2
|
-
import
|
|
2
|
+
import o from "rcompat/object";
|
|
3
3
|
import { File } from "rcompat/fs";
|
|
4
4
|
import { default as fs, doubled } from "./common.js";
|
|
5
5
|
import * as get from "./routes/exports.js";
|
|
@@ -7,18 +7,22 @@ import errors from "../errors.js";
|
|
|
7
7
|
|
|
8
8
|
const { separator } = File;
|
|
9
9
|
|
|
10
|
+
const valid_route = /^[\w\-[\]=/.]*$/u;
|
|
11
|
+
|
|
10
12
|
const make = path => {
|
|
13
|
+
!valid_route.test(path) && errors.InvalidPath.throw(path);
|
|
14
|
+
|
|
11
15
|
const double = doubled(path.split(separator)
|
|
12
|
-
.filter(part => part.startsWith("
|
|
16
|
+
.filter(part => part.startsWith("[") && part.endsWith("]"))
|
|
13
17
|
.map(part => part.slice(1, part.indexOf("="))));
|
|
14
18
|
double && errors.DoublePathParameter.throw(double, path);
|
|
15
19
|
|
|
16
|
-
const route = path.replaceAll(/\
|
|
20
|
+
const route = path.replaceAll(/\[(?<named>.*?)\]/gu, (_, named) =>
|
|
17
21
|
tryreturn(_ => {
|
|
18
22
|
const { name, type } = /^(?<name>\w+)(?<type>=\w+)?$/u.exec(named).groups;
|
|
19
23
|
const param = type === undefined ? name : `${name}$${type.slice(1)}`;
|
|
20
24
|
return `(?<${param}>[^/]{1,}?)`;
|
|
21
|
-
}).orelse(_ => errors.
|
|
25
|
+
}).orelse(_ => errors.EmptyPathParameter.throw(named, path)));
|
|
22
26
|
|
|
23
27
|
// normalize to unix
|
|
24
28
|
return new RegExp(`^/${route.replaceAll(separator, "/")}$`, "u");
|
|
@@ -26,9 +30,9 @@ const make = path => {
|
|
|
26
30
|
|
|
27
31
|
export default async (app, load = fs) => {
|
|
28
32
|
const { log } = app;
|
|
29
|
-
const directory = app.runpath(app.
|
|
33
|
+
const directory = app.runpath(app.get("location.routes"));
|
|
30
34
|
const filter = path => ([name]) => path.includes(name);
|
|
31
|
-
const routes = from(await Promise.all(["guards", "errors", "layouts"]
|
|
35
|
+
const routes = o.from(await Promise.all(["guards", "errors", "layouts"]
|
|
32
36
|
.map(async extra => [extra, await get[extra](log, directory, load)])));
|
|
33
37
|
|
|
34
38
|
return (await get.routes(log, directory, load)).map(([path, imported]) => {
|
package/src/run.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tryreturn } from "rcompat/async";
|
|
2
2
|
import { File } from "rcompat/fs";
|
|
3
|
-
import
|
|
3
|
+
import o from "rcompat/object";
|
|
4
4
|
import { runtime } from "rcompat/meta";
|
|
5
5
|
import app from "./app.js";
|
|
6
6
|
import { default as Logger, bye } from "./Logger.js";
|
|
@@ -20,7 +20,7 @@ const get_config = async root => {
|
|
|
20
20
|
(imported === undefined || Object.keys(imported).length === 0) &&
|
|
21
21
|
errors.EmptyConfigFile.warn(logger, config);
|
|
22
22
|
|
|
23
|
-
return extend(defaults, imported);
|
|
23
|
+
return o.extend(defaults, imported);
|
|
24
24
|
}).orelse(({ message }) =>
|
|
25
25
|
errors.ErrorInConfigFile.throw(message, `${runtime} ${config}`))
|
|
26
26
|
: defaults;
|
package/src/start.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serve, Response, Status } from "rcompat/http";
|
|
2
2
|
import { tryreturn } from "rcompat/async";
|
|
3
|
-
import { bold, blue } from "rcompat/colors";
|
|
3
|
+
import { bold, blue, dim } from "rcompat/colors";
|
|
4
|
+
import { resolve } from "rcompat/package";
|
|
4
5
|
import * as hooks from "./hooks/exports.js";
|
|
5
6
|
import { print } from "./Logger.js";
|
|
6
7
|
|
|
@@ -11,24 +12,24 @@ export default async (app$, mode = "development") => {
|
|
|
11
12
|
// run one-time hooks
|
|
12
13
|
let app = app$;
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
print(blue(bold(app.name)), blue(app.version), `at ${address}\n`);
|
|
17
|
-
|
|
18
|
-
app.log.info(`in ${bold(mode)} mode`, { module: "primate" });
|
|
15
|
+
const primate = await resolve(import.meta.url);
|
|
16
|
+
print(blue(bold(primate.name)), blue(primate.version), "in startup\n");
|
|
19
17
|
|
|
20
18
|
for (const hook of base_hooks) {
|
|
21
|
-
app.log.info(`running ${
|
|
19
|
+
app.log.info(`running ${dim(hook)} hooks`, { module: "primate" });
|
|
22
20
|
app = await hooks[hook](app);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
app.route = hooks.route(app);
|
|
26
|
-
app.parse = hooks.parse(app
|
|
24
|
+
app.parse = hooks.parse(app);
|
|
27
25
|
app.server = await serve(async request =>
|
|
28
26
|
tryreturn(async _ => (await hooks.handle(app))(await app.parse(request)))
|
|
29
27
|
.orelse(error => {
|
|
30
28
|
app.log.auto(error);
|
|
31
29
|
return new Response(null, { status: Status.INTERNAL_SERVER_ERROR });
|
|
32
|
-
}),
|
|
33
|
-
|
|
30
|
+
}), app.get("http"));
|
|
31
|
+
|
|
32
|
+
const { host, port } = app.get("http");
|
|
33
|
+
const address = `http${app.secure ? "s" : ""}://${host}:${port}`;
|
|
34
|
+
print(`${blue("++")} started ${dim("->")} ${dim(address)}\n`);
|
|
34
35
|
};
|
package/src/cwd.js
DELETED