primate 0.19.4 → 0.20.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.
Files changed (43) hide show
  1. package/package.json +4 -3
  2. package/src/Logger.js +48 -64
  3. package/src/app.js +86 -121
  4. package/src/bin.js +0 -1
  5. package/src/defaults/primate.config.js +12 -5
  6. package/src/dispatch.js +8 -13
  7. package/src/errors.js +6 -146
  8. package/src/errors.json +110 -0
  9. package/src/exports.js +1 -1
  10. package/src/handlers/error.js +4 -4
  11. package/src/handlers/html.js +5 -6
  12. package/src/handlers/json.js +2 -2
  13. package/src/handlers/redirect.js +3 -3
  14. package/src/handlers/stream.js +2 -2
  15. package/src/handlers/text.js +2 -2
  16. package/src/handlers/view.js +3 -3
  17. package/src/hooks/bundle.js +4 -15
  18. package/src/hooks/compile.js +21 -2
  19. package/src/hooks/copy_includes.js +24 -0
  20. package/src/hooks/handle.js +55 -42
  21. package/src/hooks/parse.js +13 -16
  22. package/src/hooks/publish.js +42 -23
  23. package/src/hooks/register.js +1 -3
  24. package/src/hooks/{handle → respond}/respond.js +1 -1
  25. package/src/hooks/route.js +25 -85
  26. package/src/loaders/common.js +34 -0
  27. package/src/loaders/exports.js +3 -0
  28. package/src/loaders/modules.js +40 -0
  29. package/src/loaders/routes/exports.js +3 -0
  30. package/src/loaders/routes/guards.js +3 -0
  31. package/src/loaders/routes/layouts.js +3 -0
  32. package/src/loaders/routes/load.js +17 -0
  33. package/src/loaders/routes/routes.js +22 -0
  34. package/src/loaders/routes.js +45 -0
  35. package/src/loaders/types.js +19 -0
  36. package/src/run.js +13 -24
  37. package/src/start.js +11 -15
  38. package/src/extend.js +0 -10
  39. package/src/http-statuses.js +0 -4
  40. /package/src/defaults/{index.html → app.html} +0 -0
  41. /package/src/hooks/{handle → respond}/duck.js +0 -0
  42. /package/src/hooks/{handle → respond}/exports.js +0 -0
  43. /package/src/hooks/{handle → respond}/mime.js +0 -0
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.19.4",
3
+ "version": "0.20.1",
4
4
  "description": "Expressive, minimal and extensible web framework",
5
5
  "homepage": "https://primatejs.com",
6
6
  "bugs": "https://github.com/primatejs/primate/issues",
7
7
  "license": "MIT",
8
8
  "files": [
9
9
  "src/**/*.js",
10
- "src/defaults/index.html",
10
+ "src/errors.json",
11
+ "src/defaults/app.html",
11
12
  "!src/**/*.spec.js"
12
13
  ],
13
14
  "bin": "src/bin.js",
@@ -17,7 +18,7 @@
17
18
  "directory": "packages/primate"
18
19
  },
19
20
  "dependencies": {
20
- "runtime-compat": "^0.17.3"
21
+ "runtime-compat": "^0.20.2"
21
22
  },
22
23
  "type": "module",
23
24
  "exports": "./src/exports.js"
package/src/Logger.js CHANGED
@@ -1,16 +1,17 @@
1
1
  import {assert, is} from "runtime-compat/dyndef";
2
2
  import {blue, bold, green, red, yellow, dim} from "runtime-compat/colors";
3
+ import {map, valmap} from "runtime-compat/object";
3
4
 
