primate 0.28.0 → 0.29.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.28.0",
3
+ "version": "0.29.1",
4
4
  "description": "Polymorphic development platform",
5
5
  "homepage": "https://primatejs.com",
6
6
  "bugs": "https://github.com/primatejs/primate/issues",
@@ -19,7 +19,7 @@
19
19
  "directory": "packages/primate"
20
20
  },
21
21
  "dependencies": {
22
- "rcompat": "^0.7.2"
22
+ "rcompat": "^0.8.0"
23
23
  },
24
24
  "engines": {
25
25
  "node": ">=18"
package/src/app.js CHANGED
@@ -5,16 +5,17 @@ import { is } from "rcompat/invariant";
5
5
  import { transform, valmap, to } from "rcompat/object";
6
6
  import { globify } from "rcompat/string";
7
7
  import * as runtime from "rcompat/meta";
8
+ import { Response, Status, MediaType } from "rcompat/http";
8
9
 
9
10
  import errors from "./errors.js";
10
11
  import to_sorted from "./to_sorted.js";
11
- import * as handlers from "./handlers/exports.js";
12
+ import * as handlers from "./handlers.js";
12
13
  import * as loaders from "./loaders/exports.js";
13
14
 
14
15
  const { DoubleFileExtension } = errors;
15
16
 
16
17
  // use user-provided file or fall back to default
17
- const index = (base, page, fallback) =>
18
+ const get_index = (base, page, fallback) =>
18
19
  tryreturn(_ => File.text(`${base.join(page)}`))
19
20
  .orelse(_ => File.text(`${base.join(fallback)}`));
20
21
 
@@ -112,7 +113,7 @@ export default async (log, root, config) => {
112
113
  const { location: { server, client, components } } = this.config;
113
114
 
114
115
  const source = this.path.components;
115
- const compile = this.extensions[component.extension]?.compile;
116
+ const compile = this.extensions[component.fullExtension]?.compile;
116
117
  if (compile === undefined) {
117
118
  const debased = `${component.path}`.replace(source, "");
118
119
 
@@ -134,15 +135,13 @@ export default async (log, root, config) => {
134
135
  headers({ script = "", style = "" } = {}) {
135
136
  const csp = Object.keys(http.csp).reduce((policy, key) =>
136
137
  `${policy}${key} ${http.csp[key]};`, "")
137
- .replace("script-src 'self'", `script-src 'self' ${script} ${
138
- this.assets
139
- .filter(({ type }) => type !== "style")
140
- .map(asset => `'${asset.integrity}'`).join(" ")
138
+ .replace("script-src 'self'", `script-src 'self' ${script} ${this.assets
139
+ .filter(({ type }) => type !== "style")
140
+ .map(asset => `'${asset.integrity}'`).join(" ")
141
141
  }`)
142
- .replace("style-src 'self'", `style-src 'self' ${style} ${
143
- this.assets
144
- .filter(({ type }) => type === "style")
145
- .map(asset => `'${asset.integrity}'`).join(" ")
142
+ .replace("style-src 'self'", `style-src 'self' ${style} ${this.assets
143
+ .filter(({ type }) => type === "style")
144
+ .map(asset => `'${asset.integrity}'`).join(" ")
146
145
  }`);
147
146
 
148
147
  return { "Content-Security-Policy": csp, "Referrer-Policy": "same-origin" };
@@ -150,18 +149,33 @@ export default async (log, root, config) => {
150
149
  runpath(...directories) {
151
150
  return this.path.build.join(...directories);
152
151
  },
153
- async render({ body, head }, page = config.pages.index, placeholders = {}) {
152
+ async render(content) {
153
+ const { assets, config: { location, pages: { index } } } = this;
154
+ const { body, head, partial, placeholders = {}, page = index } = content;
154
155
  ["body", "head"].every(used => is(placeholders[used]).undefined());
155
- const { assets, config: { location, pages } } = this;
156
156
 
157
- return to(placeholders)
157
+ return partial ? body : to(placeholders)
158
158
  // replace given placeholders, defaulting to ""
159
159
  .reduce((html, [key, value]) => html.replace(`%${key}%`, value ?? ""),
160
- await index(this.runpath(location.pages), page, pages.index))
160
+ await get_index(this.runpath(location.pages), page, index))
161
161
  // replace non-given placeholders, aside from %body% / %head%
162
162
  .replaceAll(/(?<keep>%(?:head|body)%)|%.*?%/gus, "$1")
163
163
  // replace body and head
164
- .replace("%body%", body).replace("%head%", render_head(assets, head));
164
+ .replace("%body%", body)
165
+ .replace("%head%", render_head(assets, head));
166
+ },
167
+ respond(body, { status = Status.OK, headers = {} } = {}) {
168
+ return new Response(body, { status, headers: {
169
+ ...this.headers(), "Content-Type": MediaType.TEXT_HTML, ...headers },
170
+ });
171
+ },
172
+ async view(options) {
173
+ // split render and respond options
174
+ const { status, headers, ...rest } = options;
175
+ return this.respond(await this.render(rest), { status, headers });
176
+ },
177
+ media(type, { status, headers } = {}) {
178
+ return { status, headers: { ...headers, "Content-Type": type } };
165
179
  },
166
180
  async inline(code, type) {
167
181
  const integrity = await this.hash(code);
package/src/exports.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import run from "./run.js";
2
2
 
3
- export * from "./handlers/exports.js";
3
+ export * from "./handlers.js";
4
4
 
5
5
  export { default as Logger } from "./Logger.js";
6
6
 
@@ -0,0 +1,77 @@
1
+ import { File } from "rcompat/fs";
2
+ import { MediaType, Status } from "rcompat/http";
3
+ import { identity } from "rcompat/function";
4
+ import errors from "./errors.js";
5
+
6
+ const handle = (mediatype, mapper = identity) => (body, options) => app =>
7
+ app.respond(mapper(body), app.media(mediatype, options));
8
+
9
+ // {{{ text
10
+ const text = handle(MediaType.TEXT_PLAIN);
11
+ // }}}
12
+ // {{{ json
13
+ const json = handle(MediaType.APPLICATION_JSON, JSON.stringify);
14
+ // }}}
15
+ // {{{ stream
16
+ const stream = handle(MediaType.APPLICATION_OCTET_STREAM);
17
+ // }}}
18
+ // {{{ ws
19
+ const ws = implementation => ({ server }, _, { original }) =>
20
+ server.upgrade(original, implementation);
21
+ // }}}
22
+ // {{{ sse
23
+ const sse = handle(MediaType.TEXT_EVENT_STREAM, implementation =>
24
+ new ReadableStream({
25
+ start(controller) {
26
+ implementation.open({
27
+ send(name, data) {
28
+ const event = data === undefined ? "" : `event: ${name}\n`;
29
+ const $data = data === undefined ? name : data;
30
+ controller.enqueue(`${event}data:${JSON.stringify($data)}\n\n`);
31
+ },
32
+ });
33
+ },
34
+ cancel() {
35
+ implementation.close?.();
36
+ },
37
+ }));
38
+ // }}}
39
+ // {{{ redirect
40
+ const redirect = (Location, { status = Status.FOUND } = {}) => app =>
41
+ /* no body */
42
+ app.respond(null, { status, headers: { Location } });
43
+ // }}}
44
+ // {{{ error
45
+ const error = (body = "Not Found", { status = Status.NOT_FOUND, page } = {}) =>
46
+ app => app.view({ body, status, page: page ?? app.config.pages.error });
47
+ // }}}
48
+ // {{{ html
49
+ const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
50
+ const style_re = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
51
+ const remove = /<(?<tag>script|style)>.*?<\/\k<tag>>/gus;
52
+ const html = (name, options) => async app => {
53
+ const component = await app.path.components.join(name).text();
54
+ const scripts = await Promise.all([...component.matchAll(script_re)]
55
+ .map(({ groups: { code } }) => app.inline(code, "module")));
56
+ const styles = await Promise.all([...component.matchAll(style_re)]
57
+ .map(({ groups: { code } }) => app.inline(code, "style")));
58
+ const assets = [...scripts, ...styles];
59
+
60
+ const body = component.replaceAll(remove, _ => "");
61
+ const head = assets.map(asset => asset.head).join("\n");
62
+ const script = scripts.map(asset => asset.csp).join(" ");
63
+ const style = styles.map(asset => asset.csp).join(" ");
64
+ const headers = app.headers({ script, style });
65
+
66
+ return app.view({ body, head, headers, ...options });
67
+ };
68
+ // }}}
69
+ // {{{ view
70
+ const view = (name, props, options) => async (app, ...rest) => {
71
+ const { fullExtension: extension } = new File(name);
72
+ return app.extensions[extension]?.handle(name, props, options)(app, ...rest)
73
+ ?? errors.NoHandlerForExtension.throw(extension, name);
74
+ };
75
+ // }}}
76
+
77
+ export { text, json, stream, redirect, error, html, view, ws, sse };
@@ -6,4 +6,3 @@ export { default as bundle } from "./bundle.js";
6
6
  export { default as route } from "./route.js";
7
7
  export { default as handle } from "./handle.js";
8
8
  export { default as parse } from "./parse.js";
9
- export { default as serve } from "./serve.js";
@@ -1,7 +1,7 @@
1
1
  import { Response, Status, MediaType } from "rcompat/http";
2
2
  import { cascade, tryreturn } from "rcompat/async";
3
3
  import { respond } from "./respond/exports.js";
4
- import { error as clientError } from "../handlers/exports.js";
4
+ import { error as clientError } from "../handlers.js";
5
5
 
6
6
  const guard_error = Symbol("guard_error");
7
7
  const guard = (app, guards) => async (request, next) => {
@@ -44,6 +44,7 @@ export default app => {
44
44
 
45
45
  return tryreturn(async _ => {
46
46
  const { path, guards, errors, layouts, handler } = await route(request);
47
+
47
48
  error_handler = errors?.at(-1);
48
49
 
49
50
  const pathed = { ...request, path };
@@ -52,6 +53,7 @@ export default app => {
52
53
 
53
54
  // handle request
54
55
  const response = await (await cascade(hooks, handler))(pathed);
56
+
55
57
  const $layouts = { layouts: await get_layouts(layouts, request) };
56
58
  return (await respond(response))(app, $layouts, pathed);
57
59
  }).orelse(async error => {
@@ -18,7 +18,7 @@ const is_response = value => is_response_duck(value)
18
18
  const is_stream = value => value instanceof ReadableStream
19
19
  ? stream(value) : is_response(value);
20
20
  const is_blob = value => value instanceof Blob
21
- ? stream(value.stream()) : is_stream(value);
21
+ ? stream(value.stream()) : is_stream(value);
22
22
  const is_URL = value => value instanceof URL
23
23
  ? redirect(value.href) : is_blob(value);
24
24
  const guess = value => is_URL(value);
@@ -21,8 +21,7 @@ export default app => {
21
21
  [name, type === undefined ? value : validate(types[type], value, name)],
22
22
  )));
23
23
 
24
- const is_type = (groups, pathname) => Object
25
- .entries(groups ?? {})
24
+ const is_type = (groups, pathname) => Object.entries(groups ?? {})
26
25
  .map(([name, value]) =>
27
26
  [types[name] === undefined || explicit ? name : `${name}$${name}`, value])
28
27
  .filter(([name]) => name.includes("$"))
package/src/run.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { tryreturn } from "rcompat/async";
2
2
  import { File } from "rcompat/fs";
3
3
  import { extend } from "rcompat/object";
4
+ import { runtime } from "rcompat/meta";
4
5
  import app from "./app.js";
5
6
  import { default as Logger, bye } from "./Logger.js";
6
7
  import errors from "./errors.js";
@@ -8,7 +9,6 @@ import command from "./commands/exports.js";
8
9
  import defaults from "./defaults/primate.config.js";
9
10
 
10
11
  let logger = new Logger({ level: Logger.Warn });
11
- const { runtime = "node" } = import.meta;
12
12
 
13
13
  const get_config = async root => {
14
14
  const name = "primate.config.js";
package/src/start.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { serve, Response, Status } from "rcompat/http";
2
- import { cascade, tryreturn } from "rcompat/async";
2
+ import { tryreturn } from "rcompat/async";
3
3
  import { bold, blue } from "rcompat/colors";
4
4
  import * as hooks from "./hooks/exports.js";
5
5
  import { print } from "./Logger.js";
@@ -24,14 +24,11 @@ export default async (app$, mode = "development") => {
24
24
 
25
25
  app.route = hooks.route(app);
26
26
  app.parse = hooks.parse(app.dispatch);
27
-
28
- const server = await serve(async request =>
27
+ app.server = await serve(async request =>
29
28
  tryreturn(async _ => (await hooks.handle(app))(await app.parse(request)))
30
29
  .orelse(error => {
31
30
  app.log.auto(error);
32
31
  return new Response(null, { status: Status.INTERNAL_SERVER_ERROR });
33
32
  }),
34
33
  app.config.http);
35
-
36
- await (await cascade(app.modules.serve))({ ...app, server });
37
34
  };
package/types/index.d.ts CHANGED
@@ -3,6 +3,7 @@ declare module "primate" {
3
3
 
4
4
  interface MinOptions {
5
5
  status: number,
6
+ headers: Headers | {},
6
7
  }
7
8
 
8
9
  interface ErrorOptions extends MinOptions {
@@ -11,7 +12,6 @@ declare module "primate" {
11
12
 
12
13
  interface Options extends ErrorOptions {
13
14
  placeholders: {},
14
- headers: Headers | {},
15
15
  }
16
16
 
17
17
  type Dispatcher = {
@@ -61,4 +61,9 @@ declare module "primate" {
61
61
  export function view(name: string, props: {}, options?: Options): ResponseFn;
62
62
 
63
63
  export function error(body: string, options?: ErrorOptions): ResponseFn;
64
+
65
+ export function sse(implementation: {
66
+ open?: () => void,
67
+ close?: () => void,
68
+ }, options?: MinOptions): ResponseFn;
64
69
  }
@@ -1,8 +0,0 @@
1
- import { Response, Status, MediaType } from "rcompat/http";
2
-
3
- export default (body = "Not Found", { status = Status.NOT_FOUND, page } = {}) =>
4
- async app =>
5
- new Response(await app.render({ body }, page ?? app.config.pages.error), {
6
- status,
7
- headers: { ...app.headers(), "Content-Type": MediaType.TEXT_HTML },
8
- });
@@ -1,7 +0,0 @@
1
- export { default as text } from "./text.js";
2
- export { default as json } from "./json.js";
3
- export { default as stream } from "./stream.js";
4
- export { default as redirect } from "./redirect.js";
5
- export { default as html } from "./html.js";
6
- export { default as view } from "./view.js";
7
- export { default as error } from "./error.js";
@@ -1,28 +0,0 @@
1
- import { Response, Status, MediaType } from "rcompat/http";
2
-
3
- const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
4
- const style_re = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
5
- const remove = /<(?<tag>script|style)>.*?<\/\k<tag>>/gus;
6
-
7
- const render = (body, head, { partial = false, app, page, placeholders }) =>
8
- partial ? body : app.render({ body, head }, page, placeholders);
9
-
10
- export default (name, options = {}) => async app => {
11
- const component = await app.path.components.join(name).text();
12
- const scripts = await Promise.all([...component.matchAll(script_re)]
13
- .map(({ groups: { code } }) => app.inline(code, "module")));
14
- const styles = await Promise.all([...component.matchAll(style_re)]
15
- .map(({ groups: { code } }) => app.inline(code, "style")));
16
- const assets = [...scripts, ...styles];
17
-
18
- const body = component.replaceAll(remove, _ => "");
19
- const head = assets.map(asset => asset.head).join("\n");
20
- const script = scripts.map(asset => asset.csp).join(" ");
21
- const style = styles.map(asset => asset.csp).join(" ");
22
- const headers = { script, style };
23
-
24
- return new Response(await render(body, head, { app, ...options }), {
25
- status: options.status ?? Status.OK,
26
- headers: { ...app.headers(headers), "Content-Type": MediaType.TEXT_HTML },
27
- });
28
- };
@@ -1,7 +0,0 @@
1
- import { Response, Status, MediaType } from "rcompat/http";
2
-
3
- export default (body, { status = Status.OK } = {}) => app =>
4
- new Response(JSON.stringify(body), {
5
- status,
6
- headers: { ...app.headers(), "Content-Type": MediaType.APPLICATION_JSON },
7
- });
@@ -1,8 +0,0 @@
1
- import { Response, Status } from "rcompat/http";
2
-
3
- export default (Location, { status = Status.FOUND } = {}) => app =>
4
- /* no body */
5
- new Response(null, {
6
- status,
7
- headers: { ...app.headers(), Location },
8
- });
@@ -1,8 +0,0 @@
1
- import { Response, Status, MediaType } from "rcompat/http";
2
-
3
- export default (body, { status = Status.OK } = {}) => app =>
4
- new Response(body, {
5
- status,
6
- headers: { ...app.headers(), "Content-Type":
7
- MediaType.APPLICATION_OCTET_STREAM },
8
- });
@@ -1,7 +0,0 @@
1
- import { Response, Status, MediaType } from "rcompat/http";
2
-
3
- export default (body, { status = Status.OK } = {}) => app =>
4
- new Response(body, {
5
- status,
6
- headers: { ...app.headers(), "Content-Type": MediaType.TEXT_PLAIN },
7
- });
@@ -1,7 +0,0 @@
1
- import errors from "../errors.js";
2
-
3
- export default (name, props, options) => async (app, ...rest) => {
4
- const extension = name.slice(name.lastIndexOf("."));
5
- return app.extensions[extension]?.handle(name, props, options)(app, ...rest)
6
- ?? errors.NoHandlerForExtension.throw(extension, name);
7
- };
@@ -1,6 +0,0 @@
1
- import { cascade } from "rcompat/async";
2
-
3
- export default async (app, server) => {
4
- app.log.info("running serve hooks", { module: "primate" });
5
- await (await cascade(app.modules.serve))({ ...app, server });
6
- };