primate 0.10.0 → 0.12.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 (44) hide show
  1. package/LICENSE +0 -2
  2. package/README.md +178 -68
  3. package/exports.js +1 -3
  4. package/module.json +4 -4
  5. package/package.json +9 -9
  6. package/readme/extensions/handlers/htmx/user-index.html +4 -0
  7. package/readme/extensions/handlers/htmx/user.js +23 -0
  8. package/readme/extensions/handlers/redirect/user.js +6 -0
  9. package/readme/extensions/modules/configure.js +3 -0
  10. package/readme/extensions/modules/domains/configure.js +5 -0
  11. package/readme/{domains → extensions/modules/domains}/fields.js +3 -4
  12. package/readme/{domains → extensions/modules/domains}/predicates.js +3 -3
  13. package/readme/{domains → extensions/modules/domains}/short-field-notation.js +3 -3
  14. package/readme/routing/accessing-the-request-body.js +3 -0
  15. package/readme/routing/basic.js +2 -5
  16. package/readme/serving-content/response.js +6 -0
  17. package/readme/serving-content/streams.js +1 -1
  18. package/readme/template.md +226 -0
  19. package/scripts/docs.sh +7 -0
  20. package/src/bin.js +2 -0
  21. package/src/bundle.js +3 -4
  22. package/src/config.js +48 -0
  23. package/src/duck.js +4 -0
  24. package/src/extend.spec.js +19 -27
  25. package/src/handlers/http404.js +2 -5
  26. package/src/handlers/json.js +2 -5
  27. package/src/handlers/stream.js +2 -5
  28. package/src/handlers/text.js +2 -5
  29. package/src/http-statuses.js +5 -0
  30. package/src/log.js +8 -1
  31. package/src/mimes.js +12 -0
  32. package/src/{preset/primate.js → primate.config.js} +1 -0
  33. package/src/respond.js +24 -0
  34. package/src/route.js +6 -20
  35. package/src/run.js +7 -16
  36. package/src/serve.js +35 -25
  37. package/README.template.md +0 -149
  38. package/bin/primate.js +0 -5
  39. package/src/conf.js +0 -27
  40. package/src/http-statuses.json +0 -5
  41. package/src/mimes.json +0 -12
  42. package/src/preset/stores/default.js +0 -2
  43. /package/readme/{serving-content → extensions/handlers/html}/user-index.html +0 -0
  44. /package/readme/{serving-content/html.js → extensions/handlers/html/user.js} +0 -0
