solidstep 0.3.4 → 0.4.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/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { eventHandler, toWebRequest, setHeader, setResponseStatus, } from 'vinxi/http';
1
+ import { eventHandler, getResponseStatus, toWebRequest, setHeader, setResponseStatus, } from 'vinxi/http';
2
2
  import { getManifest } from 'vinxi/manifest';
3
3
  import { generateHydrationScript, renderToString } from 'solid-js/web';
4
4
  import fileRoutes, {} from 'vinxi/routes';
@@ -9,9 +9,14 @@ import { readFile } from 'node:fs/promises';
9
9
  import { dirname } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { createNode, insertRoute, matchRoute, } from './utils/path-router';
12
- // Module cache for dynamically imported modules
12
+ import { loadInstrumentation, getInstrumentation, safeExecuteHook, createRequestContext, createResponseContext, } from './utils/instrumentation';
13
+ let instrumentationReady = null;
14
+ // Module cache for dynamically imported modules — skipped in dev so HMR invalidations are respected
13
15
  const moduleCache = new Map();
14
16
  const getCachedModule = async (importFn) => {
17
+ if (import.meta.env.DEV) {
18
+ return importFn.import();
19
+ }
15
20
  const key = importFn.src;
16
21
  if (moduleCache.has(key)) {
17
22
  return moduleCache.get(key);
@@ -59,9 +64,15 @@ const createRouteManifest = async () => {
59
64
  errorPagesMap.set(path, fileRoute);
60
65
  }
61
66
  if (fileRoute.type === 'group') {
62
- const parentPath = fileRoute.parent
63
- ? getNormalizedPath(fileRoute.parent)
64
- : '';
67
+ // `fileRoute.parent` is already a clean route path (e.g. '/dashboard'
68
+ // or '' for the root), so it must NOT go through getNormalizedPath,
69
+ // which strips a leading prefix. Normalize it the same way page
70
+ // routePaths are (drop '(group)' segments, ensure a leading slash) so
71
+ // nested parallel routes attach to their parent route.
72
+ const parentPath = `/${(fileRoute.parent || '')
73
+ .split('/')
74
+ .filter((s) => s && !s.startsWith('('))
75
+ .join('/')}`;
65
76
  const existing = groupsMap.get(parentPath) || [];
66
77
  existing.push(fileRoute);
67
78
  groupsMap.set(parentPath, existing);
@@ -207,14 +218,52 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, pageOpt
207
218
  const loaderData = {};
208
219
  const clientManifest = getManifest('client');
209
220
  const assets = [];
221
+ // Select the page variant being rendered up front so its loader can be
222
+ // pre-resolved alongside the layout loaders.
223
+ const pageToRender = toRender === 'loading'
224
+ ? entry.loadingPage
225
+ : toRender === 'error'
226
+ ? entry.errorPage
227
+ : toRender === 'not-found'
228
+ ? entry.notFoundPage
229
+ : entry.mainPage;
230
+ // Run every layout loader and the page loader concurrently instead of
231
+ // sequentially down the layout chain. Results are keyed by manifestPath and
232
+ // applied in tree order below, so loaderData ordering and per-node data are
233
+ // unchanged — only the awaits now overlap.
234
+ const loaderTargets = [];
235
+ for (const layout of entry.layouts) {
236
+ if (layout.loader) {
237
+ loaderTargets.push({
238
+ manifestPath: layout.manifestPath,
239
+ loader: layout.loader,
240
+ });
241
+ }
242
+ }
243
+ if (pageToRender?.loader) {
244
+ loaderTargets.push({
245
+ manifestPath: pageToRender.manifestPath,
246
+ loader: pageToRender.loader,
247
+ });
248
+ }
249
+ // A `$loader` import is created for every layout/page node, even when the
250
+ // file exports no loader — in that case the picked module is empty and has
251
+ // no `loader` export, so we skip it. Only nodes whose loader actually ran
252
+ // get an entry here, which is how the closures below decide whether to
253
+ // populate loaderData (matching the previous in-closure behavior).
254
+ const resolvedLoaderData = new Map();
255
+ await Promise.all(loaderTargets.map(async ({ manifestPath, loader }) => {
256
+ const { loader: loaderFn } = await getCachedModule(loader);
257
+ if (!loaderFn)
258
+ return;
259
+ const result = await loaderFn.loader(req);
260
+ resolvedLoaderData.set(manifestPath, result.data || {});
261
+ }));
210
262
  const compose = entry.layouts.reduceRight((children, layout, index) => async () => {
211
263
  const moduleSrc = `${layout.layout.src}&pick=$css`;
212
264
  const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
213
265
  assets.push(...moduleAssets);
214
266
  const { default: layoutModule } = await getCachedModule(layout.layout);
215
- const { loader: layoutLoader } = layout.loader
216
- ? await getCachedModule(layout.loader)
217
- : { loader: null };
218
267
  const { generateMeta: generateMetaPage } = layout.generateMeta
219
268
  ? await getCachedModule(layout.generateMeta)
220
269
  : { generateMeta: null };
@@ -231,9 +280,8 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, pageOpt
231
280
  };
232
281
  }
233
282
  }
234
- if (layoutLoader) {
235
- const result = await layoutLoader.loader(req);
236
- data = result.data || {};
283
+ if (resolvedLoaderData.has(layout.manifestPath)) {
284
+ data = resolvedLoaderData.get(layout.manifestPath);
237
285
  loaderData[layout.manifestPath] = data;
238
286
  }
239
287
  const slots = {};
@@ -276,27 +324,16 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, pageOpt
276
324
  },
277
325
  });
