@primate/core 0.8.7 → 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.
Files changed (66) hide show
  1. package/lib/private/App.d.ts +3 -3
  2. package/lib/private/App.js +10 -3
  3. package/lib/private/build/App.d.ts +15 -6
  4. package/lib/private/build/App.js +61 -26
  5. package/lib/private/build/Loader.d.ts +17 -0
  6. package/lib/private/build/Loader.js +2 -0
  7. package/lib/private/build/client/index.js +4 -2
  8. package/lib/private/build/client/plugin/app-request.js +2 -2
  9. package/lib/private/build/client/plugin/frontend.js +2 -1
  10. package/lib/private/build/client/plugin/route-module.js +4 -4
  11. package/lib/private/build/client/plugin/view.js +4 -0
  12. package/lib/private/build/hook.js +1 -1
  13. package/lib/private/build/server/index.js +3 -3
  14. package/lib/private/build/server/plugin/app-request.js +2 -2
  15. package/lib/private/build/server/plugin/frontend.js +6 -6
  16. package/lib/private/build/server/plugin/route.js +4 -5
  17. package/lib/private/build/server/plugin/view.js +14 -7
  18. package/lib/private/build/server/plugin/views.js +44 -0
  19. package/lib/private/build/server/plugin/virtual-routes.js +6 -3
  20. package/lib/private/build/server/plugin/virtual-templates.d.ts +4 -0
  21. package/lib/private/build/server/plugin/{virtual-pages.js → virtual-templates.js} +14 -14
  22. package/lib/private/build/shared/plugin/app-request.js +2 -2
  23. package/lib/private/client/ViewOptions.d.ts +1 -1
  24. package/lib/private/client/navigate.d.ts +2 -1
  25. package/lib/private/config/schema.d.ts +2 -2
  26. package/lib/private/config/schema.js +3 -2
  27. package/lib/private/db/test.js +47 -0
  28. package/lib/private/errors.d.ts +9 -0
  29. package/lib/private/errors.js +12 -0
  30. package/lib/private/events.d.ts +11 -0
  31. package/lib/private/events.js +24 -0
  32. package/lib/private/frontend.js +19 -8
  33. package/lib/private/location.d.ts +1 -1
  34. package/lib/private/location.js +2 -2
  35. package/lib/private/request/RequestBody.d.ts +2 -2
  36. package/lib/private/request/RequestBody.js +5 -5
  37. package/lib/private/request/route.js +6 -3
  38. package/lib/private/request/router.d.ts +1 -1
  39. package/lib/private/request/router.js +15 -4
  40. package/lib/private/response/ResponseFunction.d.ts +4 -1
  41. package/lib/private/response/ResponseLike.d.ts +1 -1
  42. package/lib/private/response/error.d.ts +2 -2
  43. package/lib/private/response/error.js +2 -2
  44. package/lib/private/response/page.d.ts +6 -0
  45. package/lib/private/response/page.js +12 -0
  46. package/lib/private/response/sse.d.ts +4 -5
  47. package/lib/private/response/sse.js +17 -14
  48. package/lib/private/response/view.d.ts +4 -4
  49. package/lib/private/route/NarrowedRequest.d.ts +7 -5
  50. package/lib/private/route/Options.d.ts +5 -3
  51. package/lib/private/route/router.d.ts +1 -1
  52. package/lib/private/route/router.js +4 -3
  53. package/lib/private/route.client.d.ts +11 -6
  54. package/lib/private/route.client.js +10 -2
  55. package/lib/private/route.d.ts +17 -6
  56. package/lib/private/serve/App.d.ts +11 -3
  57. package/lib/private/serve/App.js +28 -19
  58. package/lib/private/serve/Init.d.ts +2 -1
  59. package/lib/public/events.d.ts +2 -0
  60. package/lib/public/events.js +2 -0
  61. package/lib/public/response.d.ts +5 -6
  62. package/lib/public/response.js +2 -0
  63. package/package.json +20 -20
  64. package/lib/private/Binder.d.ts +0 -11
  65. package/lib/private/Binder.js +0 -2
  66. package/lib/private/build/server/plugin/virtual-pages.d.ts +0 -4
@@ -5,7 +5,7 @@ export default interface ViewOptions extends ResponseInit {
5
5
  style_src?: string[];
6
6
  };
7
7
  head?: string;
8
- page?: string;
8
+ template?: string;
9
9
  partial?: boolean;
10
10
  placeholders?: Omit<Dict, "body" | "head">;
11
11
  }
@@ -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: (hash: string) => void;
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, "localhost">;
12
- port: import("pema").DefaultType<import("pema").UintType<"u32", undefined>, 6161>;
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(),
@@ -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}`;
@@ -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";
@@ -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
@@ -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
- // prebuild
148
- extensions.forEach(e => {
149
- app.bind(e, async (file, { context }) => {
150
- if (context === "views" && !build_ssr)
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 pages: "pages";
6
+ readonly templates: "templates";
7
7
  readonly routes: "routes";
8
8
  readonly static: "static";
9
9
  readonly stores: "stores";
@@ -7,8 +7,8 @@ export default {
7
7
  config: "config",
8
8
  // error rendering template
9
9
  error_html: "error.html",
10
- // HTML pages
11
- pages: "pages",
10
+ // HTML templates
11
+ templates: "templates",
12
12
  // hierarchical routes
13
13
  routes: "routes",
14
14
  // static assets
@@ -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
- layouts: await Promise.all(layouts.map(layout => layout(request))),
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 router = await FileRouter.load({
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
- if (segment.startsWith("+") && !specials.includes(segment.slice(1))) {
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 = (app: ServeApp, transfer: Dict, request: RequestFacade) => MaybePromise<View | null | Response | undefined>;
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
- page?: string;
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.page HTML page to use (default: `error.html`).
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.page HTML page to use (default: `error.html`).
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
- page: options?.page ?? location.error_html,
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 Body = {
2
- close?(): undefined;
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) => new ReadableStream({
5
- cancel() {
6
- body.close?.();
7
- },
8
- start(controller) {
9
- body.open({
10
- send(name, data) {
11
- const event = data === undefined ? "" : `event: ${name}\n`;
12
- const _data = JSON.stringify(data === undefined ? name : data);
13
- controller.enqueue(encode(`${event}data:${_data}\n\n`));
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: (props: Props) => any, props: Props, options?: ViewOptions): ResponseFunction;
6
- declare function view(component: () => any, props?: Dict, options?: ViewOptions): ResponseFunction;
7
- declare function view(name: string, props?: Dict, options?: ViewOptions): ResponseFunction;
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