@@ -0,0 +1,226 @@
1
+ # Primate
2
+
3
+ Expressive, minimal and extensible framework for JavaScript.
4
+
5
+ ## Getting started
6
+
7
+ Create a route in `routes/hello.js`
8
+
9
+ ```js
10
+ // getting-started/hello.js
11
+ ```
12
+
13
+ Add `{"type": "module"}` to your `package.json` and run `npx -y primate@latest`.
14
+
15
+ ## Table of Contents
16
+
17
+ - [Serving content](#serving-content)
18
+ - [Plain text](#plain-text)
19
+ - [JSON](#json)
20
+ - [Streams](#streams)
21
+ - [Response](#response)
22
+ - [Routing](#routing)
23
+ - [Basic](#basic)
24
+ - [The request object](#the-request-object)
25
+ - [Accessing the request body](#accessing-the-request-body)
26
+ - [Regular expressions](#regular-expressions)
27
+ - [Named groups](#named-groups)
28
+ - [Aliasing](#aliasing)
29
+ - [Sharing logic across requests](#sharing-logic-across-requests)
30
+ - [Extensions](#extensions)
31
+ - [Handlers](#handlers)
32
+ - [HTML](#html)
33
+ - [Redirect](#redirect)
34
+ - [HTMX](#htmx)
35
+ - [Modules](#modules)
36
+ - [Data persistance](#data-persistance)
37
+
38
+ ## Serving content
39
+
40
+ Create a file in `routes` that exports a default function.
41
+
42
+ ### Plain text
43
+
44
+ ```js
45
+ // serving-content/plain-text.js
46
+ ```
47
+
48
+ ### JSON
49
+
50
+ ```js
51
+ // serving-content/json.js
52
+ ```
53
+
54
+ ### Streams
55
+
56
+ ```js
57
+ // serving-content/streams.js
58
+ ```
59
+
60
+ ### Response
61
+
62
+ ```js
63
+ // serving-content/response.js
64
+ ```
65
+
66
+ ## Routing
67
+
68
+ Routes map requests to responses. They are loaded from `routes`.
69
+
70
+ ### Basic
71
+
72
+ ```js
73
+ // routing/basic.js
74
+ ```
75
+
76
+ ### The request object
77
+
78
+ ```js
79
+ // routing/the-request-object.js
80
+ ```
81
+
82
+ ### Accessing the request body
83
+
84
+ For requests containing a body, Primate will attempt to parse the body according
85
+ to the content type sent along the request. Currently supported are
86
+ `application/x-www-form-urlencoded` (typically for form submission) and
87
+ `application/json`.
88
+
89
+ ```js
90
+ // routing/accessing-the-request-body.js
91
+ ```
92
+
93
+ ### Regular expressions
94
+
95
+ ```js
96
+ // routing/regular-expressions.js
97
+ ```
98
+
99
+ ### Named groups
100
+
101
+ ```js
102
+ // routing/named-groups.js
103
+ ```
104
+
105
+ ### Aliasing
106
+
107
+ ```js
108
+ // routing/aliasing.js
109
+ ```
110
+
111
+ ### Sharing logic across requests
112
+
113
+ ```js
114
+ // routing/sharing-logic-across-requests.js
115
+ ```
116
+
117
+ ## Extensions
118
+
119
+ There are two ways to extend Primate's core functionality. Handlers are used
120
+ per route to serve new types of content not supported by core. Modules extend
121
+ an app's entire scope.
122
+
123
+ Handlers and modules listed here are officially developed and supported by
124
+ Primate.
125
+
126
+ ### Handlers
127
+
128
+ #### HTML
129
+
130
+ *[`@primate/html`][primate-html]*
131
+
132
+ Serve HTML tagged templates. This handler reads HTML component files from
133
+ `components`.
134
+
135
+ Create an HTML component in `components/user-index.html`
136
+
137
+ ```html
138
+ <!-- extensions/handlers/html/user-index.html -->
139
+ ```
140
+
141
+ Create a route in `route/user.js` and serve the component in your route
142
+
143
+ ```js
144
+ // extensions/handlers/html/user.js
145
+ ```
146
+
147
+ #### Redirect
148
+
149
+ *[`@primate/redirect`][primate-redirect]*
150
+
151
+ Redirect the request.
152
+
153
+ Create a route in `route/user.js`
154
+
155
+ ```js
156
+ // extensions/handlers/redirect/user.js
157
+ ```
158
+
159
+ #### HTMX
160
+
161
+ *[`@primate/htmx`][primate-htmx]*
162
+
163
+ Serve HTML tagged templates with HTMX support. This handler reads HTML component
164
+ files from `components`.
165
+
166
+ Create an HTML component in `components/user-index.html`
167
+
168
+ ```html
169
+ <!-- extensions/handlers/htmx/user-index.html -->
170
+ ```
171
+
172
+ Create a route in `route/user.js` and serve the component in your route
173
+
174
+ ```js
175
+ // extensions/handlers/htmx/user.js
176
+ ```
177
+
178
+ ### Modules
179
+
180
+ To add modules, create a `primate.config.js` configuration file in your
181
+ project's root. This file should export a default object with the property
182
+ `modules` used for extending your app.
183
+
184
+ ```js
185
+ // extensions/modules/configure.js
186
+ ```
187
+
188
+ #### Data persistance
189
+
190
+ *[`@primate/domains`][primate-domains]*
191
+
192
+ Add data persistance in the form of ORM backed up by various drivers.
193
+
194
+ Import and initialize this module in your configuration file
195
+
196
+ ```js
197
+ // extensions/modules/domains/configure.js
198
+ ```
199
+
200
+ A domain represents a collection in a store using the static `fields` property
201
+
202
+ ```js
203
+ // extensions/modules/domains/fields.js
204
+ ```
205
+
206
+ Field types may also be specified as an array with additional predicates
207
+ aside from the type
208
+
209
+ ```js
210
+ // extensions/modules/domains/predicates.js
211
+ ```
212
+
213
+ ## Resources
214
+
215
+ * Website: https://primatejs.com
216
+ * IRC: Join the `#primate` channel on `irc.libera.chat`.
217
+
218
+ ## License
219
+
220
+ MIT
221
+
222
+ [primate-html]: https://github.com/primatejs/primate-html
223
+ [primate-redirect]: https://github.com/primatejs/primate-redirect
224
+ [primate-htmx]: https://github.com/primatejs/primate-htmx
225
+ [primate-domains]: https://github.com/primatejs/primate-domains
226
+ [primate-sessions]: https://github.com/primatejs/primate-sessions
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ npx -y embedme\
3
+ --source-root readme\
4
+ --strip-embed-comment\
5
+ --stdout readme/template.md\
6
+ > README.md
7
+
package/src/bin.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ (await import("./run.js")).default();
package/src/bundle.js CHANGED
@@ -1,8 +1,7 @@
1
- import {File} from "runtime-compat/filesystem";
2
-
3
- export default async conf => {
4
- const {paths} = conf;
1
+ import {File} from "runtime-compat/fs";
5
2
 
3
+ export default async env => {
4
+ const {paths} = env;
6
5
  if (await paths.static.exists) {
7
6
  // remove public directory in case exists
8
7
  if (await paths.public.exists) {
package/src/config.js ADDED
@@ -0,0 +1,48 @@
1
+ import {Path} from "runtime-compat/fs";
2
+ import {is} from "runtime-compat/dyndef";
3
+ import cache from "./cache.js";
4
+ import extend from "./extend.js";
5
+ import defaults from "./primate.config.js";
6
+ import * as log from "./log.js";
7
+ import package_json from "../package.json" assert {type: "json"};
8
+
9
+ const qualify = (root, paths) =>
10
+ Object.keys(paths).reduce((sofar, key) => {
11
+ const value = paths[key];
12
+ sofar[key] = typeof value === "string"
13
+ ? new Path(root, value)
14
+ : qualify(`${root}/${key}`, value);
15
+ return sofar;
16
+ }, {});
17
+
18
+ const getConfig = async (root, filename) => {
19
+ try {
20
+ return extend(defaults, (await import(root.join(filename))).default);
21
+ } catch (error) {
22
+ return defaults;
23
+ }
24
+ };
25
+
26
+ export default async (filename = "primate.config.js") => {
27
+ is(filename).string();
28
+ let root;
29
+ try {
30
+ // use module root if possible
31
+ root = await Path.root();
32
+ } catch (error) {
33
+ // fall back to current directory
34
+ root = Path.resolve();
35
+ }
36
+ const config = await getConfig(root, filename);
37
+
38
+ const env = {
39
+ ...config,
40
+ paths: qualify(root, config.paths),
41
+ root,
42
+ log: {...log, error: error => log.error(error, config),
43
+ },
44
+ };
45
+ env.log.info(`${package_json.name} \x1b[34m${package_json.version}\x1b[0m`);
46
+ const modules = await Promise.all(config.modules.map(module => module(env)));
47
+ return cache("config", filename, () => ({...env, modules}));
48
+ };
package/src/duck.js ADDED
@@ -0,0 +1,4 @@
1
+ import {Headers} from "runtime-compat/http";
2
+
3
+ export const isResponse = value =>
4
+ value.body !== undefined && value.headers instanceof Headers;
@@ -40,20 +40,20 @@ export default test => {
40
40
  });
41
41
 
42
42
  test.case("one property of a subobject", assert => {
43
- const base = {key: {"subkey": "subvalue"}};
44
- const extension = {key: {"subkey": "subvalue 2"}};
43
+ const base = {key: {subkey: "subvalue"}};
44
+ const extension = {key: {subkey: "subvalue 2"}};
45
45
  assert(extend(base, extension)).equals(extension);
46
46
  });
47
47
 
48
48
  test.case("two properties of a subobject, one replaced", assert => {
49
- const base = {key: {subkey: "subvalue", subkey2: "subvalue2"}};
50
- const extension = {key: {subkey: "subvalue 2"}};
51
- const extended = {key: {subkey: "subvalue 2", subkey2: "subvalue2"}};
52
- assert(extend(base, extension)).equals(extended);
53
- });
49
+ const base = {key: {subkey: "subvalue", subkey2: "subvalue2"}};
50
+ const extension = {key: {subkey: "subvalue 2"}};
51
+ const extended = {key: {subkey: "subvalue 2", subkey2: "subvalue2"}};
52
+ assert(extend(base, extension)).equals(extended);
53
+ });
54
54
 
55
- test.case("configuration enhancement", assert => {
56
- const default_conf = {
55
+ test.case("config enhancement", assert => {
56
+ const base = {
57
57
  base: "/",
58
58
  debug: false,
59
59
  defaults: {
@@ -61,16 +61,14 @@ export default test => {
61
61
  context: "guest",
62
62
  },
63
63
  paths: {
64
- client: "client",
65
- data: {
66
- domains: "domains",
67
- stores: "stores",
68
- },
69
64
  public: "public",
65
+ static: "static",
66
+ routes: "routes",
67
+ components: "components",
70
68
  },
71
69
  };
72
70
 
73
- const additional_conf = {
71
+ const additional = {
74
72
  debug: true,
75
73
  environment: "testing",
76
74
  defaults: {
@@ -78,11 +76,7 @@ export default test => {
78
76
  mode: "operational",
79
77
  },
80
78
  paths: {
81
- client: "client_logic",
82
- data: {
83
- stores: "storage",
84
- drivers: "drivers",
85
- },
79
+ client: "client",
86
80
  },
87
81
  };
88
82
 
@@ -96,16 +90,14 @@ export default test => {
96
90
  mode: "operational",
97
91
  },
98
92
  paths: {
99
- client: "client_logic",
100
- data: {
101
- domains: "domains",
102
- drivers: "drivers",
103
- stores: "storage",
104
- },
93
+ client: "client",
105
94
  public: "public",
95
+ static: "static",
96
+ routes: "routes",
97
+ components: "components",
106
98
  },
107
99
  };
108
100
 
109
- assert(extend(default_conf, additional_conf)).equals(extended);
101
+ assert(extend(base, additional)).equals(extended);
110
102
  });
111
103
  };
@@ -1,7 +1,4 @@
1
- const response = {
2
- body: "Page not found",
1
+ export default () => () => ["Page not found", {
3
2
  status: 404,
4
3
  headers: {"Content-Type": "text/html"},
5
- };
6
-
7
- export default () => () => ({...response});
4
+ }];
@@ -1,7 +1,4 @@
1
- const response = {
1
+ export default (_, ...keys) => async () => [JSON.stringify(await keys[0]), {
2
2
  status: 200,
3
3
  headers: {"Content-Type": "application/json"},
4
- };
5
-
6
- export default (strings, ...keys) => async () =>
7
- ({...response, body: JSON.stringify(await keys[0])});
4
+ }];
@@ -1,7 +1,4 @@
1
- const response = {
1
+ export default (_, ...keys) => async () => [await keys[0], {
2
2
  status: 200,
3
3
  headers: {"Content-Type": "application/octet-stream"},
4
- };
5
-
6
- export default (strings, ...keys) => async () =>
7
- ({...response, body: await keys[0]});
4
+ }];
@@ -1,8 +1,4 @@
1
1
  const last = -1;
2
- const response = {
3
- status: 200,
4
- headers: {"Content-Type": "text/plain"},
5
- };
6
2
 
7
3
  export default (strings, ...keys) => async () => {
8
4
  const awaitedKeys = await Promise.all(keys);
@@ -10,5 +6,6 @@ export default (strings, ...keys) => async () => {
10
6
  .slice(0, last)
11
7
  .map((string, i) => string + awaitedKeys[i])
12
8
  .join("") + strings[strings.length + last];
13
- return {...response, body};
9
+
10
+ return [body, {status: 200, headers: {"Content-Type": "text/plain"}}];
14
11
  };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ OK: 200,
3
+ Found: 302,
4
+ InternalServerError: 500,
5
+ };
package/src/log.js CHANGED
@@ -19,4 +19,11 @@ const log = new Proxy(Log, {
19
19
  log.paint(colors[property] ?? reset, message).paint(reset, " ")),
20
20
  });
21
21
 
22
- export default log;
22
+ export const info = (...args) => log.green("[info]").reset(...args).nl();
23
+
24
+ export const warn = (...args) => log.yellow("[warn]").reset(...args).nl();
25
+
26
+ export const error = (error, env) => {
27
+ log.red("[error]").reset(error.message).nl();
28
+ env.debug && console.log(error);
29
+ };
package/src/mimes.js ADDED
@@ -0,0 +1,12 @@
1
+ export default {
2
+ binary: "application/octet-stream",
3
+ css: "text/css",
4
+ html: "text/html",
5
+ jpg: "image/jpeg",
6
+ js: "text/javascript",
7
+ json: "application/json",
8
+ png: "image/png",
9
+ svg: "image/svg+xml",
10
+ woff2: "font/woff2",
11
+ webp: "image/webp",
12
+ };
@@ -18,4 +18,5 @@ export default {
18
18
  routes: "routes",
19
19
  components: "components",
20
20
  },
21
+ modules: [],
21
22
  };
package/src/respond.js ADDED
@@ -0,0 +1,24 @@
1
+ import {Blob} from "runtime-compat/fs";
2
+ import text from "./handlers/text.js";
3
+ import json from "./handlers/json.js";
4
+ import stream from "./handlers/stream.js";
5
+ import {isResponse as isResponseDuck} from "./duck.js";
6
+ import RouteError from "./errors/Route.js";
7
+
8
+ const isText = value => {
9
+ if (typeof value === "string") {
10
+ return text`${value}`;
11
+ }
12
+ throw new RouteError(`no handler found for ${value}`);
13
+ };
14
+ const isObject = value => typeof value === "object" && value !== null
15
+ ? json`${value}` : isText(value);
16
+ const isResponse = value => isResponseDuck(value)
17
+ ? () => value : isObject(value);
18
+ const isStream = value => value instanceof ReadableStream
19
+ ? stream`${value}` : isResponse(value);
20
+ const isBlob = value => value instanceof Blob
21
+ ? stream`${value}` : isStream(value);
22
+ const guess = value => isBlob(value);
23
+
24
+ export default result => typeof result === "function" ? result : guess(result);
package/src/route.js CHANGED
@@ -1,21 +1,7 @@
1
- import {ReadableStream} from "runtime-compat/streams";
2
- import {Path, File} from "runtime-compat/filesystem";
1
+ import {Path} from "runtime-compat/fs";
3
2
  import {is} from "runtime-compat/dyndef";
4
- import {http404} from "./handlers/http.js";
5
- import text from "./handlers/text.js";
6
- import json from "./handlers/json.js";
7
- import stream from "./handlers/stream.js";
8
3
  import RouteError from "./errors/Route.js";
9
4
 
10
- const isText = value => typeof value === "string" ? text`${value}` : http404``;
11
- const isObject = value => typeof value === "object" && value !== null
12
- ? json`${value}` : isText(value);
13
- const isStream = value => value instanceof ReadableStream
14
- ? stream`${value}` : isObject(value);
15
- const isFile = value => value instanceof File
16
- ? stream`${value}` : isStream(value);
17
- const guess = value => isFile(value);
18
-
19
5
  // insensitive-case equal
20
6
  const ieq = (left, right) => left.toLowerCase() === right.toLowerCase();
21
7
 
@@ -43,19 +29,19 @@ export default async definitions => {
43
29
  get: (path, callback) => add("get", path, callback),
44
30
  post: (path, callback) => add("post", path, callback),
45
31
  alias: (key, value) => aliases.push({key, value}),
46
- process: async request => {
32
+ route: async request => {
47
33
  const {method} = request.original;
48
34
  const url = new URL(`https://primatejs.com${request.pathname}`);
49
35
  const {pathname, searchParams} = url;
50
36
  const params = Object.fromEntries(searchParams);
51
- const verb = find(method, pathname, {handler: () => http404``});
37
+ const verb = find(method, pathname, {handler: () => {
38
+ throw new RouteError(`no ${method.toUpperCase()} route to ${pathname}`);
39
+ }});
52
40
  const path = pathname.split("/").filter(part => part !== "");
53
41
  const named = verb.path?.exec(pathname)?.groups ?? {};
54
42
 
55
- const result = await verb.handler(await find("map", pathname)
43
+ return verb.handler(await find("map", pathname)
56
44
  .handler({...request, pathname, params, path, named}));
57
-
58
- return typeof result === "function" ? result : guess(result);
59
45
  },
60
46
  };
61
47
  if (await definitions.exists) {
package/src/run.js CHANGED
@@ -1,22 +1,13 @@
1
+ import bundle from "./bundle.js";
2
+ import config from "./config.js";
1
3
  import serve from "./serve.js";
2
4
  import route from "./route.js";
3
- import bundle from "./bundle.js";
4
- import package_json from "../package.json" assert {type: "json"};
5
- import log from "./log.js";
6
-
7
- const extract = (modules, key) => modules.flatMap(module => module[key] ?? []);
8
-
9
- export default async conf => {
10
- log.reset("Primate").yellow(package_json.version);
11
5
 
12
- const {paths} = conf;
6
+ export default async () => {
7
+ const env = await config();
8
+ const {paths} = env;
13
9
  const router = await route(paths.routes);
14
- await bundle(conf);
10
+ await bundle(env);
15
11
 
16
- await serve({router,
17
- paths: conf.paths,
18
- from: conf.paths.public,
19
- http: conf.http,
20
- modules: extract(conf.modules ?? [], "serve"),
21
- });
12
+ serve({router, ...env});
22
13
  };