@matthesketh/utopia-server 0.0.4 → 0.1.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/dist/index.cjs CHANGED
@@ -49,6 +49,16 @@ function createComponent(Component, props, children) {
49
49
  }
50
50
 
51
51
  // src/render-to-string.ts
52
+ var VALID_TAG = /^[a-zA-Z][a-zA-Z0-9-]*$/;
53
+ function validateTag(tag) {
54
+ if (!VALID_TAG.test(tag)) throw new Error(`Invalid tag name: ${tag}`);
55
+ return tag;
56
+ }
57
+ var VALID_ATTR = /^[a-zA-Z_:@][a-zA-Z0-9_.:-]*$/;
58
+ function validateAttr(name) {
59
+ if (!VALID_ATTR.test(name)) throw new Error(`Invalid attribute name: ${name}`);
60
+ return name;
61
+ }
52
62
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
53
63
  "area",
54
64
  "base",
@@ -86,12 +96,12 @@ function serializeVNode(node) {
86
96
  }
87
97
  function serializeElement(el) {
88
98
  const tag = el.tag;
89
- let html = `<${tag}`;
99
+ let html = `<${validateTag(tag)}`;
90
100
  for (const [name, value] of Object.entries(el.attrs)) {
91
101
  if (value === "") {
92
- html += ` ${name}`;
102
+ html += ` ${validateAttr(name)}`;
93
103
  } else {
94
- html += ` ${name}="${escapeAttr(value)}"`;
104
+ html += ` ${validateAttr(name)}="${escapeAttr(value)}"`;
95
105
  }
96
106
  }
97
107
  if (VOID_ELEMENTS.has(tag.toLowerCase())) {
@@ -102,7 +112,7 @@ function serializeElement(el) {
102
112
  for (const child of el.children) {
103
113
  html += serializeVNode(child);
104
114
  }
105
- html += `</${tag}>`;
115
+ html += `</${validateTag(tag)}>`;
106
116
  return html;
107
117
  }
108
118
  function renderToString(component, props) {
@@ -116,6 +126,19 @@ function renderToString(component, props) {
116
126
 
117
127
  // src/render-to-stream.ts
118
128
  var import_node_stream = require("stream");
129
+ var VALID_TAG2 = /^[a-zA-Z][a-zA-Z0-9-]*$/;
130
+ function validateTag2(tag) {
131
+ if (!VALID_TAG2.test(tag)) throw new Error(`Invalid tag name: ${tag}`);
132
+ return tag;
133
+ }
134
+ var VALID_ATTR2 = /^[a-zA-Z_:@][a-zA-Z0-9_.:-]*$/;
135
+ function validateAttr2(name) {
136
+ if (!VALID_ATTR2.test(name)) throw new Error(`Invalid attribute name: ${name}`);
137
+ return name;
138
+ }
139
+ function escapeStyleContent(css) {
140
+ return css.replace(/<\/style/gi, "<\\/style");
141
+ }
119
142
  var VOID_ELEMENTS2 = /* @__PURE__ */ new Set([
120
143
  "area",
121
144
  "base",
@@ -145,10 +168,13 @@ function renderToStream(component, props) {
145
168
  flushStyles();
146
169
  const vnode = createComponent(component, props);
147
170
  const styles = flushStyles();
171
+ let rendered = false;
148
172
  return new import_node_stream.Readable({
149
173
  read() {
174
+ if (rendered) return;
175
+ rendered = true;
150
176
  if (styles.length > 0) {
151
- this.push(`<style>${styles.join("\n")}</style>`);
177
+ this.push(`<style>${escapeStyleContent(styles.join("\n"))}</style>`);
152
178
  }
153
179
  pushVNode(this, vnode);
154
180
  this.push(null);
@@ -169,12 +195,12 @@ function pushVNode(stream, node) {
169
195
  }
170
196
  }
171
197
  function pushElement(stream, el) {
172
- let open = `<${el.tag}`;
198
+ let open = `<${validateTag2(el.tag)}`;
173
199
  for (const [name, value] of Object.entries(el.attrs)) {
174
200
  if (value === "") {
175
- open += ` ${name}`;
201
+ open += ` ${validateAttr2(name)}`;
176
202
  } else {
177
- open += ` ${name}="${escapeAttr2(value)}"`;
203
+ open += ` ${validateAttr2(name)}="${escapeAttr2(value)}"`;
178
204
  }
179
205
  }
180
206
  open += ">";
@@ -185,27 +211,35 @@ function pushElement(stream, el) {
185
211
  for (const child of el.children) {
186
212
  pushVNode(stream, child);
187
213
  }
188
- stream.push(`</${el.tag}>`);
214
+ stream.push(`</${validateTag2(el.tag)}>`);
189
215
  }
190
216
 
191
217
  // src/server-router.ts
192
218
  var import_utopia_router = require("@matthesketh/utopia-router");
193
219
  function createServerRouter(routes, url) {
194
- const fullUrl = new URL(url, "http://localhost");
220
+ let fullUrl;
221
+ try {
222
+ fullUrl = new URL(url, "http://localhost");
223
+ } catch {
224
+ return null;
225
+ }
195
226
  return (0, import_utopia_router.matchRoute)(fullUrl, routes);
196
227
  }
197
228
 
198
229
  // src/handler.ts
230
+ function escapeStyleContent2(css) {
231
+ return css.replace(/<\/style/gi, "<\\/style");
232
+ }
199
233
  function createHandler(options) {
200
234
  const { template, render } = options;
201
235
  return async (req, res) => {
202
236
  const url = req.url ?? "/";
203
237
  try {
204
238
  const { html, css } = await render(url);
205
- const headInject = css ? `<style>${css}</style>` : "";
239
+ const headInject = css ? `<style>${escapeStyleContent2(css)}</style>` : "";
206
240
  let page = template;
207
- page = page.replace("<!--ssr-head-->", headInject);
208
- page = page.replace("<!--ssr-outlet-->", html);
241
+ page = page.split("<!--ssr-head-->").join(headInject);
242
+ page = page.split("<!--ssr-outlet-->").join(html);
209
243
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
210
244
  res.end(page);
211
245
  } catch (err) {
package/dist/index.js CHANGED
@@ -4,6 +4,16 @@ import {
4
4
  } from "./chunk-DV7AV4JO.js";
5
5
 
6
6
  // src/render-to-string.ts
7
+ var VALID_TAG = /^[a-zA-Z][a-zA-Z0-9-]*$/;
8
+ function validateTag(tag) {
9
+ if (!VALID_TAG.test(tag)) throw new Error(`Invalid tag name: ${tag}`);
10
+ return tag;
11
+ }
12
+ var VALID_ATTR = /^[a-zA-Z_:@][a-zA-Z0-9_.:-]*$/;
13
+ function validateAttr(name) {
14
+ if (!VALID_ATTR.test(name)) throw new Error(`Invalid attribute name: ${name}`);
15
+ return name;
16
+ }
7
17
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
8
18
  "area",
9
19
  "base",
@@ -41,12 +51,12 @@ function serializeVNode(node) {
41
51
  }
42
52
  function serializeElement(el) {
43
53
  const tag = el.tag;
44
- let html = `<${tag}`;
54
+ let html = `<${validateTag(tag)}`;
45
55
  for (const [name, value] of Object.entries(el.attrs)) {
46
56
  if (value === "") {
47
- html += ` ${name}`;
57
+ html += ` ${validateAttr(name)}`;
48
58
  } else {
49
- html += ` ${name}="${escapeAttr(value)}"`;
59
+ html += ` ${validateAttr(name)}="${escapeAttr(value)}"`;
50
60
  }
51
61
  }
52
62
  if (VOID_ELEMENTS.has(tag.toLowerCase())) {
@@ -57,7 +67,7 @@ function serializeElement(el) {
57
67
  for (const child of el.children) {
58
68
  html += serializeVNode(child);
59
69
  }
60
- html += `</${tag}>`;
70
+ html += `</${validateTag(tag)}>`;
61
71
  return html;
62
72
  }
63
73
  function renderToString(component, props) {
@@ -71,6 +81,19 @@ function renderToString(component, props) {
71
81
 
72
82
  // src/render-to-stream.ts
73
83
  import { Readable } from "stream";
84
+ var VALID_TAG2 = /^[a-zA-Z][a-zA-Z0-9-]*$/;
85
+ function validateTag2(tag) {
86
+ if (!VALID_TAG2.test(tag)) throw new Error(`Invalid tag name: ${tag}`);
87
+ return tag;
88
+ }
89
+ var VALID_ATTR2 = /^[a-zA-Z_:@][a-zA-Z0-9_.:-]*$/;
90
+ function validateAttr2(name) {
91
+ if (!VALID_ATTR2.test(name)) throw new Error(`Invalid attribute name: ${name}`);
92
+ return name;
93
+ }
94
+ function escapeStyleContent(css) {
95
+ return css.replace(/<\/style/gi, "<\\/style");
96
+ }
74
97
  var VOID_ELEMENTS2 = /* @__PURE__ */ new Set([
75
98
  "area",
76
99
  "base",
@@ -100,10 +123,13 @@ function renderToStream(component, props) {
100
123
  flushStyles();
101
124
  const vnode = createComponent(component, props);
102
125
  const styles = flushStyles();
126
+ let rendered = false;
103
127
  return new Readable({
104
128
  read() {
129
+ if (rendered) return;
130
+ rendered = true;
105
131
  if (styles.length > 0) {
106
- this.push(`<style>${styles.join("\n")}</style>`);
132
+ this.push(`<style>${escapeStyleContent(styles.join("\n"))}</style>`);
107
133
  }
108
134
  pushVNode(this, vnode);
109
135
  this.push(null);
@@ -124,12 +150,12 @@ function pushVNode(stream, node) {
124
150
  }
125
151
  }
126
152
  function pushElement(stream, el) {
127
- let open = `<${el.tag}`;
153
+ let open = `<${validateTag2(el.tag)}`;
128
154
  for (const [name, value] of Object.entries(el.attrs)) {
129
155
  if (value === "") {
130
- open += ` ${name}`;
156
+ open += ` ${validateAttr2(name)}`;
131
157
  } else {
132
- open += ` ${name}="${escapeAttr2(value)}"`;
158
+ open += ` ${validateAttr2(name)}="${escapeAttr2(value)}"`;
133
159
  }
134
160
  }
135
161
  open += ">";
@@ -140,27 +166,35 @@ function pushElement(stream, el) {
140
166
  for (const child of el.children) {
141
167
  pushVNode(stream, child);
142
168
  }
143
- stream.push(`</${el.tag}>`);
169
+ stream.push(`</${validateTag2(el.tag)}>`);
144
170
  }
145
171
 
146
172
  // src/server-router.ts
147
173
  import { matchRoute } from "@matthesketh/utopia-router";
148
174
  function createServerRouter(routes, url) {
149
- const fullUrl = new URL(url, "http://localhost");
175
+ let fullUrl;
176
+ try {
177
+ fullUrl = new URL(url, "http://localhost");
178
+ } catch {
179
+ return null;
180
+ }
150
181
  return matchRoute(fullUrl, routes);
151
182
  }
152
183
 
153
184
  // src/handler.ts
185
+ function escapeStyleContent2(css) {
186
+ return css.replace(/<\/style/gi, "<\\/style");
187
+ }
154
188
  function createHandler(options) {
155
189
  const { template, render } = options;
156
190
  return async (req, res) => {
157
191
  const url = req.url ?? "/";
158
192
  try {
159
193
  const { html, css } = await render(url);
160
- const headInject = css ? `<style>${css}</style>` : "";
194
+ const headInject = css ? `<style>${escapeStyleContent2(css)}</style>` : "";
161
195
  let page = template;
162
- page = page.replace("<!--ssr-head-->", headInject);
163
- page = page.replace("<!--ssr-outlet-->", html);
196
+ page = page.split("<!--ssr-head-->").join(headInject);
197
+ page = page.split("<!--ssr-outlet-->").join(html);
164
198
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
165
199
  res.end(page);
166
200
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthesketh/utopia-server",
3
- "version": "0.0.4",
3
+ "version": "0.1.0",
4
4
  "description": "Server-side rendering for UtopiaJS",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -44,8 +44,8 @@
44
44
  "dist"
45
45
  ],
46
46
  "dependencies": {
47
- "@matthesketh/utopia-core": "0.0.4",
48
- "@matthesketh/utopia-router": "0.0.4"
47
+ "@matthesketh/utopia-core": "0.1.0",
48
+ "@matthesketh/utopia-router": "0.1.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "tsup src/index.ts src/ssr-runtime.ts --format esm,cjs --dts",