primate 0.17.0 → 0.18.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 +71 -60
- package/src/app.js +44 -21
- package/src/commands/exports.js +2 -5
- package/src/errors.js +112 -0
- package/src/handlers/error.js +8 -7
- 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 +1 -0
- package/src/hooks/handle/exports.js +0 -1
- package/src/hooks/handle.js +21 -100
- package/src/hooks/parse.js +59 -0
- package/src/hooks/{handle.spec.js → parse.spec.js} +11 -27
- package/src/hooks/publish.js +1 -1
- package/src/hooks/register.js +1 -1
- package/src/hooks/route.js +44 -25
- package/src/hooks/route.spec.js +56 -18
- package/src/http-statuses.js +4 -0
- package/src/run.js +23 -15
- package/src/start.js +14 -4
- package/src/commands/build.js +0 -1
- package/src/commands/create.js +0 -42
- package/src/commands/help.js +0 -3
- package/src/hooks/handle/http-statuses.js +0 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"directory": "packages/primate"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"runtime-compat": "^0.
|
|
19
|
+
"runtime-compat": "^0.17.0"
|
|
20
20
|
},
|
|
21
21
|
"type": "module",
|
|
22
22
|
"exports": "./src/exports.js"
|
package/src/Logger.js
CHANGED
|
@@ -1,115 +1,126 @@
|
|
|
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
|
-
|
|
75
|
-
print(
|
|
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");
|
|
82
|
+
}
|
|
83
|
+
if (this.#trace && error) {
|
|
84
|
+
print(pre, color(module), "trace follows\n");
|
|
85
|
+
console.log(error);
|
|
76
86
|
}
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
get level() {
|
|
80
|
-
return
|
|
90
|
+
return this.#level;
|
|
81
91
|
}
|
|
82
92
|
|
|
83
|
-
info(message) {
|
|
84
|
-
if (this.level >=
|
|
85
|
-
this.#print(
|
|
93
|
+
info(message, args) {
|
|
94
|
+
if (this.level >= errors.Info) {
|
|
95
|
+
this.#print(green("--"), green, message, args);
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
98
|
|
|
89
|
-
warn(message) {
|
|
90
|
-
if (this.level >=
|
|
91
|
-
this.#print(
|
|
99
|
+
warn(message, args) {
|
|
100
|
+
if (this.level >= errors.Warn) {
|
|
101
|
+
this.#print(yellow("??"), yellow, message, args);
|
|
92
102
|
}
|
|
93
103
|
}
|
|
94
104
|
|
|
95
|
-
error(message) {
|
|
96
|
-
if (this.level >=
|
|
97
|
-
this.#print(
|
|
105
|
+
error(message, args, error) {
|
|
106
|
+
if (this.level >= errors.Warn) {
|
|
107
|
+
this.#print(red("!!"), red, message, args, error);
|
|
98
108
|
}
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
auto(
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
auto(error) {
|
|
112
|
+
const {level, message, ...args} = error;
|
|
113
|
+
if (level === errors.Info) {
|
|
114
|
+
return this.info(message, args, error);
|
|
104
115
|
}
|
|
105
|
-
if (
|
|
106
|
-
return this.warn(message
|
|
116
|
+
if (level === errors.Warn) {
|
|
117
|
+
return this.warn(message, args, error);
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
return this.error(message);
|
|
120
|
+
return this.error(message, args, error);
|
|
110
121
|
}
|
|
111
122
|
};
|
|
112
123
|
|
|
113
124
|
export default Logger;
|
|
114
125
|
|
|
115
|
-
export {
|
|
126
|
+
export {print, bye};
|
package/src/app.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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";
|
|
5
7
|
|
|
6
8
|
const qualify = (root, paths) =>
|
|
7
9
|
Object.keys(paths).reduce((sofar, key) => {
|
|
@@ -33,12 +35,12 @@ const hash = async (string, algorithm = "sha-384") => {
|
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
export default async (config, root, log) => {
|
|
36
|
-
const {
|
|
38
|
+
const {http} = config;
|
|
37
39
|
|
|
38
40
|
// if ssl activated, resolve key and cert early
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
if (http.ssl) {
|
|
42
|
+
http.ssl.key = root.join(http.ssl.key);
|
|
43
|
+
http.ssl.cert = root.join(http.ssl.cert);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
const paths = qualify(root, config.paths);
|
|
@@ -53,22 +55,27 @@ export default async (config, root, log) => {
|
|
|
53
55
|
|
|
54
56
|
const modules = config.modules === undefined ? [] : config.modules;
|
|
55
57
|
|
|
56
|
-
modules.every(module => module.name !== undefined ||
|
|
57
|
-
|
|
58
|
+
modules.every((module, n) => module.name !== undefined ||
|
|
59
|
+
errors.ModulesMustHaveNames.throw({n}));
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
new Set(modules.map(({name}) => name)).size !== modules.length &&
|
|
62
|
+
errors.DoubleModule.throw({
|
|
63
|
+
modules: modules.map(({name}) => name),
|
|
64
|
+
config: root.join("primate.config.js"),
|
|
65
|
+
});
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
})
|
|
67
|
+
const hookless = modules.filter(module =>
|
|
68
|
+
!Object.keys(module).some(key => Object.keys(hooks).includes(key)));
|
|
69
|
+
hookless.length > 0 && errors.ModuleHasNoHooks.warn(log, {hookless});
|
|
70
|
+
|
|
71
|
+
const {name, version} = await src.up(1).join("package.json").json();
|
|
66
72
|
|
|
67
73
|
const app = {
|
|
68
74
|
config,
|
|
69
75
|
routes,
|
|
70
|
-
secure:
|
|
71
|
-
name,
|
|
76
|
+
secure: http?.ssl !== undefined,
|
|
77
|
+
name,
|
|
78
|
+
version,
|
|
72
79
|
library: {},
|
|
73
80
|
identifiers: {},
|
|
74
81
|
replace(code) {
|
|
@@ -86,6 +93,25 @@ export default async (config, root, log) => {
|
|
|
86
93
|
paths,
|
|
87
94
|
root,
|
|
88
95
|
log,
|
|
96
|
+
generateHeaders: () => {
|
|
97
|
+
const csp = Object.keys(http.csp).reduce((policy_string, key) =>
|
|
98
|
+
`${policy_string}${key} ${http.csp[key]};`, "");
|
|
99
|
+
const scripts = app.resources
|
|
100
|
+
.map(resource => `'${resource.integrity}'`).join(" ");
|
|
101
|
+
const _csp = scripts === "" ? csp : `${csp}script-src 'self' ${scripts};`;
|
|
102
|
+
// remove inline resources
|
|
103
|
+
for (let i = app.resources.length - 1; i >= 0; i--) {
|
|
104
|
+
const resource = app.resources[i];
|
|
105
|
+
if (resource.inline) {
|
|
106
|
+
app.resources.splice(i, 1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"Content-Security-Policy": _csp,
|
|
112
|
+
"Referrer-Policy": "same-origin",
|
|
113
|
+
};
|
|
114
|
+
},
|
|
89
115
|
handlers: {...handlers},
|
|
90
116
|
render: async ({body = "", head = ""} = {}) => {
|
|
91
117
|
const html = await index(app);
|
|
@@ -111,7 +137,7 @@ export default async (config, root, log) => {
|
|
|
111
137
|
// while integrity is only really needed for scripts, it is also later
|
|
112
138
|
// used for the etag header
|
|
113
139
|
const integrity = await hash(code);
|
|
114
|
-
const _src = new Path(
|
|
140
|
+
const _src = new Path(http.static.root).join(src ?? "");
|
|
115
141
|
app.resources.push({src: `${_src}`, code, type, inline, integrity});
|
|
116
142
|
return integrity;
|
|
117
143
|
},
|
|
@@ -129,11 +155,8 @@ export default async (config, root, log) => {
|
|
|
129
155
|
},
|
|
130
156
|
modules,
|
|
131
157
|
};
|
|
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");
|
|
158
|
+
log.class.print(blue(bold(name)), blue(version),
|
|
159
|
+
`at http${app.secure ? "s" : ""}://${http.host}:${http.port}\n`);
|
|
137
160
|
// modules may load other modules
|
|
138
161
|
await Promise.all(app.modules
|
|
139
162
|
.filter(module => module.load !== undefined)
|
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);
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
ModuleHasNoHooks({hookless}) {
|
|
49
|
+
return {
|
|
50
|
+
message: ["module % has no hooks", hookless.join(", ")],
|
|
51
|
+
fix: ["ensure every module uses at least one hook or deactivate it"],
|
|
52
|
+
level: Logger.Warn,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
ModulesMustHaveNames({n}) {
|
|
56
|
+
return {
|
|
57
|
+
message: ["modules must have names"],
|
|
58
|
+
fix: ["update module at index % and inform maintainer", n],
|
|
59
|
+
level: Logger.Error,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
EmptyConfigFile({config}) {
|
|
63
|
+
return {
|
|
64
|
+
message: ["empty config file at %", config],
|
|
65
|
+
fix: ["add configuration options or remove file"],
|
|
66
|
+
level: Logger.Warn,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
NoFileForPath({pathname, config: {paths}}) {
|
|
70
|
+
return {
|
|
71
|
+
message: ["no file for %", pathname],
|
|
72
|
+
fix: ["if unintentional create a file at %%", paths.static, pathname],
|
|
73
|
+
level: Logger.Info,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
NoHandlerForExtension({name, ending}) {
|
|
77
|
+
return {
|
|
78
|
+
message: ["no handler for % extension", ending],
|
|
79
|
+
fix: ["add handler module for % files or remove %", `.${ending}`, name],
|
|
80
|
+
level: Logger.Error,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
NoRouteToPath({method, pathname, config: {paths}}) {
|
|
84
|
+
const route = `${paths.routes}/${pathname === "/" ? "index" : ""}.js`;
|
|
85
|
+
return {
|
|
86
|
+
message: ["no % route to %", method, pathname],
|
|
87
|
+
fix: ["if unintentional create a route at %", route],
|
|
88
|
+
level: Logger.Info,
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
InvalidPathParameter({named, path}) {
|
|
92
|
+
return {
|
|
93
|
+
message: ["invalid path parameter % in route %", named, path],
|
|
94
|
+
fix: ["use only latin letters and decimal digits in path parameters"],
|
|
95
|
+
level: Logger.Error,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
InvalidRouteName({path}) {
|
|
99
|
+
return {
|
|
100
|
+
message: ["invalid route name %", path],
|
|
101
|
+
fix: ["do not use dots in route names"],
|
|
102
|
+
level: Logger.Error,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
InvalidType({name}) {
|
|
106
|
+
return {
|
|
107
|
+
message: ["invalid type %", name],
|
|
108
|
+
fix: ["use only latin letters and decimal digits in types"],
|
|
109
|
+
level: Logger.Error,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
}).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/redirect.js
CHANGED
package/src/handlers/view.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import errors from "../errors.js";
|
|
2
|
+
|
|
1
3
|
export default (name, props, options) => async (app, headers) => {
|
|
2
4
|
const ending = name.slice(name.lastIndexOf(".") + 1);
|
|
3
5
|
const handler = app.handlers[ending];
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
return handler(name, {load: true, ...props}, options)(app, headers);
|
|
6
|
+
return handler?.(name, {load: true, ...props}, options)(app, headers)
|
|
7
|
+
?? errors.NoHandlerForExtension.throw({name, ending});
|
|
8
8
|
};
|
package/src/hooks/bundle.js
CHANGED
|
@@ -22,7 +22,7 @@ const pre = async app => {
|
|
|
22
22
|
export default async (app, bundle) => {
|
|
23
23
|
await pre(app);
|
|
24
24
|
if (bundle) {
|
|
25
|
-
app.log.info("running bundle hooks");
|
|
25
|
+
app.log.info("running bundle hooks", {module: "primate"});
|
|
26
26
|
await [...filter("bundle", app.modules), _ => _]
|
|
27
27
|
.reduceRight((acc, handler) => input => handler(input, acc))(app);
|
|
28
28
|
}
|
package/src/hooks/compile.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
2
2
|
|
|
3
3
|
export default async app => {
|
|
4
|
-
app.log.info("running compile hooks");
|
|
4
|
+
app.log.info("running compile hooks", {module: "primate"});
|
|
5
5
|
await [...filter("compile", app.modules), _ => _]
|
|
6
6
|
.reduceRight((acc, handler) => input => handler(input, acc))(app);
|
|
7
7
|
};
|
package/src/hooks/exports.js
CHANGED
package/src/hooks/handle.js
CHANGED
|
@@ -1,58 +1,37 @@
|
|
|
1
|
-
import {Response
|
|
1
|
+
import {Response} from "runtime-compat/http";
|
|
2
2
|
import {error as clientError} from "../handlers/exports.js";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import {mime, isResponse, respond} from "./handle/exports.js";
|
|
4
|
+
import {invalid} from "./route.js";
|
|
5
|
+
import errors from "../errors.js";
|
|
6
|
+
import {OK} from "../http-statuses.js";
|
|
5
7
|
|
|
6
8
|
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
"application/x-www-form-urlencoded": body =>
|
|
10
|
-
fromNull(Object.fromEntries(body.split("&").map(part => part.split("=")
|
|
11
|
-
.map(subpart => decodeURIComponent(subpart).replaceAll("+", " "))))),
|
|
12
|
-
"application/json": body => JSON.parse(body),
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export default async app => {
|
|
10
|
+
export default app => {
|
|
16
11
|
const {http} = app.config;
|
|
17
12
|
|
|
18
|
-
const _respond = async request => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.map(resource => `'${resource.integrity}'`).join(" ");
|
|
23
|
-
const _csp = scripts === "" ? csp : `${csp}script-src 'self' ${scripts};`;
|
|
24
|
-
// remove inline resources
|
|
25
|
-
for (let i = app.resources.length - 1; i >= 0; i--) {
|
|
26
|
-
const resource = app.resources[i];
|
|
27
|
-
if (resource.inline) {
|
|
28
|
-
app.resources.splice(i, 1);
|
|
29
|
-
}
|
|
13
|
+
const _respond = async (request, headers) => {
|
|
14
|
+
if (invalid(request.url.pathname)) {
|
|
15
|
+
errors.NoFileForPath.throw({pathname: request.url.pathname, config: app.config});
|
|
16
|
+
return;
|
|
30
17
|
}
|
|
18
|
+
return respond(await app.route(request))(app, headers);
|
|
19
|
+
};
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"Referrer-Policy": "same-origin",
|
|
35
|
-
};
|
|
21
|
+
const route = async request => {
|
|
22
|
+
const headers = app.generateHeaders();
|
|
36
23
|
|
|
37
24
|
try {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const handlers = [...modules, app.route].reduceRight((acc, handler) =>
|
|
41
|
-
input => handler(input, acc));
|
|
42
|
-
return await respond(await handlers(request))(app, headers);
|
|
25
|
+
const response = await _respond(request, headers);
|
|
26
|
+
return isResponse(response) ? response : new Response(...response);
|
|
43
27
|
} catch (error) {
|
|
44
28
|
app.log.auto(error);
|
|
45
|
-
return clientError()(app,
|
|
29
|
+
return new Response(...await clientError()(app, {}));
|
|
46
30
|
}
|
|
47
31
|
};
|
|
48
32
|
|
|
49
|
-
const route = async request => {
|
|
50
|
-
const response = await _respond(request);
|
|
51
|
-
return isResponse(response) ? response : new Response(...response);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
33
|
const staticResource = async file => new Response(file.readable, {
|
|
55
|
-
status:
|
|
34
|
+
status: OK,
|
|
56
35
|
headers: {
|
|
57
36
|
"Content-Type": mime(file.name),
|
|
58
37
|
Etag: await file.modified,
|
|
@@ -64,7 +43,7 @@ export default async app => {
|
|
|
64
43
|
!inline && src === request.url.pathname);
|
|
65
44
|
if (published !== undefined) {
|
|
66
45
|
return new Response(published.code, {
|
|
67
|
-
status:
|
|
46
|
+
status: OK,
|
|
68
47
|
headers: {
|
|
69
48
|
"Content-Type": mime(published.src),
|
|
70
49
|
Etag: published.integrity,
|
|
@@ -88,67 +67,9 @@ export default async app => {
|
|
|
88
67
|
};
|
|
89
68
|
|
|
90
69
|
const handle = async request => {
|
|
91
|
-
|
|
92
|
-
return await resource(request);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
app.log.auto(error);
|
|
95
|
-
return new Response(null, {status: statuses.InternalServerError});
|
|
96
|
-
}
|
|
70
|
+
return await resource(request);
|
|
97
71
|
};
|
|
98
72
|
|
|
99
|
-
|
|
100
|
-
const type = contents[contentType];
|
|
101
|
-
return type === undefined ? body : type(body);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const parseContent = (request, body) => {
|
|
105
|
-
try {
|
|
106
|
-
return parseContentType(request.headers.get("content-type"), body);
|
|
107
|
-
} catch (error) {
|
|
108
|
-
app.log.warn(error);
|
|
109
|
-
return body;
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const decoder = new TextDecoder();
|
|
114
|
-
|
|
115
|
-
const parseBody = async request => {
|
|
116
|
-
if (request.body === null) {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
const reader = request.body.getReader();
|
|
120
|
-
const chunks = [];
|
|
121
|
-
let result;
|
|
122
|
-
do {
|
|
123
|
-
result = await reader.read();
|
|
124
|
-
if (result.value !== undefined) {
|
|
125
|
-
chunks.push(decoder.decode(result.value));
|
|
126
|
-
}
|
|
127
|
-
} while (!result.done);
|
|
128
|
-
|
|
129
|
-
return parseContent(request, chunks.join());
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const parseRequest = async request => {
|
|
133
|
-
const cookies = request.headers.get("cookie");
|
|
134
|
-
const _url = request.url;
|
|
135
|
-
const url = new URL(_url.endsWith("/") ? _url.slice(0, -1) : _url);
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
original: request,
|
|
139
|
-
url,
|
|
140
|
-
body: await parseBody(request),
|
|
141
|
-
cookies: fromNull(cookies === null
|
|
142
|
-
? {}
|
|
143
|
-
: Object.fromEntries(cookies.split(";").map(c => c.trim().split("=")))),
|
|
144
|
-
headers: fromNull(Object.fromEntries(request.headers)),
|
|
145
|
-
query: fromNull(Object.fromEntries(url.searchParams)),
|
|
146
|
-
};
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
// handle is the last module to be executed
|
|
150
|
-
const handlers = [...filter("handle", app.modules), handle]
|
|
73
|
+
return [...filter("handle", app.modules), handle]
|
|
151
74
|
.reduceRight((acc, handler) => input => handler(input, acc));
|
|
152
|
-
|
|
153
|
-
return async request => handlers(await parseRequest(request));
|
|
154
75
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {URL} from "runtime-compat/http";
|
|
2
|
+
import fromNull from "../fromNull.js";
|
|
3
|
+
import errors from "../errors.js";
|
|
4
|
+
|
|
5
|
+
const contents = {
|
|
6
|
+
"application/x-www-form-urlencoded": body =>
|
|
7
|
+
fromNull(Object.fromEntries(body.split("&").map(part => part.split("=")
|
|
8
|
+
.map(subpart => decodeURIComponent(subpart).replaceAll("+", " "))))),
|
|
9
|
+
"application/json": body => JSON.parse(body),
|
|
10
|
+
};
|
|
11
|
+
const decoder = new TextDecoder();
|
|
12
|
+
|
|
13
|
+
export default async request => {
|
|
14
|
+
const parseContentType = (contentType, body) => {
|
|
15
|
+
const type = contents[contentType];
|
|
16
|
+
return type === undefined ? body : type(body);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const parseContent = async (request, body) => {
|
|
20
|
+
const contentType = request.headers.get("content-type");
|
|
21
|
+
try {
|
|
22
|
+
return parseContentType(contentType, body);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return errors.CannotParseBody.throw({body, contentType});
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const parseBody = async request => {
|
|
29
|
+
if (request.body === null) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const reader = request.body.getReader();
|
|
33
|
+
const chunks = [];
|
|
34
|
+
let result;
|
|
35
|
+
do {
|
|
36
|
+
result = await reader.read();
|
|
37
|
+
if (result.value !== undefined) {
|
|
38
|
+
chunks.push(decoder.decode(result.value));
|
|
39
|
+
}
|
|
40
|
+
} while (!result.done);
|
|
41
|
+
|
|
42
|
+
return parseContent(request, chunks.join());
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const cookies = request.headers.get("cookie");
|
|
46
|
+
const _url = request.url;
|
|
47
|
+
const url = new URL(_url.endsWith("/") ? _url.slice(0, -1) : _url);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
original: request,
|
|
51
|
+
url,
|
|
52
|
+
body: await parseBody(request),
|
|
53
|
+
cookies: fromNull(cookies === null
|
|
54
|
+
? {}
|
|
55
|
+
: Object.fromEntries(cookies.split(";").map(c => c.trim().split("=")))),
|
|
56
|
+
headers: fromNull(Object.fromEntries(request.headers)),
|
|
57
|
+
query: fromNull(Object.fromEntries(url.searchParams)),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
@@ -1,32 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import parse from "./parse.js";
|
|
2
2
|
import Logger from "../Logger.js";
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
log: new Logger({
|
|
6
|
-
level: Logger.Warn,
|
|
7
|
-
}),
|
|
8
|
-
config: {
|
|
9
|
-
http: {},
|
|
10
|
-
},
|
|
11
|
-
modules: [{
|
|
12
|
-
handle(request) {
|
|
13
|
-
return request;
|
|
14
|
-
},
|
|
15
|
-
}],
|
|
16
|
-
routes: [
|
|
17
|
-
["index", {get: () => "/"}],
|
|
18
|
-
["user", {get: () => "/user"}],
|
|
19
|
-
["users/{userId}a", {get: request => request}],
|
|
20
|
-
],
|
|
21
|
-
};
|
|
4
|
+
const {mark} = Logger;
|
|
22
5
|
|
|
23
6
|
const r = await (async () => {
|
|
24
7
|
const p = "https://p.com";
|
|
25
8
|
const request = (method, path = "/", options = {}) =>
|
|
26
9
|
new Request(`${p}${path}`, {method, ...options});
|
|
27
|
-
const handler = await handle(app);
|
|
28
10
|
return Object.fromEntries(["get", "post", "put", "delete"].map(verb =>
|
|
29
|
-
[verb, (...args) =>
|
|
11
|
+
[verb, (...args) => parse(request(verb.toUpperCase(), ...args))]));
|
|
30
12
|
})();
|
|
31
13
|
|
|
32
14
|
export default test => {
|
|
@@ -35,12 +17,14 @@ export default test => {
|
|
|
35
17
|
assert((await r.post("/")).body).null();
|
|
36
18
|
});
|
|
37
19
|
test.case("body is application/json", async assert => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
20
|
+
const body = JSON.stringify({foo: "bar"});
|
|
21
|
+
const contentType = "application/json";
|
|
22
|
+
const headers = {"Content-Type": contentType};
|
|
23
|
+
assert((await r.post("/", {body, headers})).body).equals({foo: "bar"});
|
|
24
|
+
|
|
25
|
+
const faulty = `${body}%`;
|
|
26
|
+
assert(() => r.post("/", {body: faulty, headers}))
|
|
27
|
+
.throws(mark("cannot parse body % as %", faulty, contentType));
|
|
44
28
|
});
|
|
45
29
|
test.case("body is application/x-www-form-urlencoded", async assert => {
|
|
46
30
|
assert((await r.post("/", {
|
package/src/hooks/publish.js
CHANGED
|
@@ -29,7 +29,7 @@ const post = async app => {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
export default async app => {
|
|
32
|
-
app.log.info("running publish hooks");
|
|
32
|
+
app.log.info("running publish hooks", {module: "primate"});
|
|
33
33
|
await [...filter("publish", app.modules), _ => _]
|
|
34
34
|
.reduceRight((acc, handler) => input => handler(input, acc))(app);
|
|
35
35
|
await post(app);
|
package/src/hooks/register.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
2
2
|
|
|
3
3
|
export default async app => {
|
|
4
|
-
app.log.info("running register hooks");
|
|
4
|
+
app.log.info("running register hooks", {module: "primate"});
|
|
5
5
|
await [...filter("register", app.modules), _ => _]
|
|
6
6
|
.reduceRight((acc, handler) => input => handler(input, acc))(app);
|
|
7
7
|
};
|
package/src/hooks/route.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import errors from "../errors.js";
|
|
2
|
+
|
|
3
|
+
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
2
4
|
|
|
3
5
|
// insensitive-case equal
|
|
4
6
|
const ieq = (left, right) => left.toLowerCase() === right.toLowerCase();
|
|
@@ -7,11 +9,20 @@ const verbs = [
|
|
|
7
9
|
// CRUD
|
|
8
10
|
"post", "get", "put", "delete",
|
|
9
11
|
// extended
|
|
10
|
-
"connect", "options", "trace", "patch",
|
|
12
|
+
"connect", "options", "trace", "patch", "head",
|
|
11
13
|
];
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
/* routes may not contain dots */
|
|
16
|
+
export const invalid = route => /\./u.test(route);
|
|
17
|
+
const toRoute = path => {
|
|
18
|
+
const double = path.split("/")
|
|
19
|
+
.filter(part => part.startsWith("{") && part.endsWith("}"))
|
|
20
|
+
.map(part => part.slice(1, part.indexOf(":")))
|
|
21
|
+
.find((part, i, array) =>
|
|
22
|
+
array.filter((_, j) => i !== j).includes(part));
|
|
23
|
+
double && errors.DoublePathParameter.throw({path, double});
|
|
24
|
+
|
|
25
|
+
const route = path
|
|
15
26
|
// transform /index -> ""
|
|
16
27
|
.replace("/index", "")
|
|
17
28
|
// transform index -> ""
|
|
@@ -23,28 +34,32 @@ const toRoute = file => {
|
|
|
23
34
|
const param = type === undefined ? name : `${name}$${type.slice(1)}`;
|
|
24
35
|
return `(?<${param}>[^/]{1,}?)`;
|
|
25
36
|
} catch (error) {
|
|
26
|
-
|
|
37
|
+
return errors.InvalidPathParameter.throw({named, path});
|
|
27
38
|
}
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
abort("same parameter twice");
|
|
34
|
-
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
invalid(route) && errors.InvalidRouteName.throw({path});
|
|
42
|
+
|
|
43
|
+
return new RegExp(`^/${route}$`, "u");
|
|
35
44
|
};
|
|
36
45
|
|
|
37
46
|
const reentry = (object, mapper) =>
|
|
38
47
|
Object.fromEntries(mapper(Object.entries(object ?? {})));
|
|
39
48
|
|
|
40
49
|
export default app => {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
const double = app.routes
|
|
51
|
+
.map(([route]) => route
|
|
52
|
+
.replaceAll("/index", "")
|
|
53
|
+
.replaceAll(/\{(?<name>\w*)(?<_>:\w+)?\}?/gu, (_, name) => `{${name}}`))
|
|
54
|
+
.find((part, i, array) =>
|
|
55
|
+
array.filter((_, j) => i !== j).includes(part));
|
|
56
|
+
|
|
57
|
+
double && errors.DoubleRoute.throw({double});
|
|
58
|
+
|
|
44
59
|
const routes = app.routes
|
|
45
60
|
.map(([route, imported]) => {
|
|
46
|
-
if (imported === undefined) {
|
|
47
|
-
|
|
61
|
+
if (imported === undefined || Object.keys(imported).length === 0) {
|
|
62
|
+
errors.EmptyRouteFile.warn(app.log, {config: app.config, route});
|
|
48
63
|
return [];
|
|
49
64
|
}
|
|
50
65
|
|
|
@@ -53,10 +68,10 @@ export default app => {
|
|
|
53
68
|
.filter(([verb]) => verbs.includes(verb))
|
|
54
69
|
.map(([method, handler]) => ({method, handler, path}));
|
|
55
70
|
}).flat();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
|
|
72
|
+
const {types = {}} = app;
|
|
73
|
+
Object.entries(types).every(([name]) => /^(?:\w*)$/u.test(name) ||
|
|
74
|
+
errors.InvalidType.throw({name}));
|
|
60
75
|
|
|
61
76
|
const isType = groups => Object
|
|
62
77
|
.entries(groups ?? {})
|
|
@@ -74,15 +89,19 @@ export default app => {
|
|
|
74
89
|
&& isPath({route, path});
|
|
75
90
|
const find = (method, path) => routes.find(route =>
|
|
76
91
|
isMethod({route, method, path}));
|
|
92
|
+
const modules = filter("route", app.modules);
|
|
77
93
|
|
|
78
94
|
return request => {
|
|
79
95
|
const {original: {method}, url: {pathname}} = request;
|
|
80
|
-
const verb = find(method, pathname) ??
|
|
81
|
-
throw
|
|
82
|
-
})();
|
|
96
|
+
const verb = find(method, pathname) ??
|
|
97
|
+
errors.NoRouteToPath.throw({method, pathname, config: app.config});
|
|
83
98
|
const path = reentry(verb.path?.exec(pathname).groups,
|
|
84
99
|
object => object.map(([key, value]) => [key.split("$")[0], value]));
|
|
85
100
|
|
|
86
|
-
|
|
101
|
+
// verb.handler is the last module to be executed
|
|
102
|
+
const handlers = [...modules, verb.handler].reduceRight((acc, handler) =>
|
|
103
|
+
input => handler(input, acc));
|
|
104
|
+
|
|
105
|
+
return handlers({...request, path});
|
|
87
106
|
};
|
|
88
107
|
};
|
package/src/hooks/route.spec.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import Logger from "../Logger.js";
|
|
1
2
|
import route from "./route.js";
|
|
2
3
|
|
|
4
|
+
const {mark} = Logger;
|
|
5
|
+
|
|
3
6
|
const app = {
|
|
7
|
+
config: {
|
|
8
|
+
paths: {
|
|
9
|
+
routes: "/routes",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
4
12
|
routes: [
|
|
5
13
|
"index",
|
|
6
14
|
"user",
|
|
@@ -36,9 +44,10 @@ export default test => {
|
|
|
36
44
|
const r = pathname => {
|
|
37
45
|
const original = new Request(`${p}${pathname}`, {method: "GET"});
|
|
38
46
|
const {url} = original;
|
|
47
|
+
const end = -1;
|
|
39
48
|
return router({
|
|
40
49
|
original,
|
|
41
|
-
url: new URL(url.endsWith("/") ? url.slice(0,
|
|
50
|
+
url: new URL(url.endsWith("/") ? url.slice(0, end) : url),
|
|
42
51
|
});
|
|
43
52
|
};
|
|
44
53
|
|
|
@@ -46,29 +55,58 @@ export default test => {
|
|
|
46
55
|
match: (url, result) => {
|
|
47
56
|
assert(r(url).url.pathname).equals(result ?? url);
|
|
48
57
|
},
|
|
49
|
-
fail: (url, result) =>
|
|
50
|
-
|
|
58
|
+
fail: (url, result) => {
|
|
59
|
+
const throws = mark("no % route to %", "GET", result ?? url);
|
|
60
|
+
assert(() => r(url)).throws(throws);
|
|
61
|
+
},
|
|
51
62
|
path: (url, result) => assert(r(url).path).equals(result),
|
|
52
63
|
assert,
|
|
53
64
|
}));
|
|
54
65
|
|
|
55
66
|
const get = () => null;
|
|
56
|
-
/*
|
|
57
|
-
test.case("
|
|
67
|
+
/* errors {{{ */
|
|
68
|
+
test.case("error DoubleRouted", ({assert}) => {
|
|
58
69
|
const post = ["post", {get}];
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const throws = mark("double route %", "post");
|
|
71
|
+
assert(() => route({routes: [post, post]})).throws(throws);
|
|
72
|
+
});
|
|
73
|
+
test.case("error DoublePathParameter", ({assert}) => {
|
|
74
|
+
const path = "{user}/{user}";
|
|
75
|
+
const throws = mark("double path parameter % in route %", "user", path);
|
|
76
|
+
assert(() => route({routes: [[path, {get}]]})).throws(throws);
|
|
77
|
+
});
|
|
78
|
+
test.case("error EmptyRoutefile", ({assert}) => {
|
|
79
|
+
const path = "user";
|
|
80
|
+
const throws = mark("empty route file at %", `/routes/${path}.js`);
|
|
81
|
+
const base = {
|
|
82
|
+
log: {
|
|
83
|
+
auto(error) {
|
|
84
|
+
throw error;
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
config: {
|
|
88
|
+
paths: {
|
|
89
|
+
routes: "/routes",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
assert(() => route({...base, routes: [[path, undefined]]})).throws(throws);
|
|
94
|
+
assert(() => route({...base, routes: [[path, {}]]})).throws(throws);
|
|
95
|
+
});
|
|
96
|
+
test.case("error InvalidRouteName", ({assert}) => {
|
|
97
|
+
const post = ["po.st", {get}];
|
|
98
|
+
const throws = mark("invalid route name %", "po.st");
|
|
99
|
+
assert(() => route({routes: [post], types: {}})).throws(throws);
|
|
100
|
+
});
|
|
101
|
+
test.case("error InvalidParameter", ({assert}) => {
|
|
102
|
+
const path = "{us$er}";
|
|
103
|
+
const throws = mark("invalid path parameter % in route %", "us$er", path);
|
|
104
|
+
assert(() => route({routes: [[path, {get}]]})).throws(throws);
|
|
105
|
+
});
|
|
106
|
+
test.case("error InvalidType", ({assert}) => {
|
|
107
|
+
const throws = mark("invalid type %", "us$er");
|
|
108
|
+
const types = {us$er: () => false};
|
|
109
|
+
assert(() => route({routes: [], types})).throws(throws);
|
|
72
110
|
});
|
|
73
111
|
/* }}} */
|
|
74
112
|
|
package/src/run.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {Path} from "runtime-compat/fs";
|
|
2
2
|
import app from "./app.js";
|
|
3
|
+
import {default as Logger, bye} from "./Logger.js";
|
|
4
|
+
import extend from "./extend.js";
|
|
5
|
+
import errors from "./errors.js";
|
|
3
6
|
import command from "./commands/exports.js";
|
|
4
|
-
import {Abort, colors, print, default as Logger} from "./Logger.js";
|
|
5
7
|
import defaults from "./defaults/primate.config.js";
|
|
6
|
-
import extend from "./extend.js";
|
|
7
8
|
|
|
8
9
|
const getRoot = async () => {
|
|
9
10
|
try {
|
|
@@ -15,19 +16,21 @@ const getRoot = async () => {
|
|
|
15
16
|
}
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
const
|
|
19
|
+
const protologger = new Logger({level: Logger.Warn});
|
|
20
|
+
|
|
19
21
|
const getConfig = async root => {
|
|
20
|
-
const
|
|
22
|
+
const name = "primate.config.js";
|
|
23
|
+
const config = root.join(name);
|
|
21
24
|
if (await config.exists) {
|
|
22
25
|
try {
|
|
23
|
-
const imported = await import(config);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
throw
|
|
26
|
+
const imported = (await import(config)).default;
|
|
27
|
+
|
|
28
|
+
(imported === undefined || Object.keys(imported).length === 0) &&
|
|
29
|
+
errors.EmptyConfigFile.warn(protologger, {config});
|
|
30
|
+
|
|
31
|
+
return extend(defaults, imported);
|
|
32
|
+
} catch ({message}) {
|
|
33
|
+
return errors.ErrorInConfigFile.throw({message, config});
|
|
31
34
|
}
|
|
32
35
|
} else {
|
|
33
36
|
return defaults;
|
|
@@ -36,11 +39,16 @@ const getConfig = async root => {
|
|
|
36
39
|
|
|
37
40
|
export default async name => {
|
|
38
41
|
const root = await getRoot();
|
|
39
|
-
|
|
42
|
+
let logger = protologger;
|
|
40
43
|
try {
|
|
41
|
-
|
|
44
|
+
const config = await getConfig(root);
|
|
45
|
+
logger = new Logger(config.logger);
|
|
46
|
+
await command(name)(await app(config, root, new Logger(config.logger)));
|
|
42
47
|
} catch (error) {
|
|
43
|
-
if (error
|
|
48
|
+
if (error.level === Logger.Error) {
|
|
49
|
+
logger.auto(error);
|
|
50
|
+
bye();
|
|
51
|
+
} else {
|
|
44
52
|
throw error;
|
|
45
53
|
}
|
|
46
54
|
}
|
package/src/start.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {serve} from "runtime-compat/http";
|
|
2
|
-
import {
|
|
1
|
+
import {serve, Response} from "runtime-compat/http";
|
|
2
|
+
import {InternalServerError} from "./http-statuses.js";
|
|
3
|
+
import {register, compile, publish, bundle, route, handle, parse}
|
|
3
4
|
from "./hooks/exports.js";
|
|
4
5
|
|
|
5
6
|
export default async (app, operations = {}) => {
|
|
@@ -16,6 +17,15 @@ export default async (app, operations = {}) => {
|
|
|
16
17
|
// bundle client-side code
|
|
17
18
|
await bundle(app, operations?.bundle);
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
const _route = route(app);
|
|
21
|
+
|
|
22
|
+
serve(async request => {
|
|
23
|
+
try {
|
|
24
|
+
// parse, handle
|
|
25
|
+
return await handle({...app, route: _route})(await parse(request));
|
|
26
|
+
} catch(error) {
|
|
27
|
+
app.log.auto(error);
|
|
28
|
+
return new Response(null, {status: InternalServerError});
|
|
29
|
+
}
|
|
30
|
+
}, app.config.http);
|
|
21
31
|
};
|
package/src/commands/build.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {default} from "./serve.js";
|
package/src/commands/create.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import {Path} from "runtime-compat/fs";
|
|
2
|
-
|
|
3
|
-
const createModule = async app => {
|
|
4
|
-
const space = 2;
|
|
5
|
-
try {
|
|
6
|
-
// will throw if cannot find a package.json up the filesystem hierarchy
|
|
7
|
-
await Path.root();
|
|
8
|
-
} catch (error) {
|
|
9
|
-
const rootConfig = JSON.stringify({
|
|
10
|
-
name: "primate-app",
|
|
11
|
-
private: true,
|
|
12
|
-
dependencies: {
|
|
13
|
-
primate: `^${app.version}`,
|
|
14
|
-
},
|
|
15
|
-
scripts: {
|
|
16
|
-
start: "npx primate",
|
|
17
|
-
dev: "npx primate dev",
|
|
18
|
-
serve: "npx primate serve",
|
|
19
|
-
},
|
|
20
|
-
type: "module",
|
|
21
|
-
}, null, space);
|
|
22
|
-
await Path.resolve().join("package.json").file.write(rootConfig);
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const createConfig = async app => {
|
|
27
|
-
const name = "primate.config.js";
|
|
28
|
-
const template = "export default {};";
|
|
29
|
-
const root = (await Path.root()).join(name);
|
|
30
|
-
if (await root.exists) {
|
|
31
|
-
app.log.warn(`${root} already exists`);
|
|
32
|
-
} else {
|
|
33
|
-
await root.file.write(template);
|
|
34
|
-
app.log.info(`created config at ${root}`);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export default async app => {
|
|
39
|
-
await createModule(app);
|
|
40
|
-
await createConfig(app);
|
|
41
|
-
};
|
|
42
|
-
|
package/src/commands/help.js
DELETED