solidstep 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/LICENSE +21 -0
- package/README.md +18 -0
- package/client.d.ts +4 -0
- package/client.d.ts.map +1 -0
- package/client.js +91 -0
- package/index.d.ts +11 -0
- package/index.d.ts.map +1 -0
- package/index.js +97 -0
- package/package.json +58 -0
- package/server.d.ts +3 -0
- package/server.d.ts.map +1 -0
- package/server.js +666 -0
- package/utils/cache.d.ts +5 -0
- package/utils/cache.d.ts.map +1 -0
- package/utils/cache.js +97 -0
- package/utils/cookies.d.ts +5 -0
- package/utils/cookies.d.ts.map +1 -0
- package/utils/cookies.js +13 -0
- package/utils/cors.d.ts +14 -0
- package/utils/cors.d.ts.map +1 -0
- package/utils/cors.js +15 -0
- package/utils/csp.d.ts +38 -0
- package/utils/csp.d.ts.map +1 -0
- package/utils/csp.js +165 -0
- package/utils/csrf.d.ts +5 -0
- package/utils/csrf.d.ts.map +1 -0
- package/utils/csrf.js +46 -0
- package/utils/error-handler.d.ts +37 -0
- package/utils/error-handler.d.ts.map +1 -0
- package/utils/error-handler.js +40 -0
- package/utils/fetch.client.d.ts +23 -0
- package/utils/fetch.client.d.ts.map +1 -0
- package/utils/fetch.client.js +55 -0
- package/utils/fetch.server.d.ts +22 -0
- package/utils/fetch.server.d.ts.map +1 -0
- package/utils/fetch.server.js +54 -0
- package/utils/hooks/action-state.d.ts +3 -0
- package/utils/hooks/action-state.d.ts.map +1 -0
- package/utils/hooks/action-state.js +13 -0
- package/utils/loader.d.ts +18 -0
- package/utils/loader.d.ts.map +1 -0
- package/utils/loader.js +23 -0
- package/utils/redirect.d.ts +5 -0
- package/utils/redirect.d.ts.map +1 -0
- package/utils/redirect.js +14 -0
- package/utils/router.d.ts +104 -0
- package/utils/router.d.ts.map +1 -0
- package/utils/router.js +258 -0
- package/utils/server-action.client.d.ts +2 -0
- package/utils/server-action.client.d.ts.map +1 -0
- package/utils/server-action.client.js +200 -0
- package/utils/server-action.server.d.ts +5 -0
- package/utils/server-action.server.d.ts.map +1 -0
- package/utils/server-action.server.js +264 -0
- package/utils/server-only.d.ts +2 -0
- package/utils/server-only.d.ts.map +1 -0
- package/utils/server-only.js +4 -0
- package/utils/types.d.ts +8 -0
- package/utils/types.d.ts.map +1 -0
- package/utils/types.js +1 -0
package/server.js
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import { eventHandler, toWebRequest } from 'vinxi/http';
|
|
2
|
+
import { getManifest } from 'vinxi/manifest';
|
|
3
|
+
import { generateHydrationScript, renderToString } from 'solid-js/web';
|
|
4
|
+
import fileRoutes, {} from 'vinxi/routes';
|
|
5
|
+
import { RedirectError } from './utils/redirect';
|
|
6
|
+
import { setCache, getCache } from './utils/cache';
|
|
7
|
+
import { handleServerFunction } from './utils/server-action.server';
|
|
8
|
+
import util from 'node:util';
|
|
9
|
+
const isPageFile = (file) => file.endsWith('page.tsx')
|
|
10
|
+
|| file.endsWith('page.jsx')
|
|
11
|
+
|| file.endsWith('page.ts')
|
|
12
|
+
|| file.endsWith('page.js');
|
|
13
|
+
const isRouteFile = (file) => file.endsWith('route.ts') || file.endsWith('route.js');
|
|
14
|
+
const parseSegment = (part) => part.startsWith('[') ? `:${part.slice(1, -1).replace(/\.\.\./, '*')}` : part;
|
|
15
|
+
const createRouteManifest = async () => {
|
|
16
|
+
const entries = {};
|
|
17
|
+
const allRoutes = [];
|
|
18
|
+
const allLayouts = [];
|
|
19
|
+
const allLoadingPages = [];
|
|
20
|
+
const allErrorPages = [];
|
|
21
|
+
const allGroups = [];
|
|
22
|
+
let notFoundPage;
|
|
23
|
+
for (const fileRoute of fileRoutes) {
|
|
24
|
+
if (fileRoute.type === 'route') {
|
|
25
|
+
allRoutes.push(fileRoute);
|
|
26
|
+
}
|
|
27
|
+
if (fileRoute.type === 'layout') {
|
|
28
|
+
allLayouts.push(fileRoute);
|
|
29
|
+
}
|
|
30
|
+
if (fileRoute.type === 'not-found') {
|
|
31
|
+
notFoundPage = fileRoute;
|
|
32
|
+
}
|
|
33
|
+
if (fileRoute.type === 'loading') {
|
|
34
|
+
allLoadingPages.push(fileRoute);
|
|
35
|
+
}
|
|
36
|
+
if (fileRoute.type === 'error') {
|
|
37
|
+
allErrorPages.push(fileRoute);
|
|
38
|
+
}
|
|
39
|
+
if (fileRoute.type === 'group') {
|
|
40
|
+
allGroups.push(fileRoute);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const fileRoute of allRoutes) {
|
|
44
|
+
const segments = fileRoute.path.split('/').slice(2).map(parseSegment);
|
|
45
|
+
const routePath = `/${segments.filter(s => !(s.startsWith('('))).join('/')}`;
|
|
46
|
+
const regex = /\?(?:pick=.*)*/g;
|
|
47
|
+
const src = fileRoute.$handler?.src.replace(regex, '');
|
|
48
|
+
if (src && isPageFile(src)) {
|
|
49
|
+
const loadingPage = allLoadingPages.find(route => {
|
|
50
|
+
const path = `/${route.path.split('/').slice(2).map(parseSegment).join('/')}`;
|
|
51
|
+
return path === routePath;
|
|
52
|
+
});
|
|
53
|
+
const matchedGroups = allGroups.filter(route => {
|
|
54
|
+
const parentPath = route.parent ? `/${route.parent.split('/').slice(2).map(parseSegment).join('/')}` : '';
|
|
55
|
+
return parentPath === routePath;
|
|
56
|
+
});
|
|
57
|
+
const groups = {};
|
|
58
|
+
if (matchedGroups && matchedGroups.length > 0) {
|
|
59
|
+
for (const group of matchedGroups) {
|
|
60
|
+
const groupName = group.path.split('/').filter(s => !(s.startsWith('('))).map(parseSegment).at(-1);
|
|
61
|
+
if (!groupName)
|
|
62
|
+
continue;
|
|
63
|
+
groups[groupName] = {
|
|
64
|
+
manifestPath: group.path,
|
|
65
|
+
page: group.$component,
|
|
66
|
+
loader: group.$loader,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
let errorPage;
|
|
71
|
+
const layouts = [];
|
|
72
|
+
for (let i = segments.length; i > (routePath === '/' ? 0 : -1); i--) {
|
|
73
|
+
const path = `/${segments.slice(0, i).join('/')}`;
|
|
74
|
+
if (!errorPage) {
|
|
75
|
+
errorPage = allErrorPages.find(route => {
|
|
76
|
+
const routePath = `/${route.path.split('/').slice(2).map(parseSegment).join('/')}`;
|
|
77
|
+
return routePath === path;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const layout = allLayouts.find(route => {
|
|
81
|
+
const routePath = `/${route.path.split('/').slice(2).map(parseSegment).join('/')}`;
|
|
82
|
+
return routePath === path;
|
|
83
|
+
});
|
|
84
|
+
if (layout) {
|
|
85
|
+
layouts.unshift({
|
|
86
|
+
manifestPath: layout.path,
|
|
87
|
+
layout: layout.$component,
|
|
88
|
+
loader: layout.$loader,
|
|
89
|
+
generateMeta: layout.$generateMeta,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
entries[routePath] = {
|
|
94
|
+
type: 'page',
|
|
95
|
+
mainPage: {
|
|
96
|
+
manifestPath: fileRoute.path,
|
|
97
|
+
page: fileRoute.$component,
|
|
98
|
+
loader: fileRoute.$loader,
|
|
99
|
+
generateMeta: fileRoute.$generateMeta,
|
|
100
|
+
options: fileRoute.$options,
|
|
101
|
+
},
|
|
102
|
+
loadingPage: loadingPage ? {
|
|
103
|
+
page: loadingPage.$component,
|
|
104
|
+
generateMeta: loadingPage.$generateMeta,
|
|
105
|
+
manifestPath: loadingPage.path,
|
|
106
|
+
} : undefined,
|
|
107
|
+
errorPage: errorPage ? {
|
|
108
|
+
page: errorPage.$component,
|
|
109
|
+
generateMeta: errorPage.$generateMeta,
|
|
110
|
+
manifestPath: errorPage.path,
|
|
111
|
+
} : undefined,
|
|
112
|
+
notFoundPage: routePath === '/' && notFoundPage ? {
|
|
113
|
+
page: notFoundPage.$component,
|
|
114
|
+
generateMeta: notFoundPage.$generateMeta,
|
|
115
|
+
manifestPath: notFoundPage.path,
|
|
116
|
+
} : undefined,
|
|
117
|
+
layouts: layouts,
|
|
118
|
+
groups: groups,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
else if (src && isRouteFile(src)) {
|
|
122
|
+
entries[routePath] = {
|
|
123
|
+
type: 'route',
|
|
124
|
+
handler: fileRoute.$handler,
|
|
125
|
+
manifestPath: fileRoute.path,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return entries;
|
|
130
|
+
};
|
|
131
|
+
const extractRouteParams = (route, url) => {
|
|
132
|
+
const routeSegments = route.split('/').filter(s => !(s.startsWith('('))).filter(Boolean);
|
|
133
|
+
const urlSegments = url.split('/').filter(Boolean);
|
|
134
|
+
const params = {};
|
|
135
|
+
let matched = true;
|
|
136
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
137
|
+
const routeSeg = routeSegments[i];
|
|
138
|
+
const urlSeg = urlSegments[i];
|
|
139
|
+
const isDynamic = routeSeg.startsWith('[') && routeSeg.endsWith(']');
|
|
140
|
+
if (isDynamic) {
|
|
141
|
+
if (routeSeg.includes('...')) {
|
|
142
|
+
// Catch-all parameter
|
|
143
|
+
const isCatchAll = routeSeg.startsWith('[[') && routeSeg.endsWith(']]');
|
|
144
|
+
const paramName = routeSeg.slice(isCatchAll ? 5 : 4, isCatchAll ? -2 : -1);
|
|
145
|
+
params[paramName] = urlSegments.slice(i);
|
|
146
|
+
break; // No more segments to match
|
|
147
|
+
}
|
|
148
|
+
const paramName = routeSeg.slice(1, -1);
|
|
149
|
+
params[paramName] = urlSeg;
|
|
150
|
+
}
|
|
151
|
+
else if (routeSeg !== urlSeg) {
|
|
152
|
+
matched = false;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (matched)
|
|
157
|
+
return { route, params };
|
|
158
|
+
};
|
|
159
|
+
const template = `
|
|
160
|
+
<!DOCTYPE html>
|
|
161
|
+
<html lang="en">
|
|
162
|
+
<head><!--app-head--></head>
|
|
163
|
+
<!--app-body-->
|
|
164
|
+
</html>
|
|
165
|
+
`;
|
|
166
|
+
const generateHtmlHead = (meta) => {
|
|
167
|
+
const head = Object.entries(meta)
|
|
168
|
+
.map(([key, value]) => {
|
|
169
|
+
if (value.type === 'title') {
|
|
170
|
+
return `<title>${value.content}</title>`;
|
|
171
|
+
}
|
|
172
|
+
if (value.type === 'meta') {
|
|
173
|
+
const attrs = Object.entries(value.attributes)
|
|
174
|
+
.map(([attrKey, attrValue]) => `${attrKey}="${attrValue}"`)
|
|
175
|
+
.join(' ');
|
|
176
|
+
return `<meta ${attrs}>`;
|
|
177
|
+
}
|
|
178
|
+
if (value.type === 'link' || value.type === 'style' || value.type === 'script') {
|
|
179
|
+
const attrs = Object.entries(value.attributes)
|
|
180
|
+
.map(([attrKey, attrValue]) => `${attrKey}="${attrValue}"`)
|
|
181
|
+
.join(' ');
|
|
182
|
+
return `<${value.type} ${attrs}></${value.type}>`;
|
|
183
|
+
}
|
|
184
|
+
return '';
|
|
185
|
+
})
|
|
186
|
+
.join('\n');
|
|
187
|
+
return head;
|
|
188
|
+
};
|
|
189
|
+
const sendNodeResponse = async (res, response) => {
|
|
190
|
+
// Set status code
|
|
191
|
+
res.statusCode = response.status;
|
|
192
|
+
// Set headers
|
|
193
|
+
response.headers.forEach((value, key) => {
|
|
194
|
+
res.setHeader(key, value);
|
|
195
|
+
});
|
|
196
|
+
// Stream the body
|
|
197
|
+
if (response.body) {
|
|
198
|
+
const reader = response.body.getReader();
|
|
199
|
+
const push = async () => {
|
|
200
|
+
const { done, value } = await reader.read();
|
|
201
|
+
if (done) {
|
|
202
|
+
res.end();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
res.write(Buffer.from(value));
|
|
206
|
+
await push();
|
|
207
|
+
};
|
|
208
|
+
await push();
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const text = await response.text();
|
|
212
|
+
res.end(text);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonce, }) => {
|
|
216
|
+
const url = req.url || '/';
|
|
217
|
+
const cachedEntry = getCache(url);
|
|
218
|
+
if (cachedEntry && toRender === 'main') {
|
|
219
|
+
return {
|
|
220
|
+
rendered: cachedEntry.rendered,
|
|
221
|
+
documentMeta: cachedEntry.documentMeta,
|
|
222
|
+
documentAssets: cachedEntry.documentAssets,
|
|
223
|
+
loaderData: cachedEntry.loaderData,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
let cachingOptions = undefined;
|
|
227
|
+
let meta = {};
|
|
228
|
+
const loaderData = {};
|
|
229
|
+
const clientManifest = getManifest('client');
|
|
230
|
+
const assets = [];
|
|
231
|
+
const compose = entry.layouts.reduceRight((children, layout, index) => async () => {
|
|
232
|
+
const moduleSrc = `${layout.layout.src}&pick=$css`;
|
|
233
|
+
const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
|
|
234
|
+
for (const asset of moduleAssets) {
|
|
235
|
+
assets.push(asset);
|
|
236
|
+
}
|
|
237
|
+
const { default: layoutModule } = await layout.layout.import();
|
|
238
|
+
const { loader: layoutLoader } = layout.loader ? await layout.loader.import() : { loader: null };
|
|
239
|
+
const { generateMeta: generateMetaPage } = layout.generateMeta ? await layout.generateMeta.import() : { generateMeta: null };
|
|
240
|
+
let data = {};
|
|
241
|
+
if (generateMetaPage) {
|
|
242
|
+
const metaData = await generateMetaPage({
|
|
243
|
+
req,
|
|
244
|
+
cspNonce,
|
|
245
|
+
});
|
|
246
|
+
if (metaData) {
|
|
247
|
+
meta = {
|
|
248
|
+
...meta,
|
|
249
|
+
...metaData
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (layoutLoader) {
|
|
254
|
+
const result = await layoutLoader.loader(req);
|
|
255
|
+
data = result.data || {};
|
|
256
|
+
loaderData[layout.manifestPath] = data;
|
|
257
|
+
}
|
|
258
|
+
const slots = {};
|
|
259
|
+
const slotPromises = [children()];
|
|
260
|
+
if (index === entry.layouts.length - 1) {
|
|
261
|
+
// last layout, we can render slots
|
|
262
|
+
const groups = entry.groups || {};
|
|
263
|
+
for (const [groupName, group] of Object.entries(groups)) {
|
|
264
|
+
slotPromises.push((async () => {
|
|
265
|
+
const moduleSrc = `${group.page.src}&pick=$css`;
|
|
266
|
+
const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
|
|
267
|
+
for (const asset of moduleAssets) {
|
|
268
|
+
assets.push(asset);
|
|
269
|
+
}
|
|
270
|
+
const { default: groupPage } = await group.page.import();
|
|
271
|
+
const { loader: groupLoader } = group.loader ? await group.loader.import() : { loader: null };
|
|
272
|
+
let data = {};
|
|
273
|
+
if (groupLoader) {
|
|
274
|
+
const result = await groupLoader.loader(req);
|
|
275
|
+
data = result.data || {};
|
|
276
|
+
loaderData[group.manifestPath] = data;
|
|
277
|
+
}
|
|
278
|
+
slots[groupName.replace('@', '')] = () => groupPage({
|
|
279
|
+
routeParams,
|
|
280
|
+
searchParams,
|
|
281
|
+
loaderData: data
|
|
282
|
+
});
|
|
283
|
+
})());
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const [childrenRendered] = await Promise.all(slotPromises);
|
|
287
|
+
return () => layoutModule({
|
|
288
|
+
children: childrenRendered,
|
|
289
|
+
routeParams,
|
|
290
|
+
searchParams,
|
|
291
|
+
loaderData: data,
|
|
292
|
+
slots: slots,
|
|
293
|
+
locals: {
|
|
294
|
+
cspNonce: cspNonce,
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}, async () => {
|
|
298
|
+
const pageToRender = toRender === 'loading'
|
|
299
|
+
? entry.loadingPage
|
|
300
|
+
: toRender === 'error'
|
|
301
|
+
? entry.errorPage
|
|
302
|
+
: toRender === 'not-found'
|
|
303
|
+
? entry.notFoundPage
|
|
304
|
+
: entry.mainPage;
|
|
305
|
+
const moduleSrc = `${pageToRender.page.src}&pick=$css`;
|
|
306
|
+
const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
|
|
307
|
+
for (const asset of moduleAssets) {
|
|
308
|
+
assets.push(asset);
|
|
309
|
+
}
|
|
310
|
+
const { default: page } = await pageToRender.page.import();
|
|
311
|
+
const { loader: pageLoader } = pageToRender.loader ? await pageToRender.loader.import() : { loader: null };
|
|
312
|
+
const { generateMeta } = pageToRender.generateMeta ? await pageToRender.generateMeta.import() : { generateMeta: null };
|
|
313
|
+
const { options } = pageToRender.options ? await pageToRender.options.import() : { options: {} };
|
|
314
|
+
if (options?.cache) {
|
|
315
|
+
cachingOptions = options.cache;
|
|
316
|
+
}
|
|
317
|
+
let data = {};
|
|
318
|
+
if (pageLoader) {
|
|
319
|
+
const result = await pageLoader.loader(req);
|
|
320
|
+
data = result.data || {};
|
|
321
|
+
loaderData[pageToRender.manifestPath] = data;
|
|
322
|
+
}
|
|
323
|
+
if (generateMeta) {
|
|
324
|
+
const metaData = await generateMeta({
|
|
325
|
+
req,
|
|
326
|
+
cspNonce,
|
|
327
|
+
});
|
|
328
|
+
if (metaData) {
|
|
329
|
+
meta = {
|
|
330
|
+
...meta,
|
|
331
|
+
...metaData
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return () => page({
|
|
336
|
+
routeParams,
|
|
337
|
+
searchParams,
|
|
338
|
+
loaderData: data,
|
|
339
|
+
locals: {
|
|
340
|
+
cspNonce: cspNonce,
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
const composed = await compose();
|
|
345
|
+
const rendered = await renderToString(() => composed());
|
|
346
|
+
if (cachingOptions && toRender === 'main') {
|
|
347
|
+
setCache(url, {
|
|
348
|
+
rendered: rendered,
|
|
349
|
+
documentMeta: meta,
|
|
350
|
+
documentAssets: assets,
|
|
351
|
+
loaderData: loaderData,
|
|
352
|
+
}, cachingOptions.ttl);
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
rendered: rendered,
|
|
356
|
+
documentMeta: meta,
|
|
357
|
+
documentAssets: assets,
|
|
358
|
+
loaderData: loaderData,
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
let routeManifest = {};
|
|
362
|
+
const hydrationScript = ({ nonce, }) => {
|
|
363
|
+
const script = generateHydrationScript();
|
|
364
|
+
return nonce ? script.replace('<script', `<script nonce="${nonce}"`) : script;
|
|
365
|
+
};
|
|
366
|
+
const onStart = async () => {
|
|
367
|
+
try {
|
|
368
|
+
routeManifest = await createRouteManifest();
|
|
369
|
+
}
|
|
370
|
+
catch (e) {
|
|
371
|
+
console.error('Error creating route manifest:', e);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
onStart();
|
|
375
|
+
const handler = eventHandler(async (event) => {
|
|
376
|
+
const req = event.node.req;
|
|
377
|
+
const res = event.node.res;
|
|
378
|
+
try {
|
|
379
|
+
if (req.url?.includes('_server')) {
|
|
380
|
+
return handleServerFunction(event);
|
|
381
|
+
}
|
|
382
|
+
const clientManifest = getManifest('client');
|
|
383
|
+
if (!routeManifest || Object.keys(routeManifest).length === 0) {
|
|
384
|
+
routeManifest = await createRouteManifest();
|
|
385
|
+
}
|
|
386
|
+
const cspNonce = event.locals?.cspNonce;
|
|
387
|
+
const url = req.url || '/';
|
|
388
|
+
// extract route params and search params
|
|
389
|
+
const params = {};
|
|
390
|
+
const searchParams = {};
|
|
391
|
+
const [pathnamePart, searchParamPart] = url.split('?');
|
|
392
|
+
if (searchParamPart) {
|
|
393
|
+
for (const param of searchParamPart.split('&')) {
|
|
394
|
+
const [key, value] = param.split('=');
|
|
395
|
+
searchParams[key] = decodeURIComponent(value || '');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const matched = Object.entries(routeManifest).find(([path, entry]) => {
|
|
399
|
+
const pattern = path
|
|
400
|
+
.replace(/:\[\*[^/\]]+\]/g, '?(.*)?') // [[...slug]] -> (.*)?
|
|
401
|
+
.replace(/:\*[^/]*/g, '.*') // :*slug or :* -> .*
|
|
402
|
+
.replace(/:[^/]+/g, '[^/]+'); // :post -> [^/]+
|
|
403
|
+
const re = new RegExp(`^${pattern}$`);
|
|
404
|
+
return re.test(pathnamePart);
|
|
405
|
+
})?.[1];
|
|
406
|
+
const routePath = matched && matched.type === 'route'
|
|
407
|
+
? matched.manifestPath.split('/').slice(2).join('/')
|
|
408
|
+
: matched && matched.type === 'page'
|
|
409
|
+
? matched.mainPage.manifestPath.split('/').slice(2).join('/')
|
|
410
|
+
: '/';
|
|
411
|
+
const routeParams = extractRouteParams(routePath, pathnamePart);
|
|
412
|
+
if (routeParams) {
|
|
413
|
+
Object.assign(params, routeParams.params);
|
|
414
|
+
}
|
|
415
|
+
if (matched && matched.type === 'route') {
|
|
416
|
+
const routeModule = await matched.handler.import();
|
|
417
|
+
const reqMethod = req.method?.toUpperCase();
|
|
418
|
+
if (reqMethod) {
|
|
419
|
+
const handler = routeModule[reqMethod];
|
|
420
|
+
if (typeof handler === 'function') {
|
|
421
|
+
const result = await handler(toWebRequest(event), {
|
|
422
|
+
params: params,
|
|
423
|
+
searchParams: searchParams,
|
|
424
|
+
});
|
|
425
|
+
await sendNodeResponse(res, result);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
throw new Error(`Method ${reqMethod} not implemented in ${matched.handler.src}`);
|
|
429
|
+
}
|
|
430
|
+
throw new Error(`Unsupported request method: ${reqMethod}`);
|
|
431
|
+
}
|
|
432
|
+
let loading = false;
|
|
433
|
+
let html = undefined;
|
|
434
|
+
let meta = {
|
|
435
|
+
charset: {
|
|
436
|
+
type: 'meta',
|
|
437
|
+
attributes: {
|
|
438
|
+
charset: 'UTF-8'
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
viewport: {
|
|
442
|
+
type: 'meta',
|
|
443
|
+
attributes: {
|
|
444
|
+
name: 'viewport',
|
|
445
|
+
content: 'width=device-width, initial-scale=1.0'
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
title: {
|
|
449
|
+
type: 'title',
|
|
450
|
+
attributes: {},
|
|
451
|
+
content: 'SolidStep'
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
const assets = await clientManifest.inputs[clientManifest.handler].assets();
|
|
455
|
+
const manifestHtml = `<script ${cspNonce ? `nonce="${cspNonce}"` : ''}>window.manifest=${JSON.stringify(await clientManifest.json())}</script>`;
|
|
456
|
+
let clientHydrationScript = undefined;
|
|
457
|
+
res.setHeader('Content-Type', 'text/html');
|
|
458
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
459
|
+
try {
|
|
460
|
+
if (!matched) {
|
|
461
|
+
try {
|
|
462
|
+
const notFoundPage = routeManifest['/'];
|
|
463
|
+
const { rendered, documentMeta, documentAssets, loaderData, } = await render({
|
|
464
|
+
toRender: 'not-found',
|
|
465
|
+
entry: notFoundPage,
|
|
466
|
+
routeParams: {},
|
|
467
|
+
searchParams: {},
|
|
468
|
+
req: toWebRequest(event),
|
|
469
|
+
cspNonce,
|
|
470
|
+
});
|
|
471
|
+
for (const asset of documentAssets) {
|
|
472
|
+
assets.push(asset);
|
|
473
|
+
}
|
|
474
|
+
clientHydrationScript = `
|
|
475
|
+
<script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
|
|
476
|
+
import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
|
|
477
|
+
main('/not-found/',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
|
|
478
|
+
</script>
|
|
479
|
+
`;
|
|
480
|
+
html = rendered;
|
|
481
|
+
meta = {
|
|
482
|
+
...meta,
|
|
483
|
+
...documentMeta
|
|
484
|
+
};
|
|
485
|
+
res.statusCode = 404;
|
|
486
|
+
}
|
|
487
|
+
catch (e) {
|
|
488
|
+
console.error('404 module not found:', e);
|
|
489
|
+
res.statusCode = 404;
|
|
490
|
+
return res.end('Not Found');
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
try {
|
|
495
|
+
const { rendered, documentMeta, documentAssets, loaderData, } = await render({
|
|
496
|
+
toRender: 'loading',
|
|
497
|
+
entry: matched,
|
|
498
|
+
routeParams: params,
|
|
499
|
+
searchParams,
|
|
500
|
+
req: toWebRequest(event),
|
|
501
|
+
cspNonce,
|
|
502
|
+
});
|
|
503
|
+
const assetsHtml = assets.concat(documentAssets).map((asset) => {
|
|
504
|
+
const attributeString = Object.entries(asset.attrs)
|
|
505
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
506
|
+
.join(' ');
|
|
507
|
+
if (asset.tag === 'script') {
|
|
508
|
+
return `<script ${attributeString}></script>`;
|
|
509
|
+
}
|
|
510
|
+
if (asset.tag === 'link') {
|
|
511
|
+
return `<link ${attributeString}>`;
|
|
512
|
+
}
|
|
513
|
+
if (asset.tag === 'style') {
|
|
514
|
+
return `<style ${attributeString}>${asset.children || ''}</style>`;
|
|
515
|
+
}
|
|
516
|
+
}).join('\n');
|
|
517
|
+
const html = `
|
|
518
|
+
<!doctype html>
|
|
519
|
+
<html lang="en">
|
|
520
|
+
<head>
|
|
521
|
+
${generateHtmlHead({
|
|
522
|
+
...meta,
|
|
523
|
+
...documentMeta,
|
|
524
|
+
})}
|
|
525
|
+
${assetsHtml}
|
|
526
|
+
${hydrationScript({ nonce: cspNonce })}
|
|
527
|
+
</head>
|
|
528
|
+
<noscript>
|
|
529
|
+
Please enable JavaScript to view the content.<br/>
|
|
530
|
+
</noscript>
|
|
531
|
+
${rendered}
|
|
532
|
+
</html>
|
|
533
|
+
`;
|
|
534
|
+
res.write(html);
|
|
535
|
+
res.write(`
|
|
536
|
+
<script type="module" data-hydration="loading" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
|
|
537
|
+
import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
|
|
538
|
+
main('${matched.loadingPage?.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
|
|
539
|
+
</script>
|
|
540
|
+
`);
|
|
541
|
+
loading = true;
|
|
542
|
+
}
|
|
543
|
+
catch (e) {
|
|
544
|
+
// skip
|
|
545
|
+
}
|
|
546
|
+
const { rendered, documentMeta, documentAssets, loaderData, } = await render({
|
|
547
|
+
toRender: 'main',
|
|
548
|
+
entry: matched,
|
|
549
|
+
routeParams: params,
|
|
550
|
+
searchParams,
|
|
551
|
+
req: toWebRequest(event),
|
|
552
|
+
cspNonce,
|
|
553
|
+
});
|
|
554
|
+
for (const asset of documentAssets) {
|
|
555
|
+
assets.push(asset);
|
|
556
|
+
}
|
|
557
|
+
clientHydrationScript = `
|
|
558
|
+
<script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
|
|
559
|
+
import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
|
|
560
|
+
main('${matched.mainPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
|
|
561
|
+
</script>
|
|
562
|
+
`;
|
|
563
|
+
html = rendered;
|
|
564
|
+
meta = {
|
|
565
|
+
...meta,
|
|
566
|
+
...documentMeta
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch (e1) {
|
|
571
|
+
if (e1 instanceof RedirectError) {
|
|
572
|
+
throw e1;
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
const errorPage = matched.errorPage;
|
|
576
|
+
if (!errorPage) {
|
|
577
|
+
throw e1;
|
|
578
|
+
}
|
|
579
|
+
const { rendered, documentMeta, documentAssets, loaderData, } = await render({
|
|
580
|
+
toRender: 'error',
|
|
581
|
+
entry: matched,
|
|
582
|
+
routeParams: params,
|
|
583
|
+
searchParams,
|
|
584
|
+
req: toWebRequest(event),
|
|
585
|
+
cspNonce,
|
|
586
|
+
});
|
|
587
|
+
for (const asset of documentAssets) {
|
|
588
|
+
assets.push(asset);
|
|
589
|
+
}
|
|
590
|
+
clientHydrationScript = `
|
|
591
|
+
<script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
|
|
592
|
+
import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
|
|
593
|
+
main('${errorPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
|
|
594
|
+
</script>
|
|
595
|
+
`;
|
|
596
|
+
html = rendered;
|
|
597
|
+
meta = {
|
|
598
|
+
...meta,
|
|
599
|
+
...documentMeta
|
|
600
|
+
};
|
|
601
|
+
res.statusCode = 500;
|
|
602
|
+
}
|
|
603
|
+
catch (e2) {
|
|
604
|
+
throw e1;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (loading) {
|
|
608
|
+
const assetsHtml = assets.map((asset) => {
|
|
609
|
+
const attributeString = Object.entries(asset.attrs)
|
|
610
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
611
|
+
.join(' ');
|
|
612
|
+
if (asset.tag === 'link') {
|
|
613
|
+
return `<link ${attributeString}>`;
|
|
614
|
+
}
|
|
615
|
+
if (asset.tag === 'style') {
|
|
616
|
+
return `<style ${attributeString}>${asset.children || ''}</style>`;
|
|
617
|
+
}
|
|
618
|
+
return '';
|
|
619
|
+
}).join('\n');
|
|
620
|
+
res.write(`
|
|
621
|
+
<script ${cspNonce ? `nonce="${cspNonce}"` : ''}>
|
|
622
|
+
const head = document.querySelector('head');
|
|
623
|
+
const scripts = Array.from(head.querySelectorAll('script'));
|
|
624
|
+
head.innerHTML = \`${generateHtmlHead(meta) + assetsHtml}\`;
|
|
625
|
+
scripts.forEach(script => {
|
|
626
|
+
head.appendChild(script);
|
|
627
|
+
});
|
|
628
|
+
document.querySelector('script[data-hydration="loading"]')?.remove();
|
|
629
|
+
const loading = document.querySelector('body');
|
|
630
|
+
loading.innerHTML = \`${html}\`;
|
|
631
|
+
</script>
|
|
632
|
+
`);
|
|
633
|
+
res.write(manifestHtml);
|
|
634
|
+
return res.end(clientHydrationScript);
|
|
635
|
+
}
|
|
636
|
+
const assetsHtml = assets.map((asset) => {
|
|
637
|
+
const attributeString = Object.entries(asset.attrs)
|
|
638
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
639
|
+
.join(' ');
|
|
640
|
+
if (asset.tag === 'script') {
|
|
641
|
+
return `<script ${attributeString} ${cspNonce ? `nonce="${cspNonce}"` : ''}></script>`;
|
|
642
|
+
}
|
|
643
|
+
if (asset.tag === 'link') {
|
|
644
|
+
return `<link ${attributeString}>`;
|
|
645
|
+
}
|
|
646
|
+
if (asset.tag === 'style') {
|
|
647
|
+
return `<style ${attributeString}>${asset.children || ''}</style>`;
|
|
648
|
+
}
|
|
649
|
+
}).join('\n');
|
|
650
|
+
const transformHtml = template
|
|
651
|
+
.replace('<!--app-head-->', `${generateHtmlHead(meta)}\n${assetsHtml}\n${hydrationScript({ nonce: cspNonce })}`)
|
|
652
|
+
.replace('<!--app-body-->', (html ?? '') + manifestHtml + clientHydrationScript);
|
|
653
|
+
return res.end(transformHtml);
|
|
654
|
+
}
|
|
655
|
+
catch (e) {
|
|
656
|
+
if (e instanceof RedirectError) {
|
|
657
|
+
res.statusCode = 302;
|
|
658
|
+
res.setHeader('Location', e.message);
|
|
659
|
+
return res.end('Redirecting...');
|
|
660
|
+
}
|
|
661
|
+
console.error(e);
|
|
662
|
+
res.statusCode = 500;
|
|
663
|
+
return res.end('Internal Server Error');
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
export default handler;
|
package/utils/cache.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const getCache: <T>(key: string) => T | null;
|
|
2
|
+
export declare const setCache: <T>(key: string, value: T, ttlMs?: number) => void;
|
|
3
|
+
export declare const invalidateCache: (key: string) => void;
|
|
4
|
+
export declare const clearAllCache: () => void;
|
|
5
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../utils/cache.ts"],"names":[],"mappings":"AA6CA,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,KAAK,MAAM,KAAG,CAAC,GAAG,IAe7C,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,KAAK,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,MAAM,SA0BhE,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,SAU1C,CAAC;AAEF,eAAO,MAAM,aAAa,YAGzB,CAAC"}
|