primate 0.29.7 → 0.30.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.29.7",
3
+ "version": "0.30.0",
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.8.3"
22
+ "rcompat": "^0.9.4"
23
23
  },
24
24
  "engines": {
25
25
  "node": ">=18"
package/src/Logger.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { assert, is } from "rcompat/invariant";
2
2
  import { blue, bold, green, red, yellow, dim } from "rcompat/colors";
3
- import { map } from "rcompat/object";
3
+ import o from "rcompat/object";
4
4
  import console from "rcompat/console";
5
5
  import { stdout } from "rcompat/stdio";
6
6
 
@@ -16,7 +16,7 @@ const mark = (format, ...params) => params.reduce((formatted, param, i) =>
16
16
  formatted.replace(`{${i}}`, bold(param)), format);
17
17
 
18
18
  const reference = (module, error) => {
19
- const base = module ? `modules/${module}` : "guide/logging";
19
+ const base = module === "primate" ? "guide/logging" : `modules/${module}`;
20
20
  return `https://primatejs.com/${base}#${hyphenate(error)}`;
21
21
  };
22
22
 
@@ -49,7 +49,7 @@ const Logger = class Logger {
49
49
  #level; #trace;
50
50
 
51
51
  static err(errors, module) {
52
- return map(errors, ([key, value]) => [key, throwable(value, key, module)]);
52
+ return o.map(errors, ([key, value]) => [key, throwable(value, key, module)]);
53
53
  }
54
54
 
55
55
  constructor({ level = levels.Error, trace = false } = {}) {
@@ -101,7 +101,7 @@ const Logger = class Logger {
101
101
 
102
102
  auto(error) {
103
103
  const { message } = error;
104
- const matches = map(levels, ([name, level]) => [level, name.toLowerCase()]);
104
+ const matches = o.map(levels, ([key, level]) => [level, key.toLowerCase()]);
105
105
  return this[matches[error.level] ?? "error"](message, error);
106
106
  }
107
107
  };
package/src/app.js CHANGED
@@ -2,9 +2,10 @@ import crypto from "rcompat/crypto";
2
2
  import { tryreturn } from "rcompat/async";
3
3
  import { File } from "rcompat/fs";
4
4
  import { is } from "rcompat/invariant";
5
- import { transform, valmap, to } from "rcompat/object";
5
+ import o from "rcompat/object";
6
6
  import { globify } from "rcompat/string";
7
7
  import * as runtime from "rcompat/meta";
8
+ import { identity } from "rcompat/function";
8
9
  import { Response, Status, MediaType } from "rcompat/http";
9
10
 
10
11
  import errors from "./errors.js";
@@ -14,8 +15,19 @@ import * as loaders from "./loaders/exports.js";
14
15
 
15
16
  const { DoubleFileExtension } = errors;
16
17
 
18
+ const to_csp = (config_csp, assets, csp) => config_csp
19
+ // only csp entries in the config will be enriched
20
+ .map(([key, directives]) =>
21
+ // enrich with application assets
22
+ [key, assets[key] ? directives.concat(...assets[key]) : directives])
23
+ .map(([key, directives]) =>
24
+ // enrich with explicit csp
25
+ [key, csp[key] ? directives.concat(...csp[key]) : directives])
26
+ .map(([key, directives]) => `${key} ${directives.join(" ")}`)
27
+ .join(";");
28
+
17
29
  // use user-provided file or fall back to default
18
- const get_index = (base, page, fallback) =>
30
+ const load = (base, page, fallback) =>
19
31
  tryreturn(_ => File.text(`${base.join(page)}`))
20
32
  .orelse(_ => File.text(`${base.join(fallback)}`));
21
33
 
@@ -52,13 +64,10 @@ const render_head = (assets, head) =>
52
64
  : tags.script({ inline, code, type, integrity, src }),
53
65
  ).join("\n").concat("\n", head ?? "");
54
66
 
55
- const { name, version } = await new File(import.meta.url).up(2)
56
- .join(runtime.manifest).json();
57
-
58
67
  export default async (log, root, config) => {
59
68
  const { http } = config;
60
69
  const secure = http?.ssl !== undefined;
61
- const path = valmap(config.location, value => root.join(value));
70
+ const path = o.valmap(config.location, value => root.join(value));
62
71
 
63
72
  // if ssl activated, resolve key and cert early
64
73
  if (secure) {
@@ -69,16 +78,15 @@ export default async (log, root, config) => {
69
78
  const error = await path.routes.join("+error.js");
70
79
 
71
80
  return {
72
- config,
73
81
  secure,
74
- name,
75
- version,
76
82
  importmaps: {},
77
83
  assets: [],
78
84
  exports: [],
79
85
  path,
80
86
  root,
81
87
  log,
88
+ // pseudostatic thus arrowbound
89
+ get: (config_key, fallback) => o.get(config, config_key) ?? fallback,
82
90
  error: {
83
91
  default: await error.exists() ? await error.import("default") : undefined,
84
92
  },
@@ -88,11 +96,11 @@ export default async (log, root, config) => {
88
96
  handle: handlers.html,
89
97
  },
90
98
  },
91
- modules: await loaders.modules(log, root, config),
99
+ modules: await loaders.modules(log, root, config.modules ?? []),
92
100
  ...runtime,
93
101
  // copy files to build folder, potentially transforming them
94
102
  async stage(source, directory, filter) {
95
- const { paths, mapper } = this.config.build.transform;
103
+ const { paths = [], mapper = identity } = this.get("build.transform", {});
96
104
  is(paths).array();
97
105
  is(mapper).function();
98
106
 
@@ -110,7 +118,7 @@ export default async (log, root, config) => {
110
118
  }));
111
119
  },
112
120
  async compile(component) {
113
- const { location: { server, client, components } } = this.config;
121
+ const { server, client, components } = this.get("location");
114
122
 
115
123
  const source = this.path.components;
116
124
  const compile = this.extensions[component.fullExtension]?.compile
@@ -133,41 +141,37 @@ export default async (log, root, config) => {
133
141
  await compile.client(component);
134
142
  }
135
143
  },
136
- headers({ script = "", style = "" } = {}) {
137
- const csp = Object.keys(http.csp).reduce((policy, key) =>
138
- `${policy}${key} ${http.csp[key]};`, "")
139
- .replace("script-src 'self'", `script-src 'self' ${script} ${this.assets
140
- .filter(({ type }) => type !== "style")
141
- .map(asset => `'${asset.integrity}'`).join(" ")
142
- }`)
143
- .replace("style-src 'self'", `style-src 'self' ${style} ${this.assets
144
- .filter(({ type }) => type === "style")
145
- .map(asset => `'${asset.integrity}'`).join(" ")
146
- }`);
144
+ headers(csp = {}) {
145
+ const http_csp = Object.entries(this.get("http.csp", {}));
147
146
 
148
- return { "Content-Security-Policy": csp, "Referrer-Policy": "same-origin" };
147
+ return {
148
+ ...this.get("http.headers", {}),
149
+ ...http_csp.length === 0 ? {} : {
150
+ "Content-Security-Policy": to_csp(http_csp, this.asset_csp, csp),
151
+ },
152
+ };
149
153
  },
150
154
  runpath(...directories) {
151
155
  return this.path.build.join(...directories);
152
156
  },
153
157
  async render(content) {
154
- const { assets, config: { location, pages: { index } } } = this;
158
+ const index = this.get("pages.index");
155
159
  const { body, head, partial, placeholders = {}, page = index } = content;
156
160
  ["body", "head"].every(used => is(placeholders[used]).undefined());
157
161
 
158
- return partial ? body : to(placeholders)
162
+ return partial ? body : Object.entries(placeholders)
159
163
  // replace given placeholders, defaulting to ""
160
164
  .reduce((html, [key, value]) => html.replace(`%${key}%`, value ?? ""),
161
- await get_index(this.runpath(location.pages), page, index))
165
+ await load(this.runpath(this.get("location.pages")), page, index))
162
166
  // replace non-given placeholders, aside from %body% / %head%
163
167
  .replaceAll(/(?<keep>%(?:head|body)%)|%.*?%/gus, "$1")
164
168
  // replace body and head
165
169
  .replace("%body%", body)
166
- .replace("%head%", render_head(assets, head));
170
+ .replace("%head%", render_head(this.assets, head));
167
171
  },
168
172
  respond(body, { status = Status.OK, headers = {} } = {}) {
169
173
  return new Response(body, { status, headers: {
170
- ...this.headers(), "Content-Type": MediaType.TEXT_HTML, ...headers },
174
+ "Content-Type": MediaType.TEXT_HTML, ...this.headers(), ...headers },
171
175
  });
172
176
  },
173
177
  async view(options) {
@@ -182,11 +186,11 @@ export default async (log, root, config) => {
182
186
  const integrity = await this.hash(code);
183
187
  const tag_name = type === "style" ? "style" : "script";
184
188
  const head = tags[tag_name]({ code, type, inline: true, integrity });
185
- return { head, csp: `'${integrity}'` };
189
+ return { head, integrity: `'${integrity}'` };
186
190
  },
187
191
  async publish({ src, code, type = "", inline = false, copy = true }) {
188
192
  if (!inline && copy) {
189
- const base = this.runpath(this.config.location.client).join(src);
193
+ const base = this.runpath(this.get("location.client")).join(src);
190
194
  await base.directory.create();
191
195
  await base.write(code);
192
196
  }
@@ -199,6 +203,13 @@ export default async (log, root, config) => {
199
203
  integrity: await this.hash(code),
200
204
  });
201
205
  }
206
+ // rehash assets_csp
207
+ this.asset_csp = this.assets.map(({ type: directive, integrity }) => [
208
+ `${directive === "style" ? "style" : "script"}-src`, integrity])
209
+ .reduce((csp, [directive, hash]) =>
210
+ ({ ...csp, [directive]: csp[directive].concat(`'${hash}'`) } ),
211
+ { "style-src": [], "script-src": [] },
212
+ );
202
213
  },
203
214
  export({ type, code }) {
204
215
  this.exports.push({ type, code });
@@ -214,14 +225,12 @@ export default async (log, root, config) => {
214
225
  return `${prefix}-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}`;
215
226
  },
216
227
  async import(module, deep_import) {
217
- const { http: { static: { root } }, location: { client } } = this.config;
218
-
219
228
  const parts = module.split("/");
220
229
  const path = [this.library, ...parts];
221
230
  const pkg = await File.resolve().join(...path, this.manifest).json();
222
231
  const exports = pkg.exports === undefined
223
232
  ? { [module]: `/${module}/${pkg.main}` }
224
- : transform(pkg.exports, entry => entry
233
+ : o.transform(pkg.exports, entry => entry
225
234
  .filter(([, export$]) =>
226
235
  export$.browser?.[deep_import] !== undefined
227
236
  || export$.browser?.default !== undefined
@@ -236,10 +245,10 @@ export default async (log, root, config) => {
236
245
  ?? value.import?.replace(".", `./${module}`),
237
246
  ]));
238
247
  const dependency = File.resolve().join(...path);
239
- const target = File.join(this.runpath(client), this.library, ...parts);
248
+ const target = this.runpath(this.get("location.client")).join(...path);
240
249
  await dependency.copy(target);
241
- this.importmaps = { ...valmap(exports, value =>
242
- File.join(root, this.library, value).normalize()),
250
+ this.importmaps = { ...o.valmap(exports, value =>
251
+ File.join(this.get("http.static.root"), this.library, value).webpath()),
243
252
  ...this.importmaps };
244
253
  },
245
254
  };
@@ -15,19 +15,16 @@ export default {
15
15
  http: {
16
16
  host: "localhost",
17
17
  port: 6161,
18
- csp: {
19
- "default-src": "'self'",
20
- "style-src": "'self'",
21
- "script-src": "'self'",
22
- "object-src": "'none'",
23
- "frame-ancestors": "'none'",
24
- "form-action": "'self'",
25
- "base-uri": "'self'",
26
- },
18
+ csp: {},
27
19
  static: {
28
20
  root: "/",
29
21
  },
30
22
  },
23
+ request: {
24
+ body: {
25
+ parse: true,
26
+ },
27
+ },
31
28
  location: {
32
29
  // renderable components
33
30
  components: "components",
@@ -54,7 +51,4 @@ export default {
54
51
  mapper: identity,
55
52
  },
56
53
  },
57
- types: {
58
- explicit: false,
59
- },
60
54
  };
package/src/dispatch.js CHANGED
@@ -1,21 +1,21 @@
1
1
  import { is } from "rcompat/invariant";
2
2
  import { tryreturn } from "rcompat/sync";
3
- import { map } from "rcompat/object";
3
+ import o from "rcompat/object";
4
4
  import { camelcased } from "rcompat/string";
5
5
  import errors from "./errors.js";
6
6
  import validate from "./validate.js";
7
7
 
8
8
  export default (patches = {}) => (object, raw, cased = true) => {
9
9
  return Object.assign(Object.create(null), {
10
- ...map(patches, ([name, patch]) => [`get${camelcased(name)}`, property => {
11
- is(property).defined(`\`${name}\` called without property`);
12
- return tryreturn(_ => validate(patch, object[property], property))
10
+ ...o.map(patches, ([name, patch]) => [`get${camelcased(name)}`, key => {
11
+ is(key).defined(`\`${name}\` called without key`);
12
+ return tryreturn(_ => validate(patch, object[key], key))
13
13
  .orelse(({ message }) => errors.MismatchedType.throw(message));
14
14
  }]),
15
- get(property) {
16
- is(property).string();
15
+ get(key) {
16
+ is(key).string();
17
17
 
18
- return object[cased ? property : property.toLowerCase()];
18
+ return object[cased ? key : key.toLowerCase()];
19
19
  },
20
20
  json() {
21
21
  return JSON.parse(JSON.stringify(object));
package/src/errors.json CHANGED
@@ -1,11 +1,6 @@
1
1
  {
2
2
  "module": "primate",
3
3
  "errors": {
4
- "MismatchedBody": {
5
- "message": "{0}: {1}",
6
- "fix": "make sure the body payload corresponds to the used content type",
7
- "level": "Error"
8
- },
9
4
  "DoubleFileExtension": {
10
5
  "message": "double file extension {0}",
11
6
  "fix": "unload one of the two handlers registering the file extension",
@@ -26,9 +21,19 @@
26
21
  "fix": "disambiguate routes",
27
22
  "level": "Error"
28
23
  },
24
+ "EmptyConfigFile": {
25
+ "message": "empty config file at {0}",
26
+ "fix": "add configuration options to the file or remove it",
27
+ "level": "Warn"
28
+ },
29
+ "EmptyPathParameter": {
30
+ "message": "empty path parameter {0} in route {1}",
31
+ "fix": "name the parameter or remove it",
32
+ "level": "Error"
33
+ },
29
34
  "EmptyRouteFile": {
30
35
  "message": "empty route file at {0}",
31
- "fix": "add routes or remove file",
36
+ "fix": "add routes to the file or remove it",
32
37
  "level": "Warn"
33
38
  },
34
39
  "EmptyDirectory": {
@@ -41,14 +46,19 @@
41
46
  "fix": "check errors in config file by running {1}",
42
47
  "level": "Error"
43
48
  },
49
+ "InvalidBodyReturned": {
50
+ "message": "invalid body returned from route, got {0}",
51
+ "fix": "return a proper body from route",
52
+ "level": "Error"
53
+ },
44
54
  "InvalidDefaultExport": {
45
55
  "message": "invalid default export at {0}",
46
56
  "fix": "use only functions for the default export",
47
57
  "level": "Error"
48
58
  },
49
- "InvalidPathParameter": {
50
- "message": "invalid path parameter {0} in route {1}",
51
- "fix": "use only latin letters and decimal digits in path parameters",
59
+ "InvalidPath": {
60
+ "message": "invalid path {0}",
61
+ "fix": "use only letters, digits, '_', '[', ']' or '=' in path filenames",
52
62
  "level": "Error"
53
63
  },
54
64
  "InvalidTypeExport": {
@@ -61,6 +71,11 @@
61
71
  "fix": "use lowercase-first latin letters and decimals in type names",
62
72
  "level": "Error"
63
73
  },
74
+ "MismatchedBody": {
75
+ "message": "{0}: {1}",
76
+ "fix": "make sure the body payload corresponds to the used content type",
77
+ "level": "Error"
78
+ },
64
79
  "MismatchedPath": {
65
80
  "message": "mismatched path {0}: {1}",
66
81
  "fix": "fix the type or the caller",
@@ -86,11 +101,6 @@
86
101
  "fix": "change {0} to an array in the config or remove this property",
87
102
  "level": "Error"
88
103
  },
89
- "EmptyConfigFile": {
90
- "message": "empty config file at {0}",
91
- "fix": "add configuration options or remove file",
92
- "level": "Warn"
93
- },
94
104
  "NoHandlerForComponent": {
95
105
  "message": "no handler for {0}",
96
106
  "fix": "add handler module for this component or remove {0}",
package/src/handlers.js CHANGED
@@ -43,7 +43,7 @@ const redirect = (Location, { status = Status.FOUND } = {}) => app =>
43
43
  // }}}
44
44
  // {{{ error
45
45
  const error = (body = "Not Found", { status = Status.NOT_FOUND, page } = {}) =>
46
- app => app.view({ body, status, page: page ?? app.config.pages.error });
46
+ app => app.view({ body, status, page: page ?? app.get("pages.error") });
47
47
  // }}}
48
48
  // {{{ html
49
49
  const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
@@ -55,15 +55,15 @@ const html = (name, options) => async app => {
55
55
  .map(({ groups: { code } }) => app.inline(code, "module")));
56
56
  const styles = await Promise.all([...component.matchAll(style_re)]
57
57
  .map(({ groups: { code } }) => app.inline(code, "style")));
58
- const assets = [...scripts, ...styles];
58
+ const style_src = styles.map(asset => asset.integrity);
59
+ const script_src = scripts.map(asset => asset.integrity);
59
60
 
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 });
61
+ return app.view({
62
+ body: component.replaceAll(remove, _ => ""),
63
+ head: [...scripts, ...styles].map(asset => asset.head).join("\n"),
64
+ headers: app.headers({ "style-src": style_src, "script-src": script_src }),
65
+ ...options,
66
+ });
67
67
  };
68
68
  // }}}
69
69
  // {{{ view
@@ -1,11 +1,8 @@
1
1
  import { File } from "rcompat/fs";
2
2
 
3
3
  export default async (app, type, post = () => undefined) => {
4
- const { config } = app;
5
- const { build } = config;
6
- const { includes } = build;
7
-
8
- const reserved = Object.values(app.config.location);
4
+ const includes = app.get("build.includes");
5
+ const reserved = Object.values(app.get("location"));
9
6
 
10
7
  if (Array.isArray(includes)) {
11
8
  await Promise.all(includes
@@ -33,9 +33,13 @@ const get_layouts = async (layouts, request) => {
33
33
  .slice(stop_at === -1 ? 0 : stop_at)
34
34
  .map(layout => layout.default(request)));
35
35
  };
36
+ // last handler, preserve final request form
37
+ const last = handler => async request => {
38
+ const response = await handler(request);
39
+ return { request, response };
40
+ };
36
41
 
37
42
  export default app => {
38
- const { config: { http: { static: { root } }, location } } = app;
39
43
  const route = request => app.route(request);
40
44
 
41
45
  const as_route = async request => {
@@ -47,15 +51,13 @@ export default app => {
47
51
 
48
52
  error_handler = errors?.at(-1);
49
53
 
50
- const pathed = { ...request, path };
51
-
52
- const hooks = [...app.modules.route, guard(app, guards)];
54
+ const hooks = [...app.modules.route, guard(app, guards), last(handler)];
53
55
 
54
56
  // handle request
55
- const response = await (await cascade(hooks, handler))(pathed);
57
+ const routed = await (await cascade(hooks))({ ...request, path });
56
58
 
57
- const $layouts = { layouts: await get_layouts(layouts, request) };
58
- return (await respond(response))(app, $layouts, pathed);
59
+ const $layouts = { layouts: await get_layouts(layouts, routed.request) };
60
+ return (await respond(routed.response))(app, $layouts, routed.request);
59
61
  }).orelse(async error => {
60
62
  app.log.auto(error);
61
63
 
@@ -73,6 +75,8 @@ export default app => {
73
75
  },
74
76
  });
75
77
 
78
+ const location = app.get("location");
79
+ const root = app.get("http.static.root");
76
80
  const client = app.runpath(location.client);
77
81
  const handle = async request => {
78
82
  const { pathname } = request.url;
@@ -1,23 +1,24 @@
1
1
  import { URL, Body } from "rcompat/http";
2
- import { from, valmap } from "rcompat/object";
2
+ import o from "rcompat/object";
3
3
  import { tryreturn } from "rcompat/async";
4
4
  import errors from "../errors.js";
5
5
 
6
- const parse_body = (request, url) =>
6
+ const get_body = (request, url) =>
7
7
  tryreturn(async _ => await Body.parse(request) ?? {})
8
8
  .orelse(error => errors.MismatchedBody.throw(url.pathname, error.message));
9
9
 
10
- export default dispatch => async original => {
10
+ export default app => async original => {
11
11
  const { headers } = original;
12
12
 
13
13
  const url = new URL(globalThis.decodeURIComponent(original.url));
14
14
  const cookies = headers.get("cookie");
15
- const body = await parse_body(original, url);
16
15
 
17
- return { original, url, body, ...valmap({
18
- query: [from(url.searchParams), url.search],
19
- headers: [from(headers), headers, false],
20
- cookies: [from(cookies?.split(";").map(cookie =>
16
+ return { original, url,
17
+ body: app.get("request.body.parse") ? await get_body(original, url) : {},
18
+ ...o.valmap({
19
+ query: [o.from(url.searchParams), url.search],
20
+ headers: [o.from(headers), headers, false],
21
+ cookies: [o.from(cookies?.split(";").map(cookie =>
21
22
  cookie.trim().split("=")) ?? []), cookies],
22
- }, value => dispatch(...value)) };
23
+ }, value => app.dispatch(...value)) };
23
24
  };
@@ -1,25 +1,21 @@
1
1
  import { File } from "rcompat/fs";
2
2
  import { cascade } from "rcompat/async";
3
- import { stringify } from "rcompat/object";
3
+ import o from "rcompat/object";
4
4
 
5
5
  const post = async app => {
6
- const { config: { http: { static: { root } } } } = app;
6
+ // after hook, publish a zero assumptions app.js (no css imports)
7
+ const src = File.join(app.get("http.static.root"), app.get("build.index"));
7
8
 
8
- {
9
- // after hook, publish a zero assumptions app.js (no css imports)
10
- const src = File.join(root, app.config.build.index);
9
+ await app.publish({
10
+ code: app.exports.filter(({ type }) => type === "script")
11
+ .map(({ code }) => code).join(""),
12
+ src,
13
+ type: "module",
14
+ });
11
15
 
12
- await app.publish({
13
- code: app.exports.filter(({ type }) => type === "script")
14
- .map(({ code }) => code).join(""),
15
- src,
16
- type: "module",
17
- });
18
-
19
- const imports = { ...app.importmaps, app: src.normalize() };
20
- const type = "importmap";
21
- await app.publish({ inline: true, code: stringify({ imports }), type });
22
- }
16
+ const imports = { ...app.importmaps, app: src.webpath() };
17
+ const type = "importmap";
18
+ await app.publish({ inline: true, code: o.stringify({ imports }), type });
23
19
 
24
20
  return app;
25
21
  };
@@ -1,44 +1,45 @@
1
1
  import { File } from "rcompat/fs";
2
2
  import { cascade } from "rcompat/async";
3
- import cwd from "../cwd.js";
4
3
  import copy_includes from "./copy_includes.js";
5
4
 
6
5
  const html = /^.*.html$/u;
7
- const defaults = cwd(import.meta, 2).join("defaults");
6
+ const defaults = new File(import.meta.url).up(2).join("defaults");
8
7
 
9
8
  const pre = async app => {
10
- const { config: { location: { pages, client, components } }, path } = app;
9
+ const location = app.get("location");
10
+ const { pages, client, components } = location;
11
11
 
12
12
  // copy framework pages
13
13
  await app.stage(defaults, pages, html);
14
14
  // overwrite transformed pages to build
15
- await path.pages.exists() && await app.stage(path.pages, pages, html);
15
+ await app.path.pages.exists() && await app.stage(app.path.pages, pages, html);
16
16
 
17
- if (await path.components.exists()) {
17
+ if (await app.path.components.exists()) {
18
18
  // copy .js files from components to build/client/components, since
19
19
  // frontend frameworks handle non-js files
20
20
  const target = File.join(client, components);
21
- await app.stage(path.components, target, /^.*.js$/u);
21
+ await app.stage(app.path.components, target, /^.*.js$/u);
22
22
  }
23
23
 
24
24
  return app;
25
25
  };
26
26
 
27
27
  const post = async app => {
28
- const { config: { location, http: { static: { root } } }, path } = app;
28
+ const _static = app.path.static;
29
+ const location = app.get("location");
29
30
 
30
- if (await path.static.exists()) {
31
+ if (await _static.exists()) {
31
32
  // copy static files to build/server/static
32
- await app.stage(path.static, File.join(location.server, location.static));
33
+ await app.stage(_static, File.join(location.server, location.static));
33
34
 
34
35
  // copy static files to build/client/static
35
- await app.stage(path.static, File.join(location.client, location.static));
36
+ await app.stage(_static, File.join(location.client, location.static));
36
37
 
37
38
  // publish JavaScript and CSS files
38
- const imports = await File.collect(path.static, /\.(?:js|css)$/u);
39
+ const imports = await File.collect(_static, /\.(?:js|css)$/u);
39
40
  await Promise.all(imports.map(async file => {
40
41
  const code = await file.text();
41
- const src = file.debase(path.static);
42
+ const src = file.debase(_static);
42
43
  const type = file.extension === ".css" ? "style" : "module";
43
44
  // already copied in `app.stage`
44
45
  await app.publish({ src, code, type, copy: false });
@@ -52,6 +53,7 @@ const post = async app => {
52
53
 
53
54
  // copy additional subdirectories to build/client
54
55
  const client = app.runpath(location.client);
56
+ const root = app.get("http.static.root");
55
57
  await copy_includes(app, location.client, async to =>
56
58
  Promise.all((await to.collect(/\.js$/u)).map(async script => {
57
59
  const src = File.join(root, script.path.replace(client, _ => ""));
@@ -2,10 +2,9 @@ import { Blob, s_streamable } from "rcompat/fs";
2
2
  import { URL, Response } from "rcompat/http";
3
3
  import { identity } from "rcompat/function";
4
4
  import { text, json, stream, redirect } from "primate";
5
+ import errors from "../errors.js";
5
6
 
6
- const not_found = value => {
7
- throw new Error(`no handler found for ${value}`);
8
- };
7
+ const not_found = value => errors.InvalidBodyReturned.throw(value);
9
8
  const is_text = value => typeof value === "string";
10
9
  const is_non_null_object = value => typeof value === "object" && value !== null;
11
10
  const is_instance = of => value => value instanceof of;
@@ -1,4 +1,4 @@
1
- import { from } from "rcompat/object";
1
+ import o from "rcompat/object";
2
2
  import { tryreturn } from "rcompat/sync";
3
3
  import errors from "../errors.js";
4
4
  import validate from "../validate.js";
@@ -10,20 +10,17 @@ const deroot = pathname => pathname.endsWith("/") && pathname !== "/"
10
10
  ? pathname.slice(0, -1) : pathname;
11
11
 
12
12
  export default app => {
13
- const { types, routes, config: { types: { explicit }, location } } = app;
13
+ const { types, routes } = app;
14
+ const location = app.get("location");
14
15
 
15
- const to_path = (route, pathname) => app.dispatch(from(Object
16
+ const to_path = (route, pathname) => app.dispatch(o.from(Object
16
17
  .entries(route.pathname.exec(pathname)?.groups ?? {})
17
- .map(([name, value]) =>
18
- [types[name] === undefined || explicit ? name : `${name}$${name}`, value])
19
18
  .map(([name, value]) => [name.split("$"), value])
20
19
  .map(([[name, type], value]) =>
21
20
  [name, type === undefined ? value : validate(types[type], value, name)],
22
21
  )));
23
22
 
24
23
  const is_type = (groups, pathname) => Object.entries(groups ?? {})
25
- .map(([name, value]) =>
26
- [types[name] === undefined || explicit ? name : `${name}$${name}`, value])
27
24
  .filter(([name]) => name.includes("$"))
28
25
  .map(([name, value]) => [name.split("$"), value])
29
26
  .map(([[name, type], value]) =>
@@ -15,16 +15,17 @@ const pre = async app => {
15
15
  };
16
16
 
17
17
  const post = async app => {
18
- const { config: { location } } = app;
18
+ const location = app.get("location");
19
19
 
20
20
  // stage routes
21
- await app.runpath(location.routes).create();
22
21
  const double = doubled((await app.path.routes.collect())
23
22
  .map(path => path.debase(app.path.routes))
24
23
  .map(path => `${path}`.slice(1, -path.extension.length)));
25
24
  double && errors.DoubleRoute.throw(double);
26
25
 
27
- await app.stage(app.path.routes, location.routes);
26
+ if (await app.path.routes.exists()) {
27
+ await app.stage(app.path.routes, location.routes);
28
+ }
28
29
  if (await app.path.types.exists()) {
29
30
  await app.stage(app.path.types, location.types);
30
31
  }
@@ -7,9 +7,7 @@ const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
7
7
  const load = (modules = []) => modules.map(module =>
8
8
  [module, load(module.load?.() ?? [])]).flat();
9
9
 
10
- export default async (log, root, config) => {
11
- const modules = config.modules ?? [];
12
-
10
+ export default async (log, root, modules) => {
13
11
  Array.isArray(modules) || errors.ModulesMustBeArray.throw("modules");
14
12
 
15
13
  modules.some(({ name }, n) => name === undefined &&
@@ -1,5 +1,5 @@
1
1
  import { tryreturn } from "rcompat/sync";
2
- import { from } from "rcompat/object";
2
+ import o from "rcompat/object";
3
3
  import { File } from "rcompat/fs";
4
4
  import { default as fs, doubled } from "./common.js";
5
5
  import * as get from "./routes/exports.js";
@@ -7,18 +7,22 @@ import errors from "../errors.js";
7
7
 
8
8
  const { separator } = File;
9
9
 
10
+ const valid_route = /^[\w\-[\]=/.]*$/u;
11
+
10
12
  const make = path => {
13
+ !valid_route.test(path) && errors.InvalidPath.throw(path);
14
+
11
15
  const double = doubled(path.split(separator)
12
- .filter(part => part.startsWith("{") && part.endsWith("}"))
16
+ .filter(part => part.startsWith("[") && part.endsWith("]"))
13
17
  .map(part => part.slice(1, part.indexOf("="))));
14
18
  double && errors.DoublePathParameter.throw(double, path);
15
19
 
16
- const route = path.replaceAll(/\{(?<named>.*?)\}/gu, (_, named) =>
20
+ const route = path.replaceAll(/\[(?<named>.*?)\]/gu, (_, named) =>
17
21
  tryreturn(_ => {
18
22
  const { name, type } = /^(?<name>\w+)(?<type>=\w+)?$/u.exec(named).groups;
19
23
  const param = type === undefined ? name : `${name}$${type.slice(1)}`;
20
24
  return `(?<${param}>[^/]{1,}?)`;
21
- }).orelse(_ => errors.InvalidPathParameter.throw(named, path)));
25
+ }).orelse(_ => errors.EmptyPathParameter.throw(named, path)));
22
26
 
23
27
  // normalize to unix
24
28
  return new RegExp(`^/${route.replaceAll(separator, "/")}$`, "u");
@@ -26,9 +30,9 @@ const make = path => {
26
30
 
27
31
  export default async (app, load = fs) => {
28
32
  const { log } = app;
29
- const directory = app.runpath(app.config.location.routes);
33
+ const directory = app.runpath(app.get("location.routes"));
30
34
  const filter = path => ([name]) => path.includes(name);
31
- const routes = from(await Promise.all(["guards", "errors", "layouts"]
35
+ const routes = o.from(await Promise.all(["guards", "errors", "layouts"]
32
36
  .map(async extra => [extra, await get[extra](log, directory, load)])));
33
37
 
34
38
  return (await get.routes(log, directory, load)).map(([path, imported]) => {
package/src/run.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { tryreturn } from "rcompat/async";
2
2
  import { File } from "rcompat/fs";
3
- import { extend } from "rcompat/object";
3
+ import o from "rcompat/object";
4
4
  import { runtime } from "rcompat/meta";
5
5
  import app from "./app.js";
6
6
  import { default as Logger, bye } from "./Logger.js";
@@ -20,7 +20,7 @@ const get_config = async root => {
20
20
  (imported === undefined || Object.keys(imported).length === 0) &&
21
21
  errors.EmptyConfigFile.warn(logger, config);
22
22
 
23
- return extend(defaults, imported);
23
+ return o.extend(defaults, imported);
24
24
  }).orelse(({ message }) =>
25
25
  errors.ErrorInConfigFile.throw(message, `${runtime} ${config}`))
26
26
  : defaults;
package/src/start.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { serve, Response, Status } from "rcompat/http";
2
2
  import { tryreturn } from "rcompat/async";
3
- import { bold, blue } from "rcompat/colors";
3
+ import { bold, blue, dim } from "rcompat/colors";
4
+ import { resolve } from "rcompat/package";
4
5
  import * as hooks from "./hooks/exports.js";
5
6
  import { print } from "./Logger.js";
6
7
 
@@ -11,24 +12,24 @@ export default async (app$, mode = "development") => {
11
12
  // run one-time hooks
12
13
  let app = app$;
13
14
 
14
- const { http } = app.config;
15
- const address = `http${app.secure ? "s" : ""}://${http.host}:${http.port}`;
16
- print(blue(bold(app.name)), blue(app.version), `at ${address}\n`);
17
-
18
- app.log.info(`in ${bold(mode)} mode`, { module: "primate" });
15
+ const primate = await resolve(import.meta.url);
16
+ print(blue(bold(primate.name)), blue(primate.version), "in startup\n");
19
17
 
20
18
  for (const hook of base_hooks) {
21
- app.log.info(`running ${bold(hook)} hooks`, { module: "primate" });
19
+ app.log.info(`running ${dim(hook)} hooks`, { module: "primate" });
22
20
  app = await hooks[hook](app);
23
21
  }
24
22
 
25
23
  app.route = hooks.route(app);
26
- app.parse = hooks.parse(app.dispatch);
24
+ app.parse = hooks.parse(app);
27
25
  app.server = await serve(async request =>
28
26
  tryreturn(async _ => (await hooks.handle(app))(await app.parse(request)))
29
27
  .orelse(error => {
30
28
  app.log.auto(error);
31
29
  return new Response(null, { status: Status.INTERNAL_SERVER_ERROR });
32
- }),
33
- app.config.http);
30
+ }), app.get("http"));
31
+
32
+ const { host, port } = app.get("http");
33
+ const address = `http${app.secure ? "s" : ""}://${host}:${port}`;
34
+ print(`${blue("++")} started ${dim("->")} ${dim(address)}\n`);
34
35
  };
package/src/cwd.js DELETED
@@ -1,3 +0,0 @@
1
- import { File } from "rcompat/fs";
2
-
3
- export default (meta, up = 1) => new File(meta.url).up(up);