@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 +47 -13
- package/dist/index.js +47 -13
- package/package.json +3 -3
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
|
-
|
|
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.
|
|
208
|
-
page = page.
|
|
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
|
-
|
|
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.
|
|
163
|
-
page = page.
|
|
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
|
|
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
|
|
48
|
-
"@matthesketh/utopia-router": "0.0
|
|
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",
|