primate 0.23.2 → 0.24.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.23.2",
3
+ "version": "0.24.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",
@@ -18,7 +18,7 @@
18
18
  "directory": "packages/primate"
19
19
  },
20
20
  "dependencies": {
21
- "runtime-compat": "^0.28.4"
21
+ "runtime-compat": "^0.29.0"
22
22
  },
23
23
  "engines": {
24
24
  "node": ">=18"
package/src/app.js CHANGED
@@ -3,7 +3,7 @@ import {tryreturn} from "runtime-compat/async";
3
3
  import {File, Path} from "runtime-compat/fs";
4
4
  import {bold, blue} from "runtime-compat/colors";
5
5
  import {is} from "runtime-compat/invariant";
6
- import {transform, valmap, to} from "runtime-compat/object";
6
+ import {transform, valmap} from "runtime-compat/object";
7
7
  import {globify} from "runtime-compat/string";
8
8
  import * as runtime from "runtime-compat/meta";
9
9
 
@@ -29,6 +29,22 @@ const attribute = attributes => Object.keys(attributes).length > 0
29
29
  : "";
30
30
  const tag = ({name, attributes = {}, code = "", close = true}) =>
31
31
  `<${name}${attribute(attributes)}${close ? `>${code}</${name}>` : "/>"}`;
32
+ const tags = {
33
+ // inline: <script type integrity>...</script>
34
+ // outline: <script type integrity src></script>
35
+ script({inline, code, type, integrity, src}) {
36
+ return inline
37
+ ? tag({name: "script", attributes: {type, integrity}, code})
38
+ : tag({name: "script", attributes: {type, integrity, src}});
39
+ },
40
+ // inline: <style>...</style>
41
+ // outline: <link rel="stylesheet" href/>
42
+ style({inline, code, href, rel = "stylesheet"}) {
43
+ return inline
44
+ ? tag({name: "style", code})
45
+ : tag({name: "link", attributes: {rel, href}, close: false});
46
+ },
47
+ };
32
48
 
33
49
  const {name, version} = await new Path(import.meta.url).up(2)
34
50
  .join(runtime.manifest).json();
@@ -96,24 +112,21 @@ export default async (log, root, config) => {
96
112
  }
97
113
  }));
98
114
  },
