@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.
Files changed (65) 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 +4 -1
  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.js +2 -1
  52. package/lib/private/route.client.d.ts +11 -6
  53. package/lib/private/route.client.js +10 -2
  54. package/lib/private/route.d.ts +17 -6
  55. package/lib/private/serve/App.d.ts +11 -3
  56. package/lib/private/serve/App.js +28 -19
  57. package/lib/private/serve/Init.d.ts +2 -1
  58. package/lib/public/events.d.ts +2 -0
  59. package/lib/public/events.js +2 -0
  60. package/lib/public/response.d.ts +5 -6
  61. package/lib/public/response.js +2 -0
  62. package/package.json +20 -20
  63. package/lib/private/Binder.d.ts +0 -11
  64. package/lib/private/Binder.js +0 -2
  65. package/lib/private/build/server/plugin/virtual-pages.d.ts +0 -4
@@ -1,11 +1,13 @@
1
1
  import type http from "@rcompat/http";
2
- import type { ObjectType, Parsed } from "pema";
2
+ import type { AsyncType, ObjectType } from "pema";
3
+ import type { Parsed } from "pema";
3
4
  type MIME = Omit<typeof http.MIME, "resolve" | "extension">;
4
5
  type MIMEValue = MIME[keyof MIME];
6
+ type BodySchema = Parsed<unknown> | AsyncType;
5
7
  type RouteOptions = {
6
8
  contentType?: MIMEValue;
7
- body?: Parsed<unknown>;
8
- path?: ObjectType;
9
+ body?: BodySchema;
10
+ path?: ObjectType | AsyncType;
9
11
  };
10
12
  export type { RouteOptions as default };
11
13
  //# sourceMappingURL=Options.d.ts.map
