primate 0.19.4 → 0.20.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 +48 -64
- package/src/app.js +86 -121
- package/src/bin.js +0 -1
- package/src/defaults/primate.config.js +12 -5
- package/src/dispatch.js +8 -13
- package/src/errors.js +6 -146
- package/src/exports.js +1 -1
- package/src/handlers/error.js +4 -4
- package/src/handlers/html.js +5 -6
- package/src/handlers/json.js +2 -2
- package/src/handlers/redirect.js +3 -3
- package/src/handlers/stream.js +2 -2
- package/src/handlers/text.js +2 -2
- package/src/handlers/view.js +3 -3
- package/src/hooks/bundle.js +4 -15
- package/src/hooks/compile.js +21 -2
- package/src/hooks/copy_includes.js +24 -0
- package/src/hooks/handle.js +55 -42
- package/src/hooks/parse.js +13 -16
- package/src/hooks/publish.js +42 -23
- package/src/hooks/register.js +1 -3
- package/src/hooks/{handle → respond}/respond.js +1 -1
- package/src/hooks/route.js +25 -85
- package/src/loaders/common.js +34 -0
- package/src/loaders/exports.js +3 -0
- package/src/loaders/modules.js +40 -0
- package/src/loaders/routes/exports.js +3 -0
- package/src/loaders/routes/guards.js +3 -0
- package/src/loaders/routes/layouts.js +3 -0
- package/src/loaders/routes/load.js +17 -0
- package/src/loaders/routes/routes.js +22 -0
- package/src/loaders/routes.js +45 -0
- package/src/loaders/types.js +19 -0
- package/src/run.js +13 -24
- package/src/start.js +11 -15
- package/src/defaults/index.html +0 -9
- package/src/extend.js +0 -10
- package/src/http-statuses.js +0 -4
- /package/src/hooks/{handle → respond}/duck.js +0 -0
- /package/src/hooks/{handle → respond}/exports.js +0 -0
- /package/src/hooks/{handle → respond}/mime.js +0 -0
package/src/errors.js
CHANGED
|
@@ -1,148 +1,8 @@
|
|
|
1
|
+
import {Path} from "runtime-compat/fs";
|
|
1
2
|
import Logger from "./Logger.js";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
EmptyTypeDirectory({root}) {
|
|
42
|
-
return {
|
|
43
|
-
message: ["empty type directory"],
|
|
44
|
-
fix: ["populate % with types or remove it", root],
|
|
45
|
-
level: Logger.Warn,
|
|
46
|
-
};
|
|
47
|
-
},
|
|
48
|
-
ErrorInConfigFile({config, message}) {
|
|
49
|
-
return {
|
|
50
|
-
message: ["error in config %", message],
|
|
51
|
-
fix: ["check errors in config file by running %", `node ${config}`],
|
|
52
|
-
level: Logger.Error,
|
|
53
|
-
};
|
|
54
|
-
},
|
|
55
|
-
InvalidPathParameter({named, path}) {
|
|
56
|
-
return {
|
|
57
|
-
message: ["invalid path parameter % in route %", named, path],
|
|
58
|
-
fix: ["use only latin letters and decimal digits in path parameters"],
|
|
59
|
-
level: Logger.Error,
|
|
60
|
-
};
|
|
61
|
-
},
|
|
62
|
-
InvalidRouteName({path}) {
|
|
63
|
-
return {
|
|
64
|
-
message: ["invalid route name %", path],
|
|
65
|
-
fix: ["do not use dots in route names"],
|
|
66
|
-
level: Logger.Error,
|
|
67
|
-
};
|
|
68
|
-
},
|
|
69
|
-
InvalidType({name}) {
|
|
70
|
-
return {
|
|
71
|
-
message: ["invalid type %", name],
|
|
72
|
-
fix: ["use only functions for the default export of types"],
|
|
73
|
-
level: Logger.Error,
|
|
74
|
-
};
|
|
75
|
-
},
|
|
76
|
-
InvalidTypeName({name}) {
|
|
77
|
-
return {
|
|
78
|
-
message: ["invalid type name %", name],
|
|
79
|
-
fix: ["use only latin letters and decimal digits in types"],
|
|
80
|
-
level: Logger.Error,
|
|
81
|
-
};
|
|
82
|
-
},
|
|
83
|
-
MismatchedPath({path, message}) {
|
|
84
|
-
return {
|
|
85
|
-
message: [`mismatched % path: ${message}`, path],
|
|
86
|
-
fix: ["if unintentional, fix the type or the caller"],
|
|
87
|
-
level: Logger.Info,
|
|
88
|
-
};
|
|
89
|
-
},
|
|
90
|
-
MismatchedType({message}) {
|
|
91
|
-
return {
|
|
92
|
-
message: [`mismatched type: ${message}`],
|
|
93
|
-
fix: ["if unintentional, fix the type or the caller"],
|
|
94
|
-
level: Logger.Info,
|
|
95
|
-
};
|
|
96
|
-
},
|
|
97
|
-
ModuleHasNoHooks({hookless}) {
|
|
98
|
-
const modules = hookless.map(({name}) => name).join(", ");
|
|
99
|
-
return {
|
|
100
|
-
message: ["module % has no hooks", modules],
|
|
101
|
-
fix: ["ensure every module uses at least one hook or deactivate it"],
|
|
102
|
-
level: Logger.Warn,
|
|
103
|
-
};
|
|
104
|
-
},
|
|
105
|
-
ModulesMustHaveNames({n}) {
|
|
106
|
-
return {
|
|
107
|
-
message: ["modules must have names"],
|
|
108
|
-
fix: ["update module at index % and inform maintainer", n],
|
|
109
|
-
level: Logger.Error,
|
|
110
|
-
};
|
|
111
|
-
},
|
|
112
|
-
EmptyConfigFile({config}) {
|
|
113
|
-
return {
|
|
114
|
-
message: ["empty config file at %", config],
|
|
115
|
-
fix: ["add configuration options or remove file"],
|
|
116
|
-
level: Logger.Warn,
|
|
117
|
-
};
|
|
118
|
-
},
|
|
119
|
-
NoFileForPath({pathname, config: {paths}}) {
|
|
120
|
-
return {
|
|
121
|
-
message: ["no file for %", pathname],
|
|
122
|
-
fix: ["if unintentional create a file at %%", paths.static, pathname],
|
|
123
|
-
level: Logger.Info,
|
|
124
|
-
};
|
|
125
|
-
},
|
|
126
|
-
NoHandlerForExtension({name, ending}) {
|
|
127
|
-
return {
|
|
128
|
-
message: ["no handler for % extension", ending],
|
|
129
|
-
fix: ["add handler module for % files or remove %", `.${ending}`, name],
|
|
130
|
-
level: Logger.Error,
|
|
131
|
-
};
|
|
132
|
-
},
|
|
133
|
-
NoRouteToPath({method, pathname, config: {paths}}) {
|
|
134
|
-
const route = `${paths.routes}${pathname === "" ? "index" : pathname}.js`;
|
|
135
|
-
return {
|
|
136
|
-
message: ["no % route to %", method, pathname],
|
|
137
|
-
fix: ["if unintentional create a route at %", route],
|
|
138
|
-
level: Logger.Info,
|
|
139
|
-
};
|
|
140
|
-
},
|
|
141
|
-
ReservedTypeName({name}) {
|
|
142
|
-
return {
|
|
143
|
-
message: ["type name % is reserved", name],
|
|
144
|
-
fix: ["do not use any reserved type names"],
|
|
145
|
-
level: Logger.Error,
|
|
146
|
-
};
|
|
147
|
-
},
|
|
148
|
-
}).map(([name, error]) => [name, Logger.throwable(error, name, "primate")]));
|
|
4
|
+
const json = await new Path(import.meta.url).up(1).join("errors.json").json();
|
|
5
|
+
|
|
6
|
+
const errors = Logger.err(json.errors, json.module);
|
|
7
|
+
|
|
8
|
+
export default errors;
|
package/src/exports.js
CHANGED
package/src/handlers/error.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Status} from "runtime-compat/http";
|
|
2
2
|
|
|
3
|
-
export default (body = "Not Found", {status = NotFound} = {}) =>
|
|
4
|
-
async
|
|
3
|
+
export default (body = "Not Found", {status = Status.NotFound} = {}) =>
|
|
4
|
+
async app => [
|
|
5
5
|
await app.render({body}), {
|
|
6
6
|
status,
|
|
7
|
-
headers: {...headers, "Content-Type": "text/html"},
|
|
7
|
+
headers: {...app.headers(), "Content-Type": "text/html"},
|
|
8
8
|
},
|
|
9
9
|
];
|
package/src/handlers/html.js
CHANGED
|
@@ -14,20 +14,19 @@ const integrate = async (html, publish, headers) => {
|
|
|
14
14
|
headers["Content-Security-Policy"] = headers["Content-Security-Policy"]
|
|
15
15
|
.replace("style-src 'self'", `style-src 'self' '${integrity}' `);
|
|
16
16
|
}
|
|
17
|
-
return html
|
|
18
|
-
.replaceAll(/<script>.*?<\/script>/gus, () => "")
|
|
19
|
-
.replaceAll(/<style>.*?<\/style>/gus, () => "");
|
|
17
|
+
return html.replaceAll(/<(?<tag>script|style)>.*?<\/\k<tag>>/gus, _ => "");
|
|
20
18
|
};
|
|
21
19
|
|
|
22
20
|
export default (component, options = {}) => {
|
|
23
|
-
const {status = 200, partial = false, load = false
|
|
21
|
+
const {status = 200, partial = false, load = false} = options;
|
|
24
22
|
|
|
25
|
-
return async
|
|
23
|
+
return async app => {
|
|
24
|
+
const headers = app.headers();
|
|
26
25
|
const body = await integrate(await load ?
|
|
27
26
|
await app.paths.components.join(component).text() : component,
|
|
28
27
|
app.publish, headers);
|
|
29
28
|
|
|
30
|
-
return [partial ? body : await app.render({body
|
|
29
|
+
return [partial ? body : await app.render({body}), {
|
|
31
30
|
status,
|
|
32
31
|
headers: {...headers, "Content-Type": "text/html"},
|
|
33
32
|
}];
|
package/src/handlers/json.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export default (body, {status = 200} = {}) =>
|
|
1
|
+
export default (body, {status = 200} = {}) => app => [
|
|
2
2
|
JSON.stringify(body), {
|
|
3
3
|
status,
|
|
4
|
-
headers: {...headers, "Content-Type": "application/json"},
|
|
4
|
+
headers: {...app.headers(), "Content-Type": "application/json"},
|
|
5
5
|
},
|
|
6
6
|
];
|
package/src/handlers/redirect.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Status} from "runtime-compat/http";
|
|
2
2
|
|
|
3
|
-
export default (Location, {status = Found} = {}) =>
|
|
3
|
+
export default (Location, {status = Status.Found} = {}) => app => [
|
|
4
4
|
/* no body */
|
|
5
5
|
null, {
|
|
6
6
|
status,
|
|
7
|
-
headers: {...headers, Location},
|
|
7
|
+
headers: {...app.headers(), Location},
|
|
8
8
|
},
|
|
9
9
|
];
|
package/src/handlers/stream.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export default (body, {status = 200} = {}) =>
|
|
1
|
+
export default (body, {status = 200} = {}) => app => [
|
|
2
2
|
body, {
|
|
3
3
|
status,
|
|
4
|
-
headers: {...headers, "Content-Type": "application/octet-stream"},
|
|
4
|
+
headers: {...app.headers(), "Content-Type": "application/octet-stream"},
|
|
5
5
|
},
|
|
6
6
|
];
|
package/src/handlers/text.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export default (body, {status = 200} = {}) =>
|
|
1
|
+
export default (body, {status = 200} = {}) => app => [
|
|
2
2
|
body, {
|
|
3
3
|
status,
|
|
4
|
-
headers: {...headers, "Content-Type": "text/plain"},
|
|
4
|
+
headers: {...app.headers(), "Content-Type": "text/plain"},
|
|
5
5
|
},
|
|
6
6
|
];
|
package/src/handlers/view.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import errors from "../errors.js";
|
|
2
2
|
|
|
3
|
-
export default (name, props, options) => async (app,
|
|
3
|
+
export default (name, props, options) => async (app, ...rest) => {
|
|
4
4
|
const ending = name.slice(name.lastIndexOf(".") + 1);
|
|
5
5
|
const handler = app.handlers[ending];
|
|
6
|
-
return handler?.(name, {load: true, ...props}, options)(app,
|
|
7
|
-
?? errors.NoHandlerForExtension.throw(
|
|
6
|
+
return handler?.(name, {load: true, ...props}, options)(app, ...rest)
|
|
7
|
+
?? errors.NoHandlerForExtension.throw(ending, name);
|
|
8
8
|
};
|
package/src/hooks/bundle.js
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
import {File} from "runtime-compat/fs";
|
|
2
|
-
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
3
2
|
|
|
4
3
|
const pre = async app => {
|
|
5
|
-
const {paths} = app;
|
|
6
|
-
|
|
7
|
-
// remove public directory in case exists
|
|
8
|
-
if (await paths.public.exists) {
|
|
9
|
-
await paths.public.file.remove();
|
|
10
|
-
}
|
|
11
|
-
await paths.public.file.create();
|
|
12
|
-
|
|
4
|
+
const {paths, config} = app;
|
|
13
5
|
if (await paths.static.exists) {
|
|
14
|
-
// copy static files to
|
|
15
|
-
|
|
16
|
-
? true
|
|
17
|
-
: !file.endsWith(".js") && !file.endsWith(".css");
|
|
18
|
-
await File.copy(paths.static, paths.public, filter);
|
|
6
|
+
// copy static files to build/client/_static
|
|
7
|
+
await File.copy(paths.static, paths.client.join(config.build.static));
|
|
19
8
|
}
|
|
20
9
|
};
|
|
21
10
|
|
|
@@ -23,7 +12,7 @@ export default async (app, bundle) => {
|
|
|
23
12
|
await pre(app);
|
|
24
13
|
if (bundle) {
|
|
25
14
|
app.log.info("running bundle hooks", {module: "primate"});
|
|
26
|
-
await [...
|
|
15
|
+
await [...app.modules.bundle, _ => _]
|
|
27
16
|
.reduceRight((acc, handler) => input => handler(input, acc))(app);
|
|
28
17
|
}
|
|
29
18
|
};
|
package/src/hooks/compile.js
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
import copy_includes from "./copy_includes.js"
|
|
2
|
+
|
|
3
|
+
const pre = async app => {
|
|
4
|
+
const {paths, config} = app;
|
|
5
|
+
const build = config.build;
|
|
6
|
+
|
|
7
|
+
// remove build directory in case exists
|
|
8
|
+
if (await paths.build.exists) {
|
|
9
|
+
await paths.build.file.remove();
|
|
10
|
+
}
|
|
11
|
+
await paths.server.file.create();
|
|
12
|
+
|
|
13
|
+
if (await paths.components.exists) {
|
|
14
|
+
await app.copy(paths.components, paths.server.join(build.app));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// copy additional subdirectories to build/server
|
|
18
|
+
await copy_includes(app, "server");
|
|
19
|
+
};
|
|
2
20
|
|
|
3
21
|
export default async app => {
|
|
22
|
+
await pre(app);
|
|
4
23
|
app.log.info("running compile hooks", {module: "primate"});
|
|
5
|
-
await [...
|
|
24
|
+
await [...app.modules.compile, _ => _]
|
|
6
25
|
.reduceRight((acc, handler) => input => handler(input, acc))(app);
|
|
7
26
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const system = ["routes", "components", "build"];
|
|
2
|
+
|
|
3
|
+
export default async (app, type, post = () => undefined) => {
|
|
4
|
+
const {paths, config} = app;
|
|
5
|
+
const {build} = config;
|
|
6
|
+
const {includes} = build;
|
|
7
|
+
|
|
8
|
+
const reserved = system.concat(build.static, build.app, build.modules);
|
|
9
|
+
|
|
10
|
+
if (Array.isArray(includes)) {
|
|
11
|
+
await Promise.all(includes
|
|
12
|
+
.filter(include => !reserved.includes(include))
|
|
13
|
+
.filter(include => /^[^/]*$/u.test(include))
|
|
14
|
+
.map(async include => {
|
|
15
|
+
const path = app.root.join(include);
|
|
16
|
+
if (await path.exists) {
|
|
17
|
+
const to = paths[type].join(include);
|
|
18
|
+
await to.file.create();
|
|
19
|
+
await app.copy(path, to);
|
|
20
|
+
await post(to);
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
};
|
package/src/hooks/handle.js
CHANGED
|
@@ -1,70 +1,83 @@
|
|
|
1
|
-
import {Response} from "runtime-compat/http";
|
|
2
|
-
import {
|
|
3
|
-
import {mime, isResponse, respond} from "./
|
|
1
|
+
import {Response, Status} from "runtime-compat/http";
|
|
2
|
+
import {tryreturn} from "runtime-compat/flow";
|
|
3
|
+
import {mime, isResponse, respond} from "./respond/exports.js";
|
|
4
4
|
import {invalid} from "./route.js";
|
|
5
|
+
import {error as clientError} from "../handlers/exports.js";
|
|
5
6
|
import errors from "../errors.js";
|
|
6
|
-
import {OK} from "../http-statuses.js";
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const guardError = Symbol("guardError");
|
|
9
9
|
|
|
10
10
|
export default app => {
|
|
11
|
-
const {http} = app
|
|
11
|
+
const {config: {http, build}, paths} = app;
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const run = async request => {
|
|
14
14
|
const {pathname} = request.url;
|
|
15
|
-
|
|
16
|
-
? errors.NoFileForPath.throw(
|
|
17
|
-
:
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const route = async request => {
|
|
21
|
-
const headers = app.generateHeaders();
|
|
15
|
+
const {path, guards, layouts, handler} = invalid(pathname)
|
|
16
|
+
? errors.NoFileForPath.throw(pathname, paths.static)
|
|
17
|
+
: await app.route(request);
|
|
22
18
|
|
|
19
|
+
// handle guards
|
|
23
20
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
guards.map(guard => {
|
|
22
|
+
const result = guard(request);
|
|
23
|
+
if (result === true) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const error = new Error();
|
|
27
|
+
error.result = result;
|
|
28
|
+
error.type = guardError;
|
|
29
|
+
throw error;
|
|
30
|
+
});
|
|
26
31
|
} catch (error) {
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
if (error.type === guardError) {
|
|
33
|
+
return (await respond(error.result))(app);
|
|
34
|
+
}
|
|
35
|
+
// rethrow if not guard error
|
|
36
|
+
throw error;
|
|
29
37
|
}
|
|
38
|
+
|
|
39
|
+
// handle request
|
|
40
|
+
const handlers = [...app.modules.route, handler]
|
|
41
|
+
.reduceRight((chain, next) => input => next(input, chain));
|
|
42
|
+
|
|
43
|
+
return (await respond(await handlers({...request, path})))(app, {
|
|
44
|
+
layouts: await Promise.all(layouts.map(layout => layout(request))),
|
|
45
|
+
});
|
|
30
46
|
};
|
|
31
47
|
|
|
32
|
-
const
|
|
33
|
-
|
|
48
|
+
const route = async request =>
|
|
49
|
+
tryreturn(async _ => {
|
|
50
|
+
const response = await run(request);
|
|
51
|
+
return isResponse(response) ? response : new Response(...response);
|
|
52
|
+
}).orelse(async error => {
|
|
53
|
+
app.log.auto(error);
|
|
54
|
+
return new Response(...await clientError()(app, {}));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const asset = async file => new Response(file.readable, {
|
|
58
|
+
status: Status.OK,
|
|
34
59
|
headers: {
|
|
35
60
|
"Content-Type": mime(file.name),
|
|
36
61
|
Etag: await file.modified,
|
|
37
62
|
},
|
|
38
63
|
});
|
|
39
64
|
|
|
40
|
-
const
|
|
41
|
-
const published = app.resources.find(({src, inline}) =>
|
|
42
|
-
!inline && src === request.url.pathname);
|
|
43
|
-
if (published !== undefined) {
|
|
44
|
-
return new Response(published.code, {
|
|
45
|
-
status: OK,
|
|
46
|
-
headers: {
|
|
47
|
-
"Content-Type": mime(published.src),
|
|
48
|
-
Etag: published.integrity,
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return route(request);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const resource = async request => {
|
|
65
|
+
const handle = async request => {
|
|
57
66
|
const {pathname} = request.url;
|
|
58
67
|
const {root} = http.static;
|
|
59
68
|
if (pathname.startsWith(root)) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
const debased = pathname.replace(root, _ => "");
|
|
70
|
+
// try static first
|
|
71
|
+
const _static = paths.client.join(build.static, debased);
|
|
72
|
+
if (await _static.isFile) {
|
|
73
|
+
return asset(_static.file);
|
|
74
|
+
}
|
|
75
|
+
const _app = app.paths.client.join(debased);
|
|
76
|
+
return await _app.isFile ? asset(_app.file) : route(request);
|
|
64
77
|
}
|
|
65
78
|
return route(request);
|
|
66
79
|
};
|
|
67
80
|
|
|
68
|
-
return [...
|
|
81
|
+
return [...app.modules.handle, handle]
|
|
69
82
|
.reduceRight((acc, handler) => input => handler(input, acc));
|
|
70
83
|
};
|
package/src/hooks/parse.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {URL} from "runtime-compat/http";
|
|
2
|
+
import {tryreturn} from "runtime-compat/flow";
|
|
2
3
|
import errors from "../errors.js";
|
|
3
4
|
|
|
5
|
+
const {fromEntries: from} = Object;
|
|
6
|
+
|
|
4
7
|
const contents = {
|
|
5
|
-
"application/x-www-form-urlencoded": body =>
|
|
6
|
-
|
|
8
|
+
"application/x-www-form-urlencoded": body => from(body.split("&")
|
|
9
|
+
.map(part => part.split("=")
|
|
7
10
|
.map(subpart => decodeURIComponent(subpart).replaceAll("+", " ")))),
|
|
8
11
|
"application/json": body => JSON.parse(body),
|
|
9
12
|
};
|
|
@@ -15,14 +18,9 @@ export default dispatch => async request => {
|
|
|
15
18
|
return type === undefined ? body : type(body);
|
|
16
19
|
};
|
|
17
20
|
|
|
18
|
-
const parseContent = async (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return parseContentType(contentType, body);
|
|
22
|
-
} catch (error) {
|
|
23
|
-
return errors.CannotParseBody.throw({body, contentType});
|
|
24
|
-
}
|
|
25
|
-
};
|
|
21
|
+
const parseContent = async (contentType, body) =>
|
|
22
|
+
tryreturn(_ => parseContentType(contentType, body))
|
|
23
|
+
.orelse(_ => errors.CannotParseBody.throw(body, contentType));
|
|
26
24
|
|
|
27
25
|
const parseBody = async request => {
|
|
28
26
|
if (request.body === null) {
|
|
@@ -38,12 +36,11 @@ export default dispatch => async request => {
|
|
|
38
36
|
}
|
|
39
37
|
} while (!result.done);
|
|
40
38
|
|
|
41
|
-
return parseContent(request, chunks.join());
|
|
39
|
+
return parseContent(request.headers.get("content-type"), chunks.join());
|
|
42
40
|
};
|
|
43
41
|
|
|
44
42
|
const cookies = request.headers.get("cookie");
|
|
45
|
-
const
|
|
46
|
-
const url = new URL(_url.endsWith("/") ? _url.slice(0, -1) : _url);
|
|
43
|
+
const url = new URL(request.url);
|
|
47
44
|
|
|
48
45
|
const body = await parseBody(request);
|
|
49
46
|
return {
|
|
@@ -52,8 +49,8 @@ export default dispatch => async request => {
|
|
|
52
49
|
body: dispatch(body),
|
|
53
50
|
cookies: dispatch(cookies === null
|
|
54
51
|
? {}
|
|
55
|
-
:
|
|
56
|
-
headers: dispatch(
|
|
57
|
-
query: dispatch(
|
|
52
|
+
: from(cookies.split(";").map(c => c.trim().split("=")))),
|
|
53
|
+
headers: dispatch(from(request.headers)),
|
|
54
|
+
query: dispatch(from(url.searchParams)),
|
|
58
55
|
};
|
|
59
56
|
};
|
package/src/hooks/publish.js
CHANGED
|
@@ -1,36 +1,55 @@
|
|
|
1
1
|
import {Path} from "runtime-compat/fs";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import {identity} from "runtime-compat/function";
|
|
3
|
+
import copy_includes from "./copy_includes.js"
|
|
4
4
|
|
|
5
5
|
const post = async app => {
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const {config} = app;
|
|
7
|
+
const build = config.build.app;
|
|
8
|
+
{
|
|
9
|
+
// after hook, publish a zero assumptions app.js (no css imports)
|
|
10
|
+
const code = app.entrypoints.filter(({type}) => type === "script")
|
|
11
|
+
.map(entrypoint => entrypoint.code).join("");
|
|
12
|
+
const src = new Path(config.http.static.root, build, config.build.index);
|
|
13
|
+
await app.publish({src, code, type: "module"});
|
|
14
|
+
|
|
15
|
+
await app.copy(app.paths.components, app.paths.client.join(build));
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
await app.publish({src, code, type: file.extension === ".js" ?
|
|
18
|
-
"module" : "style"});
|
|
19
|
-
if (file.extension === ".css") {
|
|
20
|
-
app.bootstrap({type: "style", code: `import "./${file.name}";`});
|
|
21
|
-
}
|
|
22
|
-
}));
|
|
17
|
+
const imports = {...app.importmaps, app: src.path};
|
|
18
|
+
await app.publish({
|
|
19
|
+
inline: true,
|
|
20
|
+
code: JSON.stringify({imports}, null, 2),
|
|
21
|
+
type: "importmap",
|
|
22
|
+
});
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
await
|
|
24
|
+
|
|
25
|
+
const imports = await Path.collect(app.paths.static, /\.(?:js|css)$/u);
|
|
26
|
+
await Promise.all(imports.map(async file => {
|
|
27
|
+
const code = await file.text();
|
|
28
|
+
const src = file.name;
|
|
29
|
+
const isCSS = file.extension === ".css";
|
|
30
|
+
await app.publish({src: `${config.build.static}/${src}`, code,
|
|
31
|
+
type: isCSS ? "style" : "module"});
|
|
32
|
+
if (isCSS) {
|
|
33
|
+
app.bootstrap({type: "style",
|
|
34
|
+
code: `import "../${config.build.static}/${file.name}";`});
|
|
35
|
+
}
|
|
28
36
|
}));
|
|
37
|
+
|
|
38
|
+
const source = `${app.paths.client}`;
|
|
39
|
+
const {root} = app.config.http.static;
|
|
40
|
+
// copy additional subdirectories to build/client
|
|
41
|
+
await copy_includes(app, "client", async to =>
|
|
42
|
+
Promise.all((await to.collect(/\.js$/u)).map(async script => {
|
|
43
|
+
const code = await script.text();
|
|
44
|
+
const src = new Path(root, script.path.replace(source, () => ""));
|
|
45
|
+
await app.publish({src, code, type: "module"});
|
|
46
|
+
}))
|
|
47
|
+
);
|
|
29
48
|
};
|
|
30
49
|
|
|
31
50
|
export default async app => {
|
|
32
51
|
app.log.info("running publish hooks", {module: "primate"});
|
|
33
|
-
await [...
|
|
52
|
+
await [...app.modules.publish, identity]
|
|
34
53
|
.reduceRight((acc, handler) => input => handler(input, acc))(app);
|
|
35
54
|
await post(app);
|
|
36
55
|
};
|
package/src/hooks/register.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
2
|
-
|
|
3
1
|
export default async app => {
|
|
4
2
|
app.log.info("running register hooks", {module: "primate"});
|
|
5
|
-
await [...
|
|
3
|
+
await [...app.modules.register, _ => _]
|
|
6
4
|
.reduceRight((acc, handler) => input => handler(input, acc))(app);
|
|
7
5
|
};
|
|
@@ -14,7 +14,7 @@ const isNonNullObject = value => typeof value === "object" && value !== null;
|
|
|
14
14
|
const isObject = value => isNonNullObject(value)
|
|
15
15
|
? json(value) : isText(value);
|
|
16
16
|
const isResponse = value => isResponseDuck(value)
|
|
17
|
-
?
|
|
17
|
+
? _ => value : isObject(value);
|
|
18
18
|
const isStream = value => value instanceof ReadableStream
|
|
19
19
|
? stream(value) : isResponse(value);
|
|
20
20
|
const isBlob = value => value instanceof Blob
|