primate 0.20.4 → 0.21.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 +1 -0
- package/src/app.js +31 -18
- package/src/commands/exports.js +1 -5
- package/src/defaults/primate.config.js +5 -2
- package/src/dispatch.js +8 -5
- package/src/errors.json +4 -4
- package/src/exports.js +1 -1
- package/src/handlers/error.js +7 -7
- package/src/handlers/html.js +6 -4
- package/src/handlers/json.js +6 -5
- package/src/handlers/redirect.js +4 -5
- package/src/handlers/stream.js +7 -5
- package/src/handlers/text.js +6 -5
- package/src/handlers/view.js +1 -1
- package/src/hooks/bundle.js +3 -3
- package/src/hooks/compile.js +7 -3
- package/src/hooks/copy_includes.js +2 -2
- package/src/hooks/handle.js +43 -40
- package/src/hooks/parse.js +22 -25
- package/src/hooks/publish.js +7 -4
- package/src/hooks/respond/exports.js +0 -1
- package/src/hooks/route.js +6 -7
- package/src/loaders/routes/errors.js +3 -0
- package/src/loaders/routes/exports.js +1 -0
- package/src/loaders/routes.js +7 -4
- package/src/start.js +1 -1
- package/src/validate.js +10 -0
- package/src/hooks/respond/mime.js +0 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"directory": "packages/primate"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"runtime-compat": "^0.
|
|
21
|
+
"runtime-compat": "^0.24.1"
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=18.16"
|
package/src/Logger.js
CHANGED
package/src/app.js
CHANGED
|
@@ -15,10 +15,14 @@ const base = new Path(import.meta.url).up(1);
|
|
|
15
15
|
const packager = import.meta.runtime?.packager ?? "package.json";
|
|
16
16
|
const library = import.meta.runtime?.library ?? "node_modules";
|
|
17
17
|
|
|
18
|
+
const fallback = (app, page) =>
|
|
19
|
+
tryreturn(_ => base.join("defaults", page).text())
|
|
20
|
+
.orelse(_ => base.join("defaults", app.config.pages.index).text());
|
|
21
|
+
|
|
18
22
|
// use user-provided file or fall back to default
|
|
19
|
-
const index = (app,
|
|
23
|
+
const index = (app, page) =>
|
|
20
24
|
tryreturn(_ => File.read(`${app.paths.pages.join(name)}`))
|
|
21
|
-
.orelse(_ =>
|
|
25
|
+
.orelse(_ => fallback(app, page));
|
|
22
26
|
|
|
23
27
|
const encoder = new TextEncoder();
|
|
24
28
|
const hash = async (string, algorithm = "sha-384") => {
|
|
@@ -39,8 +43,6 @@ export default async (config, root, log) => {
|
|
|
39
43
|
const secure = http?.ssl !== undefined;
|
|
40
44
|
const {name, version} = await base.up(1).join(packager).json();
|
|
41
45
|
const paths = valmap(config.paths, value => root.join(value));
|
|
42
|
-
paths.client = paths.build.join("client");
|
|
43
|
-
paths.server = paths.build.join("server");
|
|
44
46
|
|
|
45
47
|
const at = `at http${secure ? "s" : ""}://${http.host}:${http.port}\n`;
|
|
46
48
|
print(blue(bold(name)), blue(version), at);
|
|
@@ -54,6 +56,13 @@ export default async (config, root, log) => {
|
|
|
54
56
|
const types = await loaders.types(log, paths.types);
|
|
55
57
|
|
|
56
58
|
const app = {
|
|
59
|
+
build: {
|
|
60
|
+
paths: {
|
|
61
|
+
client: paths.build.join("client"),
|
|
62
|
+
server: paths.build.join("server"),
|
|
63
|
+
components: paths.build.join("components"),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
57
66
|
config,
|
|
58
67
|
secure,
|
|
59
68
|
name,
|
|
@@ -73,7 +82,7 @@ export default async (config, root, log) => {
|
|
|
73
82
|
await to.file.write(file);
|
|
74
83
|
}));
|
|
75
84
|
},
|
|
76
|
-
headers
|
|
85
|
+
headers() {
|
|
77
86
|
const csp = Object.keys(http.csp).reduce((policy_string, key) =>
|
|
78
87
|
`${policy_string}${key} ${http.csp[key]};`, "")
|
|
79
88
|
.replace("script-src 'self'", `script-src 'self' ${
|
|
@@ -93,8 +102,8 @@ export default async (config, root, log) => {
|
|
|
93
102
|
};
|
|
94
103
|
},
|
|
95
104
|
handlers: {...handlers},
|
|
96
|
-
|
|
97
|
-
const html = await index(app, page ?? config.index);
|
|
105
|
+
async render({body = "", page} = {}) {
|
|
106
|
+
const html = await index(app, page ?? config.pages.index);
|
|
98
107
|
// inline: <script type integrity>...</script>
|
|
99
108
|
// outline: <script type integrity src></script>
|
|
100
109
|
const script = ({inline, code, type, integrity, src}) => inline
|
|
@@ -105,7 +114,7 @@ export default async (config, root, log) => {
|
|
|
105
114
|
const style = ({inline, code, href, rel = "stylesheet"}) => inline
|
|
106
115
|
? tag({name: "style", code})
|
|
107
116
|
: tag({name: "link", attributes: {rel, href}, close: false});
|
|
108
|
-
const head = toSorted(
|
|
117
|
+
const head = toSorted(this.assets,
|
|
109
118
|
({type}) => -1 * (type === "importmap"))
|
|
110
119
|
.map(({src, code, type, inline, integrity}) =>
|
|
111
120
|
type === "style"
|
|
@@ -113,39 +122,43 @@ export default async (config, root, log) => {
|
|
|
113
122
|
: script({inline, code, type, integrity, src})
|
|
114
123
|
).join("\n");
|
|
115
124
|
// remove inline assets
|
|
116
|
-
|
|
125
|
+
this.assets = this.assets.filter(({inline, type}) => !inline
|
|
117
126
|
|| type === "importmap");
|
|
118
127
|
return html.replace("%body%", _ => body).replace("%head%", _ => head);
|
|
119
128
|
},
|
|
120
|
-
|
|
129
|
+
async publish({src, code, type = "", inline = false}) {
|
|
121
130
|
if (!inline) {
|
|
122
|
-
const base = paths.client.join(src);
|
|
131
|
+
const base = this.build.paths.client.join(src);
|
|
123
132
|
await base.directory.file.create();
|
|
124
133
|
await base.file.write(code);
|
|
125
134
|
}
|
|
126
135
|
if (inline || type === "style") {
|
|
127
|
-
|
|
136
|
+
this.assets.push({src: new Path(http.static.root).join(src ?? "").path,
|
|
128
137
|
code: inline ? code : "", type, inline, integrity: await hash(code)});
|
|
129
138
|
}
|
|
130
139
|
},
|
|
131
|
-
bootstrap
|
|
132
|
-
|
|
140
|
+
bootstrap({type, code}) {
|
|
141
|
+
this.entrypoints.push({type, code});
|
|
133
142
|
},
|
|
134
143
|
async import(module) {
|
|
135
144
|
const {build} = config;
|
|
136
145
|
const {root} = http.static;
|
|
137
|
-
const path = [library, module];
|
|
146
|
+
const path = [library, ...module.split("/")];
|
|
138
147
|
const pkg = await Path.resolve().join(...path, packager).json();
|
|
139
148
|
const exports = pkg.exports === undefined
|
|
140
149
|
? {[module]: `/${module}/${pkg.main}`}
|
|
141
150
|
: transform(pkg.exports, entry => entry
|
|
142
|
-
.filter(([, _export]) => _export.
|
|
151
|
+
.filter(([, _export]) => _export.browser?.default !== undefined
|
|
152
|
+
|| _export.import !== undefined
|
|
153
|
+
|| _export.default !== undefined)
|
|
143
154
|
.map(([key, value]) => [
|
|
144
155
|
key.replace(".", module),
|
|
145
|
-
value.
|
|
156
|
+
value.browser?.default.replace(".", `./${module}`)
|
|
157
|
+
?? value.default?.replace(".", `./${module}`)
|
|
158
|
+
?? value.import?.replace(".", `./${module}`),
|
|
146
159
|
]));
|
|
147
160
|
const dependency = Path.resolve().join(...path);
|
|
148
|
-
const to = new Path(paths.client, build.modules,
|
|
161
|
+
const to = new Path(this.build.paths.client, build.modules, ...module.split("/"));
|
|
149
162
|
await dependency.file.copy(to);
|
|
150
163
|
this.importmaps = {
|
|
151
164
|
...valmap(exports, value => new Path(root, build.modules, value).path),
|
package/src/commands/exports.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import {default as dev} from "./dev.js";
|
|
2
2
|
import {default as serve} from "./serve.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const run = name => commands[name] ?? dev;
|
|
7
|
-
|
|
8
|
-
export default name => name === undefined ? dev : run(name);
|
|
4
|
+
export default name => ({dev, serve})[name] ?? dev;
|
|
@@ -2,6 +2,11 @@ import Logger from "../Logger.js";
|
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
base: "/",
|
|
5
|
+
modules: [],
|
|
6
|
+
pages: {
|
|
7
|
+
index: "app.html",
|
|
8
|
+
error: "error.html",
|
|
9
|
+
},
|
|
5
10
|
logger: {
|
|
6
11
|
level: Logger.Warn,
|
|
7
12
|
trace: false,
|
|
@@ -22,7 +27,6 @@ export default {
|
|
|
22
27
|
root: "/",
|
|
23
28
|
},
|
|
24
29
|
},
|
|
25
|
-
index: "app.html",
|
|
26
30
|
paths: {
|
|
27
31
|
build: "build",
|
|
28
32
|
components: "components",
|
|
@@ -38,7 +42,6 @@ export default {
|
|
|
38
42
|
modules: "modules",
|
|
39
43
|
index: "index.js",
|
|
40
44
|
},
|
|
41
|
-
modules: [],
|
|
42
45
|
types: {
|
|
43
46
|
explicit: false,
|
|
44
47
|
},
|
package/src/dispatch.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import {is, maybe} from "runtime-compat/dyndef";
|
|
2
2
|
import {tryreturn} from "runtime-compat/sync";
|
|
3
3
|
import {map} from "runtime-compat/object";
|
|
4
|
+
import {camelcased} from "runtime-compat/string";
|
|
4
5
|
import errors from "./errors.js";
|
|
6
|
+
import validate from "./validate.js";
|
|
5
7
|
|
|
6
|
-
export default (patches = {}) => value => {
|
|
7
|
-
is(patches.get).undefined();
|
|
8
|
+
export default (patches = {}) => (value, raw, cased = true) => {
|
|
8
9
|
return Object.assign(Object.create(null), {
|
|
9
|
-
...map(patches, ([name, patch]) => [name
|
|
10
|
+
...map(patches, ([name, patch]) => [`get${camelcased(name)}`, property => {
|
|
10
11
|
is(property).defined(`\`${name}\` called without property`);
|
|
11
|
-
return tryreturn(_ => patch
|
|
12
|
+
return tryreturn(_ => validate(patch, value[property], property))
|
|
12
13
|
.orelse(({message}) => errors.MismatchedType.throw(message));
|
|
13
14
|
}]),
|
|
14
15
|
get(property) {
|
|
15
16
|
maybe(property).string();
|
|
16
|
-
return property === undefined ? value :
|
|
17
|
+
return property === undefined ? value :
|
|
18
|
+
value[cased ? property : property.toLowerCase()];
|
|
17
19
|
},
|
|
20
|
+
raw,
|
|
18
21
|
});
|
|
19
22
|
};
|
package/src/errors.json
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"level": "Warn"
|
|
33
33
|
},
|
|
34
34
|
"ErrorInConfigFile": {
|
|
35
|
-
"message": "error in config file
|
|
35
|
+
"message": "error in config file: {0}",
|
|
36
36
|
"fix": "check errors in config file by running {1}",
|
|
37
37
|
"level": "Error"
|
|
38
38
|
},
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
"level": "Error"
|
|
58
58
|
},
|
|
59
59
|
"MismatchedPath": {
|
|
60
|
-
"message": "mismatched {0}
|
|
60
|
+
"message": "mismatched path {0}: {1}",
|
|
61
61
|
"fix": "if unintentional, fix the type or the caller",
|
|
62
62
|
"level": "Info"
|
|
63
63
|
},
|
|
64
64
|
"MismatchedType": {
|
|
65
|
-
"message": "mismatched type
|
|
65
|
+
"message": "mismatched type: {0}",
|
|
66
66
|
"fix": "if unintentional, fix the type or the caller",
|
|
67
67
|
"level": "Info"
|
|
68
68
|
},
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
},
|
|
99
99
|
"NoRouteToPath": {
|
|
100
100
|
"message": "no {0} route to {1}",
|
|
101
|
-
"fix": "if unintentional create a route at {2}.js",
|
|
101
|
+
"fix": "if unintentional create a {3} route function at {2}.js",
|
|
102
102
|
"level": "Info"
|
|
103
103
|
},
|
|
104
104
|
"ReservedTypeName": {
|
package/src/exports.js
CHANGED
package/src/handlers/error.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {Status} from "runtime-compat/http";
|
|
1
|
+
import {Response, Status, MediaType} from "runtime-compat/http";
|
|
2
2
|
|
|
3
|
-
export default (body = "Not Found", {status = Status.
|
|
4
|
-
async app =>
|
|
5
|
-
|
|
3
|
+
export default (body = "Not Found", {status = Status.NOT_FOUND, page} = {}) =>
|
|
4
|
+
async app => new Response(await app.render({
|
|
5
|
+
body,
|
|
6
|
+
page: page ?? app.config.pages.error}), {
|
|
6
7
|
status,
|
|
7
|
-
headers: {...app.headers(), "Content-Type":
|
|
8
|
-
}
|
|
9
|
-
];
|
|
8
|
+
headers: {...app.headers(), "Content-Type": MediaType.TEXT_HTML},
|
|
9
|
+
});
|
package/src/handlers/html.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {Response, Status, MediaType} from "runtime-compat/http";
|
|
2
|
+
|
|
1
3
|
const script = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
|
|
2
4
|
const style = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
|
|
3
5
|
|
|
@@ -10,7 +12,7 @@ const integrate = async (html, publish) => {
|
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export default (component, options = {}) => {
|
|
13
|
-
const {status =
|
|
15
|
+
const {status = Status.OK, partial = false, load = false} = options;
|
|
14
16
|
|
|
15
17
|
return async app => {
|
|
16
18
|
const body = await integrate(await load ?
|
|
@@ -19,9 +21,9 @@ export default (component, options = {}) => {
|
|
|
19
21
|
// needs to happen before app.render()
|
|
20
22
|
const headers = app.headers();
|
|
21
23
|
|
|
22
|
-
return
|
|
24
|
+
return new Response(partial ? body : await app.render({body}), {
|
|
23
25
|
status,
|
|
24
|
-
headers: {...headers, "Content-Type":
|
|
25
|
-
}
|
|
26
|
+
headers: {...headers, "Content-Type": MediaType.TEXT_HTML},
|
|
27
|
+
});
|
|
26
28
|
};
|
|
27
29
|
};
|
package/src/handlers/json.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {Response, Status, MediaType} from "runtime-compat/http";
|
|
2
|
+
|
|
3
|
+
export default (body, {status = Status.OK} = {}) => app =>
|
|
4
|
+
new Response(JSON.stringify(body), {
|
|
3
5
|
status,
|
|
4
|
-
headers: {...app.headers(), "Content-Type":
|
|
5
|
-
}
|
|
6
|
-
];
|
|
6
|
+
headers: {...app.headers(), "Content-Type": MediaType.APPLICATION_JSON},
|
|
7
|
+
});
|
package/src/handlers/redirect.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {Status} from "runtime-compat/http";
|
|
1
|
+
import {Response, Status} from "runtime-compat/http";
|
|
2
2
|
|
|
3
|
-
export default (Location, {status = Status.
|
|
3
|
+
export default (Location, {status = Status.FOUND} = {}) => app =>
|
|
4
4
|
/* no body */
|
|
5
|
-
null, {
|
|
5
|
+
new Response(null, {
|
|
6
6
|
status,
|
|
7
7
|
headers: {...app.headers(), Location},
|
|
8
|
-
}
|
|
9
|
-
];
|
|
8
|
+
});
|
package/src/handlers/stream.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {Response, Status, MediaType} from "runtime-compat/http";
|
|
2
|
+
|
|
3
|
+
export default (body, {status = Status.OK} = {}) => app =>
|
|
4
|
+
new Response(body, {
|
|
3
5
|
status,
|
|
4
|
-
headers: {...app.headers(), "Content-Type":
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
headers: {...app.headers(), "Content-Type":
|
|
7
|
+
MediaType.APPLICATION_OCTET_STREAM},
|
|
8
|
+
});
|
package/src/handlers/text.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {Response, Status, MediaType} from "runtime-compat/http";
|
|
2
|
+
|
|
3
|
+
export default (body, {status = Status.OK} = {}) => app =>
|
|
4
|
+
new Response(body, {
|
|
3
5
|
status,
|
|
4
|
-
headers: {...app.headers(), "Content-Type":
|
|
5
|
-
}
|
|
6
|
-
];
|
|
6
|
+
headers: {...app.headers(), "Content-Type": MediaType.TEXT_PLAIN},
|
|
7
|
+
});
|
package/src/handlers/view.js
CHANGED
package/src/hooks/bundle.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {File} from "runtime-compat/fs";
|
|
2
2
|
|
|
3
3
|
const pre = async app => {
|
|
4
|
-
const {paths, config} = app;
|
|
4
|
+
const {paths, config, build} = app;
|
|
5
5
|
if (await paths.static.exists) {
|
|
6
|
-
// copy static files to build/client/
|
|
7
|
-
await File.copy(paths.static, paths.client.join(config.build.static));
|
|
6
|
+
// copy static files to build/client/static
|
|
7
|
+
await File.copy(paths.static, build.paths.client.join(config.build.static));
|
|
8
8
|
}
|
|
9
9
|
};
|
|
10
10
|
|
package/src/hooks/compile.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import copy_includes from "./copy_includes.js";
|
|
2
2
|
|
|
3
3
|
const pre = async app => {
|
|
4
|
-
const {paths, config
|
|
4
|
+
const {build, paths, config} = app;
|
|
5
5
|
|
|
6
6
|
// remove build directory in case exists
|
|
7
7
|
if (await paths.build.exists) {
|
|
8
8
|
await paths.build.file.remove();
|
|
9
9
|
}
|
|
10
|
-
await paths.server.file.create();
|
|
10
|
+
await build.paths.server.file.create();
|
|
11
|
+
await build.paths.components.file.create();
|
|
11
12
|
|
|
12
13
|
if (await paths.components.exists) {
|
|
13
|
-
|
|
14
|
+
// copy all files to build/components
|
|
15
|
+
await app.copy(paths.components, build.paths.components, /^.*$/u);
|
|
16
|
+
// copy .js files from components to build/server
|
|
17
|
+
await app.copy(paths.components, build.paths.server.join(config.build.app));
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
// copy additional subdirectories to build/server
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const system = ["routes", "components", "build"];
|
|
2
2
|
|
|
3
3
|
export default async (app, type, post = () => undefined) => {
|
|
4
|
-
const {
|
|
4
|
+
const {config} = app;
|
|
5
5
|
const {build} = config;
|
|
6
6
|
const {includes} = build;
|
|
7
7
|
|
|
@@ -14,7 +14,7 @@ export default async (app, type, post = () => undefined) => {
|
|
|
14
14
|
.map(async include => {
|
|
15
15
|
const path = app.root.join(include);
|
|
16
16
|
if (await path.exists) {
|
|
17
|
-
const to = paths[type].join(include);
|
|
17
|
+
const to = app.build.paths[type].join(include);
|
|
18
18
|
await to.file.create();
|
|
19
19
|
await app.copy(path, to);
|
|
20
20
|
await post(to);
|
package/src/hooks/handle.js
CHANGED
|
@@ -1,63 +1,65 @@
|
|
|
1
|
-
import {Response, Status} from "runtime-compat/http";
|
|
1
|
+
import {Response, Status, MediaType} from "runtime-compat/http";
|
|
2
2
|
import {tryreturn} from "runtime-compat/async";
|
|
3
|
-
import {
|
|
3
|
+
import {respond} from "./respond/exports.js";
|
|
4
4
|
import {invalid} from "./route.js";
|
|
5
5
|
import {error as clientError} from "../handlers/exports.js";
|
|
6
|
-
import
|
|
6
|
+
import _errors from "../errors.js";
|
|
7
|
+
const {NoFileForPath} = _errors;
|
|
7
8
|
|
|
8
9
|
const guardError = Symbol("guardError");
|
|
9
10
|
|
|
10
11
|
export default app => {
|
|
11
12
|
const {config: {http: {static: {root}}, build}, paths} = app;
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
+
const route = async request => {
|
|
14
15
|
const {pathname} = request.url;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
: await app.route(request);
|
|
16
|
+
// if NoFileForPath is thrown, this will remain undefined
|
|
17
|
+
let errorHandler = undefined;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
return tryreturn(async _ => {
|
|
20
|
+
const {path, guards, errors, layouts, handler} = invalid(pathname)
|
|
21
|
+
? NoFileForPath.throw(pathname, paths.static)
|
|
22
|
+
: await app.route(request);
|
|
23
|
+
errorHandler = errors?.at(-1);
|
|
24
|
+
|
|
25
|
+
// handle guards
|
|
26
|
+
try {
|
|
27
|
+
guards.every(guard => {
|
|
28
|
+
const result = guard(request);
|
|
29
|
+
if (result === true) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
const error = new Error();
|
|
33
|
+
error.result = result;
|
|
34
|
+
error.type = guardError;
|
|
35
|
+
throw error;
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error.type === guardError) {
|
|
39
|
+
return (await respond(error.result))(app);
|
|
25
40
|
}
|
|
26
|
-
|
|
27
|
-
error.result = result;
|
|
28
|
-
error.type = guardError;
|
|
41
|
+
// rethrow if not guard error
|
|
29
42
|
throw error;
|
|
30
|
-
});
|
|
31
|
-
} catch (error) {
|
|
32
|
-
if (error.type === guardError) {
|
|
33
|
-
return (await respond(error.result))(app);
|
|
34
43
|
}
|
|
35
|
-
// rethrow if not guard error
|
|
36
|
-
throw error;
|
|
37
|
-
}
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
});
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const route = async request =>
|
|
49
|
-
tryreturn(async _ => {
|
|
50
|
-
const response = await run(request);
|
|
51
|
-
return isResponse(response) ? response : new Response(...response);
|
|
45
|
+
// handle request
|
|
46
|
+
const handlers = [...app.modules.route, handler]
|
|
47
|
+
.reduceRight((next, last) => input => last(input, next));
|
|
48
|
+
return (await respond(await handlers({...request, path})))(app, {
|
|
49
|
+
layouts: await Promise.all(layouts.map(layout => layout(request))),
|
|
50
|
+
}, request);
|
|
52
51
|
}).orelse(async error => {
|
|
53
52
|
app.log.auto(error);
|
|
54
|
-
|
|
53
|
+
// the +error.js page itself could fail
|
|
54
|
+
return tryreturn(_ => respond(errorHandler())(app, {}, request))
|
|
55
|
+
.orelse(_ => clientError()(app, {}, request));
|
|
55
56
|
});
|
|
57
|
+
};
|
|
56
58
|
|
|
57
59
|
const asset = async file => new Response(file.readable, {
|
|
58
60
|
status: Status.OK,
|
|
59
61
|
headers: {
|
|
60
|
-
"Content-Type":
|
|
62
|
+
"Content-Type": MediaType.resolve(file.name),
|
|
61
63
|
Etag: await file.modified,
|
|
62
64
|
},
|
|
63
65
|
});
|
|
@@ -66,12 +68,13 @@ export default app => {
|
|
|
66
68
|
const {pathname} = request.url;
|
|
67
69
|
if (pathname.startsWith(root)) {
|
|
68
70
|
const debased = pathname.replace(root, _ => "");
|
|
71
|
+
const {client} = app.build.paths;
|
|
69
72
|
// try static first
|
|
70
|
-
const _static =
|
|
73
|
+
const _static = client.join(build.static, debased);
|
|
71
74
|
if (await _static.isFile) {
|
|
72
75
|
return asset(_static.file);
|
|
73
76
|
}
|
|
74
|
-
const _app =
|
|
77
|
+
const _app = client.join(debased);
|
|
75
78
|
return await _app.isFile ? asset(_app.file) : route(request);
|
|
76
79
|
}
|
|
77
80
|
return route(request);
|
package/src/hooks/parse.js
CHANGED
|
@@ -1,38 +1,35 @@
|
|
|
1
|
-
import {URL} from "runtime-compat/http";
|
|
1
|
+
import {URL, MediaType} from "runtime-compat/http";
|
|
2
2
|
import {tryreturn} from "runtime-compat/sync";
|
|
3
3
|
import {stringify} from "runtime-compat/streams";
|
|
4
|
+
import {from, valmap} from "runtime-compat/object";
|
|
4
5
|
import errors from "../errors.js";
|
|
5
6
|
|
|
6
|
-
const {
|
|
7
|
+
const {APPLICATION_FORM_URLENCODED, APPLICATION_JSON} = MediaType;
|
|
7
8
|
|
|
8
9
|
const contents = {
|
|
9
|
-
|
|
10
|
+
[APPLICATION_FORM_URLENCODED]: body => from(body.split("&")
|
|
10
11
|
.map(part => part.split("=")
|
|
11
12
|
.map(subpart => decodeURIComponent(subpart).replaceAll("+", " ")))),
|
|
12
|
-
|
|
13
|
+
[APPLICATION_JSON]: body => JSON.parse(body),
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const type = contents[content_type];
|
|
19
|
-
return type === undefined ? body : type(body);
|
|
20
|
-
}).orelse(_ => errors.CannotParseBody.throw(body, content_type));
|
|
21
|
-
},
|
|
22
|
-
async body({body, headers}) {
|
|
23
|
-
return body === null
|
|
24
|
-
? null
|
|
25
|
-
: this.content(headers.get("content-type"), await stringify(body));
|
|
26
|
-
},
|
|
27
|
-
};
|
|
16
|
+
const content = (type, body) =>
|
|
17
|
+
tryreturn(_ => contents[type?.split(";")[0]]?.(body) ?? body)
|
|
18
|
+
.orelse(_ => errors.CannotParseBody.throw(body, type));
|
|
28
19
|
|
|
29
|
-
export default dispatch => async
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const url = new URL(request.url);
|
|
35
|
-
const query = dispatch(from(url.searchParams));
|
|
20
|
+
export default dispatch => async original => {
|
|
21
|
+
const {headers} = original;
|
|
22
|
+
const url = new URL(original.url);
|
|
23
|
+
const body = await stringify(original.body);
|
|
24
|
+
const cookies = headers.get("cookie");
|
|
36
25
|
|
|
37
|
-
return {original
|
|
26
|
+
return {original, url,
|
|
27
|
+
...valmap({
|
|
28
|
+
body: [content(headers.get("content-type"), body), body],
|
|
29
|
+
query: [from(url.searchParams), url.search],
|
|
30
|
+
headers: [from(headers), headers, false],
|
|
31
|
+
cookies: [from(cookies?.split(";").map(cookie => cookie.trim().split("="))
|
|
32
|
+
?? []), cookies],
|
|
33
|
+
}, value => dispatch(...value)),
|
|
34
|
+
};
|
|
38
35
|
};
|
package/src/hooks/publish.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {Path} from "runtime-compat/fs";
|
|
2
2
|
import {identity} from "runtime-compat/function";
|
|
3
|
-
import copy_includes from "./copy_includes.js"
|
|
3
|
+
import copy_includes from "./copy_includes.js";
|
|
4
4
|
|
|
5
5
|
const post = async app => {
|
|
6
|
-
const {config} = app;
|
|
6
|
+
const {config, paths} = app;
|
|
7
7
|
const build = config.build.app;
|
|
8
8
|
{
|
|
9
9
|
// after hook, publish a zero assumptions app.js (no css imports)
|
|
@@ -12,7 +12,10 @@ const post = async app => {
|
|
|
12
12
|
const src = new Path(config.http.static.root, build, config.build.index);
|
|
13
13
|
await app.publish({src, code, type: "module"});
|
|
14
14
|
|
|
15
|
-
await
|
|
15
|
+
if (await paths.components.exists) {
|
|
16
|
+
// copy .js files from components to build/server
|
|
17
|
+
await app.copy(app.paths.components, app.build.paths.client.join(build));
|
|
18
|
+
}
|
|
16
19
|
|
|
17
20
|
const imports = {...app.importmaps, app: src.path};
|
|
18
21
|
await app.publish({
|
|
@@ -36,7 +39,7 @@ const post = async app => {
|
|
|
36
39
|
}
|
|
37
40
|
}));
|
|
38
41
|
|
|
39
|
-
const source = `${app.paths.client}`;
|
|
42
|
+
const source = `${app.build.paths.client}`;
|
|
40
43
|
const {root} = app.config.http.static;
|
|
41
44
|
// copy additional subdirectories to build/client
|
|
42
45
|
await copy_includes(app, "client", async to =>
|
package/src/hooks/route.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {keymap} from "runtime-compat/object";
|
|
2
2
|
import {tryreturn} from "runtime-compat/sync";
|
|
3
3
|
import errors from "../errors.js";
|
|
4
|
+
import validate from "../validate.js";
|
|
4
5
|
|
|
5
6
|
// insensitive-case equal
|
|
6
7
|
const ieq = (left, right) => left.toLowerCase() === right.toLowerCase();
|
|
@@ -18,9 +19,8 @@ export default app => {
|
|
|
18
19
|
.filter(([name]) => name.includes("$"))
|
|
19
20
|
.map(([name, value]) => [name.split("$")[1], value])
|
|
20
21
|
.every(([name, value]) =>
|
|
21
|
-
tryreturn(_ => types
|
|
22
|
-
.orelse(({message}) => errors.MismatchedPath.throw(pathname, message))
|
|
23
|
-
);
|
|
22
|
+
tryreturn(_ => validate(types[name], value, name))
|
|
23
|
+
.orelse(({message}) => errors.MismatchedPath.throw(pathname, message)));
|
|
24
24
|
const isPath = ({route, pathname}) => {
|
|
25
25
|
const result = route.pathname.exec(pathname);
|
|
26
26
|
return result === null ? false : isType(result.groups, pathname);
|
|
@@ -34,11 +34,10 @@ export default app => {
|
|
|
34
34
|
const deroot = pathname => pathname.endsWith("/") && pathname !== "/"
|
|
35
35
|
? pathname.slice(0, -1) : pathname;
|
|
36
36
|
|
|
37
|
-
return
|
|
38
|
-
const {original: {method}, url} = request;
|
|
37
|
+
return ({original: {method}, url}) => {
|
|
39
38
|
const pathname = deroot(url.pathname);
|
|
40
|
-
const route = find(method, pathname) ??
|
|
41
|
-
|
|
39
|
+
const route = find(method, pathname) ?? errors.NoRouteToPath
|
|
40
|
+
.throw(method, pathname, index(pathname), method.toLowerCase());
|
|
42
41
|
|
|
43
42
|
const path = app.dispatch(keymap(route.pathname?.exec(pathname).groups,
|
|
44
43
|
key => key.split("$")[0]));
|
package/src/loaders/routes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {tryreturn} from "runtime-compat/sync";
|
|
2
|
+
import {from, filter, valmap} from "runtime-compat/object";
|
|
2
3
|
import errors from "../errors.js";
|
|
3
4
|
import {invalid} from "../hooks/route.js";
|
|
4
5
|
import {default as fs, doubled} from "./common.js";
|
|
@@ -22,10 +23,11 @@ const make = path => {
|
|
|
22
23
|
return new RegExp(`^/${route}$`, "u");
|
|
23
24
|
};
|
|
24
25
|
|
|
26
|
+
const specials = ["guards", "errors", "layouts"];
|
|
25
27
|
export default async (log, directory, load = fs) => {
|
|
26
28
|
const routes = await get.routes(log, directory, load);
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
+
const $routes = from(await Promise.all(specials.map(async extra =>
|
|
30
|
+
[extra, await get[extra](log, directory, load)])));
|
|
29
31
|
const filter = path => ([name]) => path.includes(name);
|
|
30
32
|
|
|
31
33
|
return routes.map(([path, imported]) => {
|
|
@@ -38,8 +40,9 @@ export default async (log, directory, load = fs) => {
|
|
|
38
40
|
method,
|
|
39
41
|
handler,
|
|
40
42
|
pathname: make(path.endsWith("/") ? path.slice(0, -1) : path),
|
|
41
|
-
guards: guards.filter(filter(path)).map(([, guard]) => guard),
|
|
42
|
-
|
|
43
|
+
guards: $routes.guards.filter(filter(path)).map(([, guard]) => guard),
|
|
44
|
+
errors: $routes.errors.filter(filter(path)).map(([, error]) => error),
|
|
45
|
+
layouts: $routes.layouts.filter(filter(path)).map(([, layout]) => layout),
|
|
43
46
|
}));
|
|
44
47
|
}).flat();
|
|
45
48
|
};
|
package/src/start.js
CHANGED
|
@@ -21,7 +21,7 @@ export default async (app, operations = {}) => {
|
|
|
21
21
|
tryreturn(async _ => hooks.handle(app)(await app.parse(request)))
|
|
22
22
|
.orelse(error => {
|
|
23
23
|
app.log.auto(error);
|
|
24
|
-
return new Response(null, {status: Status.
|
|
24
|
+
return new Response(null, {status: Status.INTERNAL_SERVER_ERROR});
|
|
25
25
|
}),
|
|
26
26
|
app.config.http);
|
|
27
27
|
|
package/src/validate.js
ADDED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
const mimes = {
|
|
2
|
-
binary: "application/octet-stream",
|
|
3
|
-
css: "text/css",
|
|
4
|
-
html: "text/html",
|
|
5
|
-
jpg: "image/jpeg",
|
|
6
|
-
js: "text/javascript",
|
|
7
|
-
mjs: "text/javascript",
|
|
8
|
-
json: "application/json",
|
|
9
|
-
png: "image/png",
|
|
10
|
-
svg: "image/svg+xml",
|
|
11
|
-
woff2: "font/woff2",
|
|
12
|
-
webp: "image/webp",
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const regex = /\.(?<extension>[a-z1-9]*)$/u;
|
|
16
|
-
const match = filename => filename.match(regex)?.groups.extension;
|
|
17
|
-
|
|
18
|
-
export default filename => mimes[match(filename)] ?? mimes.binary;
|