@@ -16,7 +16,8 @@ class Router {
16
16
  throw E.hook_route_functions_not_allowed(path);
17
17
  if (options?.path !== undefined) {
18
18
  const declared = Object.keys(options.path.properties);
19
- const expected = [...path.matchAll(/\[([^\]]+)\]/g)].map(m => m[1]);
19
+ const expected = [...path.matchAll(/\[([^\]]+)\]/g)]
20
+ .map(m => m[1].replace(/^[\[.]+/, ""));
20
21
  const missing = expected.filter(k => !declared.includes(k));
21
22
  const extra = declared.filter(k => !expected.includes(k));
22
23
  if (missing.length > 0 || extra.length > 0) {
@@ -1,27 +1,32 @@
1
1
  import type ResponseLike from "#response/ResponseLike";
2
+ import type ResponseFunction from "#response/ResponseFunction";
2
3
  import type ContentTypeMap from "#route/ContentTypeMap";
3
4
  import type RouteHandler from "#route/Handler";
4
5
  import hook from "#route/hook";
5
6
  import type RouteOptions from "#route/Options";
6
7
  import type { Unpack } from "@rcompat/type";
7
- import type { Parsed } from "pema";
8
+ type InferSchema<S> = S extends {
9
+ infer: infer I;
10
+ } ? Awaited<I> : never;
8
11
  type Body<O extends RouteOptions> = O extends {
9
12
  contentType: infer _CT extends keyof ContentTypeMap;
10
- body: infer S extends Parsed<unknown>;
11
- } ? Unpack<S["infer"]> : O extends {
13
+ body: infer S;
14
+ } ? Unpack<InferSchema<S>> : O extends {
12
15
  contentType: infer CT extends keyof ContentTypeMap;
13
16
  } ? ContentTypeMap[CT] : never;
14
17
  type Path<O extends RouteOptions> = O extends {
15
- path: infer S extends Parsed<unknown>;
16
- } ? Unpack<S["infer"]> : never;
18
+ path: infer S;
19
+ } ? Unpack<InferSchema<S>> : never;
17
20
  type MethodMeta = {
18
21
  contentType?: string;
19
22
  };
20
23
  type RequestMeta = {
21
24
  headers?: HeadersInit;
22
25
  };
26
+ type Page<R> = Awaited<R> extends ResponseFunction<infer Props> ? Props : never;
23
27
  type ClientMethod<O extends RouteOptions, R = unknown> = MethodMeta & {
24
28
  _result?: R;
29
+ Page: Page<R>;
25
30
  } & (Body<O> extends never ? Path<O> extends never ? (args?: RequestMeta) => Promise<Response> : (args: {
26
31
  path: Path<O>;
27
32
  } & RequestMeta) => Promise<Response> : Path<O> extends never ? (args: {
@@ -34,7 +39,7 @@ type ClientRoute<R> = {
34
39
  [K in keyof R]: R[K] extends {
35
40
  options: infer O extends RouteOptions;
36
41
  result?: infer Result;
37
- } ? ClientMethod<O, Result> : (args?: RequestMeta) => Promise<Response>;
42
+ } ? ClientMethod<O, Result> : R[K] extends (...args: any[]) => infer Result ? ClientMethod<{}, Result> : (args?: RequestMeta) => Promise<Response>;
38
43
  };
39
44
  type WithResult<O extends RouteOptions, R = unknown> = {
40
45
  handler: RouteHandler<O>;
@@ -1,4 +1,5 @@
1
1
  import hook from "#route/hook";
2
+ import is from "@rcompat/is";
2
3
  function is_with(value) {
3
4
  return typeof value === "object" &&
4
5
  value !== null &&
@@ -33,8 +34,15 @@ function route(handlers) {
33
34
  connect(path) {
34
35
  return Object.fromEntries(Object.entries(this._handlers).map(([method, { contentType }]) => {
35
36
  const fn = async (args = {}) => {
36
- const resolved = args.path !== undefined
37
- ? path.replace(/\[([^\]]+)\]/g, (_, key) => encodeURIComponent(args.path[key] ?? `[${key}]`))
37
+ const resolved = is.defined(args.path)
38
+ ? path.replace(/\[{1,2}\.{0,3}([^\]]+)\]{1,2}/g, (match, key) => {
39
+ const is_rest = match.includes("...");
40
+ const is_optional = match.startsWith("[[");
41
+ const value = args.path[key];
42
+ if (value === undefined)
43
+ return is_optional ? "" : match;
44
+ return is_rest ? value : encodeURIComponent(value);
45
+ })
38
46
  : path;
39
47
  return fetch(resolved, {
40
48
  method: method.toUpperCase(),
@@ -1,3 +1,5 @@
1
+ import type ResponseFunction from "#response/ResponseFunction";
2
+ import type NarrowedRequest from "#route/NarrowedRequest";
1
3
  import type RouteHandler from "#route/Handler";
2
4
  import hook from "#route/hook";
3
5
  import type RouteOptions from "#route/Options";
@@ -5,19 +7,28 @@ import type { Method } from "@rcompat/http";
5
7
  declare const BRAND: unique symbol;
6
8
  type WithResult<O extends RouteOptions> = {
7
9
  [BRAND]: true;
8
- handler: RouteHandler;
10
+ handler: (request: NarrowedRequest<O>) => unknown;
9
11
  options: O;
10
12
  };
13
+ type Page<R> = Awaited<R> extends ResponseFunction<infer Props> ? Props : never;
11
14
  type AnyHandler = RouteHandler | WithResult<RouteOptions>;
12
15
  type RouteHandlers = {
13
16
  [key in Method]?: AnyHandler;
14
17
  };
15
- declare function route(handlers: RouteHandlers): {
16
- [k: string]: {
17
- handler: RouteHandler;
18
- options: RouteOptions;
19
- };
18
+ type RouteMethod<O extends RouteOptions> = {
19
+ handler: (request: NarrowedRequest<O>) => unknown;
20
+ options: O;
21
+ Page: never;
22
+ };
23
+ type RouteMethodWithResult<O extends RouteOptions, R> = Omit<RouteMethod<O>, "Page"> & {
24
+ Page: Page<R>;
25
+ };
26
+ type Routes<R extends RouteHandlers> = {
27
+ [K in keyof R]: R[K] extends WithResult<infer O extends RouteOptions> ? R[K] extends {
28
+ handler: (...args: any[]) => infer Result;
29
+ } ? RouteMethodWithResult<O, Result> : RouteMethod<O> : R[K] extends (...args: any[]) => infer Result ? RouteMethodWithResult<{}, Result> : RouteMethod<{}>;
20
30
  };
31
+ declare function route<R extends RouteHandlers>(handlers: R): Routes<R>;
21
32
  declare namespace route {
22
33
  var _a: <O extends RouteOptions>(options: O, handler: RouteHandler<O>) => WithResult<O>;
23
34
  export { _a as with };
@@ -30,18 +30,26 @@ export default class ServeApp extends App {
30
30
  media(content_type: string, response?: ResponseInit): ResponseInit;
31
31
  create_csp(): void;
32
32
  frontend(extension: string, view_response: ViewResponse): void;
33
- page(name?: string): string;
33
+ template(name?: string): string;
34
+ page(route: string): string;
34
35
  serve_assets(pathname: string): Promise<Response | undefined>;
35
36
  start(): Promise<void>;
36
37
  stop(): void;
37
38
  upgrade(request: Request, actions: Actions): null;
38
39
  route(request: RequestFacade): Promise<{
39
- errors: RouteHandler[];
40
+ errors: {
41
+ handler: RouteHandler;
42
+ path: string;
43
+ }[];
40
44
  hooks: import("../module/RequestHook.js").default[];
41
- layouts: RouteHandler[];
45
+ layouts: {
46
+ handler: RouteHandler;
47
+ path: string;
48
+ }[];
42
49
  handler: RouteHandler;
43
50
  request: any;
44
51
  is_head: boolean;
52
+ path: string;
45
53
  } | undefined>;
46
54
  }
47
55
  export {};
@@ -92,10 +92,11 @@ export default class ServeApp extends App {
92
92
  #init;
93
93
  #server;
94
94
  #views;
95
+ #pages = {};
95
96
  #csp = {};
96
97
  #assets = [];
97
98
  #serve_assets;
98
- #pages;
99
+ #templates;
99
100
  #frontends = new Map();
100
101
  #router;
101
102
  #entrypoints = {};
@@ -117,8 +118,9 @@ export default class ServeApp extends App {
117
118
  const extension = Object.keys(this.frontends).find(e => f.endsWith(e));
118
119
  return is.undefined(extension) ? name : f.slice(0, -extension.length);
119
120
  });
121
+ this.#pages = Object.fromEntries(init.pages ?? []);
120
122
  this.#serve_assets = init.assets;
121
- this.#pages = init.pages;
123
+ this.#templates = init.templates;
122
124
  const http_config = this.#init.facade[s_config].http;
123
125
  this.set(s_http, {
124
126
  host: http_config.host,
@@ -137,16 +139,16 @@ export default class ServeApp extends App {
137
139
  },
138
140
  }, init.routes.map(s => s[0]));
139
141
  if (init.mode === "development")
140
- this.register(dev_module());
142
+ this.module(dev_module());
141
143
  const assets = this.serve_assets.bind(this);
142
144
  const bound_route = (request) => route(this, request);
143
145
  if (init.session !== undefined) {
144
- this.register(session_module(init.session));
146
+ this.module(session_module(init.session));
145
147
  }
146
148
  const i18n_config = init.facade[s_config].i18n;
147
149
  if (i18n_config !== undefined)
148
- this.register(i18n_module(i18n_config));
149
- this.register(create({
150
+ this.module(i18n_module(i18n_config));
151
+ this.module(create({
150
152
  name: "builtin/handle",
151
153
  setup({ onHandle }) {
152
154
  onHandle(async (request) => {
@@ -190,7 +192,7 @@ export default class ServeApp extends App {
190
192
  }
191
193
  ;
192
194
  render(content) {
193
- const { body, head, page, partial, placeholders = {} } = content;
195
+ const { body, head, template, partial, placeholders = {} } = content;
194
196
  ["body", "head"].forEach(key => assert.undefined(placeholders[key]));
195
197
  const all_placeholders = {
196
198
  ...this.#entrypoints,
@@ -200,7 +202,7 @@ export default class ServeApp extends App {
200
202
  return partial === true ? body : Object.entries(all_placeholders)
201
203
  // replace given placeholders, defaulting to ""
202
204
  .reduce((rendered, [key, value]) => rendered
203
- .replaceAll(`%${key}%`, value?.toString() ?? ""), this.page(page))
205
+ .replaceAll(`%${key}%`, value?.toString() ?? ""), this.template(template))
204
206
  // replace non-given placeholders, aside from %body% / %head%
205
207
  .replaceAll(/(?<keep>%(?:head|body)%)|%.*?%/gus, "$1")
206
208
  // replace body and head
@@ -256,9 +258,15 @@ export default class ServeApp extends App {
256
258
  this.#frontends.set(extension, view_response);
257
259
  }
258
260
  ;
259
- page(name) {
260
- const page_name = name ?? location.app_html;
261
- return this.#pages[page_name];
261
+ template(name) {
262
+ const template_name = name ?? location.app_html;
263
+ return this.#templates[template_name];
264
+ }
265
+ page(route) {
266
+ const key = this.#pages[route];
267
+ if (is.undefined(key))
268
+ throw E.response_page_missing(route);
269
+ return key;
262
270
  }
263
271
  async #try_serve(ref) {
264
272
  if (await ref.exists() && await ref.type() === "file") {
@@ -383,12 +391,12 @@ export default class ServeApp extends App {
383
391
  const method = request.method.toLowerCase();
384
392
  const specials = matched.specials;
385
393
  const errors = (specials.error ?? [])
386
- .map(v => router.get(v)[method]?.handler)
387
- .filter(Boolean)
394
+ .map(v => ({ handler: router.get(v)[method]?.handler, path: v }))
395
+ .filter(({ handler }) => Boolean(handler))
388
396
  .toReversed();
389
397
  const layouts = (specials.layout ?? [])
390
- .map(v => router.get(v)[method]?.handler)
391
- .filter(Boolean)
398
+ .map(v => ({ handler: router.get(v)[method]?.handler, path: v }))
399
+ .filter(({ handler }) => Boolean(handler))
392
400
  .toReversed();
393
401
  const hooks = (specials.hook ?? [])
394
402
  .toReversed()
@@ -410,10 +418,11 @@ export default class ServeApp extends App {
410
418
  throw E.request_content_type_mismatch(contentType, actual);
411
419
  }
412
420
  }
421
+ const params = is.defined(path)
422
+ ? await path.parse(matched.params)
423
+ : matched.params;
413
424
  const refined = Object.assign(Object.create(request), {
414
- path: new RequestBag(is.defined(path)
415
- ? path.parse(matched.params)
416
- : matched.params, "path", {
425
+ path: new RequestBag(params, "path", {
417
426
  normalize: k => k.toLowerCase(),
418
427
  raw: request.url.pathname,
419
428
  }),
@@ -421,7 +430,7 @@ export default class ServeApp extends App {
421
430
  ? { body: request.body[BodyPatch]({ contentType, schema: body }) }
422
431
  : {}),
423
432
  });
424
- return { errors, hooks, layouts, handler, request: refined, is_head };
433
+ return { errors, hooks, layouts, handler, request: refined, is_head, path: matched.path };
425
434
  }
426
435
  }
427
436
  //# sourceMappingURL=App.js.map
@@ -18,6 +18,7 @@ type ServeInit = {
18
18
  }>;
19
19
  };
20
20
  views?: [string, Import][];
21
+ pages?: [string, string][];
21
22
  facade: AppFacade;
22
23
  routes: [string, {
23
24
  default: any;
@@ -25,7 +26,7 @@ type ServeInit = {
25
26
  mode: Mode;
26
27
  target: string;
27
28
  log: typeof LogSchema.infer;
28
- pages: Dict<string>;
29
+ templates: Dict<string>;
29
30
  session?: SessionConfig;
30
31
  };
31
32
  export type { ServeInit as default };
@@ -0,0 +1,2 @@
1
+ export { default } from "#events";
2
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { default } from "#events";
2
+ //# sourceMappingURL=events.js.map
@@ -1,18 +1,17 @@
1
1
  import binary from "#response/binary";
2
2
  import error from "#response/error";
3
+ import page from "#response/page";
3
4
  import view from "#response/view";
4
5
  import $null from "#response/null";
5
6
  declare const response: {
6
7
  binary: typeof binary;
7
8
  error: typeof error;
8
9
  json: (body: unknown, init?: ResponseInit) => (app: import("./index.js").ServeApp) => import("@rcompat/type").MaybePromise<Response>;
10
+ page: typeof page;
9
11
  redirect: (location: string, status?: 300 | 301 | 302 | 303 | 304 | 307 | 308) => import("./index.js").ResponseFunction;
10
- sse: (body: {
11
- close?(): undefined;
12
- open(events: {
13
- send(name: string, data: unknown): undefined;
14
- }): undefined;
15
- }, init?: ResponseInit) => (app: import("./index.js").ServeApp) => import("@rcompat/type").MaybePromise<Response>;
12
+ sse: (body: (source: {
13
+ send(name: string, data: unknown): void;
14
+ }) => void | (() => void), init?: ResponseInit) => (app: import("./index.js").ServeApp) => import("@rcompat/type").MaybePromise<Response>;
16
15
  text: (body: string, init?: ResponseInit) => (app: import("./index.js").ServeApp) => import("@rcompat/type").MaybePromise<Response>;
17
16
  view: typeof view;
18
17
  ws: (actions: import("@rcompat/http").Actions) => import("./index.js").ResponseFunction;
@@ -1,6 +1,7 @@
1
1
  import binary from "#response/binary";
2
2
  import error from "#response/error";
3
3
  import json from "#response/json";
4
+ import page from "#response/page";
4
5
  import redirect from "#response/redirect";
5
6
  import sse from "#response/sse";
6
7
  import text from "#response/text";
@@ -11,6 +12,7 @@ const response = {
11
12
  binary,
12
13
  error,
13
14
  json,
15
+ page,
14
16
  redirect,
15
17
  sse,
16
18
  text,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primate/core",
3
- "version": "0.8.8",
3
+ "version": "0.9.0",
4
4
  "description": "The universal web framework",
5
5
  "homepage": "https://primate.run",
6
6
  "bugs": "https://github.com/primate-run/primate/issues",
@@ -18,24 +18,24 @@
18
18
  "!/**/*.spec.*"
19
19
  ],
20
20
  "dependencies": {
21
- "@rcompat/assert": "^0.13.0",
22
- "@rcompat/cli": "^0.24.0",
23
- "@rcompat/crypto": "^0.21.0",
24
- "@rcompat/dict": "^0.10.0",
25
- "@rcompat/env": "^0.22.2",
26
- "@rcompat/error": "^0.8.0",
27
- "@rcompat/fn": "^0.10.0",
28
- "@rcompat/fs": "^0.33.2",
29
- "@rcompat/http": "^0.30.0",
30
- "@rcompat/io": "^0.10.0",
31
- "@rcompat/is": "^0.11.0",
32
- "@rcompat/kv": "^0.13.0",
33
- "@rcompat/runtime": "^0.16.0",
34
- "@rcompat/string": "^0.21.0",
35
- "@rcompat/symbol": "^0.2.0",
36
- "@rcompat/type": "^0.17.0",
37
- "esbuild": "^0.28.0",
38
- "pema": "^0.8.3"
21
+ "@rcompat/assert": "^0.14.0",
22
+ "@rcompat/cli": "^0.25.0",
23
+ "@rcompat/crypto": "^0.22.0",
24
+ "@rcompat/dict": "^0.11.0",
25
+ "@rcompat/env": "^0.23.0",
26
+ "@rcompat/error": "^0.9.0",
27
+ "@rcompat/fn": "^0.11.0",
28
+ "@rcompat/fs": "^0.35.1",
29
+ "@rcompat/http": "^0.31.0",
30
+ "@rcompat/io": "^0.11.0",
31
+ "@rcompat/is": "^0.12.0",
32
+ "@rcompat/kv": "^0.14.0",
33
+ "@rcompat/runtime": "^0.17.0",
34
+ "@rcompat/string": "^0.22.0",
35
+ "@rcompat/symbol": "^0.3.0",
36
+ "@rcompat/type": "^0.18.0",
37
+ "esbuild": "^0.28.1",
38
+ "pema": "^0.9.0"
39
39
  },
40
40
  "imports": {
41
41
  "#i18n/config": {
@@ -94,7 +94,7 @@
94
94
  }
95
95
  },
96
96
  "scripts": {
97
- "build": "npm run clean && tsgo && cp -a src/private/defaults lib/private",
97
+ "build": "npm run clean && tsc && cp -a src/private/defaults lib/private",
98
98
  "clean": "rm -rf ./lib",
99
99
  "lint": "eslint .",
100
100
  "test": "npx proby"
@@ -1,11 +0,0 @@
1
- import type { FileRef } from "@rcompat/fs";
2
- import type { MaybePromise } from "@rcompat/type";
3
- type Options = {
4
- build: {
5
- id: string;
6
- };
7
- context: string;
8
- };
9
- type Binder = (file: FileRef, options: Options) => MaybePromise<string>;
10
- export type { Binder as default };
11
- //# sourceMappingURL=Binder.d.ts.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=Binder.js.map
@@ -1,4 +0,0 @@
1
- import type BuildApp from "#build/App";
2
- import type { Plugin } from "esbuild";
3
- export default function plugin_server_virtual_pages(app: BuildApp): Plugin;
4
- //# sourceMappingURL=virtual-pages.d.ts.map