4
- const errors = {
5
+ const levels = {
5
6
  Error: 0,
6
7
  Warn: 1,
7
8
  Info: 2,
8
9
  };
9
10
 
10
11
  const print = (...messages) => process.stdout.write(messages.join(" "));
11
- const bye = () => print(dim(yellow("~~ bye\n")));
12
- const mark = (format, ...params) => params.reduce((formatted, param) =>
13
- formatted.replace("%", bold(param)), format);
12
+ const bye = _ => print(dim(yellow("~~ bye\n")));
13
+ const mark = (format, ...params) => params.reduce((formatted, param, i) =>
14
+ formatted.replace(`{${i}}`, bold(param)), format);
14
15
 
15
16
  const reference = "https://primatejs.com/reference/errors";
16
17
 
@@ -21,103 +22,86 @@ const hyphenate = classCased => classCased
21
22
  .join("")
22
23
  .slice(1);
23
24
 
25
+ const throwable = ({message, level, fix}, name, module) => ({
26
+ new(...args) {
27
+ const error = new Error(mark(message, ...args));
28
+ error.level = Logger[level];
29
+ error.fix = mark(fix, ...args);
30
+ error.name = name;
31
+ error.module = module;
32
+ return error;
33
+ },
34
+ throw(...args) {
35
+ throw this.new(...args);
36
+ },
37
+ warn(logger, ...args) {
38
+ const error = {level: Logger[level], message: mark(message, ...args),
39
+ fix: mark(fix, ...args)};
40
+ logger.auto({...error, name, module});
41
+ },
42
+ });
43
+
24
44
  const Logger = class Logger {
25
45
  #level; #trace;
26
46
 
27
- static throwable(type, name, module) {
28
- return {
29
- throw(args = {}) {
30
- const {message, level, fix} = type(args);
31
- const error = new Error(mark(...message));
32
- error.level = level;
33
- error.fix = mark(...fix);
34
- error.name = name;
35
- error.module = module;
36
- throw error;
37
- },
38
- warn(logger, ...args) {
39
- const {message, level, fix} = type(...args);
40
- const error = {level, message: mark(...message), fix: mark(...fix)};
41
- logger.auto({...error, name, module});
42
- },
43
- };
47
+ static err(errors, module) {
48
+ return map(errors, ([key, value]) => [key, throwable(value, key, module)]);
44
49
  }
45
50
 
46
- constructor({level = errors.Error, trace = false} = {}) {
47
- assert(level !== undefined && level <= errors.Info);
51
+ constructor({level = levels.Error, trace = false} = {}) {
52
+ assert(level !== undefined && level <= levels.Info);
48
53
  is(trace).boolean();
49
54
  this.#level = level;
50
55
  this.#trace = trace;
51
56
  }
52
57
 
53
- static print(...args) {
54
- print(...args);
55
- }
56
-
57
- static get mark() {
58
- return mark;
59
- }
60
-
61
58
  static get Error() {
62
- return errors.Error;
59
+ return levels.Error;
63
60
  }
64
61
 
65
62
  static get Warn() {
66
- return errors.Warn;
63
+ return levels.Warn;
67
64
  }
68
65
 
69
66
  static get Info() {
70
- return errors.Info;
71
- }
72
-
73
- get class() {
74
- return this.constructor;
67
+ return levels.Info;
75
68
  }
76
69
 
77
- #print(pre, color, message, {fix, module, name} = {}, error) {
78
- print(pre, `${module !== undefined ? `${color(module)} ` : ""}${message}`, "\n");
79
- if (fix && this.level >= errors.Warn) {
70
+ #print(pre, color, message, error = {}) {
71
+ const {fix, module, name, level} = error;
72
+ print(color(pre), `${module !== undefined ? `${color(module)} ` : ""}${message}`, "\n");
73
+ if (fix) {
80
74
  print(blue("++"), fix);
81
75
  name && print(dim(`\n -> ${reference}/${module ?? "primate"}#${hyphenate(name)}`), "\n");
82
76
  }
83
- this.#trace && error && console.log(error);
77
+ if (level === levels.Error || level === undefined && error.message) {
78
+ this.#trace && console.log(error);
79
+ }
84
80
  }
85
81
 
86
82
  get level() {
87
83
  return this.#level;
88
84
  }
89
85
 
90
- info(message, args) {
91
- if (this.level >= errors.Info) {
92
- this.#print(green("--"), green, message, args);
93
- }
86
+ info(...args) {
87
+ this.level >= levels.Info && this.#print("--", green, ...args);
94
88
  }
95
89
 
96
- warn(message, args) {
97
- if (this.level >= errors.Warn) {
98
- this.#print(yellow("??"), yellow, message, args);
99
- }
90
+ warn(...args) {
91
+ this.level >= levels.Warn && this.#print("??", yellow, ...args);
100
92
  }
101
93
 
102
- error(message, args, error) {
103
- if (this.level >= errors.Warn) {
104
- this.#print(red("!!"), red, message, args, error);
105
- }
94
+ error(...args) {
95
+ this.level >= levels.Warn && this.#print("!!", red, ...args);
106
96
  }
107
97
 
108
98
  auto(error) {
109
- const {level, message, ...args} = error;
110
- if (level === errors.Info) {
111
- return this.info(message, args, error);
112
- }
113
- if (level === errors.Warn) {
114
- return this.warn(message, args, error);
115
- }
116
-
117
- return this.error(message, args, error);
99
+ const {message} = error;
100
+ const matches = map(levels, ([name, level]) => [level, name.toLowerCase()]);
101
+ return this[matches[error.level] ?? "error"](message, error);
118
102
  }
119
103
  };
120
104
 
121
105
  export default Logger;
122
106
 
123
- export {print, bye};
107
+ export {print, bye, mark};
package/src/app.js CHANGED
@@ -1,38 +1,28 @@
1
1
  import crypto from "runtime-compat/crypto";
2
+ import {tryreturn} from "runtime-compat/flow";
2
3
  import {File, Path} from "runtime-compat/fs";
3
4
  import {bold, blue} from "runtime-compat/colors";
4
- import errors from "./errors.js";
5
+ import {transform, valmap} from "runtime-compat/object";
5
6
  import * as handlers from "./handlers/exports.js";
6
7
  import * as hooks from "./hooks/exports.js";
8
+ import * as loaders from "./loaders/exports.js";
7
9
  import dispatch from "./dispatch.js";
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
- }, {});
10
+ import {print} from "./Logger.js";
17
11
 
18
12
  const base = new Path(import.meta.url).up(1);
19
- const defaultLayout = "index.html";
13
+ // do not hard-depend on node
14
+ const packager = import.meta.runtime?.packager ?? "package.json";
15
+ const library = import.meta.runtime?.library ?? "node_modules";
20
16
 
21
- const index = async (app, layout = defaultLayout) => {
22
- const name = layout;
23
- try {
24
- // user-provided file
25
- return await File.read(`${app.paths.layouts.join(name)}`);
26
- } catch (error) {
27
- // fallback
28
- return base.join("defaults", defaultLayout).text();
29
- }
30
- };
17
+ // use user-provided file or fall back to default
18
+ const index = (app, name) =>
19
+ tryreturn(async _ => File.read(`${app.paths.pages.join(name)}`))
20
+ .orelse(async _ => base.join("defaults", app.config.index).text());
31
21
 
32
22
  const hash = async (string, algorithm = "sha-384") => {
33
23
  const encoder = new TextEncoder();
34
24
  const bytes = await crypto.subtle.digest(algorithm, encoder.encode(string));
35
- const algo = algorithm.replace("-", () => "");
25
+ const algo = algorithm.replace("-", _ => "");
36
26
  return `${algo}-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}`;
37
27
  };
38
28
 
@@ -45,92 +35,53 @@ const tag = ({name, attributes = {}, code = "", close = true}) =>
45
35
 
46
36
  export default async (config, root, log) => {
47
37
  const {http} = config;
38
+ const secure = http?.ssl !== undefined;
39
+ const {name, version} = await base.up(1).join(packager).json();
40
+ const paths = valmap(config.paths, value => root.join(value));
41
+ paths.client = paths.build.join("client");
42
+ paths.server = paths.build.join("server");
43
+
44
+ const at = `at http${secure ? "s" : ""}://${http.host}:${http.port}\n`;
45
+ print(blue(bold(name)), blue(version), at);
48
46
 
49
47
  // if ssl activated, resolve key and cert early
50
- if (http.ssl) {
48
+ if (secure) {
51
49
  http.ssl.key = root.join(http.ssl.key);
52
50
  http.ssl.cert = root.join(http.ssl.cert);
53
51
  }
54
52
 
55
- const paths = qualify(root, config.paths);
56
-
57
- const ending = ".js";
58
- const routes = paths.routes === undefined ? [] : await Promise.all(
59
- (await Path.collect(paths.routes, /^.*.js$/u))
60
- .map(async route => [
61
- `${route}`.replace(paths.routes, "").slice(1, -ending.length),
62
- (await import(route)).default,
63
- ]));
64
- const types = Object.fromEntries(
65
- paths.types === undefined ? [] : await Promise.all(
66
- (await Path.collect(paths.types , /^.*.js$/u))
67
- /* accept only lowercase-first files in type filename */
68
- .filter(path => /^[a-z]/u.test(path.name))
69
- .map(async type => [
70
- `${type}`.replace(paths.types, "").slice(1, -ending.length),
71
- (await import(type)).default,
72
- ])));
73
- if (await paths.types.exists) {
74
- Object.keys(types).length === 0
75
- && errors.EmptyTypeDirectory.warn(log, {root: paths.types});
76
- }
77
- Object.entries(types).some(([name, type]) =>
78
- typeof type !== "function" && errors.InvalidType.throw({name}));
79
-
80
- const modules = config.modules === undefined ? [] : config.modules;
81
-
82
- modules.every((module, n) => module.name !== undefined ||
83
- errors.ModulesMustHaveNames.throw({n}));
84
-
85
- new Set(modules.map(({name}) => name)).size !== modules.length &&
86
- errors.DoubleModule.throw({
87
- modules: modules.map(({name}) => name),
88
- config: root.join("primate.config.js"),
89
- });
90
-
91
- const hookless = modules.filter(module => !Object.keys(module).some(key =>
92
- [...Object.keys(hooks), "load"].includes(key)));
93
- hookless.length > 0 && errors.ModuleHasNoHooks.warn(log, {hookless});
94
-
95
- const {name, version} = await base.up(1).join("package.json").json();
53
+ const types = await loaders.types(log, paths.types);
96
54
 
97
55
  const app = {
98
56
  config,
99
- routes,
100
- secure: http?.ssl !== undefined,
57
+ secure,
101
58
  name,
102
59
  version,
103
- library: {},
104
- identifiers: {},
105
- replace(code) {
106
- const joined = Object.keys(app.identifiers).join("|");
107
- const re = `(?<=import (?:.*) from ['|"])(${joined})(?=['|"])`;
108
- return code.replaceAll(new RegExp(re, "gus"), (_, p1) => {
109
- if (app.library[p1] === undefined) {
110
- app.library[p1] = app.identifiers[p1];
111
- }
112
- return app.identifiers[p1];
113
- });
114
- },
115
- resources: [],
60
+ importmaps: {},
61
+ assets: [],
116
62
  entrypoints: [],
117
63
  paths,
118
64
  root,
119
65
  log,
120
- generateHeaders: () => {
66
+ async copy(source, target, filter = /^.*.js$/u) {
67
+ const jss = await source.collect(filter);
68
+ await Promise.all(jss.map(async js => {
69
+ const file = await js.file.read();
70
+ const to = await target.join(js.path.replace(source, ""));
71
+ await to.directory.file.create();
72
+ await to.file.write(file);
73
+ }));
74
+ },
75
+ headers: _ => {
121
76
  const csp = Object.keys(http.csp).reduce((policy_string, key) =>
122
77
  `${policy_string}${key} ${http.csp[key]};`, "");
123
- const scripts = app.resources
78
+ const scripts = app.assets
124
79
  .filter(({type}) => type !== "style")
125
- .map(resource => `'${resource.integrity}'`).join(" ");
80
+ .map(asset => `'${asset.integrity}'`).join(" ");
126
81
  const _csp = scripts === "" ? csp : `${csp}script-src 'self' ${scripts};`;
127
- // remove inline resources
128
- for (let i = app.resources.length - 1; i >= 0; i--) {
129
- const resource = app.resources[i];
130
- if (resource.inline) {
131
- app.resources.splice(i, 1);
132
- }
133
- }
82
+ // remove inline assets
83
+ app.assets = app.assets.filter(({inline, type}) => !inline
84
+ || type === "importmap");
134
85
 
135
86
  return {
136
87
  "Content-Security-Policy": _csp,
@@ -138,8 +89,8 @@ export default async (config, root, log) => {
138
89
  };
139
90
  },
140
91
  handlers: {...handlers},
141
- render: async ({body = "", head = "", layout} = {}) => {
142
- const html = await index(app, layout);
92
+ render: async ({body = "", head = "", page} = {}) => {
93
+ const html = await index(app, page ?? config.index);
143
94
  // inline: <script type integrity>...</script>
144
95
  // outline: <script type integrity src></script>
145
96
  const script = ({inline, code, type, integrity, src}) => inline
@@ -150,50 +101,64 @@ export default async (config, root, log) => {
150
101
  const style = ({inline, code, href, rel = "stylesheet"}) => inline
151
102
  ? tag({name: "style", code})
152
103
  : tag({name: "link", attributes: {rel, href}, close: false});
153
- const heads = app.resources.map(({src, code, type, inline, integrity}) =>
154
- type === "style"
155
- ? style({inline, code, href: src})
156
- : script({inline, code, type, integrity, src})
157
- ).join("\n");
104
+ const heads = app.assets
105
+ .toSorted(({type}) => -1 * (type === "importmap"))
106
+ .map(({src, code, type, inline, integrity}) =>
107
+ type === "style"
108
+ ? style({inline, code, href: src})
109
+ : script({inline, code, type, integrity, src})
110
+ ).join("\n");
158
111
  return html
159
- .replace("%body%", () => body)
160
- .replace("%head%", () => `${head}${heads}`);
112
+ .replace("%body%", _ => body)
113
+ .replace("%head%", _ => `${head}${heads}`);
161
114
  },
162
115
  publish: async ({src, code, type = "", inline = false}) => {
163
- if (type === "module") {
164
- code = app.replace(code);
116
+ if (!inline) {
117
+ const base = paths.client.join(src);
118
+ await base.directory.file.create();
119
+ await base.file.write(code);
165
120
  }
166
121
  const integrity = await hash(code);
167
122
  const _src = new Path(http.static.root).join(src ?? "");
168
- app.resources.push({src: `${_src}`, code, type, inline, integrity});
123
+ app.assets.push({src: `${_src}`, code: inline ? code : "", type, inline, integrity});
169
124
  return integrity;
170
125
  },
171
126
  bootstrap: ({type, code}) => {
172
127
  app.entrypoints.push({type, code});
173
128
  },
174
- resolve: (pkg, name) => {
175
- const exports = Object.fromEntries(Object.entries(pkg.exports)
176
- .filter(([, _export]) => _export.import !== undefined)
177
- .map(([key, value]) => [
178
- key.replace(".", name),
179
- value.import.replace(".", `./${name}`),
180
- ]));
181
- app.identifiers = {...exports, ...app.identifiers};
129
+ async import(module) {
130
+ const {build} = config;
131
+ const {root} = http.static;
132
+ const path = [library, module];
133
+ const pkg = await Path.resolve().join(...path, packager).json();
134
+ const exports = pkg.exports === undefined
135
+ ? {[module]: `/${module}/${pkg.main}`}
136
+ : transform(pkg.exports, entry => entry
137
+ .filter(([, _export]) => _export.import !== undefined)
138
+ .map(([key, value]) => [
139
+ key.replace(".", module),
140
+ value.import.replace(".", `./${module}`),
141
+ ]));
142
+ await Promise.all(Object.values(exports).map(async name => app.publish({
143
+ code: await Path.resolve().join(library, name).text(),
144
+ src: new Path(root, build.modules, name),
145
+ type: "module",
146
+ })));
147
+ this.importmaps = {
148
+ ...valmap(exports, value => new Path(root, build.modules, value).path),
149
+ ...this.importmaps};
182
150
  },
183
- modules,
184
151
  types,
152
+ routes: await loaders.routes(log, paths.routes),
153
+ dispatch: dispatch(types),
185
154
  };
186
- log.class.print(blue(bold(name)), blue(version),
187
- `at http${app.secure ? "s" : ""}://${http.host}:${http.port}\n`);
188
- // modules may load other modules
189
- await Promise.all(app.modules
190
- .filter(module => module.load !== undefined)
191
- .map(module => module.load({...app, load(dependent) {
192
- app.modules.push(dependent);
193
- }})));
194
155
 
195
- app.route = hooks.route({...app, dispatch: dispatch(types)});
196
- app.parse = hooks.parse(dispatch(types));
156
+ const modules = await loaders.modules(app, root, config);
197
157
 
198
- return app;
158
+ return {...app,
159
+ modules,
160
+ layoutDepth: Math.max(...app.routes.map(({layouts}) => layouts.length)) + 1,
161
+ route: hooks.route({...app, modules}),
162
+ parse: hooks.parse(dispatch(types)),
163
+ };
199
164
  };
package/src/bin.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
  import args from "runtime-compat/args";
3
3
  import run from "./run.js";
4
-
5
4
  await run(args[0]);
@@ -18,19 +18,26 @@ export default {
18
18
  },
19
19
  static: {
20
20
  root: "/",
21
- pure: false,
22
21
  },
23
22
  },
23
+ index: "app.html",
24
24
  paths: {
25
- layouts: "layouts",
25
+ build: "build",
26
26
  static: "static",
27
- public: "public",
28
- routes: "routes",
29
27
  components: "components",
28
+ routes: "routes",
30
29
  types: "types",
30
+ pages: "pages",
31
+ layouts: "layouts",
32
+ },
33
+ build: {
34
+ includes: [],
35
+ static: "static",
36
+ app: "app",
37
+ modules: "modules",
38
+ index: "index.js",
31
39
  },
32
40
  modules: [],
33
- dist: "app",
34
41
  types: {
35
42
  explicit: false,
36
43
  },
package/src/dispatch.js CHANGED
@@ -1,24 +1,19 @@
1
1
  import {is, maybe} from "runtime-compat/dyndef";
2
+ import {tryreturn} from "runtime-compat/flow";
3
+ import {map} from "runtime-compat/object";
2
4
  import errors from "./errors.js";
3
5
 
4
6
  export default (patches = {}) => value => {
5
7
  is(patches.get).undefined();
6
8
  return Object.assign(Object.create(null), {
7
- ...Object.fromEntries(Object.entries(patches).map(([name, patch]) =>
8
- [name, property => {
9
- is(property).defined(`\`${name}\` called without property`);
10
- try {
11
- return patch(value[property], property);
12
- } catch (error) {
13
- errors.MismatchedType.throw({message: error.message});
14
- }
15
- }])),
9
+ ...map(patches, ([name, patch]) => [name, property => {
10
+ is(property).defined(`\`${name}\` called without property`);
11
+ return tryreturn(_ => patch(value[property], property))
12
+ .orelse(({message}) => errors.MismatchedType.throw(message));
13
+ }]),
16
14
  get(property) {
17
15
  maybe(property).string();
18
- if (property !== undefined) {
19
- return value[property];
20
- }
21
- return value;
16
+ return property === undefined ? value : value[property];
22
17
  },
23
18
  });
24
19
  };