primate 0.17.0 → 0.19.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,12 +1,13 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
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
- "src/**",
9
+ "src/**/*.js",
10
+ "src/defaults/index.html",
10
11
  "!src/**/*.spec.js"
11
12
  ],
12
13
  "bin": "src/bin.js",
@@ -16,7 +17,7 @@
16
17
  "directory": "packages/primate"
17
18
  },
18
19
  "dependencies": {
19
- "runtime-compat": "^0.16.3"
20
+ "runtime-compat": "^0.17.0"
20
21
  },
21
22
  "type": "module",
22
23
  "exports": "./src/exports.js"
package/src/Logger.js CHANGED
@@ -1,115 +1,123 @@
1
1
  import {assert, is} from "runtime-compat/dyndef";
2
+ import {blue, bold, green, red, yellow, dim} from "runtime-compat/colors";
2
3
 
3
- const colors = {
4
- black: msg => `\x1b[0m${msg}\x1b[0m`,
5
- bold: msg => `\x1b[1m${msg}\x1b[0m`,
6
- red: msg => `\x1b[31m${msg}\x1b[0m`,
7
- green: msg => `\x1b[32m${msg}\x1b[0m`,
8
- yellow: msg => `\x1b[33m${msg}\x1b[0m`,
9
- blue: msg => `\x1b[34m${msg}\x1b[0m`,
10
- gray: msg => `\x1b[2m${msg}\x1b[0m`,
4
+ const errors = {
5
+ Error: 0,
6
+ Warn: 1,
7
+ Info: 2,
11
8
  };
12
9
 
13
- const error = 0;
14
- const warn = 1;
15
- const info = 2;
16
-
17
- const Abort = class Abort extends Error {};
18
- // Error natively provided
19
- const Warn = class Warn extends Error {};
20
- const Info = class Info extends Error {};
21
-
22
- const levels = new Map([
23
- [Error, error],
24
- [Warn, warn],
25
- [Info, info],
26
- ]);
10
+ 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);
27
14
 
28
- const abort = message => {
29
- throw new Abort(message);
30
- };
15
+ const reference = "https://primatejs.com/reference/errors";
31
16
 
32
- const print = (...messages) => process.stdout.write(messages.join(" "));
17
+ const hyphenate = classCased => classCased
18
+ .split("")
19
+ .map(character => character
20
+ .replace(/[A-Z]/u, capital => `-${capital.toLowerCase()}`))
21
+ .join("")
22
+ .slice(1);
33
23
 
34
24
  const Logger = class Logger {
35
25
  #level; #trace;
36
26
 
37
- constructor({level = Error, trace = false} = {}) {
38
- assert(level !== undefined && levels.get(level) <= info);
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
+ };
44
+ }
45
+
46
+ constructor({level = errors.Error, trace = false} = {}) {
47
+ assert(level !== undefined && level <= errors.Info);
39
48
  is(trace).boolean();
40
49
  this.#level = level;
41
50
  this.#trace = trace;
42
51
  }
43
52
 
44
- static get colors() {
45
- return colors;
46
- }
47
-
48
53
  static print(...args) {
49
54
  print(...args);
50
55
  }
51
56
 
57
+ static get mark() {
58
+ return mark;
59
+ }
60
+
52
61
  static get Error() {
53
- return Error;
62
+ return errors.Error;
54
63
  }
55
64
 
56
65
  static get Warn() {
57
- return Warn;
66
+ return errors.Warn;
58
67
  }
59
68
 
60
69
  static get Info() {
61
- return Info;
70
+ return errors.Info;
62
71
  }
63
72
 
64
73
  get class() {
65
74
  return this.constructor;
66
75
  }
67
76
 
68
- #print(pre, error) {
69
- if (error instanceof Error) {
70
- print(colors.bold(pre), error.message, "\n");
71
- if (this.#trace) {
72
- console.log(error);
73
- }
74
- } else {
75
- print(colors.bold(pre), error, "\n");
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) {
80
+ print(blue("++"), fix);
81
+ name && print(dim(`\n -> ${reference}/${module ?? "primate"}#${hyphenate(name)}`), "\n");
76
82
  }
