@varlabs/create-solidstep 0.1.2 → 0.1.4
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/middleware.ts +49 -0
- package/generate/app.config.ts +26 -20
- package/generate/client.ts +26 -34
- package/generate/server.ts +336 -178
- package/generate/tsconfig.json +16 -0
- package/generate/utils/cache.ts +106 -0
- package/generate/utils/cookies.ts +25 -0
- package/generate/utils/cors.ts +16 -0
- package/generate/utils/csp.ts +27 -0
- package/generate/utils/csrf.ts +62 -0
- package/generate/utils/redirect.ts +16 -0
- package/generate/utils/router.ts +137 -1
- package/generate/utils/server-only.ts +5 -0
- package/package.json +1 -1
package/generate/server.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { eventHandler } from 'vinxi/http';
|
|
1
|
+
import { eventHandler, toWebRequest } from 'vinxi/http';
|
|
2
2
|
import { getManifest } from 'vinxi/manifest';
|
|
3
3
|
import { generateHydrationScript, renderToString } from 'solid-js/web';
|
|
4
|
-
import { readdir, stat } from 'node:fs/promises';
|
|
5
|
-
import { join, relative, sep } from 'node:path';
|
|
6
4
|
import type { Meta } from './utils/types';
|
|
7
5
|
import type { ServerResponse, IncomingMessage } from 'node:http';
|
|
8
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';
|
|
9
10
|
|
|
10
11
|
type Import = {
|
|
11
12
|
src: string;
|
|
@@ -19,6 +20,7 @@ type RoutePageEntry = {
|
|
|
19
20
|
page: Import;
|
|
20
21
|
loader?: Import;
|
|
21
22
|
generateMeta?: Import;
|
|
23
|
+
options?: Import;
|
|
22
24
|
};
|
|
23
25
|
loadingPage?: {
|
|
24
26
|
manifestPath: string;
|
|
@@ -53,16 +55,27 @@ type RoutePageEntry = {
|
|
|
53
55
|
type RouteEntry = {
|
|
54
56
|
type: 'route';
|
|
55
57
|
handler: Import;
|
|
58
|
+
manifestPath: string;
|
|
56
59
|
} | RoutePageEntry;
|
|
57
60
|
|
|
58
61
|
type RouteManifest = {
|
|
59
62
|
[key: string]: RouteEntry;
|
|
60
63
|
};
|
|
61
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
|
+
|
|
62
75
|
const isPageFile = (file: string) =>
|
|
63
|
-
file.endsWith('page.tsx')
|
|
64
|
-
|| file.endsWith('page.jsx')
|
|
65
|
-
|| file.endsWith('page.ts')
|
|
76
|
+
file.endsWith('page.tsx')
|
|
77
|
+
|| file.endsWith('page.jsx')
|
|
78
|
+
|| file.endsWith('page.ts')
|
|
66
79
|
|| file.endsWith('page.js');
|
|
67
80
|
|
|
68
81
|
const isRouteFile = (file: string) =>
|
|
@@ -71,127 +84,130 @@ const isRouteFile = (file: string) =>
|
|
|
71
84
|
const parseSegment = (part: string) =>
|
|
72
85
|
part.startsWith('[') ? ':' + part.slice(1, -1).replace(/\.\.\./, '*') : part;
|
|
73
86
|
|
|
74
|
-
const createRouteManifest = async (
|
|
87
|
+
const createRouteManifest = async () => {
|
|
75
88
|
const entries: RouteManifest = {};
|
|
76
89
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} else if (isPageFile(fullPath)) {
|
|
84
|
-
const rel = relative(baseDir, fullPath);
|
|
85
|
-
const parts = rel.split(sep);
|
|
86
|
-
const segments = parts.slice(0, -1); // drop 'page.tsx'
|
|
87
|
-
|
|
88
|
-
if (segments.find(s => s.startsWith('@'))) {
|
|
89
|
-
// don't include parallel routes in the manifest
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
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;
|
|
92
96
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
const 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
|
+
};
|
|
138
148
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let errorPage: FileRoute | undefined;
|
|
152
|
+
const 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;
|
|
144
159
|
});
|
|
145
160
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
manifestPath: mainPage.path,
|
|
150
|
-
page: mainPage.$component,
|
|
151
|
-
loader: mainPage.$loader,
|
|
152
|
-
generateMeta: mainPage.$generateMeta,
|
|
153
|
-
},
|
|
154
|
-
loadingPage: loadingPage ? {
|
|
155
|
-
page: loadingPage.$component,
|
|
156
|
-
generateMeta: loadingPage.$generateMeta,
|
|
157
|
-
manifestPath: loadingPage.path,
|
|
158
|
-
} : undefined,
|
|
159
|
-
errorPage: errorPage ? {
|
|
160
|
-
page: errorPage.$component,
|
|
161
|
-
generateMeta: errorPage.$generateMeta,
|
|
162
|
-
manifestPath: errorPage.path,
|
|
163
|
-
} : undefined,
|
|
164
|
-
layouts,
|
|
165
|
-
groups,
|
|
166
|
-
notFoundPage: notFoundPage ? {
|
|
167
|
-
page: notFoundPage.$component,
|
|
168
|
-
generateMeta: notFoundPage.$generateMeta,
|
|
169
|
-
manifestPath: notFoundPage.path,
|
|
170
|
-
} : undefined,
|
|
171
|
-
};
|
|
172
|
-
} else if (isRouteFile(fullPath)) {
|
|
173
|
-
const rel = relative(baseDir, fullPath);
|
|
174
|
-
const parts = rel.split(sep);
|
|
175
|
-
const segments = parts.slice(0, -1); // drop 'route.ts'
|
|
176
|
-
|
|
177
|
-
const urlSegments = segments
|
|
178
|
-
.filter(s => !(s.startsWith('('))) // drop route groups
|
|
179
|
-
.map(parseSegment);
|
|
180
|
-
|
|
181
|
-
const routePath = '/' + urlSegments.join('/');
|
|
182
|
-
const mainRoute = fileRoutes.find(route => {
|
|
183
|
-
const path = '/' + route.path.split('/').slice(2).filter(s => !(s.startsWith('('))).map(parseSegment).join('/');
|
|
184
|
-
return (route as any).type === 'route' && path === routePath;
|
|
161
|
+
const layout = allLayouts.find(route => {
|
|
162
|
+
const routePath = '/' + route.path.split('/').slice(2).map(parseSegment).join('/');
|
|
163
|
+
return routePath === path;
|
|
185
164
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
165
|
+
if (layout) {
|
|
166
|
+
layouts.unshift({
|
|
167
|
+
manifestPath: layout.path,
|
|
168
|
+
layout: layout.$component,
|
|
169
|
+
loader: layout.$loader,
|
|
170
|
+
generateMeta: layout.$generateMeta,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
191
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
|
+
};
|
|
192
208
|
}
|
|
193
209
|
}
|
|
194
|
-
|
|
210
|
+
|
|
195
211
|
return entries;
|
|
196
212
|
};
|
|
197
213
|
|
|
@@ -199,17 +215,21 @@ const extractRouteParams = (route: string, url: string) => {
|
|
|
199
215
|
const routeSegments = route.split('/').filter(Boolean);
|
|
200
216
|
const urlSegments = url.split('/').filter(Boolean);
|
|
201
217
|
|
|
202
|
-
if (routeSegments.length !== urlSegments.length) return null;
|
|
203
|
-
|
|
204
218
|
const params = {};
|
|
205
219
|
let matched = true;
|
|
206
220
|
|
|
207
221
|
for (let i = 0; i < routeSegments.length; i++) {
|
|
208
222
|
const routeSeg = routeSegments[i];
|
|
209
223
|
const urlSeg = urlSegments[i];
|
|
210
|
-
|
|
211
224
|
const isDynamic = routeSeg.startsWith('[') && routeSeg.endsWith(']');
|
|
212
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
|
+
}
|
|
213
233
|
const paramName = routeSeg.slice(1, -1);
|
|
214
234
|
params[paramName] = urlSeg;
|
|
215
235
|
} else if (routeSeg !== urlSeg) {
|
|
@@ -289,11 +309,39 @@ const render = async (
|
|
|
289
309
|
entry: RoutePageEntry,
|
|
290
310
|
routeParams: Record<string, string>,
|
|
291
311
|
searchParams: Record<string, string>,
|
|
292
|
-
req: Request
|
|
312
|
+
req: Request,
|
|
293
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;
|
|
294
334
|
let meta: Meta = {};
|
|
335
|
+
let loaderData: Record<string, any> = {};
|
|
336
|
+
const clientManifest = getManifest('client');
|
|
337
|
+
const assets = [];
|
|
295
338
|
const compose = entry.layouts.reduceRight(
|
|
296
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
|
+
}
|
|
297
345
|
const { default: layoutModule } = await layout.layout.import();
|
|
298
346
|
const { loader: layoutLoader } = layout.loader ? await layout.loader.import() : { loader: null };
|
|
299
347
|
const { generateMeta: generateMetaPage } = layout.generateMeta ? await layout.generateMeta.import() : { generateMeta: null };
|
|
@@ -308,29 +356,41 @@ const render = async (
|
|
|
308
356
|
}
|
|
309
357
|
}
|
|
310
358
|
if (layoutLoader) {
|
|
311
|
-
const result = await layoutLoader(req);
|
|
359
|
+
const result = await layoutLoader.loader(req);
|
|
312
360
|
data = result.data || {};
|
|
361
|
+
loaderData[layout.manifestPath] = data;
|
|
313
362
|
}
|
|
314
|
-
|
|
363
|
+
const slots: Record<string, any> = {};
|
|
364
|
+
const slotPromises: any[] = [children()];
|
|
315
365
|
if (index === entry.layouts.length - 1) {
|
|
316
366
|
// last layout, we can render slots
|
|
317
367
|
const groups = entry.groups || {};
|
|
318
368
|
for (const [groupName, group] of Object.entries(groups)) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
);
|
|
331
391
|
}
|
|
332
392
|
}
|
|
333
|
-
const childrenRendered = await
|
|
393
|
+
const [childrenRendered] = await Promise.all(slotPromises);
|
|
334
394
|
return () => layoutModule({
|
|
335
395
|
children: childrenRendered,
|
|
336
396
|
routeParams,
|
|
@@ -347,13 +407,23 @@ const render = async (
|
|
|
347
407
|
: toRender === 'not-found'
|
|
348
408
|
? entry.notFoundPage
|
|
349
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
|
+
}
|
|
350
415
|
const { default: page } = await pageToRender.page.import();
|
|
351
416
|
const { loader: pageLoader } = pageToRender.loader ? await pageToRender.loader.import() : { loader: null };
|
|
352
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
|
+
}
|
|
353
422
|
let data = {};
|
|
354
423
|
if (pageLoader) {
|
|
355
|
-
const result = await pageLoader(req);
|
|
424
|
+
const result = await pageLoader.loader(req);
|
|
356
425
|
data = result.data || {};
|
|
426
|
+
loaderData[pageToRender.manifestPath] = data;
|
|
357
427
|
}
|
|
358
428
|
if (generateMeta) {
|
|
359
429
|
const metaData = await generateMeta(req);
|
|
@@ -374,25 +444,41 @@ const render = async (
|
|
|
374
444
|
|
|
375
445
|
const composed = await compose();
|
|
376
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
|
+
|
|
377
457
|
return {
|
|
378
458
|
rendered: rendered,
|
|
379
|
-
documentMeta: meta
|
|
459
|
+
documentMeta: meta,
|
|
460
|
+
documentAssets: assets,
|
|
461
|
+
loaderData: loaderData,
|
|
380
462
|
};
|
|
381
463
|
};
|
|
382
464
|
|
|
383
465
|
let routeManifest: RouteManifest = {};
|
|
384
466
|
|
|
385
467
|
const handler = eventHandler(async (event) => {
|
|
386
|
-
const clientManifest = getManifest('client');
|
|
387
|
-
|
|
388
|
-
if (!routeManifest || Object.keys(routeManifest).length === 0) {
|
|
389
|
-
routeManifest = await createRouteManifest();
|
|
390
|
-
}
|
|
391
|
-
|
|
392
468
|
const req = event.node.req;
|
|
393
469
|
const res = event.node.res;
|
|
394
470
|
|
|
395
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
|
+
|
|
396
482
|
const url = req.url || '/';
|
|
397
483
|
// extract route params and search params
|
|
398
484
|
const params: Record<string, string> = {};
|
|
@@ -406,18 +492,26 @@ const handler = eventHandler(async (event) => {
|
|
|
406
492
|
}
|
|
407
493
|
|
|
408
494
|
const matched = Object.entries(routeManifest).find(([path, entry]) => {
|
|
409
|
-
const pattern = path
|
|
495
|
+
const pattern = path
|
|
496
|
+
.replace(/:\[\*[^/\]]+\]/g, '?(.*)?') // [[...slug]] -> (.*)?
|
|
497
|
+
.replace(/:\*[^/]*/g, '.*') // :*slug or :* -> .*
|
|
498
|
+
.replace(/:[^/]+/g, '[^/]+'); // :post -> [^/]+
|
|
499
|
+
|
|
410
500
|
const re = new RegExp(`^${pattern}$`);
|
|
411
501
|
return re.test(pathnamePart);
|
|
412
502
|
})?.[1] as RouteEntry;
|
|
413
503
|
|
|
414
|
-
const routePath = matched
|
|
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
|
+
: '/';
|
|
415
509
|
|
|
416
510
|
const routeParams = extractRouteParams(routePath, pathnamePart);
|
|
417
511
|
if (routeParams) {
|
|
418
512
|
Object.assign(params, routeParams.params);
|
|
419
513
|
}
|
|
420
|
-
|
|
514
|
+
|
|
421
515
|
if (matched && matched.type === 'route') {
|
|
422
516
|
const routeModule = await matched.handler.import();
|
|
423
517
|
const reqMethod = req.method?.toUpperCase();
|
|
@@ -460,20 +554,6 @@ const handler = eventHandler(async (event) => {
|
|
|
460
554
|
}
|
|
461
555
|
};
|
|
462
556
|
const assets = await clientManifest.inputs[clientManifest.handler].assets();
|
|
463
|
-
const assetsHtml = assets.map((asset) => {
|
|
464
|
-
const attributeString = Object.entries(asset.attrs)
|
|
465
|
-
.map(([key, value]) => `${key}="${value}"`)
|
|
466
|
-
.join(' ');
|
|
467
|
-
if (asset.tag === 'script') {
|
|
468
|
-
return `<script ${attributeString}></script>`;
|
|
469
|
-
}
|
|
470
|
-
if (asset.tag === 'link') {
|
|
471
|
-
return `<link ${attributeString}>`;
|
|
472
|
-
}
|
|
473
|
-
if (asset.tag === 'style') {
|
|
474
|
-
return `<style ${attributeString}>${asset.children || ''}</style>`;
|
|
475
|
-
}
|
|
476
|
-
}).join('\n');
|
|
477
557
|
const manifestHtml = `<script>window.manifest=${JSON.stringify(await clientManifest.json())}</script>`;
|
|
478
558
|
let clientHydrationScript;
|
|
479
559
|
|
|
@@ -483,17 +563,25 @@ const handler = eventHandler(async (event) => {
|
|
|
483
563
|
if (!matched) {
|
|
484
564
|
try {
|
|
485
565
|
const notFoundPage = routeManifest['/'] as RoutePageEntry;
|
|
486
|
-
const {
|
|
566
|
+
const {
|
|
567
|
+
rendered,
|
|
568
|
+
documentMeta,
|
|
569
|
+
documentAssets,
|
|
570
|
+
loaderData,
|
|
571
|
+
} = await render(
|
|
487
572
|
'not-found',
|
|
488
573
|
notFoundPage,
|
|
489
574
|
{},
|
|
490
575
|
{},
|
|
491
|
-
|
|
576
|
+
toWebRequest(event)
|
|
492
577
|
);
|
|
578
|
+
for (const asset of documentAssets) {
|
|
579
|
+
assets.push(asset);
|
|
580
|
+
}
|
|
493
581
|
clientHydrationScript = `
|
|
494
582
|
<script type="module">
|
|
495
583
|
import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
|
|
496
|
-
main('/not-found/',${JSON.stringify(params)},${JSON.stringify(searchParams)});
|
|
584
|
+
main('/not-found/',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
|
|
497
585
|
</script>
|
|
498
586
|
`;
|
|
499
587
|
html = rendered;
|
|
@@ -509,21 +597,41 @@ const handler = eventHandler(async (event) => {
|
|
|
509
597
|
}
|
|
510
598
|
} else {
|
|
511
599
|
try {
|
|
512
|
-
const {
|
|
600
|
+
const {
|
|
601
|
+
rendered,
|
|
602
|
+
documentMeta,
|
|
603
|
+
documentAssets,
|
|
604
|
+
loaderData,
|
|
605
|
+
} = await render(
|
|
513
606
|
'loading',
|
|
514
607
|
matched as RoutePageEntry,
|
|
515
608
|
params,
|
|
516
609
|
searchParams,
|
|
517
|
-
|
|
610
|
+
toWebRequest(event)
|
|
518
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');
|
|
519
626
|
const html = `
|
|
520
627
|
<!doctype html>
|
|
521
628
|
<html lang="en">
|
|
522
629
|
<head>
|
|
523
630
|
${generateHtmlHead({
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
631
|
+
...meta,
|
|
632
|
+
...documentMeta,
|
|
633
|
+
})}
|
|
634
|
+
${assetsHtml}
|
|
527
635
|
${generateHydrationScript()}
|
|
528
636
|
</head>
|
|
529
637
|
<noscript>
|
|
@@ -536,25 +644,33 @@ const handler = eventHandler(async (event) => {
|
|
|
536
644
|
res.write(`
|
|
537
645
|
<script type="module" data-hydration="loading">
|
|
538
646
|
import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
|
|
539
|
-
main('${(matched as RoutePageEntry).loadingPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)});
|
|
647
|
+
main('${(matched as RoutePageEntry).loadingPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
|
|
540
648
|
</script>
|
|
541
649
|
`);
|
|
542
650
|
loading = true;
|
|
543
651
|
} catch (e) {
|
|
544
652
|
// skip
|
|
545
653
|
}
|
|
546
|
-
|
|
547
|
-
const {
|
|
654
|
+
|
|
655
|
+
const {
|
|
656
|
+
rendered,
|
|
657
|
+
documentMeta,
|
|
658
|
+
documentAssets,
|
|
659
|
+
loaderData,
|
|
660
|
+
} = await render(
|
|
548
661
|
'main',
|
|
549
662
|
matched as RoutePageEntry,
|
|
550
663
|
params,
|
|
551
664
|
searchParams,
|
|
552
|
-
|
|
665
|
+
toWebRequest(event)
|
|
553
666
|
);
|
|
667
|
+
for (const asset of documentAssets) {
|
|
668
|
+
assets.push(asset);
|
|
669
|
+
}
|
|
554
670
|
clientHydrationScript = `
|
|
555
671
|
<script type="module">
|
|
556
672
|
import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
|
|
557
|
-
main('${(matched as RoutePageEntry).mainPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)});
|
|
673
|
+
main('${(matched as RoutePageEntry).mainPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
|
|
558
674
|
</script>
|
|
559
675
|
`;
|
|
560
676
|
html = rendered;
|
|
@@ -564,22 +680,33 @@ const handler = eventHandler(async (event) => {
|
|
|
564
680
|
};
|
|
565
681
|
}
|
|
566
682
|
} catch (e1) {
|
|
683
|
+
if (e1 instanceof RedirectError) {
|
|
684
|
+
throw e1;
|
|
685
|
+
}
|
|
567
686
|
try {
|
|
568
687
|
const errorPage = (matched as RoutePageEntry).errorPage;
|
|
569
688
|
if (!errorPage) {
|
|
570
689
|
throw e1;
|
|
571
690
|
}
|
|
572
|
-
const {
|
|
691
|
+
const {
|
|
692
|
+
rendered,
|
|
693
|
+
documentMeta,
|
|
694
|
+
documentAssets,
|
|
695
|
+
loaderData,
|
|
696
|
+
} = await render(
|
|
573
697
|
'error',
|
|
574
698
|
matched as RoutePageEntry,
|
|
575
699
|
params,
|
|
576
700
|
searchParams,
|
|
577
|
-
|
|
701
|
+
toWebRequest(event)
|
|
578
702
|
);
|
|
703
|
+
for (const asset of documentAssets) {
|
|
704
|
+
assets.push(asset);
|
|
705
|
+
}
|
|
579
706
|
clientHydrationScript = `
|
|
580
707
|
<script type="module">
|
|
581
708
|
import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
|
|
582
|
-
main('${errorPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)});
|
|
709
|
+
main('${errorPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
|
|
583
710
|
</script>
|
|
584
711
|
`;
|
|
585
712
|
html = rendered;
|
|
@@ -594,11 +721,23 @@ const handler = eventHandler(async (event) => {
|
|
|
594
721
|
}
|
|
595
722
|
|
|
596
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');
|
|
597
736
|
res.write(`
|
|
598
737
|
<script>
|
|
599
738
|
const head = document.querySelector('head');
|
|
600
739
|
const scripts = Array.from(head.querySelectorAll('script'));
|
|
601
|
-
head.innerHTML = \`${generateHtmlHead(meta)}\`;
|
|
740
|
+
head.innerHTML = \`${generateHtmlHead(meta) + assetsHtml}\`;
|
|
602
741
|
scripts.forEach(script => {
|
|
603
742
|
head.appendChild(script);
|
|
604
743
|
});
|
|
@@ -610,6 +749,20 @@ const handler = eventHandler(async (event) => {
|
|
|
610
749
|
res.write(manifestHtml);
|
|
611
750
|
return res.end(clientHydrationScript);
|
|
612
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');
|
|
613
766
|
const transformHtml = template
|
|
614
767
|
.replace(`<!--app-head-->`, generateHtmlHead(meta) + '\n' + assetsHtml + '\n' + generateHydrationScript())
|
|
615
768
|
.replace(`<!--app-body-->`, (html ?? '') + manifestHtml + clientHydrationScript);
|
|
@@ -617,6 +770,11 @@ const handler = eventHandler(async (event) => {
|
|
|
617
770
|
}
|
|
618
771
|
}
|
|
619
772
|
} catch (e) {
|
|
773
|
+
if (e instanceof RedirectError) {
|
|
774
|
+
res.statusCode = 302;
|
|
775
|
+
res.setHeader('Location', e.message);
|
|
776
|
+
return res.end('Redirecting...');
|
|
777
|
+
}
|
|
620
778
|
console.error(e);
|
|
621
779
|
res.statusCode = 500;
|
|
622
780
|
return res.end('Internal Server Error');
|