satset-react 0.2.6 → 0.2.7

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.
@@ -13,6 +13,7 @@ const env_1 = require("./env");
13
13
  const error_overlay_1 = require("./error-overlay");
14
14
  const bundler_1 = require("./bundler");
15
15
  const response_1 = require("./response");
16
+ const storage_1 = require("./storage");
16
17
  const translation_1 = require("../core/translation");
17
18
  const util_1 = __importDefault(require("util"));
18
19
  const tui_1 = require("./tui");
@@ -235,10 +236,32 @@ async function startDevServer(config = {}) {
235
236
  // Create HTTP server
236
237
  const server = http_1.default.createServer(async (req, res) => {
237
238
  const url = req.url || '/';
238
- const rawPath = url.split('?')[0].split('#')[0] || '/';
239
- const effectivePath = stripLocaleFromPath(rawPath);
239
+ // Parse cookies early for redirection
240
240
  const cookieStore = parseRequestCookies(req);
241
241
  (0, response_1.setCurrentRequestCookies)(cookieStore);
242
+ const rawPath = url.split('?')[0].split('#')[0] || '/';
243
+ // Virtual Routing / Locale Detection
244
+ let locale = 'en-US';
245
+ let effectivePath = rawPath;
246
+ const segments = rawPath.split('/').filter(Boolean);
247
+ const firstSegment = segments[0];
248
+ // 1. Detect from URL (Virtual Path)
249
+ if (firstSegment && /^[a-zA-Z]{2}(?:-[a-zA-Z]{2})?$/.test(firstSegment)) {
250
+ locale = firstSegment;
251
+ effectivePath = stripLocaleFromPath(rawPath);
252
+ }
253
+ else {
254
+ // 2. Detect from Cookie / Header
255
+ if (cookieStore && cookieStore['SATSET_LANG']) {
256
+ locale = cookieStore['SATSET_LANG'];
257
+ }
258
+ else if (req.headers['accept-language']) {
259
+ const accept = req.headers['accept-language'].split(',')[0].trim();
260
+ const match = accept.match(/^[a-zA-Z]{2}(?:-[a-zA-Z]{2})?/);
261
+ if (match)
262
+ locale = match[0];
263
+ }
264
+ }
242
265
  // Serve bundled JS files
243
266
  if (url.startsWith('/_satset/')) {
244
267
  // Remove query parameters from the URL before resolving file path
@@ -414,7 +437,7 @@ async function startDevServer(config = {}) {
414
437
  }
415
438
  const apiMatch = (0, file_system_1.matchRoute)(effectivePath, apiRoutes);
416
439
  if (apiMatch) {
417
- await handleApiRoute(apiMatch.route, req, res, root, tempDir, apiMatch.params);
440
+ await handleApiRoute(apiMatch.route, req, res, root, tempDir, apiMatch.params, locale);
418
441
  return;
419
442
  }
420
443
  if (effectivePath.startsWith('/api/')) {
@@ -424,13 +447,13 @@ async function startDevServer(config = {}) {
424
447
  }
425
448
  const matched = (0, file_system_1.matchRoute)(effectivePath, routes);
426
449
  if (matched) {
427
- await handlePageRoute(matched.route, req, res, root, tempDir, matched.params, routes, publicDir);
450
+ await handlePageRoute(matched.route, req, res, root, tempDir, matched.params, routes, publicDir, locale);
428
451
  return;
429
452
  }
430
453
  const notFoundRoute = routes.find(r => r.path === '/404') ||
431
454
  routes.find(r => r.path === '/not-found');
432
455
  if (notFoundRoute) {
433
- await handlePageRoute(notFoundRoute, req, res, root, tempDir, {}, routes, publicDir);
456
+ await handlePageRoute(notFoundRoute, req, res, root, tempDir, {}, routes, publicDir, locale);
434
457
  return;
435
458
  }
436
459
  res.writeHead(404, { 'Content-Type': 'text/html' });
@@ -534,126 +557,124 @@ async function buildClientBundle(root, outdir, routes) {
534
557
  if (layoutRoute && layoutRoute.component) {
535
558
  const layoutRelative = path_1.default.relative(root, layoutRoute.component).replace(/\\/g, '/');
536
559
  layoutImport = `import Layout from '../../${layoutRelative}';`;
537
- appCreationBlock = `
538
- const pageElement = React.createElement(React.Suspense, { fallback: null }, React.createElement(PageComponent, props));
539
- const withLayout = React.createElement(Layout, null, pageElement);
540
- const App = React.createElement(
541
- I18nProvider,
542
- {
543
- initialLocale,
544
- dictionaries: window.__SATSET_DICTIONARIES__
545
- },
546
- withLayout
560
+ appCreationBlock = `
561
+ const pageElement = React.createElement(React.Suspense, { fallback: null }, React.createElement(PageComponent, props));
562
+ const withLayout = React.createElement(Layout, null, pageElement);
563
+ const App = React.createElement(
564
+ I18nProvider,
565
+ {
566
+ initialLocale,
567
+ dictionaries: window.__SATSET_DICTIONARIES__
568
+ },
569
+ withLayout
547
570
  );`;
548
571
  }
549
572
  else {
550
- appCreationBlock = `
551
- const App = React.createElement(
552
- I18nProvider,
553
- {
554
- initialLocale,
555
- dictionaries: window.__SATSET_DICTIONARIES__
556
- },
557
- React.createElement(React.Suspense, { fallback: null }, React.createElement(PageComponent, props))
573
+ appCreationBlock = `
574
+ const App = React.createElement(
575
+ I18nProvider,
576
+ {
577
+ initialLocale,
578
+ dictionaries: window.__SATSET_DICTIONARIES__
579
+ },
580
+ React.createElement(React.Suspense, { fallback: null }, React.createElement(PageComponent, props))
558
581
  );`;
559
582
  }
560
- const entryContent = `
561
- import React from 'react';
562
- import { hydrateRoot, createRoot } from 'react-dom/client';
563
- import { I18nProvider } from 'satset-react';
564
- ${layoutImport}
565
-
566
- // Import all page components
567
- ${importLines.join('\n')}
568
- // Route definitions
569
- const routeDefs = [
570
- ${routeDefLines.join('\n')}
571
- ];
572
-
573
- function stripLocale(pathname) {
574
- if (!pathname) return '/';
575
- const raw = pathname.split('?')[0].split('#')[0];
576
- const segments = raw.split('/').filter(Boolean);
577
- if (!segments.length) return '/';
578
- const first = segments[0];
579
- const localePattern = /^[a-zA-Z]{2}(?:-[a-zA-Z]{2})?$/;
580
- if (localePattern.test(first)) {
581
- const rest = segments.slice(1);
582
- return rest.length ? '/' + rest.join('/') : '/';
583
- }
584
- return raw.startsWith('/') ? raw : '/' + raw;
585
- }
586
-
587
- function matchPath(pathname) {
588
- const normalized = stripLocale(pathname);
589
- const pathSegments = normalized.split('/').filter(Boolean);
590
- for (const r of routeDefs) {
591
- const routeSegments = r.path.split('/').filter(Boolean);
592
-
593
- // catch-all
594
- if (r.path.includes('*')) {
595
- const catchIndex = routeSegments.findIndex(s => s.startsWith('*'));
596
- const paramName = routeSegments[catchIndex].slice(1);
597
- const params = {};
598
- params[paramName] = pathSegments.slice(catchIndex).join('/');
599
- return { component: r.component, params };
600
- }
601
-
602
- if (routeSegments.length !== pathSegments.length) continue;
603
-
604
- let matched = true;
605
- const params = {};
606
-
607
- for (let i = 0; i < routeSegments.length; i++) {
608
- const rs = routeSegments[i];
609
- const ps = pathSegments[i];
610
-
611
- if (rs.startsWith(':')) {
612
- params[rs.slice(1)] = ps;
613
- } else if (rs !== ps) {
614
- matched = false;
615
- break;
616
- }
617
- }
618
-
619
- if (matched) {
620
- return { component: r.component, params };
621
- }
622
- }
623
-
624
- // fallback to root route
625
- return { component: routeDefs.find(r => r.path === '/')?.component, params: {} };
626
- }
627
-
628
- // Hydrate based on current path
629
- const currentPath = stripLocale(window.location.pathname);
630
- const match = matchPath(currentPath);
631
- // expose routes and params to Router
632
- window.__SATSET_ROUTES__ = routeDefs.map(r => r.path);
633
- window.__SATSET_PARAMS__ = match.params || {};
634
- window.__SATSET_DICTIONARIES__ = ${JSON.stringify(dictionaries)};
635
-
636
- const PageComponent = match.component;
637
-
638
- if (PageComponent) {
639
- const root = document.getElementById('root');
640
- if (root) {
641
- const props = match.params ? { params: match.params } : undefined;
642
-
643
- const pathSegments = window.location.pathname.split('/').filter(Boolean);
644
- const firstSegment = pathSegments[0];
645
- const initialLocale = (firstSegment && /^[a-zA-Z]{2}(?:-[a-zA-Z]{2})?$/.test(firstSegment)) ? firstSegment : 'en-US';
646
-
647
- ${appCreationBlock}
648
-
649
- if (root.hasChildNodes()) {
650
- hydrateRoot(root, App);
651
- } else {
652
- const rootInstance = createRoot(root);
653
- rootInstance.render(App);
654
- }
655
- }
656
- }
583
+ const entryContent = `
584
+ import React from 'react';
585
+ import { hydrateRoot, createRoot } from 'react-dom/client';
586
+ import { I18nProvider } from 'satset-react';
587
+ ${layoutImport}
588
+
589
+ // Import all page components
590
+ ${importLines.join('\n')}
591
+ // Route definitions
592
+ const routeDefs = [
593
+ ${routeDefLines.join('\n')}
594
+ ];
595
+
596
+ function stripLocale(pathname) {
597
+ if (!pathname) return '/';
598
+ const raw = pathname.split('?')[0].split('#')[0];
599
+ const segments = raw.split('/').filter(Boolean);
600
+ if (!segments.length) return '/';
601
+ const first = segments[0];
602
+ const localePattern = /^[a-zA-Z]{2}(?:-[a-zA-Z]{2})?$/;
603
+ if (localePattern.test(first)) {
604
+ const rest = segments.slice(1);
605
+ return rest.length ? '/' + rest.join('/') : '/';
606
+ }
607
+ return raw.startsWith('/') ? raw : '/' + raw;
608
+ }
609
+
610
+ function matchPath(pathname) {
611
+ const normalized = stripLocale(pathname);
612
+ const pathSegments = normalized.split('/').filter(Boolean);
613
+ for (const r of routeDefs) {
614
+ const routeSegments = r.path.split('/').filter(Boolean);
615
+
616
+ // catch-all
617
+ if (r.path.includes('*')) {
618
+ const catchIndex = routeSegments.findIndex(s => s.startsWith('*'));
619
+ const paramName = routeSegments[catchIndex].slice(1);
620
+ const params = {};
621
+ params[paramName] = pathSegments.slice(catchIndex).join('/');
622
+ return { component: r.component, params };
623
+ }
624
+
625
+ if (routeSegments.length !== pathSegments.length) continue;
626
+
627
+ let matched = true;
628
+ const params = {};
629
+
630
+ for (let i = 0; i < routeSegments.length; i++) {
631
+ const rs = routeSegments[i];
632
+ const ps = pathSegments[i];
633
+
634
+ if (rs.startsWith(':')) {
635
+ params[rs.slice(1)] = ps;
636
+ } else if (rs !== ps) {
637
+ matched = false;
638
+ break;
639
+ }
640
+ }
641
+
642
+ if (matched) {
643
+ return { component: r.component, params };
644
+ }
645
+ }
646
+
647
+ // fallback to root route
648
+ return { component: routeDefs.find(r => r.path === '/')?.component, params: {} };
649
+ }
650
+
651
+ // Hydrate based on current path
652
+ const currentPath = stripLocale(window.location.pathname);
653
+ const match = matchPath(currentPath);
654
+ // expose routes and params to Router
655
+ window.__SATSET_ROUTES__ = routeDefs.map(r => r.path);
656
+ window.__SATSET_PARAMS__ = match.params || {};
657
+ window.__SATSET_DICTIONARIES__ = ${JSON.stringify(dictionaries)};
658
+
659
+ const PageComponent = match.component;
660
+
661
+ if (PageComponent) {
662
+ const root = document.getElementById('root');
663
+ if (root) {
664
+ const props = match.params ? { params: match.params } : undefined;
665
+
666
+ const initialLocale = window.__SATSET_LOCALE__ || 'en-US';
667
+
668
+ ${appCreationBlock}
669
+
670
+ if (root.hasChildNodes()) {
671
+ hydrateRoot(root, App);
672
+ } else {
673
+ const rootInstance = createRoot(root);
674
+ rootInstance.render(App);
675
+ }
676
+ }
677
+ }
657
678
  `;
658
679
  const entryPath = path_1.default.join(outdir, '_entry.tsx');
659
680
  fs_1.default.writeFileSync(entryPath, entryContent);
@@ -697,745 +718,828 @@ ${appCreationBlock}
697
718
  console.error(`❌ ERROR [${formatTime()}] Client bundle failed:`, error && error.message ? error.message : String(error));
698
719
  }
699
720
  }
700
- async function handleApiRoute(route, req, res, root, tempDir, params = {}) {
701
- try {
702
- // Compile API module to CJS before requiring to support TS/ESM sources.
703
- // Cache the compiled file between requests and only re-bundle when the
704
- // source file is newer. Use a per-route file name derived from the
705
- // component's relative path to avoid collisions between different API
706
- // handlers that share the same base file name (e.g. multiple `route.ts`).
707
- const relComponentPath = path_1.default
708
- .relative(root, route.component)
709
- .replace(/\\/g, '/')
710
- .replace(/\.[^.]+$/, '');
711
- const safeComponentKey = relComponentPath.replace(/[^a-zA-Z0-9_-]/g, '_');
712
- const compiledApiPath = path_1.default.join(tempDir, safeComponentKey + '.api.server.js');
713
- console.log('[API Debug] route:', route.path, 'component:', route.component);
714
- console.log('[API Debug] safeKey:', safeComponentKey, 'compiledPath:', compiledApiPath);
715
- let ApiModule = null;
716
- async function safeBundleApi(entry, outfile) {
717
- try {
718
- await bundler_1.bundler.bundleServer({ entryPoint: entry, outfile, minify: false, root, sourcemap: true });
721
+ async function handleApiRoute(route, req, res, root, tempDir, params = {}, locale = 'en-US') {
722
+ const dictionaries = getDictionaries(root);
723
+ return storage_1.requestContext.run({ locale, dictionaries, params, pathname: req.url || '/' }, async () => {
724
+ try {
725
+ // Compile API module to CJS before requiring to support TS/ESM sources.
726
+ // Cache the compiled file between requests and only re-bundle when the
727
+ // source file is newer. Use a per-route file name derived from the
728
+ // component's relative path to avoid collisions between different API
729
+ // handlers that share the same base file name (e.g. multiple `route.ts`).
730
+ const relComponentPath = path_1.default
731
+ .relative(root, route.component)
732
+ .replace(/\\/g, '/')
733
+ .replace(/\.[^.]+$/, '');
734
+ const safeComponentKey = relComponentPath.replace(/[^a-zA-Z0-9_-]/g, '_');
735
+ const compiledApiPath = path_1.default.join(tempDir, safeComponentKey + '.api.server.js');
736
+ console.log('[API Debug] route:', route.path, 'component:', route.component);
737
+ console.log('[API Debug] safeKey:', safeComponentKey, 'compiledPath:', compiledApiPath);
738
+ let ApiModule = null;
739
+ async function safeBundleApi(entry, outfile) {
740
+ try {
741
+ await bundler_1.bundler.bundleServer({ entryPoint: entry, outfile, minify: false, root, sourcemap: true });
742
+ }
743
+ catch (err) {
744
+ const msg = String(err && err.message ? err.message : err);
745
+ if (/ENOSPC|not enough space|There is not enough space/i.test(msg)) {
746
+ try {
747
+ if (fs_1.default.existsSync(tempDir)) {
748
+ fs_1.default.rmSync(tempDir, { recursive: true, force: true });
749
+ }
750
+ fs_1.default.mkdirSync(tempDir, { recursive: true });
751
+ }
752
+ catch (cleanupErr) {
753
+ console.error('Failed to cleanup temp dir:', cleanupErr);
754
+ }
755
+ await bundler_1.bundler.bundleServer({ entryPoint: entry, outfile, minify: false, root, sourcemap: false });
756
+ return;
757
+ }
758
+ throw err;
759
+ }
719
760
  }
720
- catch (err) {
721
- const msg = String(err && err.message ? err.message : err);
722
- if (/ENOSPC|not enough space|There is not enough space/i.test(msg)) {
761
+ try {
762
+ let needsBundle = true;
763
+ if (fs_1.default.existsSync(compiledApiPath)) {
723
764
  try {
724
- if (fs_1.default.existsSync(tempDir)) {
725
- fs_1.default.rmSync(tempDir, { recursive: true, force: true });
765
+ const srcStat = fs_1.default.statSync(route.component);
766
+ const outStat = fs_1.default.statSync(compiledApiPath);
767
+ if (outStat.mtimeMs >= srcStat.mtimeMs) {
768
+ needsBundle = false;
726
769
  }
727
- fs_1.default.mkdirSync(tempDir, { recursive: true });
728
- }
729
- catch (cleanupErr) {
730
- console.error('Failed to cleanup temp dir:', cleanupErr);
731
770
  }
732
- await bundler_1.bundler.bundleServer({ entryPoint: entry, outfile, minify: false, root, sourcemap: false });
733
- return;
771
+ catch (e) { }
772
+ }
773
+ if (needsBundle) {
774
+ await safeBundleApi(route.component, compiledApiPath);
734
775
  }
735
- throw err;
736
- }
737
- }
738
- try {
739
- let needsBundle = true;
740
- if (fs_1.default.existsSync(compiledApiPath)) {
741
776
  try {
742
- const srcStat = fs_1.default.statSync(route.component);
743
- const outStat = fs_1.default.statSync(compiledApiPath);
744
- if (outStat.mtimeMs >= srcStat.mtimeMs) {
745
- needsBundle = false;
746
- }
777
+ delete require.cache[require.resolve(compiledApiPath)];
747
778
  }
748
779
  catch (e) { }
780
+ ApiModule = require(compiledApiPath);
749
781
  }
750
- if (needsBundle) {
751
- await safeBundleApi(route.component, compiledApiPath);
752
- }
753
- try {
754
- delete require.cache[require.resolve(compiledApiPath)];
782
+ catch (err) {
783
+ console.error('API bundling failed for', route.component, err);
784
+ const errMsg = String(err && err.message ? err.message : err);
785
+ const message = /ENOSPC|not enough space|There is not enough space/i.test(errMsg)
786
+ ? 'API bundling failed: not enough disk space. Free disk space or delete .satset/temp'
787
+ : 'API bundling failed';
788
+ res.writeHead(500, { 'Content-Type': 'application/json' });
789
+ res.end(JSON.stringify({ error: { code: 500, message }, file: route.component }));
790
+ return;
755
791
  }
756
- catch (e) { }
757
- ApiModule = require(compiledApiPath);
758
- }
759
- catch (err) {
760
- console.error('API bundling failed for', route.component, err);
761
- const errMsg = String(err && err.message ? err.message : err);
762
- const message = /ENOSPC|not enough space|There is not enough space/i.test(errMsg)
763
- ? 'API bundling failed: not enough disk space. Free disk space or delete .satset/temp'
764
- : 'API bundling failed';
765
- res.writeHead(500, { 'Content-Type': 'application/json' });
766
- res.end(JSON.stringify({ error: { code: 500, message }, file: route.component }));
767
- return;
768
- }
769
- function resolveHandler(mod) {
770
- if (!mod)
771
- return null;
772
- // default export function
773
- if (typeof mod === 'function')
774
- return { default: mod };
775
- if (mod && typeof mod === 'object') {
776
- if (typeof mod.default === 'function')
777
- return { default: mod.default };
778
- // method-named exports (GET, POST, etc.)
779
- const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
780
- const out = {};
781
- for (const m of methods) {
782
- if (typeof mod[m] === 'function')
783
- out[m] = mod[m];
792
+ function resolveHandler(mod) {
793
+ if (!mod)
794
+ return null;
795
+ // default export function
796
+ if (typeof mod === 'function')
797
+ return { default: mod };
798
+ if (mod && typeof mod === 'object') {
799
+ if (typeof mod.default === 'function')
800
+ return { default: mod.default };
801
+ // method-named exports (GET, POST, etc.)
802
+ const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
803
+ const out = {};
804
+ for (const m of methods) {
805
+ if (typeof mod[m] === 'function')
806
+ out[m] = mod[m];
807
+ }
808
+ if (Object.keys(out).length)
809
+ return out;
784
810
  }
785
- if (Object.keys(out).length)
786
- return out;
811
+ return null;
787
812
  }
788
- return null;
789
- }
790
- const handlerObj = resolveHandler(ApiModule);
791
- const method = (req.method || 'GET').toUpperCase();
792
- async function callHandlerWithNodeStyle(fn) {
793
- // If function declares at least two args, assume Node-style (req, res)
794
- if (fn.length >= 2) {
795
- const maybe = await fn(req, res, params);
796
- // If the handler returns a SatsetResponse, send it
797
- if (response_1.SatsetResponse.isSatsetResponse(maybe)) {
798
- (0, response_1.sendSatsetResponse)(res, maybe);
813
+ const handlerObj = resolveHandler(ApiModule);
814
+ const method = (req.method || 'GET').toUpperCase();
815
+ async function callHandlerWithNodeStyle(fn) {
816
+ // If function declares at least two args, assume Node-style (req, res)
817
+ if (fn.length >= 2) {
818
+ // Inject locale into request object for Node-style handlers
819
+ req.locale = locale;
820
+ req.lang = locale;
821
+ const maybe = await fn(req, res, params);
822
+ // If the handler returns a SatsetResponse, send it
823
+ if (response_1.SatsetResponse.isSatsetResponse(maybe)) {
824
+ (0, response_1.sendSatsetResponse)(res, maybe);
825
+ }
826
+ // assume the handler handled the Node res
827
+ return true;
799
828
  }
800
- // assume the handler handled the Node res
801
- return true;
802
- }
803
- return false;
804
- }
805
- async function callHandlerWithWebStyle(fn) {
806
- // Build a lightweight Request-like object for route handlers
807
- const webReq = await (0, response_1.buildSatsetRequest)(req);
808
- const result = await fn(webReq, { params });
809
- if (response_1.SatsetResponse.isSatsetResponse(result)) {
810
- (0, response_1.sendSatsetResponse)(res, result);
811
- return true;
829
+ return false;
812
830
  }
813
- // If result is a native Response-like object with json/text, try to handle
814
- if (result && typeof result === 'object' && typeof result.status === 'number' && (result.headers || result.json)) {
815
- // Try basic mapping
816
- const status = result.status || 200;
817
- // Normalize headers: support Headers instance, array of tuples, or plain object
818
- let headers = {};
819
- const rawHeaders = result.headers;
820
- if (rawHeaders) {
821
- if (typeof rawHeaders.get === 'function' && typeof rawHeaders.entries === 'function') {
822
- headers = Object.fromEntries(rawHeaders.entries());
831
+ async function callHandlerWithWebStyle(fn) {
832
+ // Build a lightweight Request-like object for route handlers
833
+ const webReq = await (0, response_1.buildSatsetRequest)(req);
834
+ // Inject locale into context
835
+ const context = { params, locale, lang: locale };
836
+ const result = await fn(webReq, context);
837
+ if (response_1.SatsetResponse.isSatsetResponse(result)) {
838
+ (0, response_1.sendSatsetResponse)(res, result);
839
+ return true;
840
+ }
841
+ // If result is a native Response-like object with json/text, try to handle
842
+ if (result && typeof result === 'object' && typeof result.status === 'number' && (result.headers || result.json)) {
843
+ // Try basic mapping
844
+ const status = result.status || 200;
845
+ // Normalize headers: support Headers instance, array of tuples, or plain object
846
+ let headers = {};
847
+ const rawHeaders = result.headers;
848
+ if (rawHeaders) {
849
+ if (typeof rawHeaders.get === 'function' && typeof rawHeaders.entries === 'function') {
850
+ headers = Object.fromEntries(rawHeaders.entries());
851
+ }
852
+ else if (Array.isArray(rawHeaders)) {
853
+ headers = Object.fromEntries(rawHeaders);
854
+ }
855
+ else if (typeof rawHeaders === 'object') {
856
+ headers = rawHeaders;
857
+ }
823
858
  }
824
- else if (Array.isArray(rawHeaders)) {
825
- headers = Object.fromEntries(rawHeaders);
859
+ let body = undefined;
860
+ try {
861
+ if (typeof result.json === 'function') {
862
+ body = await result.json();
863
+ res.writeHead(status, { 'Content-Type': 'application/json', ...headers });
864
+ res.end(JSON.stringify(body));
865
+ return true;
866
+ }
867
+ if (typeof result.text === 'function') {
868
+ body = await result.text();
869
+ res.writeHead(status, headers);
870
+ res.end(body);
871
+ return true;
872
+ }
826
873
  }
827
- else if (typeof rawHeaders === 'object') {
828
- headers = rawHeaders;
874
+ catch (e) {
875
+ // fallthrough
829
876
  }
830
877
  }
831
- let body = undefined;
832
- try {
833
- if (typeof result.json === 'function') {
834
- body = await result.json();
835
- res.writeHead(status, { 'Content-Type': 'application/json', ...headers });
836
- res.end(JSON.stringify(body));
837
- return true;
838
- }
839
- if (typeof result.text === 'function') {
840
- body = await result.text();
841
- res.writeHead(status, headers);
842
- res.end(body);
843
- return true;
844
- }
878
+ // If result is an object, return as JSON
879
+ if (result && typeof result === 'object') {
880
+ const sat = response_1.SatsetResponse.json(result);
881
+ (0, response_1.sendSatsetResponse)(res, sat);
882
+ return true;
845
883
  }
846
- catch (e) {
847
- // fallthrough
884
+ // If result is string or number, send as text
885
+ if (result != null) {
886
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
887
+ res.end(String(result));
888
+ return true;
848
889
  }
890
+ // undefined -> assume handler will manage res; if not, leave to caller
891
+ return false;
849
892
  }
850
- // If result is an object, return as JSON
851
- if (result && typeof result === 'object') {
852
- const sat = response_1.SatsetResponse.json(result);
853
- (0, response_1.sendSatsetResponse)(res, sat);
854
- return true;
855
- }
856
- // If result is string or number, send as text
857
- if (result != null) {
858
- res.writeHead(200, { 'Content-Type': 'text/plain' });
859
- res.end(String(result));
860
- return true;
861
- }
862
- // undefined -> assume handler will manage res; if not, leave to caller
863
- return false;
864
- }
865
- // Prefer method-named export for App-style handlers
866
- if (handlerObj && typeof handlerObj[method] === 'function') {
867
- const fn = handlerObj[method];
868
- // Try node-style first
869
- const nodeTook = await callHandlerWithNodeStyle(fn);
870
- if (nodeTook)
893
+ // Prefer method-named export for App-style handlers
894
+ if (handlerObj && typeof handlerObj[method] === 'function') {
895
+ const fn = handlerObj[method];
896
+ // Try node-style first
897
+ const nodeTook = await callHandlerWithNodeStyle(fn);
898
+ if (nodeTook)
899
+ return;
900
+ // Fallback to Web-style
901
+ const webTook = await callHandlerWithWebStyle(fn);
902
+ if (webTook)
903
+ return;
904
+ // no response produced
905
+ res.writeHead(204);
906
+ res.end();
871
907
  return;
872
- // Fallback to Web-style
873
- const webTook = await callHandlerWithWebStyle(fn);
874
- if (webTook)
908
+ }
909
+ // Default export fallback
910
+ if (handlerObj && typeof handlerObj.default === 'function') {
911
+ const fn = handlerObj.default;
912
+ const nodeTook = await callHandlerWithNodeStyle(fn);
913
+ if (nodeTook)
914
+ return;
915
+ const webTook = await callHandlerWithWebStyle(fn);
916
+ if (webTook)
917
+ return;
918
+ res.writeHead(204);
919
+ res.end();
875
920
  return;
876
- // no response produced
877
- res.writeHead(204);
878
- res.end();
879
- return;
921
+ }
922
+ res.writeHead(405, { 'Content-Type': 'application/json' });
923
+ res.end(JSON.stringify({ error: { code: 405, message: 'Method not allowed' } }));
880
924
  }
881
- // Default export fallback
882
- if (handlerObj && typeof handlerObj.default === 'function') {
883
- const fn = handlerObj.default;
884
- const nodeTook = await callHandlerWithNodeStyle(fn);
885
- if (nodeTook)
886
- return;
887
- const webTook = await callHandlerWithWebStyle(fn);
888
- if (webTook)
889
- return;
890
- res.writeHead(204);
891
- res.end();
892
- return;
925
+ catch (error) {
926
+ const errorPayload = {
927
+ code: 500,
928
+ message: error && error.message ? error.message : 'Internal Server Error',
929
+ };
930
+ res.writeHead(500, { 'Content-Type': 'application/json' });
931
+ res.end(JSON.stringify({ error: errorPayload, file: route.component, stack: error && error.stack }));
893
932
  }
894
- res.writeHead(405, { 'Content-Type': 'application/json' });
895
- res.end(JSON.stringify({ error: { code: 405, message: 'Method not allowed' } }));
896
- }
897
- catch (error) {
898
- const errorPayload = {
899
- code: 500,
900
- message: error && error.message ? error.message : 'Internal Server Error',
901
- };
902
- res.writeHead(500, { 'Content-Type': 'application/json' });
903
- res.end(JSON.stringify({ error: errorPayload, file: route.component, stack: error && error.stack }));
904
- }
933
+ });
905
934
  }
906
- async function handlePageRoute(route, req, res, root, tempDir, initialParams = {}, allRoutes = [], publicDirName = 'public') {
907
- try {
908
- const env = (0, env_1.loadEnv)(root, 'development');
909
- const envScript = (0, env_1.getPublicEnvScript)(env.publicVars);
910
- if (isClientOnlyPage(route.component)) {
911
- const routePaths = allRoutes.length ? allRoutes.map(r => r.path) : [];
912
- let metaHtml = '';
913
- let htmlLang = 'en';
914
- let faviconLink = '';
915
- try {
916
- const urlSegments = req.url?.split('/').filter(Boolean) || [];
917
- const firstSegment = urlSegments[0];
918
- const locale = (firstSegment && /^[a-zA-Z]{2}(?:-[a-zA-Z]{2})?$/.test(firstSegment)) ? firstSegment : 'en-US';
919
- const dictionaries = getDictionaries(root);
935
+ async function handlePageRoute(route, req, res, root, tempDir, initialParams = {}, allRoutes = [], publicDirName = 'public', locale = 'en-US') {
936
+ const dictionaries = getDictionaries(root);
937
+ return storage_1.requestContext.run({ locale, dictionaries, params: initialParams, pathname: req.url || '/' }, async () => {
938
+ try {
939
+ const env = (0, env_1.loadEnv)(root, 'development');
940
+ const envScript = (0, env_1.getPublicEnvScript)(env.publicVars);
941
+ if (isClientOnlyPage(route.component)) {
942
+ const routePaths = allRoutes.length ? allRoutes.map(r => r.path) : [];
943
+ let metaHtml = '';
944
+ let htmlLang = locale;
945
+ let faviconLink = '';
920
946
  try {
921
- const { renderMetaTags } = await import('../assets/metadata.js');
922
- const baseName = path_1.default.basename(route.component).replace(/\.[^.]+$/, '');
923
- const compiledMetaPath = path_1.default.join(tempDir, baseName + '.meta.server.js');
947
+ // dictionaries already loaded in context
948
+ // Auto-inject hreflang tags for SEO
924
949
  try {
925
- await bundler_1.bundler.bundleServer({
926
- entryPoint: route.component,
927
- outfile: compiledMetaPath,
928
- minify: false,
929
- root,
930
- sourcemap: false,
931
- });
932
- try {
933
- delete require.cache[require.resolve(compiledMetaPath)];
934
- }
935
- catch (e) {
936
- }
937
- const PageModule = require(compiledMetaPath);
938
- let metaObj = null;
939
- if (PageModule && PageModule.metadata) {
940
- metaObj = PageModule.metadata;
941
- }
942
- else if (PageModule && typeof PageModule.getMetadata === 'function') {
943
- const t = (key, params) => {
944
- const dict = dictionaries[locale] || dictionaries['en-US'] || dictionaries[Object.keys(dictionaries)[0]] || {};
945
- let text = dict[key] || key;
946
- if (params) {
947
- Object.entries(params).forEach(([k, v]) => {
948
- text = text.replace(new RegExp(`{${k}}`, 'g'), v);
949
- });
950
+ const supportedLocales = Object.keys(dictionaries);
951
+ if (supportedLocales.length > 0) {
952
+ const host = req.headers.host || 'localhost';
953
+ // Determine protocol (assume http for dev, but honor x-forwarded-proto if present)
954
+ const proto = req.headers['x-forwarded-proto'] || 'http';
955
+ const origin = `${proto}://${host}`;
956
+ const urlObj = new URL(req.url || '/', origin);
957
+ const currentPath = urlObj.pathname;
958
+ // Determine clean path (strip locale prefix if present in URL)
959
+ let cleanPath = currentPath;
960
+ if (currentPath === `/${locale}` || currentPath.startsWith(`/${locale}/`)) {
961
+ cleanPath = currentPath.substring(locale.length + 1);
962
+ if (!cleanPath.startsWith('/'))
963
+ cleanPath = '/' + cleanPath;
964
+ }
965
+ supportedLocales.forEach(lang => {
966
+ let href = origin;
967
+ if (lang !== 'en-US') { // Assuming en-US is default/root
968
+ href += `/${lang}`;
950
969
  }
951
- return text;
952
- };
970
+ href += cleanPath === '/' ? '' : cleanPath;
971
+ metaHtml += `<link rel="alternate" hreflang="${lang}" href="${href}" />\n`;
972
+ });
973
+ // Add x-default pointing to default language (en-US)
974
+ const defaultHref = origin + (cleanPath === '/' ? '' : cleanPath);
975
+ metaHtml += `<link rel="alternate" hreflang="x-default" href="${defaultHref}" />\n`;
976
+ }
977
+ }
978
+ catch (e) {
979
+ // ignore hreflang generation errors
980
+ }
981
+ try {
982
+ const { renderMetaTags } = await import('../assets/metadata.js');
983
+ const baseName = path_1.default.basename(route.component).replace(/\.[^.]+$/, '');
984
+ const compiledMetaPath = path_1.default.join(tempDir, baseName + '.meta.server.js');
985
+ try {
986
+ await bundler_1.bundler.bundleServer({
987
+ entryPoint: route.component,
988
+ outfile: compiledMetaPath,
989
+ minify: false,
990
+ root,
991
+ sourcemap: false,
992
+ });
953
993
  try {
954
- metaObj = await PageModule.getMetadata({ params: initialParams, locale, t });
994
+ delete require.cache[require.resolve(compiledMetaPath)];
955
995
  }
956
996
  catch (e) {
957
- metaObj = null;
958
997
  }
998
+ const PageModule = require(compiledMetaPath);
999
+ let metaObj = null;
1000
+ if (PageModule && PageModule.metadata) {
1001
+ metaObj = PageModule.metadata;
1002
+ }
1003
+ else if (PageModule && typeof PageModule.getMetadata === 'function') {
1004
+ const t = (key, params) => {
1005
+ const dict = dictionaries[locale] || dictionaries['en-US'] || dictionaries[Object.keys(dictionaries)[0]] || {};
1006
+ let text = dict[key] || key;
1007
+ if (params) {
1008
+ Object.entries(params).forEach(([k, v]) => {
1009
+ text = text.replace(new RegExp(`{${k}}`, 'g'), v);
1010
+ });
1011
+ }
1012
+ return text;
1013
+ };
1014
+ try {
1015
+ metaObj = await PageModule.getMetadata({ params: initialParams, locale, t });
1016
+ }
1017
+ catch (e) {
1018
+ metaObj = null;
1019
+ }
1020
+ }
1021
+ if (metaObj && typeof metaObj.lang === 'string' && metaObj.lang.trim()) {
1022
+ htmlLang = metaObj.lang.trim();
1023
+ }
1024
+ metaHtml = renderMetaTags(metaObj);
959
1025
  }
960
- if (metaObj && typeof metaObj.lang === 'string' && metaObj.lang.trim()) {
961
- htmlLang = metaObj.lang.trim();
1026
+ catch (e) {
962
1027
  }
963
- metaHtml = renderMetaTags(metaObj);
964
1028
  }
965
1029
  catch (e) {
966
1030
  }
967
- }
968
- catch (e) {
969
- }
970
- try {
971
- let computedFavicon = null;
972
- const publicPath = path_1.default.join(root, publicDirName);
973
- if (fs_1.default.existsSync(path_1.default.join(publicPath, 'favicon.ico')))
974
- computedFavicon = '/favicon.ico';
975
- else if (fs_1.default.existsSync(path_1.default.join(publicPath, 'favicon.png')))
976
- computedFavicon = '/favicon.png';
977
- if (computedFavicon) {
978
- faviconLink = `<link rel="icon" href="${computedFavicon}" />`;
1031
+ try {
1032
+ let computedFavicon = null;
1033
+ const publicPath = path_1.default.join(root, publicDirName);
1034
+ if (fs_1.default.existsSync(path_1.default.join(publicPath, 'favicon.ico')))
1035
+ computedFavicon = '/favicon.ico';
1036
+ else if (fs_1.default.existsSync(path_1.default.join(publicPath, 'favicon.png')))
1037
+ computedFavicon = '/favicon.png';
1038
+ if (computedFavicon) {
1039
+ faviconLink = `<link rel="icon" href="${computedFavicon}" />`;
1040
+ }
1041
+ }
1042
+ catch (e) {
979
1043
  }
980
1044
  }
981
1045
  catch (e) {
982
1046
  }
983
- }
984
- catch (e) {
985
- }
986
- const initialParamsScript = `<script>
987
- window.__SATSET_ROUTES__ = ${JSON.stringify(routePaths)};
988
- window.__SATSET_PARAMS__ = ${JSON.stringify(initialParams)};
989
- window.__SATSET_DICTIONARIES__ = ${JSON.stringify(getDictionaries(root))};
1047
+ const initialParamsScript = `<script>
1048
+ window.__SATSET_ROUTES__ = ${JSON.stringify(routePaths)};
1049
+ window.__SATSET_PARAMS__ = ${JSON.stringify(initialParams)};
1050
+ window.__SATSET_DICTIONARIES__ = ${JSON.stringify(getDictionaries(root))};
1051
+ window.__SATSET_LOCALE__ = "${locale}";
990
1052
  </script>`;
991
- const html = `
992
- <!DOCTYPE html>
993
- <html lang="${htmlLang}">
994
- <head>
995
- <meta charset="UTF-8" />
996
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
997
- ${metaHtml}
998
- ${faviconLink}
999
- <link rel="stylesheet" href="/_satset/globals.css" />
1000
- </head>
1001
- <body>
1002
- <div id="root"></div>
1003
- <script>${envScript}</script>
1004
- ${initialParamsScript}
1005
- <script type="module" src="/_satset/_entry.js"></script>
1006
- <script src="/__hmr"></script>
1007
- </body>
1008
- </html>
1053
+ const html = `
1054
+ <!DOCTYPE html>
1055
+ <html lang="${htmlLang}" suppressHydrationWarning>
1056
+ <head>
1057
+ <meta charset="UTF-8" />
1058
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1059
+ ${metaHtml}
1060
+ ${faviconLink}
1061
+ <link rel="stylesheet" href="/_satset/globals.css" />
1062
+ </head>
1063
+ <body>
1064
+ <div id="root"></div>
1065
+ <script>${envScript}</script>
1066
+ ${initialParamsScript}
1067
+ <script type="module" src="/_satset/_entry.js"></script>
1068
+ <script src="/__hmr"></script>
1069
+ </body>
1070
+ </html>
1009
1071
  `;
1010
- res.writeHead(200, { 'Content-Type': 'text/html' });
1011
- res.end(html);
1012
- return;
1013
- }
1014
- // Server-side render the component
1015
- // Compile page module to a temporary server file so Node can require it (handles tsx/esm)
1016
- const baseName = path_1.default.basename(route.component).replace(/\.[^.]+$/, '');
1017
- const compiledServerPath = path_1.default.join(tempDir, baseName + '.server.js');
1018
- let PageModule = null;
1019
- // Helper: bundle server build with low-disk fallback
1020
- async function safeBundleServer(entry, outfile) {
1021
- try {
1022
- await bundler_1.bundler.bundleServer({ entryPoint: entry, outfile, minify: false, root, sourcemap: true });
1072
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1073
+ res.end(html);
1074
+ return;
1023
1075
  }
1024
- catch (err) {
1025
- const msg = String(err && err.message ? err.message : err);
1026
- // detect low-disk / ENOSPC errors (esbuild reports "There is not enough space on the disk" in Windows)
1027
- if (/ENOSPC|not enough space|There is not enough space/i.test(msg)) {
1028
- console.warn('Low disk space detected while bundling. Attempting to clear temp and retry without sourcemaps.');
1029
- try {
1030
- // clear temp dir to free space
1031
- if (fs_1.default.existsSync(tempDir)) {
1032
- fs_1.default.rmSync(tempDir, { recursive: true, force: true });
1076
+ // Server-side render the component
1077
+ // Compile page module to a temporary server file so Node can require it (handles tsx/esm)
1078
+ const baseName = path_1.default.basename(route.component).replace(/\.[^.]+$/, '');
1079
+ const compiledServerPath = path_1.default.join(tempDir, baseName + '.server.js');
1080
+ let PageModule = null;
1081
+ // Helper: bundle server build with low-disk fallback
1082
+ async function safeBundleServer(entry, outfile) {
1083
+ try {
1084
+ await bundler_1.bundler.bundleServer({ entryPoint: entry, outfile, minify: false, root, sourcemap: true });
1085
+ }
1086
+ catch (err) {
1087
+ const msg = String(err && err.message ? err.message : err);
1088
+ // detect low-disk / ENOSPC errors (esbuild reports "There is not enough space on the disk" in Windows)
1089
+ if (/ENOSPC|not enough space|There is not enough space/i.test(msg)) {
1090
+ console.warn('Low disk space detected while bundling. Attempting to clear temp and retry without sourcemaps.');
1091
+ try {
1092
+ // clear temp dir to free space
1093
+ if (fs_1.default.existsSync(tempDir)) {
1094
+ fs_1.default.rmSync(tempDir, { recursive: true, force: true });
1095
+ }
1096
+ fs_1.default.mkdirSync(tempDir, { recursive: true });
1097
+ }
1098
+ catch (cleanupErr) {
1099
+ console.error('Failed to cleanup temp dir:', cleanupErr);
1100
+ }
1101
+ try {
1102
+ await bundler_1.bundler.bundleServer({ entryPoint: entry, outfile, minify: false, root, sourcemap: false });
1103
+ return;
1104
+ }
1105
+ catch (retryErr) {
1106
+ // still failed
1107
+ throw retryErr;
1033
1108
  }
1034
- fs_1.default.mkdirSync(tempDir, { recursive: true });
1035
- }
1036
- catch (cleanupErr) {
1037
- console.error('Failed to cleanup temp dir:', cleanupErr);
1038
- }
1039
- try {
1040
- await bundler_1.bundler.bundleServer({ entryPoint: entry, outfile, minify: false, root, sourcemap: false });
1041
- return;
1042
- }
1043
- catch (retryErr) {
1044
- // still failed
1045
- throw retryErr;
1046
1109
  }
1110
+ // Not a disk-space issue, rethrow
1111
+ throw err;
1047
1112
  }
1048
- // Not a disk-space issue, rethrow
1049
- throw err;
1050
1113
  }
1051
- }
1052
- try {
1053
- // Bundle the page for Node (CJS)
1054
- await safeBundleServer(route.component, compiledServerPath);
1055
1114
  try {
1056
- delete require.cache[require.resolve(compiledServerPath)];
1057
- }
1058
- catch (e) {
1059
- // ignore
1060
- }
1061
- PageModule = require(compiledServerPath);
1062
- }
1063
- catch (err) {
1064
- // If bundling fails, show an overlay with the bundling error and do NOT require the original TSX (which would crash)
1065
- console.error('SSR bundling failed for', route.component, err);
1066
- let message = 'SSR bundling failed';
1067
- const errMsg = String(err && err.message ? err.message : err);
1068
- if (/ENOSPC|not enough space|There is not enough space/i.test(errMsg)) {
1069
- message = 'SSR bundling failed: not enough disk space. Try freeing disk space or deleting the .satset/temp folder.';
1070
- }
1071
- const errorInfo = {
1072
- message,
1073
- stack: err && err.stack ? err.stack : String(err),
1074
- file: route.component,
1075
- };
1076
- const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errorInfo);
1077
- res.writeHead(500, { 'Content-Type': 'text/html' });
1078
- res.end(overlayHTML);
1079
- return;
1080
- }
1081
- function resolveExportedComponent(mod) {
1082
- if (!mod)
1083
- return null;
1084
- if (typeof mod === 'function')
1085
- return mod;
1086
- if (mod && typeof mod === 'object') {
1087
- if (typeof mod.default === 'function')
1088
- return mod.default;
1089
- if (mod.default && typeof mod.default === 'object' && typeof mod.default.default === 'function')
1090
- return mod.default.default;
1091
- for (const key of Object.keys(mod)) {
1092
- if (typeof mod[key] === 'function')
1093
- return mod[key];
1115
+ // Bundle the page for Node (CJS)
1116
+ await safeBundleServer(route.component, compiledServerPath);
1117
+ try {
1118
+ delete require.cache[require.resolve(compiledServerPath)];
1094
1119
  }
1120
+ catch (e) {
1121
+ // ignore
1122
+ }
1123
+ PageModule = require(compiledServerPath);
1095
1124
  }
1096
- return null;
1097
- }
1098
- const PageComponent = resolveExportedComponent(PageModule);
1099
- console.log('SSR: used component from', compiledServerPath, 'PageModule keys:', Object.keys(PageModule || {}), 'Resolved component type:', typeof PageComponent);
1100
- console.log('SSR: PageModule keys:', Object.keys(PageModule || {}), 'PageComponent type:', typeof PageComponent);
1101
- // Simple SSR with optional global layout composition
1102
- const React = require('react');
1103
- const { renderToString } = require('react-dom/server');
1104
- let pageHTML = '<div>Loading...</div>';
1105
- async function renderComponentToHtml(Comp, props = {}) {
1106
- if (!Comp || typeof Comp !== 'function') {
1107
- throw new Error('Invalid component to render');
1108
- }
1109
- const isAsync = Comp.constructor && Comp.constructor.name === 'AsyncFunction';
1110
- if (isAsync) {
1111
- const maybeNode = Comp(props);
1112
- const resolved = await Promise.resolve(maybeNode);
1113
- return renderToString(resolved);
1114
- }
1115
- return renderToString(React.createElement(Comp, props));
1116
- }
1117
- let statusCode = 200;
1118
- // Determine locale for SSR
1119
- const urlSegments = req.url?.split('/').filter(Boolean) || [];
1120
- const firstSegment = urlSegments[0];
1121
- const locale = (firstSegment && /^[a-zA-Z]{2}(?:-[a-zA-Z]{2})?$/.test(firstSegment)) ? firstSegment : 'en-US';
1122
- const dictionaries = getDictionaries(root);
1123
- try {
1124
- if (!PageComponent || typeof PageComponent !== 'function') {
1125
- console.error('SSR Error: resolved page component is not a function. PageModule:', util_1.default.inspect(PageModule, { depth: 2 }));
1126
- const diagHtml = `<!doctype html><html><body><h1>500 - Component export error</h1><pre>${util_1.default.format('PageModule keys: %o\nResolved: %o', Object.keys(PageModule || {}), PageComponent)}</pre></body></html>`;
1125
+ catch (err) {
1126
+ // If bundling fails, show an overlay with the bundling error and do NOT require the original TSX (which would crash)
1127
+ console.error('SSR bundling failed for', route.component, err);
1128
+ let message = 'SSR bundling failed';
1129
+ const errMsg = String(err && err.message ? err.message : err);
1130
+ if (/ENOSPC|not enough space|There is not enough space/i.test(errMsg)) {
1131
+ message = 'SSR bundling failed: not enough disk space. Try freeing disk space or deleting the .satset/temp folder.';
1132
+ }
1133
+ const errorInfo = {
1134
+ message,
1135
+ stack: err && err.stack ? err.stack : String(err),
1136
+ file: route.component,
1137
+ };
1138
+ const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errorInfo);
1127
1139
  res.writeHead(500, { 'Content-Type': 'text/html' });
1128
- res.end(diagHtml);
1140
+ res.end(overlayHTML);
1129
1141
  return;
1130
1142
  }
1131
- let LayoutComponent = null;
1132
- try {
1133
- const layoutRoute = allRoutes.find(r => r.path === '/layout');
1134
- if (layoutRoute && layoutRoute.component) {
1135
- const layoutBase = path_1.default.basename(layoutRoute.component).replace(/\.[^.]+$/, '');
1136
- const layoutCompiled = path_1.default.join(tempDir, layoutBase + '.layout.server.js');
1137
- await bundler_1.bundler.bundleServer({ entryPoint: layoutRoute.component, outfile: layoutCompiled, minify: false, root });
1138
- try {
1139
- delete require.cache[require.resolve(layoutCompiled)];
1140
- }
1141
- catch (e) { }
1142
- const LayoutModule = require(layoutCompiled);
1143
- function resolveExportedComp(mod) {
1144
- if (!mod)
1145
- return null;
1146
- if (typeof mod === 'function')
1147
- return mod;
1148
- if (mod && typeof mod === 'object') {
1149
- if (typeof mod.default === 'function')
1150
- return mod.default;
1151
- for (const key of Object.keys(mod)) {
1152
- if (typeof mod[key] === 'function')
1153
- return mod[key];
1154
- }
1155
- }
1156
- return null;
1157
- }
1158
- LayoutComponent = resolveExportedComp(LayoutModule);
1159
- if (!LayoutComponent) {
1160
- console.warn('Layout module found but no component exported:', layoutRoute.component);
1161
- }
1162
- else {
1163
- console.log('Using layout module for SSR:', layoutRoute.component);
1143
+ function resolveExportedComponent(mod) {
1144
+ if (!mod)
1145
+ return null;
1146
+ if (typeof mod === 'function')
1147
+ return mod;
1148
+ if (mod && typeof mod === 'object') {
1149
+ if (typeof mod.default === 'function')
1150
+ return mod.default;
1151
+ if (mod.default && typeof mod.default === 'object' && typeof mod.default.default === 'function')
1152
+ return mod.default.default;
1153
+ for (const key of Object.keys(mod)) {
1154
+ if (typeof mod[key] === 'function')
1155
+ return mod[key];
1164
1156
  }
1165
1157
  }
1158
+ return null;
1166
1159
  }
1167
- catch (e) {
1168
- const msg = e?.message ?? String(e);
1169
- console.warn('Could not load layout module:', msg);
1170
- }
1171
- const isPageAsync = PageComponent &&
1172
- PageComponent.constructor &&
1173
- PageComponent.constructor.name === 'AsyncFunction';
1174
- let pageNode;
1175
- if (isPageAsync) {
1176
- const maybeNode = PageComponent({ params: initialParams });
1177
- pageNode = await Promise.resolve(maybeNode);
1178
- }
1179
- else {
1180
- pageNode = React.createElement(PageComponent, { params: initialParams });
1181
- }
1182
- if (LayoutComponent) {
1183
- const Wrapper = () => React.createElement(translation_1.I18nProvider, { initialLocale: locale, dictionaries }, React.createElement(LayoutComponent, null, pageNode));
1184
- pageHTML = await renderComponentToHtml(Wrapper);
1185
- }
1186
- else {
1187
- const Wrapper = () => React.createElement(translation_1.I18nProvider, { initialLocale: locale, dictionaries }, pageNode);
1188
- pageHTML = await renderComponentToHtml(Wrapper);
1189
- }
1190
- }
1191
- catch (err) {
1192
- console.error('SSR Error:', err);
1193
- if (err && err.__SATSET_REDIRECT) {
1194
- const info = err.__SATSET_REDIRECT || {};
1195
- const url = typeof info.url === 'string' && info.url.length ? info.url : '/';
1196
- const status = typeof info.status === 'number' ? info.status : 307;
1197
- const sat = response_1.SatsetResponse.redirect(url, status);
1198
- (0, response_1.sendSatsetResponse)(res, sat);
1199
- return;
1160
+ const PageComponent = resolveExportedComponent(PageModule);
1161
+ console.log('SSR: used component from', compiledServerPath, 'PageModule keys:', Object.keys(PageModule || {}), 'Resolved component type:', typeof PageComponent);
1162
+ console.log('SSR: PageModule keys:', Object.keys(PageModule || {}), 'PageComponent type:', typeof PageComponent);
1163
+ // Simple SSR with optional global layout composition
1164
+ const React = require('react');
1165
+ const { renderToString } = require('react-dom/server');
1166
+ let pageHTML = '<div>Loading...</div>';
1167
+ async function renderComponentToHtml(Comp, props = {}) {
1168
+ if (!Comp || typeof Comp !== 'function') {
1169
+ throw new Error('Invalid component to render');
1170
+ }
1171
+ const isAsync = Comp.constructor && Comp.constructor.name === 'AsyncFunction';
1172
+ if (isAsync) {
1173
+ const maybeNode = Comp(props);
1174
+ const resolved = await Promise.resolve(maybeNode);
1175
+ return renderToString(resolved);
1176
+ }
1177
+ return renderToString(React.createElement(Comp, props));
1200
1178
  }
1201
- if (err && err.__SATSET_NOT_FOUND) {
1202
- statusCode = 404;
1203
- const errorPayload = { code: 404, message: 'Page not found' };
1179
+ let statusCode = 200;
1180
+ // Determine locale for SSR
1181
+ // locale is now passed as argument
1182
+ // const dictionaries = getDictionaries(root); // Already defined in outer scope and context
1183
+ try {
1184
+ if (!PageComponent || typeof PageComponent !== 'function') {
1185
+ console.error('SSR Error: resolved page component is not a function. PageModule:', util_1.default.inspect(PageModule, { depth: 2 }));
1186
+ const diagHtml = `<!doctype html><html><body><h1>500 - Component export error</h1><pre>${util_1.default.format('PageModule keys: %o\nResolved: %o', Object.keys(PageModule || {}), PageComponent)}</pre></body></html>`;
1187
+ res.writeHead(500, { 'Content-Type': 'text/html' });
1188
+ res.end(diagHtml);
1189
+ return;
1190
+ }
1191
+ // Recursive Layout Loading
1192
+ const layouts = [];
1204
1193
  try {
1205
- const nf = allRoutes.find(r => r.path === '/404') ||
1206
- allRoutes.find(r => r.path === '/not-found');
1207
- if (nf && nf.component) {
1208
- const nfBase = path_1.default.basename(nf.component).replace(/\.[^.]+$/, '');
1209
- const nfCompiled = path_1.default.join(tempDir, nfBase + '.notfound.server.js');
1210
- await bundler_1.bundler.bundleServer({ entryPoint: nf.component, outfile: nfCompiled, minify: false, root });
1194
+ const findLayoutFiles = (dir) => {
1195
+ const results = [];
1196
+ let current = dir;
1197
+ const rootSrc = path_1.default.join(root, 'src');
1198
+ // Go up until we reach the root
1199
+ while (current.startsWith(root)) {
1200
+ const extensions = ['.tsx', '.jsx', '.ts', '.js'];
1201
+ for (const ext of extensions) {
1202
+ const layoutPath = path_1.default.join(current, `layout${ext}`);
1203
+ if (fs_1.default.existsSync(layoutPath)) {
1204
+ // Check if already added (to avoid duplicates if loop logic is flawed)
1205
+ if (!results.includes(layoutPath))
1206
+ results.unshift(layoutPath);
1207
+ break;
1208
+ }
1209
+ }
1210
+ if (current === root)
1211
+ break;
1212
+ current = path_1.default.dirname(current);
1213
+ }
1214
+ return results;
1215
+ };
1216
+ const pageDir = path_1.default.dirname(route.component);
1217
+ const layoutFiles = findLayoutFiles(pageDir);
1218
+ for (const layoutFile of layoutFiles) {
1219
+ const layoutBase = path_1.default.basename(layoutFile).replace(/\.[^.]+$/, '');
1220
+ const crypto = require('crypto');
1221
+ const hash = crypto.createHash('md5').update(layoutFile).digest('hex').substring(0, 8);
1222
+ const layoutCompiled = path_1.default.join(tempDir, `${layoutBase}.${hash}.layout.server.js`);
1223
+ await bundler_1.bundler.bundleServer({ entryPoint: layoutFile, outfile: layoutCompiled, minify: false, root });
1211
1224
  try {
1212
- delete require.cache[require.resolve(nfCompiled)];
1225
+ delete require.cache[require.resolve(layoutCompiled)];
1213
1226
  }
1214
1227
  catch (e) { }
1215
- const NfModule = require(nfCompiled);
1216
- const NfComp = (typeof NfModule === 'function') ? NfModule : (NfModule && typeof NfModule.default === 'function') ? NfModule.default : null;
1217
- if (NfComp) {
1218
- pageHTML = await renderComponentToHtml(NfComp, { params: initialParams, error: errorPayload });
1219
- }
1220
- else {
1221
- pageHTML = '<h1>404 - Page Not Found</h1>';
1228
+ const LayoutModule = require(layoutCompiled);
1229
+ let LayoutComp = null;
1230
+ if (typeof LayoutModule === 'function')
1231
+ LayoutComp = LayoutModule;
1232
+ else if (LayoutModule && typeof LayoutModule === 'object') {
1233
+ if (typeof LayoutModule.default === 'function')
1234
+ LayoutComp = LayoutModule.default;
1235
+ else {
1236
+ for (const key of Object.keys(LayoutModule)) {
1237
+ if (typeof LayoutModule[key] === 'function') {
1238
+ LayoutComp = LayoutModule[key];
1239
+ break;
1240
+ }
1241
+ }
1242
+ }
1222
1243
  }
1223
- }
1224
- else {
1225
- pageHTML = '<h1>404 - Page Not Found</h1>';
1244
+ if (LayoutComp)
1245
+ layouts.push(LayoutComp);
1226
1246
  }
1227
1247
  }
1228
1248
  catch (e) {
1229
- pageHTML = '<h1>404 - Page Not Found</h1>';
1249
+ console.warn('Could not load layout modules:', e);
1230
1250
  }
1231
- }
1232
- else {
1233
- const maybeStatus = (err && err.statusCode) ??
1234
- (err && err.status) ??
1235
- (err && typeof err.code === 'number' ? err.code : undefined);
1236
- if (typeof maybeStatus === 'number' && maybeStatus >= 400 && maybeStatus <= 599) {
1237
- statusCode = maybeStatus;
1251
+ const isPageAsync = PageComponent &&
1252
+ PageComponent.constructor &&
1253
+ PageComponent.constructor.name === 'AsyncFunction';
1254
+ let pageNode;
1255
+ if (isPageAsync) {
1256
+ const maybeNode = PageComponent({ params: initialParams, searchParams: {} });
1257
+ pageNode = await Promise.resolve(maybeNode);
1238
1258
  }
1239
1259
  else {
1240
- statusCode = 500;
1260
+ pageNode = React.createElement(PageComponent, { params: initialParams, searchParams: {} });
1241
1261
  }
1242
- const errorPayload = {
1243
- code: statusCode,
1244
- message: (err && err.message) ? err.message : (statusCode === 404 ? 'Page not found' : 'Server error'),
1245
- };
1246
- const candidatePaths = [];
1247
- candidatePaths.push(`/${statusCode}`);
1248
- if (statusCode === 404)
1249
- candidatePaths.push('/404', '/not-found');
1250
- if (statusCode === 500)
1251
- candidatePaths.push('/500');
1252
- candidatePaths.push('/error');
1253
- let errorRoute = null;
1254
- for (const p of candidatePaths) {
1255
- const found = allRoutes.find(r => r.path === p);
1256
- if (found) {
1257
- errorRoute = found;
1258
- break;
1259
- }
1262
+ let appElement = pageNode;
1263
+ // Wrap from inner to outer
1264
+ for (let i = layouts.length - 1; i >= 0; i--) {
1265
+ appElement = React.createElement(layouts[i], { params: initialParams, locale }, appElement);
1260
1266
  }
1261
- try {
1262
- if (errorRoute && errorRoute.component) {
1263
- const erBase = path_1.default.basename(errorRoute.component).replace(/\.[^.]+$/, '');
1264
- const erCompiled = path_1.default.join(tempDir, erBase + '.error.server.js');
1265
- await bundler_1.bundler.bundleServer({ entryPoint: errorRoute.component, outfile: erCompiled, minify: false, root });
1266
- try {
1267
- delete require.cache[require.resolve(erCompiled)];
1268
- }
1269
- catch (e) { }
1270
- const ErModule = require(erCompiled);
1271
- const ErComp = (typeof ErModule === 'function') ? ErModule : (ErModule && typeof ErModule.default === 'function') ? ErModule.default : null;
1272
- if (ErComp) {
1273
- pageHTML = await renderComponentToHtml(ErComp, { error: errorPayload, reset: () => { } });
1267
+ const Wrapper = () => React.createElement(translation_1.I18nProvider, { initialLocale: locale, dictionaries }, appElement);
1268
+ pageHTML = await renderComponentToHtml(Wrapper);
1269
+ }
1270
+ catch (err) {
1271
+ console.error('SSR Error:', err);
1272
+ if (err && err.__SATSET_REDIRECT) {
1273
+ const info = err.__SATSET_REDIRECT || {};
1274
+ const url = typeof info.url === 'string' && info.url.length ? info.url : '/';
1275
+ const status = typeof info.status === 'number' ? info.status : 307;
1276
+ const sat = response_1.SatsetResponse.redirect(url, status);
1277
+ (0, response_1.sendSatsetResponse)(res, sat);
1278
+ return;
1279
+ }
1280
+ if (err && err.__SATSET_NOT_FOUND) {
1281
+ statusCode = 404;
1282
+ const errorPayload = { code: 404, message: 'Page not found' };
1283
+ try {
1284
+ const nf = allRoutes.find(r => r.path === '/404') ||
1285
+ allRoutes.find(r => r.path === '/not-found');
1286
+ if (nf && nf.component) {
1287
+ const nfBase = path_1.default.basename(nf.component).replace(/\.[^.]+$/, '');
1288
+ const nfCompiled = path_1.default.join(tempDir, nfBase + '.notfound.server.js');
1289
+ await bundler_1.bundler.bundleServer({ entryPoint: nf.component, outfile: nfCompiled, minify: false, root });
1290
+ try {
1291
+ delete require.cache[require.resolve(nfCompiled)];
1292
+ }
1293
+ catch (e) { }
1294
+ const NfModule = require(nfCompiled);
1295
+ const NfComp = (typeof NfModule === 'function') ? NfModule : (NfModule && typeof NfModule.default === 'function') ? NfModule.default : null;
1296
+ if (NfComp) {
1297
+ pageHTML = await renderComponentToHtml(NfComp, { params: initialParams, error: errorPayload });
1298
+ }
1299
+ else {
1300
+ pageHTML = '<h1>404 - Page Not Found</h1>';
1301
+ }
1274
1302
  }
1275
1303
  else {
1276
- throw err;
1304
+ pageHTML = '<h1>404 - Page Not Found</h1>';
1277
1305
  }
1278
1306
  }
1279
- else {
1280
- throw err;
1307
+ catch (e) {
1308
+ pageHTML = '<h1>404 - Page Not Found</h1>';
1281
1309
  }
1282
1310
  }
1283
- catch (e) {
1284
- // Attempt to extract title from metadata for the error overlay
1285
- let pageTitle;
1311
+ else {
1312
+ const maybeStatus = (err && err.statusCode) ??
1313
+ (err && err.status) ??
1314
+ (err && typeof err.code === 'number' ? err.code : undefined);
1315
+ if (typeof maybeStatus === 'number' && maybeStatus >= 400 && maybeStatus <= 599) {
1316
+ statusCode = maybeStatus;
1317
+ }
1318
+ else {
1319
+ statusCode = 500;
1320
+ }
1321
+ const errorPayload = {
1322
+ code: statusCode,
1323
+ message: (err && err.message) ? err.message : (statusCode === 404 ? 'Page not found' : 'Server error'),
1324
+ };
1325
+ const candidatePaths = [];
1326
+ candidatePaths.push(`/${statusCode}`);
1327
+ if (statusCode === 404)
1328
+ candidatePaths.push('/404', '/not-found');
1329
+ if (statusCode === 500)
1330
+ candidatePaths.push('/500');
1331
+ candidatePaths.push('/error');
1332
+ let errorRoute = null;
1333
+ for (const p of candidatePaths) {
1334
+ const found = allRoutes.find(r => r.path === p);
1335
+ if (found) {
1336
+ errorRoute = found;
1337
+ break;
1338
+ }
1339
+ }
1286
1340
  try {
1287
- if (PageModule) {
1288
- if (PageModule.metadata && PageModule.metadata.title) {
1289
- pageTitle = PageModule.metadata.title;
1341
+ if (errorRoute && errorRoute.component) {
1342
+ const erBase = path_1.default.basename(errorRoute.component).replace(/\.[^.]+$/, '');
1343
+ const erCompiled = path_1.default.join(tempDir, erBase + '.error.server.js');
1344
+ await bundler_1.bundler.bundleServer({ entryPoint: errorRoute.component, outfile: erCompiled, minify: false, root });
1345
+ try {
1346
+ delete require.cache[require.resolve(erCompiled)];
1290
1347
  }
1291
- else if (typeof PageModule.getMetadata === 'function') {
1292
- const t = (key, params) => {
1293
- const dict = dictionaries[locale] || dictionaries['en-US'] || dictionaries[Object.keys(dictionaries)[0]] || {};
1294
- let text = dict[key] || key;
1295
- if (params) {
1296
- Object.entries(params).forEach(([k, v]) => {
1297
- text = text.replace(new RegExp(`{${k}}`, 'g'), v);
1298
- });
1299
- }
1300
- return text;
1301
- };
1302
- const meta = await PageModule.getMetadata({ params: initialParams, locale, t });
1303
- if (meta && meta.title)
1304
- pageTitle = meta.title;
1348
+ catch (e) { }
1349
+ const ErModule = require(erCompiled);
1350
+ const ErComp = (typeof ErModule === 'function') ? ErModule : (ErModule && typeof ErModule.default === 'function') ? ErModule.default : null;
1351
+ if (ErComp) {
1352
+ pageHTML = await renderComponentToHtml(ErComp, { error: errorPayload, reset: () => { } });
1353
+ }
1354
+ else {
1355
+ throw err;
1305
1356
  }
1306
1357
  }
1358
+ else {
1359
+ throw err;
1360
+ }
1307
1361
  }
1308
- catch (metaErr) {
1309
- // ignore metadata extraction errors during error handling
1362
+ catch (e) {
1363
+ // Attempt to extract title from metadata for the error overlay
1364
+ let pageTitle;
1365
+ try {
1366
+ if (PageModule) {
1367
+ if (PageModule.metadata && PageModule.metadata.title) {
1368
+ pageTitle = PageModule.metadata.title;
1369
+ }
1370
+ else if (typeof PageModule.getMetadata === 'function') {
1371
+ const t = (key, params) => {
1372
+ const dict = dictionaries[locale] || dictionaries['en-US'] || dictionaries[Object.keys(dictionaries)[0]] || {};
1373
+ let text = dict[key] || key;
1374
+ if (params) {
1375
+ Object.entries(params).forEach(([k, v]) => {
1376
+ text = text.replace(new RegExp(`{${k}}`, 'g'), v);
1377
+ });
1378
+ }
1379
+ return text;
1380
+ };
1381
+ const meta = await PageModule.getMetadata({ params: initialParams, locale, t });
1382
+ if (meta && meta.title)
1383
+ pageTitle = meta.title;
1384
+ }
1385
+ }
1386
+ }
1387
+ catch (metaErr) {
1388
+ // ignore metadata extraction errors during error handling
1389
+ }
1390
+ const errorInfo = {
1391
+ message: err?.message || String(err),
1392
+ stack: err?.stack,
1393
+ file: route.component,
1394
+ title: pageTitle,
1395
+ };
1396
+ const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errorInfo);
1397
+ res.writeHead(500, { 'Content-Type': 'text/html' });
1398
+ res.end(overlayHTML);
1399
+ return;
1310
1400
  }
1311
- const errorInfo = {
1312
- message: err?.message || String(err),
1313
- stack: err?.stack,
1314
- file: route.component,
1315
- title: pageTitle,
1316
- };
1317
- const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errorInfo);
1318
- res.writeHead(500, { 'Content-Type': 'text/html' });
1319
- res.end(overlayHTML);
1320
- return;
1321
1401
  }
1322
1402
  }
1323
- }
1324
- // expose routes + initial params to client for Router
1325
- const routePaths = allRoutes.length ? allRoutes.map(r => r.path) : [];
1326
- // Check whether layout and CSS ended up in the rendered HTML for debugging
1327
- try {
1328
- const hasHeader = pageHTML.includes('<header');
1329
- const hasFooter = pageHTML.includes('<footer');
1330
- console.log(`SSR: rendered page includes header=${hasHeader} footer=${hasFooter}`);
1331
- const cssPath = path_1.default.join(tempDir, 'globals.css');
1332
- console.log('SSR: globals.css exists at', cssPath, fs_1.default.existsSync(cssPath));
1333
- }
1334
- catch (e) {
1335
- // ignore
1336
- }
1337
- // attempt to obtain metadata from the page module (static or via getMetadata)
1338
- let metaHtml = '';
1339
- let htmlLang = 'en';
1340
- try {
1341
- const { renderMetaTags } = await import('../assets/metadata.js');
1342
- // metadata may be exported as `metadata` or `getMetadata` function
1343
- let metaObj = null;
1344
- if (PageModule && PageModule.metadata) {
1345
- metaObj = PageModule.metadata;
1403
+ // expose routes + initial params to client for Router
1404
+ const routePaths = allRoutes.length ? allRoutes.map(r => r.path) : [];
1405
+ // Check whether layout and CSS ended up in the rendered HTML for debugging
1406
+ try {
1407
+ const hasHeader = pageHTML.includes('<header');
1408
+ const hasFooter = pageHTML.includes('<footer');
1409
+ console.log(`SSR: rendered page includes header=${hasHeader} footer=${hasFooter}`);
1410
+ const cssPath = path_1.default.join(tempDir, 'globals.css');
1411
+ console.log('SSR: globals.css exists at', cssPath, fs_1.default.existsSync(cssPath));
1346
1412
  }
1347
- else if (PageModule && typeof PageModule.getMetadata === 'function') {
1348
- try {
1349
- const t = (key, params) => {
1350
- const dict = dictionaries[locale] || dictionaries['en-US'] || dictionaries[Object.keys(dictionaries)[0]] || {};
1351
- let text = dict[key] || key;
1352
- if (params) {
1353
- Object.entries(params).forEach(([k, v]) => {
1354
- text = text.replace(new RegExp(`{${k}}`, 'g'), v);
1355
- });
1356
- }
1357
- return text;
1358
- };
1359
- metaObj = await PageModule.getMetadata({ params: initialParams, locale, t });
1413
+ catch (e) {
1414
+ // ignore
1415
+ }
1416
+ // attempt to obtain metadata from the page module (static or via getMetadata)
1417
+ let metaHtml = '';
1418
+ let htmlLang = 'en';
1419
+ try {
1420
+ const { renderMetaTags } = await import('../assets/metadata.js');
1421
+ // metadata may be exported as `metadata` or `getMetadata` function
1422
+ let metaObj = null;
1423
+ if (PageModule && PageModule.metadata) {
1424
+ metaObj = PageModule.metadata;
1360
1425
  }
1361
- catch (e) {
1362
- metaObj = null;
1426
+ else if (PageModule && typeof PageModule.getMetadata === 'function') {
1427
+ try {
1428
+ const t = (key, params) => {
1429
+ const dict = dictionaries[locale] || dictionaries['en-US'] || dictionaries[Object.keys(dictionaries)[0]] || {};
1430
+ let text = dict[key] || key;
1431
+ if (params) {
1432
+ Object.entries(params).forEach(([k, v]) => {
1433
+ text = text.replace(new RegExp(`{${k}}`, 'g'), v);
1434
+ });
1435
+ }
1436
+ return text;
1437
+ };
1438
+ metaObj = await PageModule.getMetadata({ params: initialParams, locale, t });
1439
+ }
1440
+ catch (e) {
1441
+ metaObj = null;
1442
+ }
1443
+ }
1444
+ // The `<Head>` component was removed; rely on exported `metadata` or `getMetadata` from the page/layout module.
1445
+ // Detect html lang from metadata if provided
1446
+ if (metaObj && typeof metaObj.lang === 'string' && metaObj.lang.trim()) {
1447
+ htmlLang = metaObj.lang.trim();
1363
1448
  }
1449
+ metaHtml = renderMetaTags(metaObj);
1364
1450
  }
1365
- // The `<Head>` component was removed; rely on exported `metadata` or `getMetadata` from the page/layout module.
1366
- // Detect html lang from metadata if provided
1367
- if (metaObj && typeof metaObj.lang === 'string' && metaObj.lang.trim()) {
1368
- htmlLang = metaObj.lang.trim();
1451
+ catch (e) {
1452
+ // ignore
1369
1453
  }
1370
- metaHtml = renderMetaTags(metaObj);
1371
- }
1372
- catch (e) {
1373
- // ignore
1374
- }
1375
- const initialParamsScript = `<script>
1376
- window.__SATSET_ROUTES__ = ${JSON.stringify(routePaths)};
1377
- window.__SATSET_PARAMS__ = ${JSON.stringify(initialParams)};
1378
- window.__SATSET_DICTIONARIES__ = ${JSON.stringify(getDictionaries(root))};
1454
+ const initialParamsScript = `<script>
1455
+ window.__SATSET_ROUTES__ = ${JSON.stringify(routePaths)};
1456
+ window.__SATSET_PARAMS__ = ${JSON.stringify(initialParams)};
1457
+ window.__SATSET_DICTIONARIES__ = ${JSON.stringify(getDictionaries(root))};
1458
+ window.__SATSET_LOCALE__ = "${locale}";
1379
1459
  </script>`;
1380
- // Determine favicon at request time (in case it was created/changed)
1381
- let computedFavicon = null;
1382
- try {
1383
- const publicPath = path_1.default.join(root, publicDirName);
1384
- if (fs_1.default.existsSync(path_1.default.join(publicPath, 'favicon.ico')))
1385
- computedFavicon = '/favicon.ico';
1386
- else if (fs_1.default.existsSync(path_1.default.join(publicPath, 'favicon.png')))
1387
- computedFavicon = '/favicon.png';
1388
- }
1389
- catch (e) {
1390
- // ignore
1391
- }
1392
- const faviconLink = computedFavicon ? `<link rel="icon" href="${computedFavicon}" />` : '';
1393
- const html = `
1394
- <!DOCTYPE html>
1395
- <html lang="${htmlLang}">
1396
- <head>
1397
- <meta charset="UTF-8" />
1398
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1399
- ${metaHtml}
1400
- ${faviconLink}
1401
- <link rel="stylesheet" href="/_satset/globals.css" />
1402
- </head>
1403
- <body>
1404
- <div id="root">${pageHTML}</div>
1405
- <script>${envScript}</script>
1406
- ${initialParamsScript}
1407
- <script type="module" src="/_satset/_entry.js"></script>
1408
- <script src="/__hmr"></script>
1409
- <!-- ... rest of scripts -->
1410
- </body>
1411
- </html>
1460
+ // Determine favicon at request time (in case it was created/changed)
1461
+ let computedFavicon = null;
1462
+ try {
1463
+ const publicPath = path_1.default.join(root, publicDirName);
1464
+ if (fs_1.default.existsSync(path_1.default.join(publicPath, 'favicon.ico')))
1465
+ computedFavicon = '/favicon.ico';
1466
+ else if (fs_1.default.existsSync(path_1.default.join(publicPath, 'favicon.png')))
1467
+ computedFavicon = '/favicon.png';
1468
+ }
1469
+ catch (e) {
1470
+ // ignore
1471
+ }
1472
+ const faviconLink = computedFavicon ? `<link rel="icon" href="${computedFavicon}" />` : '';
1473
+ let html = '';
1474
+ const trimmedPageHTML = pageHTML.trim();
1475
+ if (trimmedPageHTML.toLowerCase().startsWith('<html')) {
1476
+ html = '<!DOCTYPE html>\n' + trimmedPageHTML;
1477
+ const headContent = `
1478
+ <meta charset="UTF-8" />
1479
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1480
+ ${metaHtml}
1481
+ ${faviconLink}
1482
+ <link rel="stylesheet" href="/_satset/globals.css" />`;
1483
+ if (html.includes('</head>')) {
1484
+ html = html.replace('</head>', `${headContent}</head>`);
1485
+ }
1486
+ const bodyScripts = `
1487
+ <script>${envScript}</script>
1488
+ ${initialParamsScript}
1489
+ <script type="module" src="/_satset/_entry.js"></script>
1490
+ <script src="/__hmr"></script>`;
1491
+ if (html.includes('</body>')) {
1492
+ html = html.replace('</body>', `${bodyScripts}</body>`);
1493
+ }
1494
+ }
1495
+ else {
1496
+ html = `
1497
+ <!DOCTYPE html>
1498
+ <html lang="${htmlLang}" suppressHydrationWarning>
1499
+ <head>
1500
+ <meta charset="UTF-8" />
1501
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1502
+ ${metaHtml}
1503
+ ${faviconLink}
1504
+ <link rel="stylesheet" href="/_satset/globals.css" />
1505
+ </head>
1506
+ <body>
1507
+ <div id="root">${pageHTML}</div>
1508
+ <script>${envScript}</script>
1509
+ ${initialParamsScript}
1510
+ <script type="module" src="/_satset/_entry.js"></script>
1511
+ <script src="/__hmr"></script>
1512
+ </body>
1513
+ </html>
1412
1514
  `;
1413
- res.writeHead(statusCode, { 'Content-Type': 'text/html' });
1414
- res.end(html);
1415
- }
1416
- catch (error) {
1417
- const errorInfo = {
1418
- message: error.message,
1419
- stack: error.stack,
1420
- file: route.component,
1421
- };
1422
- if (route.component && fs_1.default.existsSync(route.component)) {
1423
- const stackLines = error.stack?.split('\n') || [];
1424
- const lineMatch = stackLines[0]?.match(/:(\d+):(\d+)/);
1425
- if (lineMatch) {
1426
- const line = parseInt(lineMatch[1]);
1427
- errorInfo.line = line;
1428
- errorInfo.column = parseInt(lineMatch[2]);
1429
- errorInfo.code = (0, error_overlay_1.extractCodeSnippet)(route.component, line);
1430
1515
  }
1516
+ res.writeHead(statusCode, { 'Content-Type': 'text/html' });
1517
+ res.end(html);
1431
1518
  }
1432
- const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errorInfo);
1433
- res.writeHead(500, { 'Content-Type': 'text/html' });
1434
- res.end(overlayHTML);
1435
- }
1436
- finally {
1437
- (0, response_1.setCurrentRequestCookies)(null);
1438
- }
1519
+ catch (error) {
1520
+ const errorInfo = {
1521
+ message: error.message,
1522
+ stack: error.stack,
1523
+ file: route.component,
1524
+ };
1525
+ if (route.component && fs_1.default.existsSync(route.component)) {
1526
+ const stackLines = error.stack?.split('\n') || [];
1527
+ const lineMatch = stackLines[0]?.match(/:(\d+):(\d+)/);
1528
+ if (lineMatch) {
1529
+ const line = parseInt(lineMatch[1]);
1530
+ errorInfo.line = line;
1531
+ errorInfo.column = parseInt(lineMatch[2]);
1532
+ errorInfo.code = (0, error_overlay_1.extractCodeSnippet)(route.component, line);
1533
+ }
1534
+ }
1535
+ const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errorInfo);
1536
+ res.writeHead(500, { 'Content-Type': 'text/html' });
1537
+ res.end(overlayHTML);
1538
+ }
1539
+ finally {
1540
+ (0, response_1.setCurrentRequestCookies)(null);
1541
+ }
1542
+ });
1439
1543
  }
1440
1544
  async function runMiddleware(root, tempDir, req, res, effectivePath) {
1441
1545
  try {