99
- headers() {
115
+ headers({script = "", style = ""} = {}) {
100
116
  const csp = Object.keys(http.csp).reduce((policy, key) =>
101
117
  `${policy}${key} ${http.csp[key]};`, "")
102
- .replace("script-src 'self'", `script-src 'self' ${
118
+ .replace("script-src 'self'", `script-src 'self' ${script} ${
103
119
  this.assets
104
120
  .filter(({type}) => type !== "style")
105
121
  .map(asset => `'${asset.integrity}'`).join(" ")
106
- } `)
107
- .replace("style-src 'self'", `style-src 'self' ${
122
+ }`)
123
+ .replace("style-src 'self'", `style-src 'self' ${style} ${
108
124
  this.assets
109
125
  .filter(({type}) => type === "style")
110
126
  .map(asset => `'${asset.integrity}'`).join(" ")
111
- } `);
127
+ }`);
112
128
 
113
- return {
114
- "Content-Security-Policy": csp,
115
- "Referrer-Policy": "same-origin",
116
- };
129
+ return {"Content-Security-Policy": csp, "Referrer-Policy": "same-origin"};
117
130
  },
118
131
  runpath(...directories) {
119
132
  return this.path.build.join(...directories);
@@ -122,29 +135,22 @@ export default async (log, root, config) => {
122
135
  const {location: {pages}} = this.config;
123
136
 
124
137
  const html = await index(this.runpath(pages), page, config.pages.index);
125
- // inline: <script type integrity>...</script>
126
- // outline: <script type integrity src></script>
127
- const script = ({inline, code, type, integrity, src}) => inline
128
- ? tag({name: "script", attributes: {type, integrity}, code})
129
- : tag({name: "script", attributes: {type, integrity, src}});
130
- // inline: <style>...</style>
131
- // outline: <link rel="stylesheet" href/>
132
- const style = ({inline, code, href, rel = "stylesheet"}) => inline
133
- ? tag({name: "style", code})
134
- : tag({name: "link", attributes: {rel, href}, close: false});
135
-
136
- const heads = head.concat("\n", to_sorted(this.assets,
138
+
139
+ const heads = to_sorted(this.assets,
137
140
  ({type}) => -1 * (type === "importmap"))
138
141
  .map(({src, code, type, inline, integrity}) =>
139
142
  type === "style"
140
- ? style({inline, code, href: src})
141
- : script({inline, code, type, integrity, src})
142
- ).join("\n"));
143
- // remove inline assets
144
- this.assets = this.assets.filter(({inline, type}) => !inline
145
- || type === "importmap");
143
+ ? tags.style({inline, code, href: src})
144
+ : tags.script({inline, code, type, integrity, src})
145
+ ).join("\n").concat("\n", head);
146
146
  return html.replace("%body%", _ => body).replace("%head%", _ => heads);
147
147
  },
148
+ async inline(code, type) {
149
+ const integrity = await this.hash(code);
150
+ const tag_name = type === "style" ? "style" : "script";
151
+ const head = tags[tag_name]({code, type, inline: true, integrity});
152
+ return {head, csp: `'${integrity}'`};
153
+ },
148
154
  async publish({src, code, type = "", inline = false, copy = true}) {
149
155
  if (!inline && copy) {
150
156
  const base = this.runpath(this.config.location.client).join(src);
@@ -173,7 +179,7 @@ export default async (log, root, config) => {
173
179
  const prefix = algorithm.replace("-", _ => "");
174
180
  return `${prefix}-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}`;
175
181
  },
176
- async import(module) {
182
+ async import(module, deep_import) {
177
183
  const {http: {static: {root}}, location: {client}} = this.config;
178
184
 
179
185
  const parts = module.split("/");
@@ -182,12 +188,16 @@ export default async (log, root, config) => {
182
188
  const exports = pkg.exports === undefined
183
189
  ? {[module]: `/${module}/${pkg.main}`}
184
190
  : transform(pkg.exports, entry => entry
185
- .filter(([, export$]) => export$.browser?.default !== undefined
191
+ .filter(([, export$]) =>
192
+ export$.browser?.[deep_import] !== undefined
193
+ || export$.browser?.default !== undefined
186
194
  || export$.import !== undefined
187
195
  || export$.default !== undefined)
188
196
  .map(([key, value]) => [
189
- key.replace(".", module),
190
- value.browser?.default.replace(".", `./${module}`)
197
+ key.replace(".", deep_import === undefined
198
+ ? module : `${module}/${deep_import}`),
199
+ value.browser?.[deep_import]?.replace(".", `./${module}`)
200
+ ?? value.browser?.default.replace(".", `./${module}`)
191
201
  ?? value.default?.replace(".", `./${module}`)
192
202
  ?? value.import?.replace(".", `./${module}`),
193
203
  ]));
@@ -1,26 +1,29 @@
1
1
  import {Response, Status, MediaType} from "runtime-compat/http";
2
2
 
3
- const script = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
4
- const style = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
3
+ const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;
4
+ const style_re = /(?<=<style)>(?<code>.*?)(?=<\/style>)/gus;
5
5
  const remove = /<(?<tag>script|style)>.*?<\/\k<tag>>/gus;
6
- const inline = true;
7
6
 
8
7
  export default (name, options = {}) => {
9
8
  const {status = Status.OK, partial = false} = options;
10
9
 
11
10
  return async app => {
12
11
  const html = await app.path.components.join(name).text();
13
- await Promise.all([...html.matchAll(script)]
14
- .map(({groups: {code}}) => app.publish({code, inline})));
15
- await Promise.all([...html.matchAll(style)]
16
- .map(({groups: {code}}) => app.publish({code, type: "style", inline})));
12
+ const scripts = await Promise.all([...html.matchAll(script_re)]
13
+ .map(({groups: {code}}) => app.inline(code, "module")));
14
+ const styles = await Promise.all([...html.matchAll(style_re)]
15
+ .map(({groups: {code}}) => app.inline(code, "style")));
16
+ const assets = [...scripts, ...styles];
17
+
17
18
  const body = html.replaceAll(remove, _ => "");
18
- // needs to happen before app.render()
19
- const headers = app.headers();
19
+ const head = assets.map(asset => asset.head).join("\n");
20
+ const script = scripts.map(asset => asset.csp).join(" ");
21
+ const style = styles.map(asset => asset.csp).join(" ");
22
+ const headers = {script, style};
20
23
 
21
- return new Response(partial ? body : await app.render({body}), {
24
+ return new Response(partial ? body : await app.render({body, head}), {
22
25
  status,
23
- headers: {...headers, "Content-Type": MediaType.TEXT_HTML},
26
+ headers: {...app.headers(headers), "Content-Type": MediaType.TEXT_HTML},
24
27
  });
25
28
  };
26
29
  };
@@ -25,9 +25,8 @@ const post = async app => {
25
25
  }
26
26
 
27
27
  const imports = {...app.importmaps, app: src.path};
28
- const inline = true;
29
28
  const type = "importmap";
30
- await app.publish({inline, code: stringify({imports}), type});
29
+ await app.publish({inline: true, code: stringify({imports}), type});
31
30
  }
32
31
 
33
32
  if (await path.static.exists) {