278
326
  }, async () => {
279
- const pageToRender = toRender === 'loading'
280
- ? entry.loadingPage
281
- : toRender === 'error'
282
- ? entry.errorPage
283
- : toRender === 'not-found'
284
- ? entry.notFoundPage
285
- : entry.mainPage;
286
327
  const moduleSrc = `${pageToRender.page.src}&pick=$css`;
287
328
  const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
288
329
  assets.push(...moduleAssets);
289
330
  const { default: page } = await getCachedModule(pageToRender.page);
290
- const { loader: pageLoader } = pageToRender.loader
291
- ? await getCachedModule(pageToRender.loader)
292
- : { loader: null };
293
331
  const { generateMeta } = pageToRender.generateMeta
294
332
  ? await getCachedModule(pageToRender.generateMeta)
295
333
  : { generateMeta: null };
296
334
  let data = {};
297
- if (pageLoader) {
298
- const result = await pageLoader.loader(req);
299
- data = result.data || {};
335
+ if (resolvedLoaderData.has(pageToRender.manifestPath)) {
336
+ data = resolvedLoaderData.get(pageToRender.manifestPath);
300
337
  loaderData[pageToRender.manifestPath] = data;
301
338
  }
302
339
  if (generateMeta) {
@@ -364,9 +401,16 @@ const onStart = async () => {
364
401
  catch (e) {
365
402
  console.error('Error creating route manifest:', e);
366
403
  }
404
+ // Load instrumentation
405
+ const instrumentation = await loadInstrumentation();
406
+ if (instrumentation?.register) {
407
+ await safeExecuteHook('register', instrumentation.register);
408
+ }
367
409
  };
368
- onStart();
410
+ instrumentationReady = onStart();
369
411
  const handler = eventHandler(async (event) => {
412
+ if (instrumentationReady)
413
+ await instrumentationReady;
370
414
  const req = toWebRequest(event);
371
415
  try {
372
416
  if (req.url.includes('/.well-known/appspecific/com.chrome.devtools.json')) {
@@ -390,21 +434,45 @@ const handler = eventHandler(async (event) => {
390
434
  const matched = match?.handler;
391
435
  const params = match?.params || {};
392
436
  if (matched && matched.type === 'route') {
393
- const routeModule = await getCachedModule(matched.handler);
394
- const reqMethod = req.method?.toUpperCase();
395
- if (reqMethod) {
396
- const handler = routeModule[reqMethod];
397
- if (typeof handler === 'function') {
398
- const result = await handler(req, {
399
- params: params,
400
- searchParams: searchParams,
401
- });
402
- return result;
437
+ const inst = getInstrumentation();
438
+ const reqCtx = createRequestContext(req, {
439
+ routePath: matched.routePath || 'unknown',
440
+ routeType: 'api',
441
+ params,
442
+ searchParams,
443
+ });
444
+ await safeExecuteHook('onRequest', inst?.onRequest, req, reqCtx);
445
+ try {
446
+ const routeModule = await getCachedModule(matched.handler);
447
+ const reqMethod = req.method?.toUpperCase();
448
+ if (reqMethod) {
449
+ const handler = routeModule[reqMethod];
450
+ if (typeof handler === 'function') {
451
+ const result = await handler(req, {
452
+ params: params,
453
+ searchParams: searchParams,
454
+ });
455
+ const respCtx = createResponseContext(reqCtx, getResponseStatus(event) || 200);
456
+ await safeExecuteHook('onResponseEnd', inst?.onResponseEnd, req, respCtx);
457
+ return result;
458
+ }
459
+ throw new Error(`Method ${reqMethod} not implemented in ${matched.handler.src}`);
403
460
  }
404
- throw new Error(`Method ${reqMethod} not implemented in ${matched.handler.src}`);
461
+ throw new Error(`Unsupported request method: ${reqMethod}`);
462
+ }
463
+ catch (error) {
464
+ await safeExecuteHook('onRequestError', inst?.onRequestError, error, req, reqCtx);
465
+ throw error;
405
466
  }
406
- throw new Error(`Unsupported request method: ${reqMethod}`);
407
467
  }
468
+ const inst = getInstrumentation();
469
+ const reqCtx = createRequestContext(req, {
470
+ routePath: matched ? pathnamePart : '/not-found',
471
+ routeType: matched ? 'page' : 'not-found',
472
+ params,
473
+ searchParams,
474
+ });
475
+ await safeExecuteHook('onRequest', inst?.onRequest, req, reqCtx);
408
476
  let loading = false;
409
477
  let html = undefined;
410
478
  let meta = {
@@ -444,95 +512,97 @@ const handler = eventHandler(async (event) => {
444
512
  async start(controller) {
445
513
  const encoder = new TextEncoder();
446
514
  const push = (text) => controller.enqueue(encoder.encode(text));
515
+ let streamError = null;
447
516
  try {
448
- if (!matched) {
449
- try {
450
- const match = matchRoute(routeManifest, '/');
451
- const notFoundEntry = match.handler;
452
- if (!notFoundEntry) {
453
- throw new Error('No not-found page configured');
454
- }
455
- const { rendered, documentMeta, documentAssets, loaderData, } = await render({
456
- toRender: 'not-found',
457
- entry: notFoundEntry,
458
- routeParams: {},
459
- searchParams: {},
460
- req: req,
461
- pageOptions: {},
462
- cspNonce,
463
- });
464
- assets.push(...documentAssets);
465
- clientHydrationScript = `
517
+ try {
518
+ if (!matched) {
519
+ try {
520
+ const match = matchRoute(routeManifest, '/');
521
+ const notFoundEntry = match.handler;
522
+ if (!notFoundEntry) {
523
+ throw new Error('No not-found page configured');
524
+ }
525
+ const { rendered, documentMeta, documentAssets, loaderData, } = await render({
526
+ toRender: 'not-found',
527
+ entry: notFoundEntry,
528
+ routeParams: {},
529
+ searchParams: {},
530
+ req: req,
531
+ pageOptions: {},
532
+ cspNonce,
533
+ });
534
+ assets.push(...documentAssets);
535
+ clientHydrationScript = `
466
536
  <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
467
537
  import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
468
538
  main('/not-found/',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
469
539
  </script>
470
540
  `;
471
- html = rendered;
472
- meta = {
473
- ...meta,
474
- ...documentMeta,
475
- };
476
- setResponseStatus(404);
477
- }
478
- catch (e) {
479
- console.error('404 module not found:', e);
480
- setResponseStatus(404);
481
- push('Not Found');
482
- controller.close();
483
- return;
484
- }
485
- }
486
- else {
487
- const { options } = matched
488
- .mainPage.options
489
- ? await getCachedModule(matched.mainPage
490
- .options)
491
- : { options: {} };
492
- if (options?.responseHeaders) {
493
- const headers = options.responseHeaders;
494
- for (const [key, value] of Object.entries(headers)) {
495
- setHeader(key, value);
541
+ html = rendered;
542
+ meta = {
543
+ ...meta,
544
+ ...documentMeta,
545
+ };
546
+ setResponseStatus(404);
496
547
  }
497
- }
498
- try {
499
- if (!matched.loadingPage) {
500
- throw new Error('No loading page');
548
+ catch (e) {
549
+ console.error('404 module not found:', e);
550
+ setResponseStatus(404);
551
+ push('Not Found');
552
+ controller.close();
553
+ return;
501
554
  }
502
- const { rendered, documentMeta, documentAssets, loaderData, } = await render({
503
- toRender: 'loading',
504
- entry: matched,
505
- routeParams: params,
506
- searchParams,
507
- req: req,
508
- pageOptions: options,
509
- cspNonce,
510
- });
511
- const assetsHtml = assets
512
- .concat(documentAssets)
513
- .map((asset) => {
514
- const attributeString = Object.entries(asset.attrs)
515
- .map(([key, value]) => `${key}="${value}"`)
516
- .join(' ');
517
- if (asset.tag === 'script') {
518
- return `<script ${attributeString}></script>`;
519
- }
520
- if (asset.tag === 'link') {
521
- return `<link ${attributeString}>`;
555
+ }
556
+ else {
557
+ const { options } = matched
558
+ .mainPage.options
559
+ ? await getCachedModule(matched.mainPage
560
+ .options)
561
+ : { options: {} };
562
+ if (options?.responseHeaders) {
563
+ const headers = options.responseHeaders;
564
+ for (const [key, value] of Object.entries(headers)) {
565
+ setHeader(key, value);
522
566
  }
523
- if (asset.tag === 'style') {
524
- return `<style ${attributeString}>${asset.children || ''}</style>`;
567
+ }
568
+ try {
569
+ if (!matched.loadingPage) {
570
+ throw new Error('No loading page');
525
571
  }
526
- })
527
- .join('\n');
528
- const html = `
572
+ const { rendered, documentMeta, documentAssets, loaderData, } = await render({
573
+ toRender: 'loading',
574
+ entry: matched,
575
+ routeParams: params,
576
+ searchParams,
577
+ req: req,
578
+ pageOptions: options,
579
+ cspNonce,
580
+ });
581
+ const assetsHtml = assets
582
+ .concat(documentAssets)
583
+ .map((asset) => {
584
+ const attributeString = Object.entries(asset.attrs)
585
+ .map(([key, value]) => `${key}="${value}"`)
586
+ .join(' ');
587
+ if (asset.tag === 'script') {
588
+ return `<script ${attributeString}></script>`;
589
+ }
590
+ if (asset.tag === 'link') {
591
+ return `<link ${attributeString}>`;
592
+ }
593
+ if (asset.tag === 'style') {
594
+ return `<style ${attributeString}>${asset.children || ''}</style>`;
595
+ }
596
+ })
597
+ .join('\n');
598
+ const html = `
529
599
  <!doctype html>
530
600
  <html lang="en">
531
601
  <head>
532
602
  ${generateHtmlHead({
533
- ...meta,
534
- ...documentMeta,
535
- })}
603
+ ...meta,
604
+ ...documentMeta,
605
+ })}
536
606
  ${assetsHtml}
537
607
  ${hydrationScript({ nonce: cspNonce })}
538
608
  </head>
@@ -542,151 +612,171 @@ const handler = eventHandler(async (event) => {
542
612
  ${rendered}
543
613
  </html>
544
614
  `;
545
- push(html);
546
- push(`
615
+ push(html);
616
+ push(`
547
617
  <script type="module" data-hydration="loading" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
548
618
  import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
549
619
  main('${matched.loadingPage?.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
550
620
  </script>
551
621
  `);
552
- loading = true;
553
- }
554
- catch (e) {
555
- // skip
556
- }
557
- const { rendered, documentMeta, documentAssets, loaderData, } = await render({
558
- toRender: 'main',
559
- entry: matched,
560
- routeParams: params,
561
- searchParams,
562
- req: req,
563
- pageOptions: options,
564
- cspNonce,
565
- });
566
- assets.push(...documentAssets);
567
- clientHydrationScript = `
622
+ loading = true;
623
+ }
624
+ catch (e) {
625
+ // skip
626
+ }
627
+ const { rendered, documentMeta, documentAssets, loaderData, } = await render({
628
+ toRender: 'main',
629
+ entry: matched,
630
+ routeParams: params,
631
+ searchParams,
632
+ req: req,
633
+ pageOptions: options,
634
+ cspNonce,
635
+ });
636
+ assets.push(...documentAssets);
637
+ clientHydrationScript = `
568
638
  <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
569
639
  import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
570
640
  main('${matched.mainPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
571
641
  </script>
572
642
  `;
573
- html = rendered;
574
- meta = {
575
- ...meta,
576
- ...documentMeta,
577
- };
578
- setResponseStatus(200);
579
- }
580
- }
581
- catch (e1) {
582
- if (e1 instanceof RedirectError ||
583
- e1.name === 'RedirectError') {
584
- setHeader('Location', e1.message);
585
- setResponseStatus(302);
586
- controller.close();
587
- return;
588
- }
589
- if (import.meta.env.DEV) {
590
- console.error(e1);
643
+ html = rendered;
644
+ meta = {
645
+ ...meta,
646
+ ...documentMeta,
647
+ };
648
+ setResponseStatus(200);
649
+ }
591
650
  }
592
- try {
593
- const errorPage = matched
594
- .errorPage;
595
- if (!errorPage) {
596
- throw e1;
651
+ catch (e1) {
652
+ streamError = e1;
653
+ if (e1 instanceof RedirectError ||
654
+ e1.name === 'RedirectError') {
655
+ setHeader('Location', e1.message);
656
+ setResponseStatus(302);
657
+ controller.close();
658
+ return;
597
659
  }
598
- const { rendered, documentMeta, documentAssets, loaderData, } = await render({
599
- toRender: 'error',
600
- entry: matched,
601
- routeParams: params,
602
- searchParams,
603
- req: req,
604
- pageOptions: {},
605
- cspNonce,
606
- error: e1,
607
- });
608
- assets.push(...documentAssets);
609
- clientHydrationScript = `
660
+ if (import.meta.env.DEV) {
661
+ console.error(e1);
662
+ }
663
+ try {
664
+ const errorPage = matched
665
+ .errorPage;
666
+ if (!errorPage) {
667
+ throw e1;
668
+ }
669
+ const { rendered, documentMeta, documentAssets, loaderData, } = await render({
670
+ toRender: 'error',
671
+ entry: matched,
672
+ routeParams: params,
673
+ searchParams,
674
+ req: req,
675
+ pageOptions: {},
676
+ cspNonce,
677
+ error: e1,
678
+ });
679
+ assets.push(...documentAssets);
680
+ clientHydrationScript = `
610
681
  <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
611
682
  import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
612
683
  main('${errorPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
613
684
  </script>
614
685
  `;
615
- html = rendered;
616
- meta = {
617
- ...meta,
618
- ...documentMeta,
619
- };
620
- // statusCode = 500;
621
- setResponseStatus(500);
686
+ html = rendered;
687
+ meta = {
688
+ ...meta,
689
+ ...documentMeta,
690
+ };
691
+ // statusCode = 500;
692
+ setResponseStatus(500);
693
+ }
694
+ catch (e2) {
695
+ throw e1;
696
+ }
622
697
  }
623
- catch (e2) {
624
- throw e1;
698
+ if (loading) {
699
+ const assetsHtml = assets
700
+ .map((asset) => {
701
+ const attributeString = Object.entries(asset.attrs)
702
+ .map(([key, value]) => `${key}="${value}"`)
703
+ .join(' ');
704
+ if (asset.tag === 'link') {
705
+ return `<link ${attributeString}>`;
706
+ }
707
+ if (asset.tag === 'style') {
708
+ return `<style ${attributeString}>${asset.children || ''}</style>`;
709
+ }
710
+ return '';
711
+ })
712
+ .join('\n');
713
+ push(`<template id="__page_html__">${html}</template>`);
714
+ push(`
715
+ <script ${cspNonce ? `nonce="${cspNonce}"` : ''}>
716
+ const head = document.querySelector('head');
717
+ const scripts = Array.from(head.querySelectorAll('script'));
718
+ head.innerHTML = ${JSON.stringify(generateHtmlHead(meta) + assetsHtml)};
719
+ scripts.forEach(script => {
720
+ head.appendChild(script);
721
+ });
722
+ document.querySelector('script[data-hydration="loading"]')?.remove();
723
+ const loading = document.querySelector('body');
724
+ const template = document.getElementById('__page_html__');
725
+ loading.innerHTML = template.innerHTML;
726
+ template.remove();
727
+ </script>
728
+ `);
729
+ push(manifestHtml);
730
+ push(clientHydrationScript);
731
+ controller.close();
732
+ return;
625
733
  }
626
- }
627
- if (loading) {
628
734
  const assetsHtml = assets
629
735
  .map((asset) => {
630
736
  const attributeString = Object.entries(asset.attrs)
631
737
  .map(([key, value]) => `${key}="${value}"`)
632
738
  .join(' ');
739
+ if (asset.tag === 'script') {
740
+ return `<script ${attributeString} ${cspNonce ? `nonce="${cspNonce}"` : ''}></script>`;
741
+ }
633
742
  if (asset.tag === 'link') {
634
743
  return `<link ${attributeString}>`;
635
744
  }
636
745
  if (asset.tag === 'style') {
637
746
  return `<style ${attributeString}>${asset.children || ''}</style>`;
638
747
  }
639
- return '';
640
748
  })
641
749
  .join('\n');
642
- push(`<template id="__page_html__">${html}</template>`);
643
- push(`
644
- <script ${cspNonce ? `nonce="${cspNonce}"` : ''}>
645
- const head = document.querySelector('head');
646
- const scripts = Array.from(head.querySelectorAll('script'));
647
- head.innerHTML = \`${generateHtmlHead(meta) + assetsHtml}\`;
648
- scripts.forEach(script => {
649
- head.appendChild(script);
650
- });
651
- document.querySelector('script[data-hydration="loading"]')?.remove();
652
- const loading = document.querySelector('body');
653
- const template = document.getElementById('__page_html__');
654
- loading.innerHTML = template.innerHTML;
655
- template.remove();
656
- </script>
657
- `);
658
- push(manifestHtml);
659
- push(clientHydrationScript);
750
+ const transformHtml = template
751
+ .replace('<!--app-head-->', `${generateHtmlHead(meta)}\n${assetsHtml}\n${hydrationScript({ nonce: cspNonce })}`)
752
+ .replace('<!--app-body-->', (html ?? '') + manifestHtml + clientHydrationScript);
753
+ push(transformHtml);
660
754
  controller.close();
661
755
  return;
662
756
  }
663
- const assetsHtml = assets
664
- .map((asset) => {
665
- const attributeString = Object.entries(asset.attrs)
666
- .map(([key, value]) => `${key}="${value}"`)
667
- .join(' ');
668
- if (asset.tag === 'script') {
669
- return `<script ${attributeString} ${cspNonce ? `nonce="${cspNonce}"` : ''}></script>`;
670
- }
671
- if (asset.tag === 'link') {
672
- return `<link ${attributeString}>`;
673
- }
674
- if (asset.tag === 'style') {
675
- return `<style ${attributeString}>${asset.children || ''}</style>`;
757
+ catch (error) {
758
+ streamError = streamError ?? error;
759
+ throw error;
760
+ }
761
+ finally {
762
+ const statusCode = getResponseStatus(event) || 200;
763
+ const respCtx = createResponseContext(reqCtx, statusCode);
764
+ await safeExecuteHook('onResponseEnd', inst?.onResponseEnd, req, respCtx);
765
+ if (streamError) {
766
+ await safeExecuteHook('onRequestError', inst?.onRequestError, streamError, req, reqCtx);
676
767
  }
677
- })
678
- .join('\n');
679
- const transformHtml = template
680
- .replace('<!--app-head-->', `${generateHtmlHead(meta)}\n${assetsHtml}\n${hydrationScript({ nonce: cspNonce })}`)
681
- .replace('<!--app-body-->', (html ?? '') + manifestHtml + clientHydrationScript);
682
- push(transformHtml);
683
- controller.close();
684
- return;
768
+ }
685
769
  },
686
770
  });
687
771
  return stream;
688
772
  }
689
773
  catch (e) {
774
+ if (e instanceof RedirectError || e.name === 'RedirectError') {
775
+ return new Response('', {
776
+ status: 302,
777
+ headers: { Location: e.message },
778
+ });
779
+ }
690
780
  console.error(e);
691
781
  return new Response('Internal Server Error', { status: 500 });
692
782
  }