primate 0.27.6 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.27.6",
3
+ "version": "0.29.0",
4
4
  "description": "Polymorphic development platform",
5
5
  "homepage": "https://primatejs.com",
6
6
  "bugs": "https://github.com/primatejs/primate/issues",
@@ -9,7 +9,8 @@
9
9
  "src/**/*.js",
10
10
  "src/errors.json",
11
11
  "src/defaults/*.html",
12
- "!src/**/*.spec.js"
12
+ "!src/**/*.spec.js",
13
+ "types/*.ts"
13
14
  ],
14
15
  "bin": "src/bin.js",
15
16
  "repository": {
@@ -18,11 +19,12 @@
18
19
  "directory": "packages/primate"
19
20
  },
20
21
  "dependencies": {
21
- "rcompat": "^0.5.1"
22
+ "rcompat": "^0.7.2"
22
23
  },
23
24
  "engines": {
24
25
  "node": ">=18"
25
26
  },
26
27
  "type": "module",
28
+ "types": "./types/index.d.ts",
27
29
  "exports": "./src/exports.js"
28
30
  }
package/src/app.js CHANGED
@@ -1,22 +1,23 @@
1
1
  import crypto from "rcompat/crypto";
2
2
  import { tryreturn } from "rcompat/async";
3
- import { Path } from "rcompat/fs";
3
+ import { File } from "rcompat/fs";
4
4
  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
- tryreturn(_ => Path.read(`${base.join(page)}`))
19
- .orelse(_ => Path.read(`${base.join(fallback)}`));
18
+ const get_index = (base, page, fallback) =>
19
+ tryreturn(_ => File.text(`${base.join(page)}`))
20
+ .orelse(_ => File.text(`${base.join(fallback)}`));
20
21
 
21
22
  const encoder = new TextEncoder();
22
23
 
@@ -51,7 +52,7 @@ const render_head = (assets, head) =>
51
52
  : tags.script({ inline, code, type, integrity, src }),
52
53
  ).join("\n").concat("\n", head ?? "");
53
54
 
54
- const { name, version } = await new Path(import.meta.url).up(2)
55
+ const { name, version } = await new File(import.meta.url).up(2)
55
56
  .join(runtime.manifest).json();
56
57
 
