@primate/core 0.8.8 → 0.9.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/lib/private/App.d.ts +3 -3
- package/lib/private/App.js +10 -3
- package/lib/private/build/App.d.ts +15 -6
- package/lib/private/build/App.js +61 -26
- package/lib/private/build/Loader.d.ts +17 -0
- package/lib/private/build/Loader.js +2 -0
- package/lib/private/build/client/index.js +4 -2
- package/lib/private/build/client/plugin/app-request.js +2 -2
- package/lib/private/build/client/plugin/frontend.js +2 -1
- package/lib/private/build/client/plugin/route-module.js +4 -4
- package/lib/private/build/client/plugin/view.js +4 -0
- package/lib/private/build/hook.js +1 -1
- package/lib/private/build/server/index.js +3 -3
- package/lib/private/build/server/plugin/app-request.js +2 -2
- package/lib/private/build/server/plugin/frontend.js +6 -6
- package/lib/private/build/server/plugin/route.js +4 -5
- package/lib/private/build/server/plugin/view.js +14 -7
- package/lib/private/build/server/plugin/views.js +44 -0
- package/lib/private/build/server/plugin/virtual-routes.js +4 -1
- package/lib/private/build/server/plugin/virtual-templates.d.ts +4 -0
- package/lib/private/build/server/plugin/{virtual-pages.js → virtual-templates.js} +14 -14
- package/lib/private/build/shared/plugin/app-request.js +2 -2
- package/lib/private/client/ViewOptions.d.ts +1 -1
- package/lib/private/client/navigate.d.ts +2 -1
- package/lib/private/config/schema.d.ts +2 -2
- package/lib/private/config/schema.js +3 -2
- package/lib/private/db/test.js +47 -0
- package/lib/private/errors.d.ts +9 -0
- package/lib/private/errors.js +12 -0
- package/lib/private/events.d.ts +11 -0
- package/lib/private/events.js +24 -0
- package/lib/private/frontend.js +19 -8
- package/lib/private/location.d.ts +1 -1
- package/lib/private/location.js +2 -2
- package/lib/private/request/RequestBody.d.ts +2 -2
- package/lib/private/request/RequestBody.js +5 -5
- package/lib/private/request/route.js +6 -3
- package/lib/private/request/router.d.ts +1 -1
- package/lib/private/request/router.js +15 -4
- package/lib/private/response/ResponseFunction.d.ts +4 -1
- package/lib/private/response/ResponseLike.d.ts +1 -1
- package/lib/private/response/error.d.ts +2 -2
- package/lib/private/response/error.js +2 -2
- package/lib/private/response/page.d.ts +6 -0
- package/lib/private/response/page.js +12 -0
- package/lib/private/response/sse.d.ts +4 -5
- package/lib/private/response/sse.js +17 -14
- package/lib/private/response/view.d.ts +4 -4
- package/lib/private/route/NarrowedRequest.d.ts +7 -5
- package/lib/private/route/Options.d.ts +5 -3
- package/lib/private/route/router.js +2 -1
- package/lib/private/route.client.d.ts +11 -6
- package/lib/private/route.client.js +10 -2
- package/lib/private/route.d.ts +17 -6
- package/lib/private/serve/App.d.ts +11 -3
- package/lib/private/serve/App.js +28 -19
- package/lib/private/serve/Init.d.ts +2 -1
- package/lib/public/events.d.ts +2 -0
- package/lib/public/events.js +2 -0
- package/lib/public/response.d.ts +5 -6
- package/lib/public/response.js +2 -0
- package/package.json +20 -20
- package/lib/private/Binder.d.ts +0 -11
- package/lib/private/Binder.js +0 -2
- package/lib/private/build/server/plugin/virtual-pages.d.ts +0 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
declare const scroll_hash: (hash: string) => void;
|
|
1
2
|
type Location = {
|
|
2
3
|
hash: string;
|
|
3
4
|
pathname: string;
|
|
@@ -8,7 +9,7 @@ declare function go(href: string, event?: Event): Promise<void>;
|
|
|
8
9
|
declare const navigate: {
|
|
9
10
|
go: typeof go;
|
|
10
11
|
goto: typeof goto;
|
|
11
|
-
scroll_hash:
|
|
12
|
+
scroll_hash: typeof scroll_hash;
|
|
12
13
|
};
|
|
13
14
|
export default navigate;
|
|
14
15
|
//# sourceMappingURL=navigate.d.ts.map
|
|
@@ -8,8 +8,8 @@ declare const _default: ObjectType<import("pema").NormalizeSchemaObject<{
|
|
|
8
8
|
readonly http: {
|
|
9
9
|
csp: import("pema").OptionalType<import("pema").RecordType<import("pema").StringType, import("pema").ArrayType<import("pema").StringType, undefined>, undefined>>;
|
|
10
10
|
headers: import("pema").OptionalType<import("pema").RecordType<import("pema").StringType, import("pema").StringType, undefined>>;
|
|
11
|
-
host: import("pema").DefaultType<import("pema").StringType,
|
|
12
|
-
port: import("pema").DefaultType<import("pema").UintType<"u32", undefined>,
|
|
11
|
+
host: import("pema").DefaultType<import("pema").StringType, string>;
|
|
12
|
+
port: import("pema").DefaultType<import("pema").UintType<"u32", undefined>, number>;
|
|
13
13
|
ssl: {
|
|
14
14
|
cert: import("pema").OptionalType<import("pema").StringType>;
|
|
15
15
|
key: import("pema").OptionalType<import("pema").StringType>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import env from "@rcompat/env";
|
|
1
2
|
import p from "pema";
|
|
2
3
|
export default p({
|
|
3
4
|
conditions: p.array(p.string).unique().default([]),
|
|
4
5
|
http: {
|
|
5
6
|
csp: p.dict(p.array(p.string)).optional(),
|
|
6
7
|
headers: p.dict().optional(),
|
|
7
|
-
host: p.string.default("localhost"),
|
|
8
|
-
port: p.uint.port().default(6161),
|
|
8
|
+
host: p.string.default(() => env.try("HOST") ?? "localhost"),
|
|
9
|
+
port: p.uint.port().default(() => +(env.try("PORT") ?? 6161)),
|
|
9
10
|
ssl: {
|
|
10
11
|
cert: p.string.optional(),
|
|
11
12
|
key: p.string.optional(),
|
package/lib/private/db/test.js
CHANGED
|
@@ -153,6 +153,7 @@ export default (db) => {
|
|
|
153
153
|
},
|
|
154
154
|
});
|
|
155
155
|
const USER_STORES = [User, UserN, UserB];
|
|
156
|
+
const Status = p.enum({ UNCONFIRMED: 0, CONFIRMED: 1 });
|
|
156
157
|
const Type = store({
|
|
157
158
|
table: "type",
|
|
158
159
|
db,
|
|
@@ -175,6 +176,7 @@ export default (db) => {
|
|
|
175
176
|
u64: p.u64.optional(),
|
|
176
177
|
u8: p.u8.optional(),
|
|
177
178
|
json: p.json().optional(),
|
|
179
|
+
status: Status.optional(),
|
|
178
180
|
},
|
|
179
181
|
});
|
|
180
182
|
// this stresses identifier quoting in CREATE/INSERT/SELECT/UPDATE/DELETE
|
|
@@ -678,6 +680,51 @@ export default (db) => {
|
|
|
678
680
|
assert((await Type.get(t.id))[any(k)]).equals(ub);
|
|
679
681
|
});
|
|
680
682
|
});
|
|
683
|
+
$type("enum: stores and retrieves by underlying value", async (assert) => {
|
|
684
|
+
const t = await Type.insert({ status: Status.CONFIRMED });
|
|
685
|
+
assert(t.status).equals(1);
|
|
686
|
+
assert((await Type.get(t.id)).status).equals(1);
|
|
687
|
+
});
|
|
688
|
+
$type("enum: round-trips both declared members", async (assert) => {
|
|
689
|
+
const u = await Type.insert({ status: Status.UNCONFIRMED });
|
|
690
|
+
const c = await Type.insert({ status: Status.CONFIRMED });
|
|
691
|
+
assert((await Type.get(u.id)).status).equals(0);
|
|
692
|
+
assert((await Type.get(c.id)).status).equals(1);
|
|
693
|
+
});
|
|
694
|
+
$type("enum: update changes stored value", async (assert) => {
|
|
695
|
+
const t = await Type.insert({ status: Status.UNCONFIRMED });
|
|
696
|
+
await Type.update(t.id, { set: { status: Status.CONFIRMED } });
|
|
697
|
+
assert((await Type.get(t.id)).status).equals(Status.CONFIRMED);
|
|
698
|
+
});
|
|
699
|
+
$type("enum: rejects value outside declared set", async (assert) => {
|
|
700
|
+
try {
|
|
701
|
+
// 2 is a valid u8 but not a declared enum member
|
|
702
|
+
await Type.insert({ status: 2 });
|
|
703
|
+
assert("[did not throw]").equals("[threw]");
|
|
704
|
+
}
|
|
705
|
+
catch (e) {
|
|
706
|
+
assert(e.message).type();
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
$type("enum: nameOf resolves stored value to its key", async (assert) => {
|
|
710
|
+
const t = await Type.insert({ status: Status.CONFIRMED });
|
|
711
|
+
const got = await Type.get(t.id);
|
|
712
|
+
assert(Status.nameOf(got.status)).equals("CONFIRMED");
|
|
713
|
+
});
|
|
714
|
+
$type("enum: optional field can be omitted", async (assert) => {
|
|
715
|
+
const t = await Type.insert({ string: "no-status" });
|
|
716
|
+
assert(t.status).undefined();
|
|
717
|
+
assert((await Type.get(t.id)).status).undefined();
|
|
718
|
+
});
|
|
719
|
+
$type("enum: where clause matches on underlying value", async (assert) => {
|
|
720
|
+
await Type.insert({ string: "enum-where", status: Status.CONFIRMED });
|
|
721
|
+
await Type.insert({ string: "enum-where", status: Status.UNCONFIRMED });
|
|
722
|
+
const confirmed = await Type.find({
|
|
723
|
+
where: { string: "enum-where", status: Status.CONFIRMED },
|
|
724
|
+
});
|
|
725
|
+
assert(confirmed.length).equals(1);
|
|
726
|
+
assert(confirmed[0].status).equals(Status.CONFIRMED);
|
|
727
|
+
});
|
|
681
728
|
[64n, 128n].forEach(i => {
|
|
682
729
|
$type(`i${i}`, async (assert) => {
|
|
683
730
|
const k = `i${i}`;
|
package/lib/private/errors.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ declare function build_body_requires_content_type(): import("@rcompat/error").Te
|
|
|
15
15
|
declare function build_unrecognized_loader(seen: string): import("@rcompat/error").TemplateError;
|
|
16
16
|
declare function build_path_schema_mismatch(route: string, method: string, expected: string[], declared: string[]): import("@rcompat/error").TemplateError;
|
|
17
17
|
declare function build_reserved_entrypoints(conflicts: string[]): import("@rcompat/error").TemplateError;
|
|
18
|
+
declare function build_duplicate_route_page(route: string): import("@rcompat/error").TemplateError;
|
|
18
19
|
declare function config_file_missing(): import("@rcompat/error").TemplateError;
|
|
19
20
|
declare function config_file_empty(file: FileRef): import("@rcompat/error").TemplateError;
|
|
20
21
|
declare function config_file_error(file: FileRef, cause: Error): import("@rcompat/error").TemplateError;
|
|
@@ -28,6 +29,8 @@ declare function request_body_already_parsed(): import("@rcompat/error").Templat
|
|
|
28
29
|
declare function request_bag_missing_key(bag: string, key: string): import("@rcompat/error").TemplateError;
|
|
29
30
|
declare function request_content_type_mismatch(expected: MIMEValue, actual: string): import("@rcompat/error").TemplateError;
|
|
30
31
|
declare function response_invalid_body(body: string): import("@rcompat/error").TemplateError;
|
|
32
|
+
declare function response_page_context_missing(): import("@rcompat/error").TemplateError;
|
|
33
|
+
declare function response_page_missing(route: string): import("@rcompat/error").TemplateError;
|
|
31
34
|
declare function hook_route_functions_not_allowed(file: string): import("@rcompat/error").TemplateError;
|
|
32
35
|
declare function hook_not_allowed(file: string): import("@rcompat/error").TemplateError;
|
|
33
36
|
declare function hook_unused(file: string): import("@rcompat/error").TemplateError;
|
|
@@ -64,6 +67,7 @@ declare const errors: {
|
|
|
64
67
|
build_unrecognized_loader: typeof build_unrecognized_loader;
|
|
65
68
|
build_path_schema_mismatch: typeof build_path_schema_mismatch;
|
|
66
69
|
build_reserved_entrypoints: typeof build_reserved_entrypoints;
|
|
70
|
+
build_duplicate_route_page: typeof build_duplicate_route_page;
|
|
67
71
|
config_file_missing: typeof config_file_missing;
|
|
68
72
|
config_missing: typeof config_missing;
|
|
69
73
|
config_file_empty: typeof config_file_empty;
|
|
@@ -77,6 +81,8 @@ declare const errors: {
|
|
|
77
81
|
request_bag_missing_key: typeof request_bag_missing_key;
|
|
78
82
|
request_content_type_mismatch: typeof request_content_type_mismatch;
|
|
79
83
|
response_invalid_body: typeof response_invalid_body;
|
|
84
|
+
response_page_context_missing: typeof response_page_context_missing;
|
|
85
|
+
response_page_missing: typeof response_page_missing;
|
|
80
86
|
hook_route_functions_not_allowed: typeof hook_route_functions_not_allowed;
|
|
81
87
|
hook_not_allowed: typeof hook_not_allowed;
|
|
82
88
|
hook_unused: typeof hook_unused;
|
|
@@ -114,6 +120,7 @@ export declare const Code: {
|
|
|
114
120
|
build_unrecognized_loader: "build_unrecognized_loader";
|
|
115
121
|
build_path_schema_mismatch: "build_path_schema_mismatch";
|
|
116
122
|
build_reserved_entrypoints: "build_reserved_entrypoints";
|
|
123
|
+
build_duplicate_route_page: "build_duplicate_route_page";
|
|
117
124
|
config_file_missing: "config_file_missing";
|
|
118
125
|
config_missing: "config_missing";
|
|
119
126
|
config_file_empty: "config_file_empty";
|
|
@@ -127,6 +134,8 @@ export declare const Code: {
|
|
|
127
134
|
request_bag_missing_key: "request_bag_missing_key";
|
|
128
135
|
request_content_type_mismatch: "request_content_type_mismatch";
|
|
129
136
|
response_invalid_body: "response_invalid_body";
|
|
137
|
+
response_page_context_missing: "response_page_context_missing";
|
|
138
|
+
response_page_missing: "response_page_missing";
|
|
130
139
|
hook_route_functions_not_allowed: "hook_route_functions_not_allowed";
|
|
131
140
|
hook_not_allowed: "hook_not_allowed";
|
|
132
141
|
hook_unused: "hook_unused";
|
package/lib/private/errors.js
CHANGED
|
@@ -43,6 +43,9 @@ function build_path_schema_mismatch(route, method, expected, declared) {
|
|
|
43
43
|
function build_reserved_entrypoints(conflicts) {
|
|
44
44
|
return t `entrypoint name(s) reserved: ${conflicts.map(n => `"${n}"`).join(", ")}`;
|
|
45
45
|
}
|
|
46
|
+
function build_duplicate_route_page(route) {
|
|
47
|
+
return t `multiple pages found for route ${route}`;
|
|
48
|
+
}
|
|
46
49
|
const BUILD = error.coded({
|
|
47
50
|
build_missing_binary_addon,
|
|
48
51
|
build_missing_route,
|
|
@@ -53,6 +56,7 @@ const BUILD = error.coded({
|
|
|
53
56
|
build_unrecognized_loader,
|
|
54
57
|
build_path_schema_mismatch,
|
|
55
58
|
build_reserved_entrypoints,
|
|
59
|
+
build_duplicate_route_page,
|
|
56
60
|
});
|
|
57
61
|
function config_file_missing() {
|
|
58
62
|
return t `missing ${"config/app.ts"}`;
|
|
@@ -114,8 +118,16 @@ const REQUEST = error.coded({
|
|
|
114
118
|
function response_invalid_body(body) {
|
|
115
119
|
return t `invalid body ${body} returned from route`;
|
|
116
120
|
}
|
|
121
|
+
function response_page_context_missing() {
|
|
122
|
+
return t `${"response.page"} requires a route context`;
|
|
123
|
+
}
|
|
124
|
+
function response_page_missing(route) {
|
|
125
|
+
return t `no page found for route ${route}`;
|
|
126
|
+
}
|
|
117
127
|
const RESPONSE = error.coded({
|
|
118
128
|
response_invalid_body,
|
|
129
|
+
response_page_context_missing,
|
|
130
|
+
response_page_missing,
|
|
119
131
|
});
|
|
120
132
|
function hook_route_functions_not_allowed(file) {
|
|
121
133
|
return t `${file}: route functions may not be used inside +hook files`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Listener<Event> = (event: Event) => void;
|
|
2
|
+
type Channel<Key, Event> = Readonly<{
|
|
3
|
+
emit(key: Key, event: Event): void;
|
|
4
|
+
subscribe(key: Key, listener: Listener<Event>): () => void;
|
|
5
|
+
}>;
|
|
6
|
+
declare function channel<Key, Event>(): Channel<Key, Event>;
|
|
7
|
+
declare const events: {
|
|
8
|
+
channel: typeof channel;
|
|
9
|
+
};
|
|
10
|
+
export default events;
|
|
11
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function channel() {
|
|
2
|
+
const listeners = new Map();
|
|
3
|
+
return {
|
|
4
|
+
emit(key, event) {
|
|
5
|
+
for (const listener of [...listeners.get(key) ?? []])
|
|
6
|
+
listener(event);
|
|
7
|
+
},
|
|
8
|
+
subscribe(key, listener) {
|
|
9
|
+
const set = listeners.get(key) ?? new Set();
|
|
10
|
+
set.add(listener);
|
|
11
|
+
listeners.set(key, set);
|
|
12
|
+
return () => {
|
|
13
|
+
set.delete(listener);
|
|
14
|
+
if (set.size === 0)
|
|
15
|
+
listeners.delete(key);
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const events = {
|
|
21
|
+
channel,
|
|
22
|
+
};
|
|
23
|
+
export default events;
|
|
24
|
+
//# sourceMappingURL=events.js.map
|
package/lib/private/frontend.js
CHANGED
|
@@ -144,17 +144,19 @@ export default function frontend_module(init) {
|
|
|
144
144
|
(app.mode !== "development" || !init.client);
|
|
145
145
|
const compile_server = init.compile?.server;
|
|
146
146
|
const compile_client = init.compile?.client;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
app.register(module_name, {
|
|
148
|
+
type: "frontend",
|
|
149
|
+
extensions: [...extensions],
|
|
150
|
+
client: init.client,
|
|
151
|
+
async onLoad(file) {
|
|
152
|
+
if (!build_ssr)
|
|
151
153
|
return "";
|
|
152
154
|
// production: just compile to JS, don't bundle yet
|
|
153
155
|
if (compile_server !== undefined) {
|
|
154
156
|
return await compile_server(await file.text(), file);
|
|
155
157
|
}
|
|
156
158
|
return await file.text();
|
|
157
|
-
}
|
|
159
|
+
},
|
|
158
160
|
});
|
|
159
161
|
// build
|
|
160
162
|
if (init.root !== undefined && compile_server !== undefined) {
|
|
@@ -164,7 +166,6 @@ export default function frontend_module(init) {
|
|
|
164
166
|
// publish
|
|
165
167
|
if (compile_client !== undefined) {
|
|
166
168
|
if (init.client) {
|
|
167
|
-
app.frontends.set(module_name, [...extensions]);
|
|
168
169
|
conditions.forEach(c => app.conditions.add(c));
|
|
169
170
|
}
|
|
170
171
|
app.plugin("client", {
|
|
@@ -198,6 +199,7 @@ export default function frontend_module(init) {
|
|
|
198
199
|
}
|
|
199
200
|
const views_filter = new RegExp(`^${module_name}:views`);
|
|
200
201
|
const views_base = app.root.join(location.views);
|
|
202
|
+
const pages_base = app.root.join(location.routes);
|
|
201
203
|
build.onResolve({ filter: views_filter }, ({ path }) => {
|
|
202
204
|
return { namespace: `${module_name}`, path };
|
|
203
205
|
});
|
|
@@ -206,17 +208,26 @@ export default function frontend_module(init) {
|
|
|
206
208
|
recursive: true,
|
|
207
209
|
filter: c => extensions.includes(c.fullExtension),
|
|
208
210
|
});
|
|
211
|
+
const pages = await pages_base.files({
|
|
212
|
+
recursive: true,
|
|
213
|
+
filter: c => extensions.includes(c.fullExtension),
|
|
214
|
+
});
|
|
209
215
|
let contents = "";
|
|
210
216
|
for (const view of views) {
|
|
211
217
|
const { path } = view.debase(views_base, "/");
|
|
212
218
|
contents += `export { default as ${await normalize(path)} }
|
|
213
219
|
from "view:${path}";\n`;
|
|
220
|
+
}
|
|
221
|
+
for (const page of pages) {
|
|
222
|
+
const { path } = page.debase(pages_base, "/");
|
|
223
|
+
contents += `export { default as ${await normalize(`$page/${path}`)} }
|
|
224
|
+
from "page:${path}";\n`;
|
|
214
225
|
}
|
|
215
226
|
return {
|
|
216
227
|
contents,
|
|
217
228
|
resolveDir: app.root.path,
|
|
218
|
-
watchDirs: [views_base.path],
|
|
219
|
-
watchFiles: views.map(view => view.path),
|
|
229
|
+
watchDirs: [views_base.path, pages_base.path],
|
|
230
|
+
watchFiles: views.map(view => view.path).concat(pages.map(page => page.path)),
|
|
220
231
|
};
|
|
221
232
|
});
|
|
222
233
|
const filter = new RegExp(`(${extensions.map(e => e.replace(".", "\\.")).join("|")})$`);
|
|
@@ -3,7 +3,7 @@ declare const _default: {
|
|
|
3
3
|
readonly client: "client";
|
|
4
4
|
readonly config: "config";
|
|
5
5
|
readonly error_html: "error.html";
|
|
6
|
-
readonly
|
|
6
|
+
readonly templates: "templates";
|
|
7
7
|
readonly routes: "routes";
|
|
8
8
|
readonly static: "static";
|
|
9
9
|
readonly stores: "stores";
|
package/lib/private/location.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import http from "@rcompat/http";
|
|
2
2
|
import type { Dict, JSONValue } from "@rcompat/type";
|
|
3
|
-
import type { Parsed } from "pema";
|
|
3
|
+
import type { AsyncType, Parsed } from "pema";
|
|
4
4
|
type Form = Dict<string>;
|
|
5
5
|
type Files = Dict<File>;
|
|
6
6
|
type MIMES = typeof http.MIME;
|
|
7
7
|
type MIME = MIMES[keyof MIMES];
|
|
8
8
|
type BodyOptions = {
|
|
9
9
|
contentType: MIME;
|
|
10
|
-
schema: Parsed<unknown
|
|
10
|
+
schema: Parsed<unknown> | AsyncType;
|
|
11
11
|
};
|
|
12
12
|
declare const patched: unique symbol;
|
|
13
13
|
export default class RequestBody {
|
|
@@ -26,7 +26,7 @@ export default class RequestBody {
|
|
|
26
26
|
this.#parse(http.MIME.APPLICATION_JSON);
|
|
27
27
|
const raw = await this.#request.json();
|
|
28
28
|
return this.#options?.contentType === http.MIME.APPLICATION_JSON
|
|
29
|
-
? this.#options.schema.parse(raw)
|
|
29
|
+
? await this.#options.schema.parse(raw)
|
|
30
30
|
: raw;
|
|
31
31
|
}
|
|
32
32
|
async form() {
|
|
@@ -37,7 +37,7 @@ export default class RequestBody {
|
|
|
37
37
|
}
|
|
38
38
|
const raw = form;
|
|
39
39
|
return this.#options?.contentType === http.MIME.APPLICATION_X_WWW_FORM_URLENCODED
|
|
40
|
-
? this.#options.schema.parse(raw)
|
|
40
|
+
? await this.#options.schema.parse(raw)
|
|
41
41
|
: raw;
|
|
42
42
|
}
|
|
43
43
|
async multipart() {
|
|
@@ -54,21 +54,21 @@ export default class RequestBody {
|
|
|
54
54
|
}
|
|
55
55
|
const raw = { form, files };
|
|
56
56
|
return this.#options?.contentType === http.MIME.MULTIPART_FORM_DATA
|
|
57
|
-
? this.#options.schema.parse(raw)
|
|
57
|
+
? await this.#options.schema.parse(raw)
|
|
58
58
|
: raw;
|
|
59
59
|
}
|
|
60
60
|
async text() {
|
|
61
61
|
this.#parse(http.MIME.TEXT_PLAIN);
|
|
62
62
|
const raw = await this.#request.text();
|
|
63
63
|
return this.#options?.contentType === http.MIME.TEXT_PLAIN
|
|
64
|
-
? this.#options.schema.parse(raw)
|
|
64
|
+
? await this.#options.schema.parse(raw)
|
|
65
65
|
: raw;
|
|
66
66
|
}
|
|
67
67
|
async blob() {
|
|
68
68
|
this.#parse(http.MIME.APPLICATION_OCTET_STREAM);
|
|
69
69
|
const raw = await this.#request.blob();
|
|
70
70
|
return this.#options?.contentType === http.MIME.APPLICATION_OCTET_STREAM
|
|
71
|
-
? this.#options.schema.parse(raw)
|
|
71
|
+
? await this.#options.schema.parse(raw)
|
|
72
72
|
: raw;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -46,14 +46,17 @@ export default async function (app, partial_request) {
|
|
|
46
46
|
app.log.trace `no route for pathname ${partial_request.url.pathname}`;
|
|
47
47
|
return response_error()(app, {}, partial_request);
|
|
48
48
|
}
|
|
49
|
-
const { errors, hooks = [], layouts, handler, is_head } = route;
|
|
49
|
+
const { errors, hooks = [], layouts, handler, is_head, path } = route;
|
|
50
50
|
errorRoute = errors[0];
|
|
51
51
|
const internal_hooks = app.route_hooks.map(wrap_hook);
|
|
52
52
|
const route_hooks = hooks.map(wrap_hook);
|
|
53
53
|
const last = async (req, _next) => result(req, await handler(req));
|
|
54
54
|
const { request, response } = await run([...internal_hooks, ...route_hooks, last], route.request);
|
|
55
55
|
const full = await respond(response)(app, {
|
|
56
|
-
|
|
56
|
+
route: path,
|
|
57
|
+
layouts: await Promise.all(layouts.map(({ handler, path }) => Promise.resolve(handler(request)).then(response => typeof response === "function"
|
|
58
|
+
? (app, transfer, request) => response(app, { ...transfer, route: path }, request)
|
|
59
|
+
: response))),
|
|
57
60
|
}, request);
|
|
58
61
|
return is_head
|
|
59
62
|
? new Response(null, { headers: full.headers, status: full.status })
|
|
@@ -70,7 +73,7 @@ export default async function (app, partial_request) {
|
|
|
70
73
|
app.log.error(error);
|
|
71
74
|
// the +error.js page itself could fail
|
|
72
75
|
try {
|
|
73
|
-
return respond(await errorRoute(request))(app, {}, request);
|
|
76
|
+
return respond(await errorRoute.handler(request))(app, { route: errorRoute.path }, request);
|
|
74
77
|
}
|
|
75
78
|
catch {
|
|
76
79
|
return response_error()(app, {}, request);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FileRef } from "@rcompat/fs";
|
|
2
2
|
import FileRouter from "@rcompat/fs/FileRouter";
|
|
3
3
|
export default _default;
|
|
4
|
-
declare function _default(directory: FileRef, extensions: string[]): Promise<FileRouter>;
|
|
4
|
+
declare function _default(directory: FileRef, extensions: string[], excluded_extensions?: string[]): Promise<FileRouter>;
|
|
5
5
|
//# sourceMappingURL=router.d.ts.map
|
|
@@ -3,8 +3,18 @@ import FileRouter from "@rcompat/fs/FileRouter";
|
|
|
3
3
|
const re = /^[a-zA-Z0-9\-_+[\].]+$/;
|
|
4
4
|
const specials = ["error", "hook", "layout"];
|
|
5
5
|
const p = /^(?:[^[\]]+|\[(?:\.{3})?[a-zA-Z0-9_]+\]|\[\[(?:\.{3})?[a-zA-Z0-9_]+\]\])$/;
|
|
6
|
-
export default async (directory, extensions) => {
|
|
7
|
-
const
|
|
6
|
+
export default async (directory, extensions, excluded_extensions = []) => {
|
|
7
|
+
const escaped = extensions.map(e => e.replace(".", "\\."));
|
|
8
|
+
const filter = new RegExp(`^.*(${escaped.join("|")})$`, "u");
|
|
9
|
+
const route_files = (await directory.files({ filter, recursive: true }))
|
|
10
|
+
.filter(file => !excluded_extensions.some(ext => file.path.endsWith(ext)))
|
|
11
|
+
.map(file => {
|
|
12
|
+
const extension = extensions
|
|
13
|
+
.toSorted((a, b) => b.length - a.length)
|
|
14
|
+
.find(extension => file.path.endsWith(extension));
|
|
15
|
+
return file.debase(directory).path.slice(1, -extension.length);
|
|
16
|
+
});
|
|
17
|
+
const router = FileRouter.init({
|
|
8
18
|
directory: directory.toString(),
|
|
9
19
|
extensions,
|
|
10
20
|
specials: {
|
|
@@ -12,12 +22,13 @@ export default async (directory, extensions) => {
|
|
|
12
22
|
hook: { recursive: true },
|
|
13
23
|
layout: { recursive: true },
|
|
14
24
|
},
|
|
15
|
-
});
|
|
25
|
+
}, route_files);
|
|
16
26
|
router.all().map(route => {
|
|
17
27
|
const { path, segment } = route;
|
|
18
28
|
if (!re.test(segment))
|
|
19
29
|
throw E.route_invalid_characters(path, re);
|
|
20
|
-
|
|
30
|
+
const special = segment.slice(1).split(".")[0];
|
|
31
|
+
if (segment.startsWith("+") && !specials.includes(special)) {
|
|
21
32
|
throw E.route_invalid_special_file(path);
|
|
22
33
|
}
|
|
23
34
|
if (!p.test(segment))
|
|
@@ -2,6 +2,9 @@ import type View from "#client/View";
|
|
|
2
2
|
import type RequestFacade from "#request/RequestFacade";
|
|
3
3
|
import type ServeApp from "#serve/App";
|
|
4
4
|
import type { Dict, MaybePromise } from "@rcompat/type";
|
|
5
|
-
type ResponseFunction =
|
|
5
|
+
type ResponseFunction<Props = unknown> = {
|
|
6
|
+
(app: ServeApp, transfer: Dict, request: RequestFacade): MaybePromise<View | null | Response | undefined>;
|
|
7
|
+
readonly _props?: Props;
|
|
8
|
+
};
|
|
6
9
|
export { ResponseFunction as default };
|
|
7
10
|
//# sourceMappingURL=ResponseFunction.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type ResponseFunction from "#response/ResponseFunction";
|
|
2
2
|
import type Streamable from "@rcompat/fs/Streamable";
|
|
3
3
|
import type { Dict } from "@rcompat/type";
|
|
4
|
-
type ResponseLike = string | Dict | Dict[] | URL | Blob | ReadableStream | Streamable | Response | ResponseFunction | null;
|
|
4
|
+
type ResponseLike = string | Dict | Dict[] | URL | Blob | ReadableStream | Streamable | Response | ResponseFunction<any> | null;
|
|
5
5
|
export { ResponseLike as default };
|
|
6
6
|
//# sourceMappingURL=ResponseLike.d.ts.map
|
|
@@ -2,7 +2,7 @@ import type ResponseFunction from "#response/ResponseFunction";
|
|
|
2
2
|
import type { ValidStatus } from "@rcompat/http";
|
|
3
3
|
type Options = {
|
|
4
4
|
body?: string;
|
|
5
|
-
|
|
5
|
+
template?: string;
|
|
6
6
|
status?: ValidStatus;
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
@@ -10,7 +10,7 @@ type Options = {
|
|
|
10
10
|
*
|
|
11
11
|
* @param options.body HTML `%body%` replacement (default: `"Not Found"`).
|
|
12
12
|
* @param options.status Request status (default: `404 Not Found`).
|
|
13
|
-
* @param options.
|
|
13
|
+
* @param options.template HTML template to use (default: `error.html`).
|
|
14
14
|
* @return Response function.
|
|
15
15
|
*/
|
|
16
16
|
export default function error(options?: Options): ResponseFunction;
|
|
@@ -8,14 +8,14 @@ const sse_reload = `<script>
|
|
|
8
8
|
*
|
|
9
9
|
* @param options.body HTML `%body%` replacement (default: `"Not Found"`).
|
|
10
10
|
* @param options.status Request status (default: `404 Not Found`).
|
|
11
|
-
* @param options.
|
|
11
|
+
* @param options.template HTML template to use (default: `error.html`).
|
|
12
12
|
* @return Response function.
|
|
13
13
|
*/
|
|
14
14
|
export default function error(options) {
|
|
15
15
|
return app => app.view({
|
|
16
16
|
body: options?.body ?? "Not Found",
|
|
17
17
|
head: app.mode === "development" ? sse_reload : undefined,
|
|
18
|
-
|
|
18
|
+
template: options?.template ?? location.error_html,
|
|
19
19
|
status: options?.status ?? http.Status.NOT_FOUND,
|
|
20
20
|
});
|
|
21
21
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type ViewOptions from "#client/ViewOptions";
|
|
2
|
+
import type ResponseFunction from "#response/ResponseFunction";
|
|
3
|
+
import type { Dict } from "@rcompat/type";
|
|
4
|
+
declare function page<Props extends Dict>(props?: Props, options?: ViewOptions): ResponseFunction<Props>;
|
|
5
|
+
export default page;
|
|
6
|
+
//# sourceMappingURL=page.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import E from "#errors";
|
|
2
|
+
import response_view from "#response/view";
|
|
3
|
+
import is from "@rcompat/is";
|
|
4
|
+
function page(props, options) {
|
|
5
|
+
return (app, transfer, request) => {
|
|
6
|
+
if (!is.string(transfer.route))
|
|
7
|
+
throw E.response_page_context_missing();
|
|
8
|
+
return response_view(app.page(transfer.route), props, options)(app, transfer, request);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export default page;
|
|
12
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
type
|
|
2
|
-
|
|
3
|
-
open(events: {
|
|
4
|
-
send(name: string, data: unknown): undefined;
|
|
5
|
-
}): undefined;
|
|
1
|
+
type Source = {
|
|
2
|
+
send(name: string, data: unknown): void;
|
|
6
3
|
};
|
|
4
|
+
type Cleanup = void | (() => void);
|
|
5
|
+
type Body = (source: Source) => Cleanup;
|
|
7
6
|
/**
|
|
8
7
|
* Open a server-sent event stream
|
|
9
8
|
* @param body implementation body
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import response from "#response";
|
|
2
2
|
import http from "@rcompat/http";
|
|
3
3
|
const encode = (input) => new TextEncoder().encode(input);
|
|
4
|
-
const handle = (body) =>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
4
|
+
const handle = (body) => {
|
|
5
|
+
let cleanup;
|
|
6
|
+
return new ReadableStream({
|
|
7
|
+
cancel() {
|
|
8
|
+
cleanup?.();
|
|
9
|
+
},
|
|
10
|
+
start(controller) {
|
|
11
|
+
cleanup = body({
|
|
12
|
+
send(name, data) {
|
|
13
|
+
const event = data === undefined ? "" : `event: ${name}\n`;
|
|
14
|
+
const _data = JSON.stringify(data === undefined ? name : data);
|
|
15
|
+
controller.enqueue(encode(`${event}data:${_data}\n\n`));
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
};
|
|
18
21
|
/**
|
|
19
22
|
* Open a server-sent event stream
|
|
20
23
|
* @param body implementation body
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type ViewOptions from "#client/ViewOptions";
|
|
2
2
|
import type ResponseFunction from "#response/ResponseFunction";
|
|
3
3
|
import type { Dict } from "@rcompat/type";
|
|
4
|
-
declare function view(component: abstract new (...args: any[]) => any, props?: Dict, options?: ViewOptions): ResponseFunction
|
|
5
|
-
declare function view<Props>(component:
|
|
6
|
-
declare function view(component:
|
|
7
|
-
declare function view(name: string, props?:
|
|
4
|
+
declare function view(component: abstract new (...args: any[]) => any, props?: Dict, options?: ViewOptions): ResponseFunction<Dict>;
|
|
5
|
+
declare function view<Props>(component: unknown, props: Props, options?: ViewOptions): ResponseFunction<Props>;
|
|
6
|
+
declare function view(component: unknown, props?: Dict, options?: ViewOptions): ResponseFunction<Dict>;
|
|
7
|
+
declare function view<Props extends Dict>(name: string, props?: Props, options?: ViewOptions): ResponseFunction<Props>;
|
|
8
8
|
export default view;
|
|
9
9
|
//# sourceMappingURL=view.d.ts.map
|
|
@@ -3,7 +3,9 @@ import type RequestBody from "#request/RequestBody";
|
|
|
3
3
|
import type RequestFacade from "#request/RequestFacade";
|
|
4
4
|
import type RouteOptions from "#route/Options";
|
|
5
5
|
import type { Unpack } from "@rcompat/type";
|
|
6
|
-
|
|
6
|
+
type InferSchema<S> = S extends {
|
|
7
|
+
infer: infer I;
|
|
8
|
+
} ? Awaited<I> : never;
|
|
7
9
|
type ContentTypeMethod = {
|
|
8
10
|
"application/json": "json";
|
|
9
11
|
"text/plain": "text";
|
|
@@ -13,13 +15,13 @@ type ContentTypeMethod = {
|
|
|
13
15
|
};
|
|
14
16
|
type NarrowedBody<O extends RouteOptions, B extends RequestBody> = O extends {
|
|
15
17
|
contentType: infer CT extends keyof ContentTypeMethod;
|
|
16
|
-
body:
|
|
18
|
+
body: infer S;
|
|
17
19
|
} ? Omit<B, ContentTypeMethod[CT]> & {
|
|
18
|
-
[K in ContentTypeMethod[CT]]: () => Promise<Unpack<
|
|
20
|
+
[K in ContentTypeMethod[CT]]: () => Promise<Unpack<InferSchema<S>>>;
|
|
19
21
|
} : B;
|
|
20
22
|
type NarrowedPath<O extends RouteOptions> = O extends {
|
|
21
|
-
path:
|
|
22
|
-
} ? RequestBag<Unpack<
|
|
23
|
+
path: infer S;
|
|
24
|
+
} ? RequestBag<Unpack<InferSchema<S>>> : RequestBag;
|
|
23
25
|
type NarrowedRequest<O extends RouteOptions> = Omit<RequestFacade, "body" | "path"> & {
|
|
24
26
|
body: NarrowedBody<O, RequestBody>;
|
|
25
27
|
path: NarrowedPath<O>;
|