83
+ this.#trace && error && console.log(error);
77
84
  }
78
85
 
79
86
  get level() {
80
- return levels.get(this.#level);
87
+ return this.#level;
81
88
  }
82
89
 
83
- info(message) {
84
- if (this.level >= levels.get(Info)) {
85
- this.#print(colors.green("--"), message);
90
+ info(message, args) {
91
+ if (this.level >= errors.Info) {
92
+ this.#print(green("--"), green, message, args);
86
93
  }
87
94
  }
88
95
 
89
- warn(message) {
90
- if (this.level >= levels.get(Warn)) {
91
- this.#print(colors.yellow("??"), message);
96
+ warn(message, args) {
97
+ if (this.level >= errors.Warn) {
98
+ this.#print(yellow("??"), yellow, message, args);
92
99
  }
93
100
  }
94
101
 
95
- error(message) {
96
- if (this.level >= levels.get(Error)) {
97
- this.#print(colors.red("!!"), message);
102
+ error(message, args, error) {
103
+ if (this.level >= errors.Warn) {
104
+ this.#print(red("!!"), red, message, args, error);
98
105
  }
99
106
  }
100
107
 
101
- auto(message) {
102
- if (message instanceof Info) {
103
- return this.info(message.message);
108
+ auto(error) {
109
+ const {level, message, ...args} = error;
110
+ if (level === errors.Info) {
111
+ return this.info(message, args, error);
104
112
  }
105
- if (message instanceof Warn) {
106
- return this.warn(message.message);
113
+ if (level === errors.Warn) {
114
+ return this.warn(message, args, error);
107
115
  }
108
116
 
109
- return this.error(message);
117
+ return this.error(message, args, error);
110
118
  }
111
119
  };
112
120
 
113
121
  export default Logger;
114
122
 
115
- export {colors, levels, print, abort, Abort};
123
+ export {print, bye};
package/src/app.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import crypto from "runtime-compat/crypto";
2
2
  import {File, Path} from "runtime-compat/fs";
3
+ import {bold, blue} from "runtime-compat/colors";
4
+ import errors from "./errors.js";
3
5
  import * as handlers from "./handlers/exports.js";
4
- import {abort} from "./Logger.js";
6
+ import * as hooks from "./hooks/exports.js";
7
+ import dispatch from "./dispatch.js";
5
8
 
6
9
  const qualify = (root, paths) =>
7
10
  Object.keys(paths).reduce((sofar, key) => {
@@ -12,16 +15,17 @@ const qualify = (root, paths) =>
12
15
  return sofar;
13
16
  }, {});
14
17
 
15
- const src = new Path(import.meta.url).up(1);
18
+ const base = new Path(import.meta.url).up(1);
19
+ const defaultLayout = "index.html";
16
20
 
17
- const index = async app => {
18
- const name = "index.html";
21
+ const index = async (app, layout = defaultLayout) => {
22
+ const name = layout;
19
23
  try {
20
24
  // user-provided file
21
- return await File.read(`${app.paths.static.join(name)}`);
25
+ return await File.read(`${app.paths.layouts.join(name)}`);
22
26
  } catch (error) {
23
27
  // fallback
24
- return src.join("defaults", name).text();
28
+ return base.join("defaults", defaultLayout).text();
25
29
  }
26
30
  };
27
31
 
@@ -32,13 +36,20 @@ const hash = async (string, algorithm = "sha-384") => {
32
36
  return `${algo}-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}`;
33
37
  };
34
38
 
39
+ const attribute = attributes => Object.keys(attributes).length > 0 ?
40
+ " ".concat(Object.entries(attributes)
41
+ .map(([key, value]) => `${key}="${value}"`).join(" "))
42
+ : "";
43
+ const tag = ({name, attributes = {}, code = "", close = true}) =>
44
+ `<${name}${attribute(attributes)}${close ? `>${code}</${name}>` : "/>"}`;
45
+
35
46
  export default async (config, root, log) => {
36
- const {name, version} = await src.up(1).join("package.json").json();
47
+ const {http} = config;
37
48
 
38
49
  // if ssl activated, resolve key and cert early
39
- if (config.http.ssl) {
40
- config.http.ssl.key = root.join(config.http.ssl.key);
41
- config.http.ssl.cert = root.join(config.http.ssl.cert);
50
+ if (http.ssl) {
51
+ http.ssl.key = root.join(http.ssl.key);
52
+ http.ssl.cert = root.join(http.ssl.cert);
42
53
  }
43
54
 
44
55
  const paths = qualify(root, config.paths);
@@ -50,25 +61,41 @@ export default async (config, root, log) => {
50
61
  `${route}`.replace(paths.routes, "").slice(1, -ending.length),
51
62
  (await import(route)).default,
52
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
+ Object.entries(types).some(([name, type]) =>
74
+ typeof type !== "function" && errors.InvalidType.throw({name}));
53
75
 
54
76
  const modules = config.modules === undefined ? [] : config.modules;
55
77
 
56
- modules.every(module => module.name !== undefined ||
57
- abort("all modules must have names"));
78
+ modules.every((module, n) => module.name !== undefined ||
79
+ errors.ModulesMustHaveNames.throw({n}));
58
80
 
59
- if (new Set(modules.map(module => module.name)).size !== modules.length) {
60
- abort("same module twice");
61
- }
81
+ new Set(modules.map(({name}) => name)).size !== modules.length &&
82
+ errors.DoubleModule.throw({
83
+ modules: modules.map(({name}) => name),
84
+ config: root.join("primate.config.js"),
85
+ });
62
86
 
63
- modules.every(module => Object.entries(module).length > 1) || (() => {
64
- log.warn("some modules haven't subscribed to any hooks");
65
- })();
87
+ const hookless = modules.filter(module =>
88
+ !Object.keys(module).some(key => Object.keys(hooks).includes(key)));
89
+ hookless.length > 0 && errors.ModuleHasNoHooks.warn(log, {hookless});
90
+
91
+ const {name, version} = await base.up(1).join("package.json").json();
66
92
 
67
93
  const app = {
68
94
  config,
69
95
  routes,
70
- secure: config.http?.ssl !== undefined,
71
- name, version,
96
+ secure: http?.ssl !== undefined,
97
+ name,
98
+ version,
72
99
  library: {},
73
100
  identifiers: {},
74
101
  replace(code) {
@@ -86,20 +113,44 @@ export default async (config, root, log) => {
86
113
  paths,
87
114
  root,
88
115
  log,
116
+ generateHeaders: () => {
117
+ const csp = Object.keys(http.csp).reduce((policy_string, key) =>
118
+ `${policy_string}${key} ${http.csp[key]};`, "");
119
+ const scripts = app.resources
120
+ .filter(({type}) => type !== "style")
121
+ .map(resource => `'${resource.integrity}'`).join(" ");
122
+ const _csp = scripts === "" ? csp : `${csp}script-src 'self' ${scripts};`;
123
+ // remove inline resources
124
+ for (let i = app.resources.length - 1; i >= 0; i--) {
125
+ const resource = app.resources[i];
126
+ if (resource.inline) {
127
+ app.resources.splice(i, 1);
128
+ }
129
+ }
130
+
131
+ return {
132
+ "Content-Security-Policy": _csp,
133
+ "Referrer-Policy": "same-origin",
134
+ };
135
+ },
89
136
  handlers: {...handlers},
90
- render: async ({body = "", head = ""} = {}) => {
91
- const html = await index(app);
92
- const heads = app.resources.map(({src, code, type, inline, integrity}) => {
93
- const tag = type === "style" ? "link" : "script";
94
- const pre = type === "style"
95
- ? `<${tag} rel="stylesheet"`
96
- : `<${tag} type="${type}" integrity="${integrity}"`;
97
- const middle = type === "style"
98
- ? ` href="${src}">`
99
- : ` src="${src}">`;
100
- const post = type === "style" ? "" : `</${tag}>`;
101
- return inline ? `${pre}>${code}${post}` : `${pre}${middle}${post}`;
102
- }).join("\n");
137
+ render: async ({body = "", head = "", layout} = {}) => {
138
+ const html = await index(app, layout);
139
+ // inline: <script type integrity>...</script>
140
+ // outline: <script type integrity src></script>
141
+ const script = ({inline, code, type, integrity, src}) => inline
142
+ ? tag({name: "script", attributes: {type, integrity}, code})
143
+ : tag({name: "script", attributes: {type, integrity, src}});
144
+ // inline: <style>...</style>
145
+ // outline: <link rel="stylesheet" href/>
146
+ const style = ({inline, code, href, rel = "stylesheet"}) => inline
147
+ ? tag({name: "style", code})
148
+ : tag({name: "link", attributes: {rel, href}, close: false});
149
+ const heads = app.resources.map(({src, code, type, inline, integrity}) =>
150
+ type === "style"
151
+ ? style({inline, code, href: src})
152
+ : script({inline, code, type, integrity, src})
153
+ ).join("\n");
103
154
  return html
104
155
  .replace("%body%", () => body)
105
156
  .replace("%head%", () => `${head}${heads}`);
@@ -108,10 +159,8 @@ export default async (config, root, log) => {
108
159
  if (type === "module") {
109
160
  code = app.replace(code);
110
161
  }
111
- // while integrity is only really needed for scripts, it is also later
112
- // used for the etag header
113
162
  const integrity = await hash(code);
114
- const _src = new Path(config.http.static.root).join(src ?? "");
163
+ const _src = new Path(http.static.root).join(src ?? "");
115
164
  app.resources.push({src: `${_src}`, code, type, inline, integrity});
116
165
  return integrity;
117
166
  },
@@ -128,12 +177,10 @@ export default async (config, root, log) => {
128
177
  app.identifiers = {...exports, ...app.identifiers};
129
178
  },
130
179
  modules,
180
+ types,
131
181
  };
132
- const {print, colors} = log.class;
133
- print(colors.blue(colors.bold(name)), colors.blue(version), "");
134
- const type = app.secure ? "https" : "http";
135
- const address = `${type}://${config.http.host}:${config.http.port}`;
136
- print(colors.gray(`at ${address}`), "\n");
182
+ log.class.print(blue(bold(name)), blue(version),
183
+ `at http${app.secure ? "s" : ""}://${http.host}:${http.port}\n`);
137
184
  // modules may load other modules
138
185
  await Promise.all(app.modules
139
186
  .filter(module => module.load !== undefined)
@@ -141,5 +188,8 @@ export default async (config, root, log) => {
141
188
  app.modules.push(dependent);
142
189
  }})));
143
190
 
191
+ app.route = hooks.route({...app, dispatch: dispatch(types)});
192
+ app.parse = hooks.parse(dispatch(types));
193
+
144
194
  return app;
145
195
  };
@@ -1,11 +1,8 @@
1
1
  import {default as dev} from "./dev.js";
2
2
  import {default as serve} from "./serve.js";
3
- import {default as build} from "./build.js";
4
- import {default as create} from "./create.js";
5
- import {default as help} from "./help.js";
6
3
 
7
- const commands = {dev, serve, build, create, help};
4
+ const commands = {dev, serve};
8
5
 
9
- const run = name => commands[name] ?? help;
6
+ const run = name => commands[name] ?? dev;
10
7
 
11
8
  export default name => name === undefined ? dev : run(name);
@@ -10,6 +10,7 @@ export default {
10
10
  port: 6161,
11
11
  csp: {
12
12
  "default-src": "'self'",
13
+ "style-src": "'self'",
13
14
  "object-src": "'none'",
14
15
  "frame-ancestors": "'none'",
15
16
  "form-action": "'self'",
@@ -21,6 +22,7 @@ export default {
21
22
  },
22
23
  },
23
24
  paths: {
25
+ layouts: "layouts",
24
26
  static: "static",
25
27
  public: "public",
26
28
  routes: "routes",
@@ -29,4 +31,7 @@ export default {
29
31
  },
30
32
  modules: [],
31
33
  dist: "app",
34
+ types: {
35
+ explicit: false,
36
+ },
32
37
  };
@@ -0,0 +1,24 @@
1
+ import {is, maybe} from "runtime-compat/dyndef";
2
+ import errors from "./errors.js";
3
+
4
+ export default (patches = {}) => value => {
5
+ is(patches.get).undefined();
6
+ 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
+ }])),
16
+ get(property) {
17
+ maybe(property).string();
18
+ if (property !== undefined) {
19
+ return value[property];
20
+ }
21
+ return value;
22
+ },
23
+ });
24
+ };
package/src/errors.js ADDED
@@ -0,0 +1,141 @@
1
+ import Logger from "./Logger.js";
2
+
3
+ export default Object.fromEntries(Object.entries({
4
+ CannotParseBody({body, contentType}) {
5
+ return {
6
+ message: ["cannot parse body % as %", body, contentType],
7
+ fix: ["use a different content type or fix body"],
8
+ level: Logger.Warn,
9
+ };
10
+ },
11
+ DoubleModule({modules, config}) {
12
+ const double = modules.find((module, i, array) =>
13
+ array.filter((_, j) => i !== j).includes(module));
14
+ return {
15
+ message: ["double module % in %", double, config],
16
+ fix: ["load % only once", double],
17
+ level: Logger.Error,
18
+ };
19
+ },
20
+ DoublePathParameter({path, double}) {
21
+ return {
22
+ message: ["double path parameter % in route %", double, path],
23
+ fix: ["disambiguate path parameters in route names"],
24
+ level: Logger.Error,
25
+ };
26
+ },
27
+ DoubleRoute({double}) {
28
+ return {
29
+ message: ["double route %", double],
30
+ fix: ["disambiguate route % and %", double, `${double}/index`],
31
+ level: Logger.Error,
32
+ };
33
+ },
34
+ EmptyRouteFile({config: {paths}, route}) {
35
+ return {
36
+ message: ["empty route file at %", `${paths.routes}/${route}.js`],
37
+ fix: ["add routes or remove file"],
38
+ level: Logger.Warn,
39
+ };
40
+ },
41
+ ErrorInConfigFile({config, message}) {
42
+ return {
43
+ message: ["error in config %", message],
44
+ fix: ["check errors in config file by running %", `node ${config}`],
45
+ level: Logger.Error,
46
+ };
47
+ },
48
+ InvalidPathParameter({named, path}) {
49
+ return {
50
+ message: ["invalid path parameter % in route %", named, path],
51
+ fix: ["use only latin letters and decimal digits in path parameters"],
52
+ level: Logger.Error,
53
+ };
54
+ },
55
+ InvalidRouteName({path}) {
56
+ return {
57
+ message: ["invalid route name %", path],
58
+ fix: ["do not use dots in route names"],
59
+ level: Logger.Error,
60
+ };
61
+ },
62
+ InvalidType({name}) {
63
+ return {
64
+ message: ["invalid type %", name],
65
+ fix: ["use only functions for the default export of types"],
66
+ level: Logger.Error,
67
+ };
68
+ },
69
+ InvalidTypeName({name}) {
70
+ return {
71
+ message: ["invalid type name %", name],
72
+ fix: ["use only latin letters and decimal digits in types"],
73
+ level: Logger.Error,
74
+ };
75
+ },
76
+ MismatchedPath({path, message}) {
77
+ return {
78
+ message: [`mismatched % path: ${message}`, path],
79
+ fix: ["if unintentional, fix the type or the caller"],
80
+ level: Logger.Info,
81
+ };
82
+ },
83
+ MismatchedType({message}) {
84
+ return {
85
+ message: [`mismatched type: ${message}`],
86
+ fix: ["if unintentional, fix the type or the caller"],
87
+ level: Logger.Info,
88
+ };
89
+ },
90
+ ModuleHasNoHooks({hookless}) {
91
+ const modules = hookless.map(({name}) => name).join(", ");
92
+ return {
93
+ message: ["module % has no hooks", modules],
94
+ fix: ["ensure every module uses at least one hook or deactivate it"],
95
+ level: Logger.Warn,
96
+ };
97
+ },
98
+ ModulesMustHaveNames({n}) {
99
+ return {
100
+ message: ["modules must have names"],
101
+ fix: ["update module at index % and inform maintainer", n],
102
+ level: Logger.Error,
103
+ };
104
+ },
105
+ EmptyConfigFile({config}) {
106
+ return {
107
+ message: ["empty config file at %", config],
108
+ fix: ["add configuration options or remove file"],
109
+ level: Logger.Warn,
110
+ };
111
+ },
112
+ NoFileForPath({pathname, config: {paths}}) {
113
+ return {
114
+ message: ["no file for %", pathname],
115
+ fix: ["if unintentional create a file at %%", paths.static, pathname],
116
+ level: Logger.Info,
117
+ };
118
+ },
119
+ NoHandlerForExtension({name, ending}) {
120
+ return {
121
+ message: ["no handler for % extension", ending],
122
+ fix: ["add handler module for % files or remove %", `.${ending}`, name],
123
+ level: Logger.Error,
124
+ };
125
+ },
126
+ NoRouteToPath({method, pathname, config: {paths}}) {
127
+ const route = `${paths.routes}${pathname === "" ? "index" : pathname}.js`;
128
+ return {
129
+ message: ["no % route to %", method, pathname],
130
+ fix: ["if unintentional create a route at %", route],
131
+ level: Logger.Info,
132
+ };
133
+ },
134
+ ReservedTypeName({name}) {
135
+ return {
136
+ message: ["type name % is reserved", name],
137
+ fix: ["do not use any reserved type names"],
138
+ level: Logger.Error,
139
+ };
140
+ },
141
+ }).map(([name, error]) => [name, Logger.throwable(error, name, "primate")]));
@@ -1,8 +1,9 @@
1
- const _404 = "Not Found";
1
+ import {NotFound} from "../http-statuses.js";
2
2
 
3
- export default (body = _404, {status = 404} = {}) => async (app, headers) => [
4
- await app.render({body}), {
5
- status,
6
- headers: {...headers, "Content-Type": "text/html"},
7
- },
8
- ];
3
+ export default (body = "Not Found", {status = NotFound} = {}) =>
4
+ async (app, headers) => [
5
+ await app.render({body}), {
6
+ status,
7
+ headers: {...headers, "Content-Type": "text/html"},
8
+ },
9
+ ];
@@ -1,11 +1,33 @@
1
- export default (component, flags = {}) => {
2
- const {status = 200, partial = false, load = false} = flags;
1
+ const script = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
2
+ const style = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
3
+
4
+ const integrate = async (html, publish, headers) => {
5
+ const scripts = await Promise.all([...html.matchAll(script)]
6
+ .map(({groups: {code}}) => publish({code, inline: true})));
7
+ for (const integrity of scripts) {
8
+ headers["Content-Security-Policy"] = headers["Content-Security-Policy"]
9
+ .replace("script-src 'self' ", `script-src 'self' '${integrity}' `);
10
+ }
11
+ const styles = await Promise.all([...html.matchAll(style)]
12
+ .map(({groups: {code}}) => publish({code, type: "style", inline: true})));
13
+ for (const integrity of styles) {
14
+ headers["Content-Security-Policy"] = headers["Content-Security-Policy"]
15
+ .replace("style-src 'self'", `style-src 'self' '${integrity}' `);
16
+ }
17
+ return html
18
+ .replaceAll(/<script>.*?<\/script>/gus, () => "")
19
+ .replaceAll(/<style>.*?<\/style>/gus, () => "");
20
+ };
21
+
22
+ export default (component, options = {}) => {
23
+ const {status = 200, partial = false, load = false, layout} = options;
3
24
 
4
25
  return async (app, headers) => {
5
- const body = load ?
6
- await app.paths.components.join(component).text() : component;
26
+ const body = await integrate(await load ?
27
+ await app.paths.components.join(component).text() : component,
28
+ app.publish, headers);
7
29
 
8
- return [partial ? body : await app.render({body}), {
30
+ return [partial ? body : await app.render({body, layout}), {
9
31
  status,
10
32
  headers: {...headers, "Content-Type": "text/html"},
11
33
  }];