57
58
  export default async (log, root, config) => {
@@ -100,7 +101,7 @@ export default async (log, root, config) => {
100
101
 
101
102
  await Promise.all((await source.collect(filter)).map(async path => {
102
103
  const debased = path.debase(this.root).path.slice(1);
103
- const filename = new Path(directory).join(path.debase(source));
104
+ const filename = File.join(directory, path.debase(source));
104
105
  const target = await target_base.join(filename.debase(directory));
105
106
  await target.directory.create();
106
107
  await (regexs.some(regex => regex.test(debased))
@@ -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);
@@ -177,7 +191,7 @@ export default async (log, root, config) => {
177
191
  }
178
192
  if (inline || type === "style") {
179
193
  this.assets.push({
180
- src: new Path(http.static.root).join(src ?? "").path,
194
+ src: File.join(http.static.root, src ?? "").path,
181
195
  code: inline ? code : "",
182
196
  type,
183
197
  inline,
@@ -203,7 +217,7 @@ export default async (log, root, config) => {
203
217
 
204
218
  const parts = module.split("/");
205
219
  const path = [this.library, ...parts];
206
- const pkg = await Path.resolve().join(...path, this.manifest).json();
220
+ const pkg = await File.resolve().join(...path, this.manifest).json();
207
221
  const exports = pkg.exports === undefined
208
222
  ? { [module]: `/${module}/${pkg.main}` }
209
223
  : transform(pkg.exports, entry => entry
@@ -220,11 +234,11 @@ export default async (log, root, config) => {
220
234
  ?? value.default?.replace(".", `./${module}`)
221
235
  ?? value.import?.replace(".", `./${module}`),
222
236
  ]));
223
- const dependency = Path.resolve().join(...path);
224
- const target = new Path(this.runpath(client), this.library, ...parts);
237
+ const dependency = File.resolve().join(...path);
238
+ const target = File.join(this.runpath(client), this.library, ...parts);
225
239
  await dependency.copy(target);
226
240
  this.importmaps = {
227
- ...valmap(exports, value => new Path(root, this.library, value).path),
241
+ ...valmap(exports, value => File.join(root, this.library, value).path),
228
242
  ...this.importmaps };
229
243
  },
230
244
  };
package/src/cwd.js CHANGED
@@ -1,3 +1,3 @@
1
- import { Path } from "rcompat/fs";
1
+ import { File } from "rcompat/fs";
2
2
 
3
- export default (meta, up = 1) => new Path(meta.url).up(up);
3
+ export default (meta, up = 1) => new File(meta.url).up(up);
package/src/errors.js CHANGED
@@ -1,7 +1,7 @@
1
- import { Path } from "rcompat/fs";
1
+ import { File } from "rcompat/fs";
2
2
  import Logger from "./Logger.js";
3
3
 
4
- const json = await new Path(import.meta.url).up(1).join("errors.json").json();
4
+ const json = await new File(import.meta.url).up(1).join("errors.json").json();
5
5
 
6
6
  const errors = Logger.err(json.errors, json.module);
7
7
 
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 };
@@ -1,4 +1,4 @@
1
- import { Path } from "rcompat/fs";
1
+ import { File } from "rcompat/fs";
2
2
 
3
3
  export default async (app, type, post = () => undefined) => {
4
4
  const { config } = app;
@@ -14,7 +14,7 @@ export default async (app, type, post = () => undefined) => {
14
14
  .map(async include => {
15
15
  const path = app.root.join(include);
16
16
  if (await path.exists()) {
17
- const target = Path.join(type, include);
17
+ const target = File.join(type, include);
18
18
  await app.stage(path, target);
19
19
  await post(target);
20
20
  }
@@ -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 => {
@@ -5,15 +5,15 @@ import errors from "../errors.js";
5
5
 
6
6
  const deslash = url => url.replaceAll(/(?<!http:)\/{2,}/gu, _ => "/");
7
7
 
8
- const parse_body = (body, headers, url) =>
9
- tryreturn(async _ => Body.parse(body, headers.get("content-type")) ?? {})
8
+ const parse_body = (request, url) =>
9
+ tryreturn(async _ => await Body.parse(request) ?? {})
10
10
  .orelse(error => errors.MismatchedBody.throw(url.pathname, error.message));
11
11
 
12
12
  export default dispatch => async original => {
13
13
  const { headers } = original;
14
14
  const url = new URL(deslash(globalThis.decodeURIComponent(original.url)));
15
15
  const cookies = headers.get("cookie");
16
- const body = await parse_body(original.body, headers, url);
16
+ const body = await parse_body(original, url);
17
17
 
18
18
  return { original, url, body, ...valmap({
19
19
  query: [from(url.searchParams), url.search],
@@ -1,4 +1,4 @@
1
- import { Path } from "rcompat/fs";
1
+ import { File } from "rcompat/fs";
2
2
  import { cascade } from "rcompat/async";
3
3
  import { stringify } from "rcompat/object";
4
4
 
@@ -7,7 +7,7 @@ const post = async app => {
7
7
 
8
8
  {
9
9
  // after hook, publish a zero assumptions app.js (no css imports)
10
- const src = new Path(root, app.config.build.index);
10
+ const src = File.join(root, app.config.build.index);
11
11
 
12
12
  await app.publish({
13
13
  code: app.exports.filter(({ type }) => type === "script")
@@ -1,4 +1,4 @@
1
- import { Path } from "rcompat/fs";
1
+ import { File } from "rcompat/fs";
2
2
  import { cascade } from "rcompat/async";
3
3
  import cwd from "../cwd.js";
4
4
  import copy_includes from "./copy_includes.js";
@@ -17,7 +17,7 @@ const pre = async app => {
17
17
  if (await path.components.exists()) {
18
18
  // copy .js files from components to build/client/components, since
19
19
  // frontend frameworks handle non-js files
20
- const target = Path.join(client, components);
20
+ const target = File.join(client, components);
21
21
  await app.stage(path.components, target, /^.*.js$/u);
22
22
  }
23
23
 
@@ -29,13 +29,13 @@ const post = async app => {
29
29
 
30
30
  if (await path.static.exists()) {
31
31
  // copy static files to build/server/static
32
- await app.stage(path.static, new Path(location.server, location.static));
32
+ await app.stage(path.static, File.join(location.server, location.static));
33
33
 
34
34
  // copy static files to build/client/static
35
- await app.stage(path.static, new Path(location.client, location.static));
35
+ await app.stage(path.static, File.join(location.client, location.static));
36
36
 
37
37
  // publish JavaScript and CSS files
38
- const imports = await Path.collect(path.static, /\.(?:js|css)$/u);
38
+ const imports = await File.collect(path.static, /\.(?:js|css)$/u);
39
39
  await Promise.all(imports.map(async file => {
40
40
  const code = await file.text();
41
41
  const src = file.debase(path.static);
@@ -54,7 +54,7 @@ const post = async app => {
54
54
  const client = app.runpath(location.client);
55
55
  await copy_includes(app, location.client, async to =>
56
56
  Promise.all((await to.collect(/\.js$/u)).map(async script => {
57
- const src = new Path(root, script.path.replace(client, _ => ""));
57
+ const src = File.join(root, script.path.replace(client, _ => ""));
58
58
  await app.publish({ src, code: await script.text(), type: "module" });
59
59
  })),
60
60
  );
@@ -15,10 +15,10 @@ const is_object = value => is_non_null_object(value)
15
15
  ? json(value) : is_text(value);
16
16
  const is_response = value => is_response_duck(value)
17
17
  ? _ => value : is_object(value);
18
- const isStream = value => value instanceof ReadableStream
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) : isStream(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("$"))
@@ -1,4 +1,4 @@
1
- import { Path } from "rcompat/fs";
1
+ import { File } from "rcompat/fs";
2
2
  import { identity } from "rcompat/function";
3
3
  import errors from "../errors.js";
4
4
 
@@ -17,13 +17,13 @@ export default async ({
17
17
  warn = true,
18
18
  } = {}) => {
19
19
  const objects = directory === undefined ? [] : await Promise.all(
20
- (await Path.collect(directory, /^.*.js$/u, { recursive }))
20
+ (await File.collect(directory, /^.*.js$/u, { recursive }))
21
21
  .filter(filter)
22
22
  .map(async path => [
23
23
  `${path}`.replace(directory, _ => "").slice(1, -ending.length),
24
- (await import(path)),
24
+ await import(path),
25
25
  ]));
26
- warn && await Path.exists(directory) && empty(log)(objects, name, directory);
26
+ warn && await directory.exists() && empty(log)(objects, name, directory);
27
27
 
28
28
  return objects;
29
29
  };
@@ -1,4 +1,4 @@
1
- import { Path } from "rcompat/fs";
1
+ import { File } from "rcompat/fs";
2
2
  import errors from "../../errors.js";
3
3
  import to_sorted from "../../to_sorted.js";
4
4
 
@@ -10,7 +10,7 @@ export default type => async (log, directory, load) => {
10
10
  .map(([name, object]) => [name.replace(replace, () => ""), object]),
11
11
  ([a], [b]) => a.length - b.length);
12
12
 
13
- const resolve = name => new Path(directory, name, `+${type}.js`);
13
+ const resolve = name => File.join(directory, name, `+${type}.js`);
14
14
  objects.some(([name, value]) => typeof value.default !== "function"
15
15
  && errors.InvalidDefaultExport.throw(resolve(name)));
16
16
 
@@ -1,4 +1,4 @@
1
- import { Path } from "rcompat/fs";
1
+ import { File } from "rcompat/fs";
2
2
  import { is } from "rcompat/invariant";
3
3
  import { tryreturn } from "rcompat/sync";
4
4
  import errors from "../errors.js";
@@ -10,7 +10,7 @@ export default async (log, directory, load = fs) => {
10
10
  const types = (await load({ log, directory, name: "types", filter }))
11
11
  .map(([name, type]) => [name, type.default]);
12
12
 
13
- const resolve = name => new Path(directory, name);
13
+ const resolve = name => File.join(directory, name);
14
14
  types.every(([name, type]) => tryreturn(_ => {
15
15
  is(type).object();
16
16
  is(type.base).string();
package/src/run.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { tryreturn } from "rcompat/async";
2
- import { Path } from "rcompat/fs";
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";
@@ -28,7 +28,7 @@ const get_config = async root => {
28
28
 
29
29
  export default async name => tryreturn(async _ => {
30
30
  // use module root if possible, fall back to current directory
31
- const root = await tryreturn(_ => Path.root()).orelse(_ => Path.resolve());
31
+ const root = await tryreturn(_ => File.root()).orelse(_ => File.resolve());
32
32
  const config = await get_config(root);
33
33
  logger = new Logger(config.logger);
34
34
  await command(name)(await app(logger, root, config));
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
  };
@@ -0,0 +1,69 @@
1
+ declare module "primate" {
2
+ type App = any;
3
+
4
+ interface MinOptions {
5
+ status: number,
6
+ headers: Headers | {},
7
+ }
8
+
9
+ interface ErrorOptions extends MinOptions {
10
+ page: string,
11
+ }
12
+
13
+ interface Options extends ErrorOptions {
14
+ placeholders: {},
15
+ }
16
+
17
+ type Dispatcher = {
18
+ get(property: string): string,
19
+ };
20
+
21
+ type RequestFacade = {
22
+ body: {}
23
+ path: Dispatcher,
24
+ query: Dispatcher,
25
+ cookies: Dispatcher,
26
+ headers: Dispatcher,
27
+ original: Request,
28
+ };
29
+
30
+ type ResponseFn = (app: App, ...rest: any) => Response;
31
+ type ResponseFacade =
32
+ string
33
+ | object
34
+ | URL
35
+ | Blob
36
+ | ReadableStream
37
+ | Response
38
+ | ResponseFn;
39
+
40
+ type RouteFunction = (request?: RequestFacade) => ResponseFacade;
41
+
42
+ type Streamable = ReadableStream | Blob;
43
+
44
+ export type Route = {
45
+ get?: RouteFunction,
46
+ post?: RouteFunction,
47
+ put?: RouteFunction,
48
+ delete?: RouteFunction,
49
+ };
50
+
51
+ export function text(body: string, options?: MinOptions): ResponseFn;
52
+
53
+ export function json(body: {}, options?: MinOptions): ResponseFn;
54
+
55
+ export function stream(body: Streamable, options?: MinOptions): ResponseFn;
56
+
57
+ export function redirect(location: string, options?: MinOptions): ResponseFn;
58
+
59
+ export function html(name: string, options?: MinOptions): ResponseFn;
60
+
61
+ export function view(name: string, props: {}, options?: Options): ResponseFn;
62
+
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;
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
- };