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