@varlabs/create-solidstep 0.1.2 → 0.1.3

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.
@@ -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 (baseDir = 'app') => {
87
+ const createRouteManifest = async () => {
75
88
  const entries: RouteManifest = {};
76
89
 
77
- const walk = async (dir: string) => {
78
- const contents = await readdir(dir);
79
- for (const entry of contents) {
80
- const fullPath = join(dir, entry);
81
- if ((await stat(fullPath)).isDirectory()) {
82
- await walk(fullPath);
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
- const urlSegments = segments
94
- .filter(s => !(s.startsWith('('))) // drop route groups
95
- .map(parseSegment);
96
- const routePath = '/' + urlSegments.join('/');
97
- const mainPage = fileRoutes.find(route => {
98
- const path = '/' + route.path.split('/').slice(2).filter(s => !(s.startsWith('('))).map(parseSegment).join('/');
99
- return (route as any).type === 'route' && path === routePath;
100
- });
101
- const loadingPage = fileRoutes.find(route => {
102
- const path = '/' + route.path.split('/').slice(2).filter(s => !(s.startsWith('('))).map(parseSegment).join('/');
103
- return (route as any).type === 'loading' && path === routePath;
104
- });
105
- let errorPage: RouteModule;
106
- const layouts: RoutePageEntry['layouts'] = [];
107
- const fileSegments = segments
108
- .map(parseSegment);
109
- for (let i = 0; i < fileSegments.length + 1; i++) {
110
- const route = '/' + fileSegments.slice(0, fileSegments.length-i).join('/');
111
-
112
- for (const fileRoute of fileRoutes) {
113
- const path = '/' + fileRoute.path.split('/').slice(2).map(parseSegment).join('/');
114
- if (!errorPage && (fileRoute as any).type === 'error' && path === route) {
115
- errorPage = fileRoute;
116
- }
117
- if ((fileRoute as any).type === 'layout' && path === route) {
118
- layouts.unshift({
119
- layout: fileRoute.$component,
120
- loader: fileRoute.$loader,
121
- generateMeta: fileRoute.$generateMeta,
122
- manifestPath: fileRoute.path,
123
- });
124
- }
125
- }
126
- }
127
- let groups: RoutePageEntry['groups'] = {};
128
- for (const fileRoute of fileRoutes) {
129
- const groupParentPath = (fileRoute as any).parent ? '/' + (fileRoute as any).parent.split('/').slice(2).filter(s => !(s.startsWith('('))).map(parseSegment).join('/') : '';
130
- if ((fileRoute as any).type === 'group' && groupParentPath === routePath) {
131
- const groupName = fileRoute.path.split('/').filter(s => !(s.startsWith('('))).map(parseSegment).at(-1);
132
- groups[groupName] = {
133
- page: fileRoute.$component,
134
- loader: fileRoute.$loader,
135
- manifestPath: fileRoute.path,
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
- let notFoundPage: RouteModule | undefined;
140
- if (routePath === '/') {
141
- notFoundPage = fileRoutes.find(route => {
142
- const path = '/' + route.path.split('/').slice(2).join('/');
143
- return (route as any).type === 'not-found' && path === routePath;
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
- entries[routePath] = {
147
- type: 'page',
148
- mainPage: {
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
- entries[routePath] = {
188
- type: 'route',
189
- handler: mainRoute.$handler,
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
- await walk(baseDir);
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
- let slots: Record<string, any> = {};
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
- const { default: groupPage } = await group.page.import();
320
- const { loader: groupLoader } = group.loader ? await group.loader.import() : { loader: null };
321
- let data = {};
322
- if (groupLoader) {
323
- const result = await groupLoader(req);
324
- data = result.data || {};
325
- }
326
- slots[groupName.replace('@', '')] = () => groupPage({
327
- routeParams,
328
- searchParams,
329
- loaderData: data
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 children();
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.replace(/:[^/]+/g, '[^/]+').replace(/\*$/, '.*');
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 ? (matched as RoutePageEntry).mainPage.manifestPath.split('/').slice(2).join('/') : '/';
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 { rendered, documentMeta } = await render(
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
- req as unknown as Request
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 { rendered, documentMeta } = await render(
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
- req as unknown as Request
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
- ...meta,
525
- ...documentMeta,
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 { rendered, documentMeta } = await render(
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
- req as unknown as Request
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 { rendered, documentMeta } = await render(
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
- req as unknown as Request
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');