primate 0.17.0 → 0.19.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 +4 -3
- package/src/Logger.js +68 -60
- package/src/app.js +91 -41
- package/src/commands/exports.js +2 -5
- package/src/defaults/primate.config.js +5 -0
- package/src/dispatch.js +24 -0
- package/src/errors.js +141 -0
- package/src/handlers/error.js +8 -7
- package/src/handlers/html.js +27 -5
- package/src/handlers/redirect.js +3 -1
- package/src/handlers/view.js +4 -4
- package/src/hooks/bundle.js +1 -1
- package/src/hooks/compile.js +1 -1
- package/src/hooks/exports.js +2 -0
- package/src/hooks/handle/exports.js +0 -1
- package/src/hooks/handle.js +20 -104
- package/src/hooks/parse.js +59 -0
- package/src/hooks/publish.js +1 -1
- package/src/hooks/register.js +1 -1
- package/src/hooks/route.js +61 -42
- package/src/hooks/serve.js +8 -0
- package/src/http-statuses.js +4 -0
- package/src/run.js +23 -15
- package/src/start.js +24 -9
- package/src/commands/build.js +0 -1
- package/src/commands/create.js +0 -42
- package/src/commands/help.js +0 -3
- package/src/fromNull.js +0 -1
- package/src/hooks/handle/http-statuses.js +0 -5
- package/src/hooks/handle/mime.spec.js +0 -13
- package/src/hooks/handle/respond.spec.js +0 -12
- package/src/hooks/handle.spec.js +0 -88
- package/src/hooks/route.spec.js +0 -143
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Expressive, minimal and extensible web framework",
|
|
5
5
|
"homepage": "https://primatejs.com",
|
|
6
6
|
"bugs": "https://github.com/primatejs/primate/issues",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"files": [
|
|
9
|
-
"src
|
|
9
|
+
"src/**/*.js",
|
|
10
|
+
"src/defaults/index.html",
|
|
10
11
|
"!src/**/*.spec.js"
|
|
11
12
|
],
|
|
12
13
|
"bin": "src/bin.js",
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
"directory": "packages/primate"
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"runtime-compat": "^0.
|
|
20
|
+
"runtime-compat": "^0.17.0"
|
|
20
21
|
},
|
|
21
22
|
"type": "module",
|
|
22
23
|
"exports": "./src/exports.js"
|
package/src/Logger.js
CHANGED
|
@@ -1,115 +1,123 @@
|
|
|
1
1
|
import {assert, is} from "runtime-compat/dyndef";
|
|
2
|
+
import {blue, bold, green, red, yellow, dim} from "runtime-compat/colors";
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
green: msg => `\x1b[32m${msg}\x1b[0m`,
|
|
8
|
-
yellow: msg => `\x1b[33m${msg}\x1b[0m`,
|
|
9
|
-
blue: msg => `\x1b[34m${msg}\x1b[0m`,
|
|
10
|
-
gray: msg => `\x1b[2m${msg}\x1b[0m`,
|
|
4
|
+
const errors = {
|
|
5
|
+
Error: 0,
|
|
6
|
+
Warn: 1,
|
|
7
|
+
Info: 2,
|
|
11
8
|
};
|
|
12
9
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const Abort = class Abort extends Error {};
|
|
18
|
-
// Error natively provided
|
|
19
|
-
const Warn = class Warn extends Error {};
|
|
20
|
-
const Info = class Info extends Error {};
|
|
21
|
-
|
|
22
|
-
const levels = new Map([
|
|
23
|
-
[Error, error],
|
|
24
|
-
[Warn, warn],
|
|
25
|
-
[Info, info],
|
|
26
|
-
]);
|
|
10
|
+
const print = (...messages) => process.stdout.write(messages.join(" "));
|
|
11
|
+
const bye = () => print(dim(yellow("~~ bye\n")));
|
|
12
|
+
const mark = (format, ...params) => params.reduce((formatted, param) =>
|
|
13
|
+
formatted.replace("%", bold(param)), format);
|
|
27
14
|
|
|
28
|
-
const
|
|
29
|
-
throw new Abort(message);
|
|
30
|
-
};
|
|
15
|
+
const reference = "https://primatejs.com/reference/errors";
|
|
31
16
|
|
|
32
|
-
const
|
|
17
|
+
const hyphenate = classCased => classCased
|
|
18
|
+
.split("")
|
|
19
|
+
.map(character => character
|
|
20
|
+
.replace(/[A-Z]/u, capital => `-${capital.toLowerCase()}`))
|
|
21
|
+
.join("")
|
|
22
|
+
.slice(1);
|
|
33
23
|
|
|
34
24
|
const Logger = class Logger {
|
|
35
25
|
#level; #trace;
|
|
36
26
|
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
static throwable(type, name, module) {
|
|
28
|
+
return {
|
|
29
|
+
throw(args = {}) {
|
|
30
|
+
const {message, level, fix} = type(args);
|
|
31
|
+
const error = new Error(mark(...message));
|
|
32
|
+
error.level = level;
|
|
33
|
+
error.fix = mark(...fix);
|
|
34
|
+
error.name = name;
|
|
35
|
+
error.module = module;
|
|
36
|
+
throw error;
|
|
37
|
+
},
|
|
38
|
+
warn(logger, ...args) {
|
|
39
|
+
const {message, level, fix} = type(...args);
|
|
40
|
+
const error = {level, message: mark(...message), fix: mark(...fix)};
|
|
41
|
+
logger.auto({...error, name, module});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
constructor({level = errors.Error, trace = false} = {}) {
|
|
47
|
+
assert(level !== undefined && level <= errors.Info);
|
|
39
48
|
is(trace).boolean();
|
|
40
49
|
this.#level = level;
|
|
41
50
|
this.#trace = trace;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
static get colors() {
|
|
45
|
-
return colors;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
53
|
static print(...args) {
|
|
49
54
|
print(...args);
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
static get mark() {
|
|
58
|
+
return mark;
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
static get Error() {
|
|
53
|
-
return Error;
|
|
62
|
+
return errors.Error;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
static get Warn() {
|
|
57
|
-
return Warn;
|
|
66
|
+
return errors.Warn;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
static get Info() {
|
|
61
|
-
return Info;
|
|
70
|
+
return errors.Info;
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
get class() {
|
|
65
74
|
return this.constructor;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
#print(pre, error) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
print(colors.bold(pre), error, "\n");
|
|
77
|
+
#print(pre, color, message, {fix, module, name} = {}, error) {
|
|
78
|
+
print(pre, `${module !== undefined ? `${color(module)} ` : ""}${message}`, "\n");
|
|
79
|
+
if (fix && this.level >= errors.Warn) {
|
|
80
|
+
print(blue("++"), fix);
|
|
81
|
+
name && print(dim(`\n -> ${reference}/${module ?? "primate"}#${hyphenate(name)}`), "\n");
|
|
76
82
|
}
|
|
83
|
+
this.#trace && error && console.log(error);
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
get level() {
|
|
80
|
-
return
|
|
87
|
+
return this.#level;
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
info(message) {
|
|
84
|
-
if (this.level >=
|
|
85
|
-
this.#print(
|
|
90
|
+
info(message, args) {
|
|
91
|
+
if (this.level >= errors.Info) {
|
|
92
|
+
this.#print(green("--"), green, message, args);
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
warn(message) {
|
|
90
|
-
if (this.level >=
|
|
91
|
-
this.#print(
|
|
96
|
+
warn(message, args) {
|
|
97
|
+
if (this.level >= errors.Warn) {
|
|
98
|
+
this.#print(yellow("??"), yellow, message, args);
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
|
|
95
|
-
error(message) {
|
|
96
|
-
if (this.level >=
|
|
97
|
-
this.#print(
|
|
102
|
+
error(message, args, error) {
|
|
103
|
+
if (this.level >= errors.Warn) {
|
|
104
|
+
this.#print(red("!!"), red, message, args, error);
|
|
98
105
|
}
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
auto(
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
auto(error) {
|
|
109
|
+
const {level, message, ...args} = error;
|
|
110
|
+
if (level === errors.Info) {
|
|
111
|
+
return this.info(message, args, error);
|
|
104
112
|
}
|
|
105
|
-
if (
|
|
106
|
-
return this.warn(message
|
|
113
|
+
if (level === errors.Warn) {
|
|
114
|
+
return this.warn(message, args, error);
|
|
107
115
|
}
|
|
108
116
|
|
|
109
|
-
return this.error(message);
|
|
117
|
+
return this.error(message, args, error);
|
|
110
118
|
}
|
|
111
119
|
};
|
|
112
120
|
|
|
113
121
|
export default Logger;
|
|
114
122
|
|
|
115
|
-
export {
|
|
123
|
+
export {print, bye};
|
package/src/app.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import crypto from "runtime-compat/crypto";
|
|
2
2
|
import {File, Path} from "runtime-compat/fs";
|
|
3
|
+
import {bold, blue} from "runtime-compat/colors";
|
|
4
|
+
import errors from "./errors.js";
|
|
3
5
|
import * as handlers from "./handlers/exports.js";
|
|
4
|
-
import
|
|
6
|
+
import * as hooks from "./hooks/exports.js";
|
|
7
|
+
import dispatch from "./dispatch.js";
|
|
5
8
|
|
|
6
9
|
const qualify = (root, paths) =>
|
|
7
10
|
Object.keys(paths).reduce((sofar, key) => {
|
|
@@ -12,16 +15,17 @@ const qualify = (root, paths) =>
|
|
|
12
15
|
return sofar;
|
|
13
16
|
}, {});
|
|
14
17
|
|
|
15
|
-
const
|
|
18
|
+
const base = new Path(import.meta.url).up(1);
|
|
19
|
+
const defaultLayout = "index.html";
|
|
16
20
|
|
|
17
|
-
const index = async app => {
|
|
18
|
-
const name =
|
|
21
|
+
const index = async (app, layout = defaultLayout) => {
|
|
22
|
+
const name = layout;
|
|
19
23
|
try {
|
|
20
24
|
// user-provided file
|
|
21
|
-
return await File.read(`${app.paths.
|
|
25
|
+
return await File.read(`${app.paths.layouts.join(name)}`);
|
|
22
26
|
} catch (error) {
|
|
23
27
|
// fallback
|
|
24
|
-
return
|
|
28
|
+
return base.join("defaults", defaultLayout).text();
|
|
25
29
|
}
|
|
26
30
|
};
|
|
27
31
|
|
|
@@ -32,13 +36,20 @@ const hash = async (string, algorithm = "sha-384") => {
|
|
|
32
36
|
return `${algo}-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}`;
|
|
33
37
|
};
|
|
34
38
|
|
|
39
|
+
const attribute = attributes => Object.keys(attributes).length > 0 ?
|
|
40
|
+
" ".concat(Object.entries(attributes)
|
|
41
|
+
.map(([key, value]) => `${key}="${value}"`).join(" "))
|
|
42
|
+
: "";
|
|
43
|
+
const tag = ({name, attributes = {}, code = "", close = true}) =>
|
|
44
|
+
`<${name}${attribute(attributes)}${close ? `>${code}</${name}>` : "/>"}`;
|
|
45
|
+
|
|
35
46
|
export default async (config, root, log) => {
|
|
36
|
-
const {
|
|
47
|
+
const {http} = config;
|
|
37
48
|
|
|
38
49
|
// if ssl activated, resolve key and cert early
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
if (http.ssl) {
|
|
51
|
+
http.ssl.key = root.join(http.ssl.key);
|
|
52
|
+
http.ssl.cert = root.join(http.ssl.cert);
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
const paths = qualify(root, config.paths);
|
|
@@ -50,25 +61,41 @@ export default async (config, root, log) => {
|
|
|
50
61
|
`${route}`.replace(paths.routes, "").slice(1, -ending.length),
|
|
51
62
|
(await import(route)).default,
|
|
52
63
|
]));
|
|
64
|
+
const types = Object.fromEntries(
|
|
65
|
+
paths.types === undefined ? [] : await Promise.all(
|
|
66
|
+
(await Path.collect(paths.types , /^.*.js$/u))
|
|
67
|
+
/* accept only lowercase-first files in type filename */
|
|
68
|
+
.filter(path => /^[a-z]/u.test(path.name))
|
|
69
|
+
.map(async type => [
|
|
70
|
+
`${type}`.replace(paths.types, "").slice(1, -ending.length),
|
|
71
|
+
(await import(type)).default,
|
|
72
|
+
])));
|
|
73
|
+
Object.entries(types).some(([name, type]) =>
|
|
74
|
+
typeof type !== "function" && errors.InvalidType.throw({name}));
|
|
53
75
|
|
|
54
76
|
const modules = config.modules === undefined ? [] : config.modules;
|
|
55
77
|
|
|
56
|
-
modules.every(module => module.name !== undefined ||
|
|
57
|
-
|
|
78
|
+
modules.every((module, n) => module.name !== undefined ||
|
|
79
|
+
errors.ModulesMustHaveNames.throw({n}));
|
|
58
80
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
new Set(modules.map(({name}) => name)).size !== modules.length &&
|
|
82
|
+
errors.DoubleModule.throw({
|
|
83
|
+
modules: modules.map(({name}) => name),
|
|
84
|
+
config: root.join("primate.config.js"),
|
|
85
|
+
});
|
|
62
86
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
})
|
|
87
|
+
const hookless = modules.filter(module =>
|
|
88
|
+
!Object.keys(module).some(key => Object.keys(hooks).includes(key)));
|
|
89
|
+
hookless.length > 0 && errors.ModuleHasNoHooks.warn(log, {hookless});
|
|
90
|
+
|
|
91
|
+
const {name, version} = await base.up(1).join("package.json").json();
|
|
66
92
|
|
|
67
93
|
const app = {
|
|
68
94
|
config,
|
|
69
95
|
routes,
|
|
70
|
-
secure:
|
|
71
|
-
name,
|
|
96
|
+
secure: http?.ssl !== undefined,
|
|
97
|
+
name,
|
|
98
|
+
version,
|
|
72
99
|
library: {},
|
|
73
100
|
identifiers: {},
|
|
74
101
|
replace(code) {
|
|
@@ -86,20 +113,44 @@ export default async (config, root, log) => {
|
|
|
86
113
|
paths,
|
|
87
114
|
root,
|
|
88
115
|
log,
|
|
116
|
+
generateHeaders: () => {
|
|
117
|
+
const csp = Object.keys(http.csp).reduce((policy_string, key) =>
|
|
118
|
+
`${policy_string}${key} ${http.csp[key]};`, "");
|
|
119
|
+
const scripts = app.resources
|
|
120
|
+
.filter(({type}) => type !== "style")
|
|
121
|
+
.map(resource => `'${resource.integrity}'`).join(" ");
|
|
122
|
+
const _csp = scripts === "" ? csp : `${csp}script-src 'self' ${scripts};`;
|
|
123
|
+
// remove inline resources
|
|
124
|
+
for (let i = app.resources.length - 1; i >= 0; i--) {
|
|
125
|
+
const resource = app.resources[i];
|
|
126
|
+
if (resource.inline) {
|
|
127
|
+
app.resources.splice(i, 1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"Content-Security-Policy": _csp,
|
|
133
|
+
"Referrer-Policy": "same-origin",
|
|
134
|
+
};
|
|
135
|
+
},
|
|
89
136
|
handlers: {...handlers},
|
|
90
|
-
render: async ({body = "", head = ""} = {}) => {
|
|
91
|
-
const html = await index(app);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
render: async ({body = "", head = "", layout} = {}) => {
|
|
138
|
+
const html = await index(app, layout);
|
|
139
|
+
// inline: <script type integrity>...</script>
|
|
140
|
+
// outline: <script type integrity src></script>
|
|
141
|
+
const script = ({inline, code, type, integrity, src}) => inline
|
|
142
|
+
? tag({name: "script", attributes: {type, integrity}, code})
|
|
143
|
+
: tag({name: "script", attributes: {type, integrity, src}});
|
|
144
|
+
// inline: <style>...</style>
|
|
145
|
+
// outline: <link rel="stylesheet" href/>
|
|
146
|
+
const style = ({inline, code, href, rel = "stylesheet"}) => inline
|
|
147
|
+
? tag({name: "style", code})
|
|
148
|
+
: tag({name: "link", attributes: {rel, href}, close: false});
|
|
149
|
+
const heads = app.resources.map(({src, code, type, inline, integrity}) =>
|
|
150
|
+
type === "style"
|
|
151
|
+
? style({inline, code, href: src})
|
|
152
|
+
: script({inline, code, type, integrity, src})
|
|
153
|
+
).join("\n");
|
|
103
154
|
return html
|
|
104
155
|
.replace("%body%", () => body)
|
|
105
156
|
.replace("%head%", () => `${head}${heads}`);
|
|
@@ -108,10 +159,8 @@ export default async (config, root, log) => {
|
|
|
108
159
|
if (type === "module") {
|
|
109
160
|
code = app.replace(code);
|
|
110
161
|
}
|
|
111
|
-
// while integrity is only really needed for scripts, it is also later
|
|
112
|
-
// used for the etag header
|
|
113
162
|
const integrity = await hash(code);
|
|
114
|
-
const _src = new Path(
|
|
163
|
+
const _src = new Path(http.static.root).join(src ?? "");
|
|
115
164
|
app.resources.push({src: `${_src}`, code, type, inline, integrity});
|
|
116
165
|
return integrity;
|
|
117
166
|
},
|
|
@@ -128,12 +177,10 @@ export default async (config, root, log) => {
|
|
|
128
177
|
app.identifiers = {...exports, ...app.identifiers};
|
|
129
178
|
},
|
|
130
179
|
modules,
|
|
180
|
+
types,
|
|
131
181
|
};
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const type = app.secure ? "https" : "http";
|
|
135
|
-
const address = `${type}://${config.http.host}:${config.http.port}`;
|
|
136
|
-
print(colors.gray(`at ${address}`), "\n");
|
|
182
|
+
log.class.print(blue(bold(name)), blue(version),
|
|
183
|
+
`at http${app.secure ? "s" : ""}://${http.host}:${http.port}\n`);
|
|
137
184
|
// modules may load other modules
|
|
138
185
|
await Promise.all(app.modules
|
|
139
186
|
.filter(module => module.load !== undefined)
|
|
@@ -141,5 +188,8 @@ export default async (config, root, log) => {
|
|
|
141
188
|
app.modules.push(dependent);
|
|
142
189
|
}})));
|
|
143
190
|
|
|
191
|
+
app.route = hooks.route({...app, dispatch: dispatch(types)});
|
|
192
|
+
app.parse = hooks.parse(dispatch(types));
|
|
193
|
+
|
|
144
194
|
return app;
|
|
145
195
|
};
|
package/src/commands/exports.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import {default as dev} from "./dev.js";
|
|
2
2
|
import {default as serve} from "./serve.js";
|
|
3
|
-
import {default as build} from "./build.js";
|
|
4
|
-
import {default as create} from "./create.js";
|
|
5
|
-
import {default as help} from "./help.js";
|
|
6
3
|
|
|
7
|
-
const commands = {dev, serve
|
|
4
|
+
const commands = {dev, serve};
|
|
8
5
|
|
|
9
|
-
const run = name => commands[name] ??
|
|
6
|
+
const run = name => commands[name] ?? dev;
|
|
10
7
|
|
|
11
8
|
export default name => name === undefined ? dev : run(name);
|
|
@@ -10,6 +10,7 @@ export default {
|
|
|
10
10
|
port: 6161,
|
|
11
11
|
csp: {
|
|
12
12
|
"default-src": "'self'",
|
|
13
|
+
"style-src": "'self'",
|
|
13
14
|
"object-src": "'none'",
|
|
14
15
|
"frame-ancestors": "'none'",
|
|
15
16
|
"form-action": "'self'",
|
|
@@ -21,6 +22,7 @@ export default {
|
|
|
21
22
|
},
|
|
22
23
|
},
|
|
23
24
|
paths: {
|
|
25
|
+
layouts: "layouts",
|
|
24
26
|
static: "static",
|
|
25
27
|
public: "public",
|
|
26
28
|
routes: "routes",
|
|
@@ -29,4 +31,7 @@ export default {
|
|
|
29
31
|
},
|
|
30
32
|
modules: [],
|
|
31
33
|
dist: "app",
|
|
34
|
+
types: {
|
|
35
|
+
explicit: false,
|
|
36
|
+
},
|
|
32
37
|
};
|
package/src/dispatch.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {is, maybe} from "runtime-compat/dyndef";
|
|
2
|
+
import errors from "./errors.js";
|
|
3
|
+
|
|
4
|
+
export default (patches = {}) => value => {
|
|
5
|
+
is(patches.get).undefined();
|
|
6
|
+
return Object.assign(Object.create(null), {
|
|
7
|
+
...Object.fromEntries(Object.entries(patches).map(([name, patch]) =>
|
|
8
|
+
[name, property => {
|
|
9
|
+
is(property).defined(`\`${name}\` called without property`);
|
|
10
|
+
try {
|
|
11
|
+
return patch(value[property], property);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
errors.MismatchedType.throw({message: error.message});
|
|
14
|
+
}
|
|
15
|
+
}])),
|
|
16
|
+
get(property) {
|
|
17
|
+
maybe(property).string();
|
|
18
|
+
if (property !== undefined) {
|
|
19
|
+
return value[property];
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
};
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import Logger from "./Logger.js";
|
|
2
|
+
|
|
3
|
+
export default Object.fromEntries(Object.entries({
|
|
4
|
+
CannotParseBody({body, contentType}) {
|
|
5
|
+
return {
|
|
6
|
+
message: ["cannot parse body % as %", body, contentType],
|
|
7
|
+
fix: ["use a different content type or fix body"],
|
|
8
|
+
level: Logger.Warn,
|
|
9
|
+
};
|
|
10
|
+
},
|
|
11
|
+
DoubleModule({modules, config}) {
|
|
12
|
+
const double = modules.find((module, i, array) =>
|
|
13
|
+
array.filter((_, j) => i !== j).includes(module));
|
|
14
|
+
return {
|
|
15
|
+
message: ["double module % in %", double, config],
|
|
16
|
+
fix: ["load % only once", double],
|
|
17
|
+
level: Logger.Error,
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
DoublePathParameter({path, double}) {
|
|
21
|
+
return {
|
|
22
|
+
message: ["double path parameter % in route %", double, path],
|
|
23
|
+
fix: ["disambiguate path parameters in route names"],
|
|
24
|
+
level: Logger.Error,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
DoubleRoute({double}) {
|
|
28
|
+
return {
|
|
29
|
+
message: ["double route %", double],
|
|
30
|
+
fix: ["disambiguate route % and %", double, `${double}/index`],
|
|
31
|
+
level: Logger.Error,
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
EmptyRouteFile({config: {paths}, route}) {
|
|
35
|
+
return {
|
|
36
|
+
message: ["empty route file at %", `${paths.routes}/${route}.js`],
|
|
37
|
+
fix: ["add routes or remove file"],
|
|
38
|
+
level: Logger.Warn,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
ErrorInConfigFile({config, message}) {
|
|
42
|
+
return {
|
|
43
|
+
message: ["error in config %", message],
|
|
44
|
+
fix: ["check errors in config file by running %", `node ${config}`],
|
|
45
|
+
level: Logger.Error,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
InvalidPathParameter({named, path}) {
|
|
49
|
+
return {
|
|
50
|
+
message: ["invalid path parameter % in route %", named, path],
|
|
51
|
+
fix: ["use only latin letters and decimal digits in path parameters"],
|
|
52
|
+
level: Logger.Error,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
InvalidRouteName({path}) {
|
|
56
|
+
return {
|
|
57
|
+
message: ["invalid route name %", path],
|
|
58
|
+
fix: ["do not use dots in route names"],
|
|
59
|
+
level: Logger.Error,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
InvalidType({name}) {
|
|
63
|
+
return {
|
|
64
|
+
message: ["invalid type %", name],
|
|
65
|
+
fix: ["use only functions for the default export of types"],
|
|
66
|
+
level: Logger.Error,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
InvalidTypeName({name}) {
|
|
70
|
+
return {
|
|
71
|
+
message: ["invalid type name %", name],
|
|
72
|
+
fix: ["use only latin letters and decimal digits in types"],
|
|
73
|
+
level: Logger.Error,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
MismatchedPath({path, message}) {
|
|
77
|
+
return {
|
|
78
|
+
message: [`mismatched % path: ${message}`, path],
|
|
79
|
+
fix: ["if unintentional, fix the type or the caller"],
|
|
80
|
+
level: Logger.Info,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
MismatchedType({message}) {
|
|
84
|
+
return {
|
|
85
|
+
message: [`mismatched type: ${message}`],
|
|
86
|
+
fix: ["if unintentional, fix the type or the caller"],
|
|
87
|
+
level: Logger.Info,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
ModuleHasNoHooks({hookless}) {
|
|
91
|
+
const modules = hookless.map(({name}) => name).join(", ");
|
|
92
|
+
return {
|
|
93
|
+
message: ["module % has no hooks", modules],
|
|
94
|
+
fix: ["ensure every module uses at least one hook or deactivate it"],
|
|
95
|
+
level: Logger.Warn,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
ModulesMustHaveNames({n}) {
|
|
99
|
+
return {
|
|
100
|
+
message: ["modules must have names"],
|
|
101
|
+
fix: ["update module at index % and inform maintainer", n],
|
|
102
|
+
level: Logger.Error,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
EmptyConfigFile({config}) {
|
|
106
|
+
return {
|
|
107
|
+
message: ["empty config file at %", config],
|
|
108
|
+
fix: ["add configuration options or remove file"],
|
|
109
|
+
level: Logger.Warn,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
NoFileForPath({pathname, config: {paths}}) {
|
|
113
|
+
return {
|
|
114
|
+
message: ["no file for %", pathname],
|
|
115
|
+
fix: ["if unintentional create a file at %%", paths.static, pathname],
|
|
116
|
+
level: Logger.Info,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
NoHandlerForExtension({name, ending}) {
|
|
120
|
+
return {
|
|
121
|
+
message: ["no handler for % extension", ending],
|
|
122
|
+
fix: ["add handler module for % files or remove %", `.${ending}`, name],
|
|
123
|
+
level: Logger.Error,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
NoRouteToPath({method, pathname, config: {paths}}) {
|
|
127
|
+
const route = `${paths.routes}${pathname === "" ? "index" : pathname}.js`;
|
|
128
|
+
return {
|
|
129
|
+
message: ["no % route to %", method, pathname],
|
|
130
|
+
fix: ["if unintentional create a route at %", route],
|
|
131
|
+
level: Logger.Info,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
ReservedTypeName({name}) {
|
|
135
|
+
return {
|
|
136
|
+
message: ["type name % is reserved", name],
|
|
137
|
+
fix: ["do not use any reserved type names"],
|
|
138
|
+
level: Logger.Error,
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
}).map(([name, error]) => [name, Logger.throwable(error, name, "primate")]));
|
package/src/handlers/error.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import {NotFound} from "../http-statuses.js";
|
|
2
2
|
|
|
3
|
-
export default (body =
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
export default (body = "Not Found", {status = NotFound} = {}) =>
|
|
4
|
+
async (app, headers) => [
|
|
5
|
+
await app.render({body}), {
|
|
6
|
+
status,
|
|
7
|
+
headers: {...headers, "Content-Type": "text/html"},
|
|
8
|
+
},
|
|
9
|
+
];
|
package/src/handlers/html.js
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const script = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
|
|
2
|
+
const style = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
|
|
3
|
+
|
|
4
|
+
const integrate = async (html, publish, headers) => {
|
|
5
|
+
const scripts = await Promise.all([...html.matchAll(script)]
|
|
6
|
+
.map(({groups: {code}}) => publish({code, inline: true})));
|
|
7
|
+
for (const integrity of scripts) {
|
|
8
|
+
headers["Content-Security-Policy"] = headers["Content-Security-Policy"]
|
|
9
|
+
.replace("script-src 'self' ", `script-src 'self' '${integrity}' `);
|
|
10
|
+
}
|
|
11
|
+
const styles = await Promise.all([...html.matchAll(style)]
|
|
12
|
+
.map(({groups: {code}}) => publish({code, type: "style", inline: true})));
|
|
13
|
+
for (const integrity of styles) {
|
|
14
|
+
headers["Content-Security-Policy"] = headers["Content-Security-Policy"]
|
|
15
|
+
.replace("style-src 'self'", `style-src 'self' '${integrity}' `);
|
|
16
|
+
}
|
|
17
|
+
return html
|
|
18
|
+
.replaceAll(/<script>.*?<\/script>/gus, () => "")
|
|
19
|
+
.replaceAll(/<style>.*?<\/style>/gus, () => "");
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default (component, options = {}) => {
|
|
23
|
+
const {status = 200, partial = false, load = false, layout} = options;
|
|
3
24
|
|
|
4
25
|
return async (app, headers) => {
|
|
5
|
-
const body = load ?
|
|
6
|
-
await app.paths.components.join(component).text() : component
|
|
26
|
+
const body = await integrate(await load ?
|
|
27
|
+
await app.paths.components.join(component).text() : component,
|
|
28
|
+
app.publish, headers);
|
|
7
29
|
|
|
8
|
-
return [partial ? body : await app.render({body}), {
|
|
30
|
+
return [partial ? body : await app.render({body, layout}), {
|
|
9
31
|
status,
|
|
10
32
|
headers: {...headers, "Content-Type": "text/html"},
|
|
11
33
|
}];
|