underpost 3.2.70 → 3.2.90

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.
Files changed (60) hide show
  1. package/.github/workflows/publish.ci.yml +3 -3
  2. package/.github/workflows/release.cd.yml +1 -1
  3. package/CHANGELOG.md +1358 -1038
  4. package/CLI-HELP.md +39 -16
  5. package/README.md +3 -3
  6. package/bin/build.js +10 -4
  7. package/bin/deploy.js +18 -16
  8. package/docker-compose.yml +1 -1
  9. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  10. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  11. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  12. package/manifests/deployment/playwright/deployment.yaml +1 -1
  13. package/manifests/mongodb/kustomization.yaml +4 -1
  14. package/manifests/mongodb/statefulset.yaml +4 -0
  15. package/manifests/mongodb/storage-class.yaml +9 -2
  16. package/package.json +20 -20
  17. package/scripts/nat-iptables.sh +10 -4
  18. package/scripts/test-monitor.sh +4 -3
  19. package/src/api/core/core.controller.js +4 -65
  20. package/src/api/core/core.router.js +8 -14
  21. package/src/api/default/default.controller.js +2 -70
  22. package/src/api/default/default.router.js +7 -17
  23. package/src/api/document/document.controller.js +5 -77
  24. package/src/api/document/document.router.js +9 -13
  25. package/src/api/file/file.controller.js +9 -53
  26. package/src/api/file/file.router.js +14 -6
  27. package/src/api/test/test.controller.js +8 -53
  28. package/src/api/test/test.router.js +1 -4
  29. package/src/cli/cluster.js +771 -66
  30. package/src/cli/db.js +6 -4
  31. package/src/cli/deploy.js +1715 -168
  32. package/src/cli/docker-compose.js +19 -24
  33. package/src/cli/fs.js +0 -1
  34. package/src/cli/image.js +40 -13
  35. package/src/cli/index.js +129 -35
  36. package/src/cli/ipfs.js +82 -11
  37. package/src/cli/monitor.js +1 -1
  38. package/src/cli/release.js +4 -0
  39. package/src/cli/repository.js +14 -3
  40. package/src/cli/run.js +2253 -439
  41. package/src/cli/secrets.js +969 -0
  42. package/src/cli/ssh.js +38 -39
  43. package/src/client/components/core/Modal.js +38 -4
  44. package/src/client-builder/client-build.js +94 -11
  45. package/src/client-builder/ssr.js +27 -73
  46. package/src/db/mongo/MongoBootstrap.js +295 -54
  47. package/src/db/mongo/MongooseDB.js +47 -32
  48. package/src/index.js +1 -1
  49. package/src/server/conf.js +1307 -6
  50. package/src/server/cri.js +70 -0
  51. package/src/server/downloader.js +3 -3
  52. package/src/server/middlewares.js +152 -0
  53. package/src/server/underpost-gateway.js +1073 -0
  54. package/src/server/underpost-ingress.js +364 -0
  55. package/test/cluster-instances.test.js +435 -0
  56. package/test/deploy-node-placement.test.js +45 -0
  57. package/test/instance-traffic-plan.test.js +710 -0
  58. package/test/sops-secret-store.test.js +612 -0
  59. package/test/underpost-gateway.test.js +469 -0
  60. package/test/underpost-ingress.test.js +253 -0
package/src/cli/ssh.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import { generateRandomPasswordSelection } from '../client/components/core/CommonJs.js';
8
8
  import { pbcopy, shellExec } from '../server/process.js';
9
9
  import { loggerFactory } from '../server/logger.js';
10
+ import { waitForPort } from '../server/conf.js';
10
11
  import fs from 'fs-extra';
11
12
  import Underpost from '../index.js';
12
13
 
@@ -532,12 +533,28 @@ EOF`);
532
533
  if (!host) throw new Error('copyDirToNode requires a host');
533
534
  if (!localDir || !fs.existsSync(localDir)) throw new Error(`copyDirToNode: local dir not found: ${localDir}`);
534
535
  if (!remoteDir) throw new Error('copyDirToNode requires a remoteDir');
535
- shellExec(`chmod 600 ${keyPath}`, { silent: true, silentOnError: true, disableLog: true });
536
- const sshOpts = `-i ${keyPath} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${port}`;
537
- shellExec(`ssh ${sshOpts} ${user}@${host} 'mkdir -p ${remoteDir}'`);
538
- shellExec(`tar -C ${localDir} -c . | ssh ${sshOpts} ${user}@${host} 'tar -C ${remoteDir} -x'`);
539
- const fixups = `${owner ? `chown -R ${owner} ${remoteDir}; ` : ''}${mode ? `chmod -R ${mode} ${remoteDir}` : ''}`.trim();
540
- if (fixups) shellExec(`ssh ${sshOpts} ${user}@${host} '${fixups}'`);
536
+ try {
537
+ shellExec(`chmod 600 ${keyPath}`, { silent: true, silentOnError: true, disableLog: true });
538
+ const sshOpts = `-i ${keyPath} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${port}`;
539
+ shellExec(`ssh ${sshOpts} ${user}@${host} 'mkdir -p ${remoteDir}'`, {
540
+ silent: true,
541
+ disableLog: true,
542
+ });
543
+ shellExec(`tar -C ${localDir} -c . | ssh ${sshOpts} ${user}@${host} 'tar -C ${remoteDir} -x'`, {
544
+ silent: true,
545
+ disableLog: true,
546
+ });
547
+ const fixups =
548
+ `${owner ? `chown -R ${owner} ${remoteDir}; ` : ''}${mode ? `chmod -R ${mode} ${remoteDir}` : ''}`.trim();
549
+ if (fixups)
550
+ shellExec(`ssh ${sshOpts} ${user}@${host} '${fixups}'`, {
551
+ silent: true,
552
+ disableLog: true,
553
+ });
554
+ } catch (err) {
555
+ logger.error(`copyDirToNode failed`);
556
+ process.exit(1);
557
+ }
541
558
  },
542
559
 
543
560
  /**
@@ -590,8 +607,8 @@ EOF
590
607
  },
591
608
 
592
609
  /**
593
- * Waits until a TCP SSH port becomes reachable on a host.
594
- * @async
610
+ * Waits until a TCP SSH port becomes reachable on a host. Delegates to
611
+ * {@link ServerConfBuilder.waitForPort}, which owns the probe.
595
612
  * @function waitForSshPort
596
613
  * @memberof UnderpostSSH
597
614
  * @param {object} params
@@ -601,25 +618,14 @@ EOF
601
618
  * @param {number} [params.intervalMs=3000] - Poll interval.
602
619
  * @returns {Promise<boolean>} True once the port accepts connections, false on timeout.
603
620
  */
604
- waitForSshPort: async ({ host, port = 22, timeoutMs = 10 * 60 * 1000, intervalMs = 3000 }) => {
605
- const deadline = Date.now() + timeoutMs;
606
- while (Date.now() < deadline) {
607
- const probe = shellExec(
608
- `timeout 5 bash -c '</dev/tcp/${host}/${port}' >/dev/null 2>&1 && echo open || echo closed`,
609
- { silent: true, stdout: true, silentOnError: true, disableLog: true },
610
- );
611
- if (`${probe}`.trim() === 'open') return true;
612
- await new Promise((r) => setTimeout(r, intervalMs));
613
- }
614
- logger.warn(`SSH port ${host}:${port} not reachable within timeout`);
615
- return false;
616
- },
621
+ waitForSshPort: ({ host, port = 22, timeoutMs = 10 * 60 * 1000, intervalMs = 3000 }) =>
622
+ waitForPort({ host, port, open: true, timeoutMs, intervalMs }),
617
623
 
618
624
  /**
619
625
  * Waits until a host's SSH port stops accepting connections (e.g. while it
620
626
  * reboots). Used to detect a reboot edge before waiting for the port to come
621
627
  * back up, so callers don't latch onto the pre-reboot (ephemeral) sshd.
622
- * @async
628
+ * Delegates to {@link ServerConfBuilder.waitForPort}.
623
629
  * @function waitForSshPortClosed
624
630
  * @memberof UnderpostSSH
625
631
  * @param {object} params
@@ -629,18 +635,8 @@ EOF
629
635
  * @param {number} [params.intervalMs=3000] - Poll interval.
630
636
  * @returns {Promise<boolean>} True once the port is closed, false on timeout.
631
637
  */
632
- waitForSshPortClosed: async ({ host, port = 22, timeoutMs = 3 * 60 * 1000, intervalMs = 3000 }) => {
633
- const deadline = Date.now() + timeoutMs;
634
- while (Date.now() < deadline) {
635
- const probe = shellExec(
636
- `timeout 5 bash -c '</dev/tcp/${host}/${port}' >/dev/null 2>&1 && echo open || echo closed`,
637
- { silent: true, stdout: true, silentOnError: true, disableLog: true },
638
- );
639
- if (`${probe}`.trim() === 'closed') return true;
640
- await new Promise((r) => setTimeout(r, intervalMs));
641
- }
642
- return false;
643
- },
638
+ waitForSshPortClosed: ({ host, port = 22, timeoutMs = 3 * 60 * 1000, intervalMs = 3000 }) =>
639
+ waitForPort({ host, port, open: false, timeoutMs, intervalMs }),
644
640
 
645
641
  /**
646
642
  * Orchestrates a non-interactive, key-only SSH session against a freshly
@@ -703,11 +699,14 @@ EOF
703
699
 
704
700
  let last = { ok: false, code: 255, stdout: '', stderr: '', attempts: 0 };
705
701
  for (let attempt = 1; attempt <= retries; attempt++) {
706
- const result = shellExec(`ssh ${sshOpts} ${user}@${host} bash -s <<'UNDERPOST_SSH_BATCH_EOF'\n${command}\nUNDERPOST_SSH_BATCH_EOF`, {
707
- stdout: false,
708
- silentOnError: true,
709
- disableLog: true,
710
- });
702
+ const result = shellExec(
703
+ `ssh ${sshOpts} ${user}@${host} bash -s <<'UNDERPOST_SSH_BATCH_EOF'\n${command}\nUNDERPOST_SSH_BATCH_EOF`,
704
+ {
705
+ stdout: false,
706
+ silentOnError: true,
707
+ disableLog: true,
708
+ },
709
+ );
711
710
  last = {
712
711
  ok: result.code === 0,
713
712
  code: result.code,
@@ -425,8 +425,8 @@ class Modal {
425
425
  s(`.main-body-btn-ui-menu-menu`).classList.add('hide');
426
426
  s(`.main-body-btn-ui-menu-close`).classList.remove('hide');
427
427
  if (s(`.btn-bar-center-icon-menu`)) {
428
- s(`.btn-bar-center-icon-close`).classList.remove('hide');
429
- s(`.btn-bar-center-icon-menu`).classList.add('hide');
428
+ sa(`.btn-bar-center-icon-close`).forEach((el) => el.classList.remove('hide'));
429
+ sa(`.btn-bar-center-icon-menu`).forEach((el) => el.classList.add('hide'));
430
430
  }
431
431
 
432
432
  s(`.main-body-btn-container`).style[
@@ -449,8 +449,8 @@ class Modal {
449
449
  s(`.main-body-btn-ui-menu-close`).classList.add('hide');
450
450
  s(`.main-body-btn-ui-menu-menu`).classList.remove('hide');
451
451
  if (s(`.btn-bar-center-icon-menu`)) {
452
- s(`.btn-bar-center-icon-menu`).classList.remove('hide');
453
- s(`.btn-bar-center-icon-close`).classList.add('hide');
452
+ sa(`.btn-bar-center-icon-menu`).forEach((el) => el.classList.remove('hide'));
453
+ sa(`.btn-bar-center-icon-close`).forEach((el) => el.classList.add('hide'));
454
454
  }
455
455
  s(`.main-body-btn-container`).style[
456
456
  true || (options.mode && options.mode.match('right')) ? 'right' : 'left'
@@ -683,6 +683,34 @@ class Modal {
683
683
  >
684
684
  </div>`
685
685
  : ''}
686
+ ${idModal === 'modal-menu' && options.mode !== 'slide-menu-right'
687
+ ? html`<div
688
+ class="abs main-btn-menu-top-container"
689
+ style="bottom: 0px; left: 0px; z-index: 10; height: ${originHeightTopBar}px; width: ${originHeightTopBar}px"
690
+ >
691
+ ${await BtnIcon.instance({
692
+ style: `height: 100%`,
693
+ class: `in fll main-btn-menu-top action-bar-box action-btn-center-top`,
694
+ label: html`<div class="abs center">
695
+ <i class="far fa-square btn-bar-center-icon-square hide"></i>
696
+ <span class="btn-bar-center-icon-close hide">${barConfig.buttons.close.label}</span>
697
+ <span class="btn-bar-center-icon-menu">${barConfig.buttons.menu.label}</span>
698
+ </div>`,
699
+ })}
700
+ </div>
701
+
702
+ <style>
703
+ .a-link-top-banner {
704
+ padding-left: 35px;
705
+ }
706
+ .main-body-btn-bar-custom {
707
+ top: 50px !important;
708
+ }
709
+ .main-body-btn-menu {
710
+ display: none;
711
+ }
712
+ </style>`
713
+ : ''}
686
714
  </div>`,
687
715
  );
688
716
  EventsUI.onClick(`.action-btn-profile-log-in`, () => {
@@ -697,6 +725,12 @@ class Modal {
697
725
  }
698
726
  s(`.main-btn-sign-up`).click();
699
727
  });
728
+ if (idModal === 'modal-menu' && options.mode !== 'slide-menu-right') {
729
+ EventsUI.onClick(`.action-btn-center-top`, (e) => {
730
+ e.preventDefault();
731
+ Modal.actionBtnCenter();
732
+ });
733
+ }
700
734
  s(`.input-info-${inputSearchBoxId}`).style.textAlign = 'left';
701
735
  htmls(`.input-info-${inputSearchBoxId}`, '');
702
736
  const inputInfoNode = s(`.input-info-${inputSearchBoxId}`).cloneNode(true);
@@ -23,12 +23,81 @@ import { shellExec } from '../server/process.js';
23
23
  import { SitemapStream, streamToPromise } from 'sitemap';
24
24
  import { Readable } from 'stream';
25
25
  import { buildIcons } from './client-icons.js';
26
+ import { statusPageBuildSegment } from '../server/underpost-gateway.js';
26
27
  import Underpost from '../index.js';
27
28
  import { buildDocs } from './client-build-docs.js';
28
29
  import { ssrFactory } from './ssr.js';
29
30
 
30
31
  // Static Site Generation (SSG)
31
32
 
33
+ const STATUS_PAGE_VIEW_PATH = /^\/([1-5]\d{2})$/;
34
+
35
+ // Views a route intercepts before the workload sees the request. They are
36
+ // declared by the flag that already marks them as the app's default for that
37
+ // condition, so a new one becomes edge-served by adding its flag here rather
38
+ // than by naming its path in a second place.
39
+ const INTERCEPT_VIEW_FLAGS = ['maintenanceDefault', 'offlineDefault'];
40
+
41
+ /**
42
+ * Resolves the SSR views that render an HTTP status page into the static
43
+ * artifacts they build to. A view is a status page when its route path is a
44
+ * bare status code (`/404`, `/500`, `/503`), which the build writes to
45
+ * `<path>/index.html` inside the served bundle.
46
+ *
47
+ * Single source of truth for PWA status-page routing: this build writes the
48
+ * artifact, and `deploy --build-manifest` points its HTTPRoute rules at the
49
+ * same resolved URL — neither side hardcodes a status code.
50
+ * @function statusPageRoutesFactory
51
+ * @param {Array<object>} [views] - SSR view entries from `conf.ssr.json`.
52
+ * @param {string} [proxyPath] - The client's proxy sub-path (`/`, `/peer`, ...).
53
+ * @returns {Array<{status: string, routePath: string, indexUrl: string, title: string, client: string}>}
54
+ * One entry per status view, in declaration order.
55
+ * @memberof clientBuild
56
+ */
57
+ const statusPageRoutesFactory = ({ views = [], proxyPath = '/' } = {}) => {
58
+ const prefix = !proxyPath || proxyPath === '/' ? '' : proxyPath.replace(/\/$/, '');
59
+ return (Array.isArray(views) ? views : [])
60
+ .map((view) => ({ view, status: STATUS_PAGE_VIEW_PATH.exec(view?.path || '')?.[1] }))
61
+ .filter(({ status }) => status !== undefined)
62
+ .map(({ view, status }) => ({
63
+ status,
64
+ routePath: `${prefix}${view.path}`,
65
+ indexUrl: `${prefix}${view.path}/index.html`,
66
+ title: view.title,
67
+ client: view.client,
68
+ }));
69
+ };
70
+
71
+ /**
72
+ * Resolves the SSR views a gateway route intercepts and serves statically —
73
+ * the maintenance and offline documents. They carry no request-time logic, so
74
+ * the workload never needs to see them; the same build that writes the artifact
75
+ * hands `deploy --build-manifest` the URL its HTTPRoute rule targets.
76
+ * @function staticContextRoutesFactory
77
+ * @param {Array<object>} [views] - SSR view entries from `conf.ssr.json`.
78
+ * @param {string} [proxyPath] - The client's proxy sub-path (`/`, `/peer`, ...).
79
+ * @returns {Array<{context: string, routePath: string, indexUrl: string, title: string, client: string}>}
80
+ * One entry per intercepted view, in declaration order.
81
+ * @memberof clientBuild
82
+ */
83
+ const staticContextRoutesFactory = ({ views = [], proxyPath = '/' } = {}) => {
84
+ const prefix = !proxyPath || proxyPath === '/' ? '' : proxyPath.replace(/\/$/, '');
85
+ return (Array.isArray(views) ? views : [])
86
+ .filter(
87
+ (view) =>
88
+ view?.path &&
89
+ !STATUS_PAGE_VIEW_PATH.test(view.path) &&
90
+ INTERCEPT_VIEW_FLAGS.some((flag) => view[flag] === true),
91
+ )
92
+ .map((view) => ({
93
+ context: view.path.replace(/^\/+|\/+$/g, ''),
94
+ routePath: `${prefix}${view.path}`,
95
+ indexUrl: `${prefix}${view.path}/index.html`,
96
+ title: view.title,
97
+ client: view.client,
98
+ }));
99
+ };
100
+
32
101
  /**
33
102
  * Recursively copies files from source to destination, but only files that don't exist in destination.
34
103
  * @function copyNonExistingFiles
@@ -742,14 +811,12 @@ const buildClient = async (
742
811
  }
743
812
 
744
813
  if (views) {
745
- if (
746
- !(
747
- enableLiveRebuild &&
748
- !options.liveClientBuildPaths.find(
749
- (p) => p.srcBuildPath.startsWith(`./src/client/ssr`) || p.srcBuildPath.slice(-9) === '.index.js',
750
- )
814
+ if (!(
815
+ enableLiveRebuild &&
816
+ !options.liveClientBuildPaths.find(
817
+ (p) => p.srcBuildPath.startsWith(`./src/client/ssr`) || p.srcBuildPath.slice(-9) === '.index.js',
751
818
  )
752
- )
819
+ ))
753
820
  for (const view of views) {
754
821
  const buildPath = `${
755
822
  rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
@@ -977,6 +1044,8 @@ Sitemap: ${sitemapBaseUrl}/sitemap.xml`,
977
1044
  // when the network is unreachable.
978
1045
  const ssrClientConf = confSSR[getCapVariableName(client)] || {};
979
1046
  const ssrViews = Array.isArray(ssrClientConf.views) ? ssrClientConf.views : [];
1047
+ const statusPageRoutes = statusPageRoutesFactory({ views: ssrViews, proxyPath: path });
1048
+ if (statusPageRoutes.length > 0) logger.info('ssr status page routes', statusPageRoutes);
980
1049
  const PRE_CACHED_RESOURCES = [];
981
1050
  let offlineFallbackUrl = null;
982
1051
  let maintenanceFallbackUrl = null;
@@ -999,9 +1068,16 @@ Sitemap: ${sitemapBaseUrl}/sitemap.xml`,
999
1068
  renderApi: { JSONweb },
1000
1069
  });
1001
1070
 
1002
- const buildPath = `${
1003
- rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
1004
- }${view.path === '/' ? view.path : `${view.path}/`}`;
1071
+ // A status view is built under `status-pages/<status>/`, not on its own
1072
+ // `/<status>` route: the gateway serves it by intercepting the
1073
+ // runtime's error, and a document on that route would give the runtime
1074
+ // a page of its own to serve or redirect to for the same condition.
1075
+ const statusCode = statusPageRoutes.find((route) => route.routePath === `${proxyPrefix}${view.path}`)?.status;
1076
+ const clientRoot =
1077
+ rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath;
1078
+ const buildPath = statusCode
1079
+ ? `${clientRoot}/${dir.dirname(statusPageBuildSegment(statusCode))}/`
1080
+ : `${clientRoot}${view.path === '/' ? view.path : `${view.path}/`}`;
1005
1081
 
1006
1082
  const indexUrl = buildIndexUrl(view.path);
1007
1083
  if (view.offlineDefault) {
@@ -1089,4 +1165,11 @@ ${swTransformedJs}`,
1089
1165
  }
1090
1166
  };
1091
1167
 
1092
- export { buildClient, copyNonExistingFiles, unzipClientBuild, mergeClientBuildZip };
1168
+ export {
1169
+ buildClient,
1170
+ copyNonExistingFiles,
1171
+ unzipClientBuild,
1172
+ mergeClientBuildZip,
1173
+ staticContextRoutesFactory,
1174
+ statusPageRoutesFactory,
1175
+ };
@@ -9,9 +9,8 @@ import vm from 'node:vm';
9
9
 
10
10
  import Underpost from '../index.js';
11
11
 
12
- import { srcFormatted, JSONweb } from './client-formatted.js';
12
+ import { srcFormatted } from './client-formatted.js';
13
13
  import { loggerFactory } from '../server/logger.js';
14
- import { getRootDirectory } from '../server/process.js';
15
14
 
16
15
  const logger = loggerFactory(import.meta);
17
16
 
@@ -47,78 +46,33 @@ const sanitizeHtml = (res, req, html) => {
47
46
  };
48
47
 
49
48
  /**
50
- * Factory function to create Express middleware for handling 404 and 500 errors.
51
- * It generates server-side rendered HTML for these error pages. If static error pages exist, it redirects to them.
52
- * @param {object} options - The options for creating the middleware.
53
- * @param {object} options.app - The Express app instance.
54
- * @param {string} options.directory - The directory for the instance's static files.
55
- * @param {string} options.rootHostPath - The root path for the host's public files.
56
- * @param {string} options.path - The base path for the instance.
57
- * @returns {Promise<{error500: Function, error400: Function}>} A promise that resolves to an object containing the 500 and 404 error handling middleware.
49
+ * Creates the Express middleware that terminates an unmatched request and an
50
+ * unhandled error.
51
+ *
52
+ * Both return a bare status and nothing else. Status page delivery belongs to
53
+ * the edge: the gateway intercepts the status and serves the declared document
54
+ * from `underpost-gateway`, preserving this response's code and the client's
55
+ * URI. A runtime that rendered its own page, redirected to one, or fetched one
56
+ * over HTTP would be competing with that and would be the only one of the
57
+ * three runtimes doing so.
58
+ * @param {string} [path] - The instance's proxy sub-path, used only to alias `/home`.
59
+ * @returns {Promise<{error500: Function, error400: Function}>} The two terminators.
58
60
  * @memberof ServerSideRendering
59
61
  */
60
- const ssrMiddlewareFactory = async ({ app, directory, rootHostPath, path }) => {
61
- const Render = await ssrFactory();
62
- const ssrPath = path === '/' ? path : `${path}/`;
63
-
64
- // Build default html src for 404 and 500
65
-
66
- const defaultHtmlSrc404 = Render({
67
- title: '404 Not Found',
68
- ssrPath,
69
- ssrHeadComponents: '',
70
- ssrBodyComponents: (await ssrFactory(`./src/client/ssr/body/404.js`))(),
71
- renderPayload: {
72
- apiBasePath: process.env.BASE_API,
73
- version: Underpost.version,
74
- },
75
- renderApi: {
76
- JSONweb,
77
- },
78
- });
79
- const path404 = `${directory ? directory : `${getRootDirectory()}${rootHostPath}`}/404/index.html`;
80
- const page404 = fs.existsSync(path404) ? `${path === '/' ? '' : path}/404` : undefined;
81
-
82
- const defaultHtmlSrc500 = Render({
83
- title: '500 Server Error',
84
- ssrPath,
85
- ssrHeadComponents: '',
86
- ssrBodyComponents: (await ssrFactory(`./src/client/ssr/body/500.js`))(),
87
- renderPayload: {
88
- apiBasePath: process.env.BASE_API,
89
- version: Underpost.version,
90
- },
91
- renderApi: {
92
- JSONweb,
93
- },
94
- });
95
- const path500 = `${directory ? directory : `${getRootDirectory()}${rootHostPath}`}/500/index.html`;
96
- const page500 = fs.existsSync(path500) ? `${path === '/' ? '' : path}/500` : undefined;
97
-
98
- return {
99
- error500: function (err, req, res, next) {
100
- logger.error(err, err.stack);
101
- if (page500) return res.status(500).redirect(page500);
102
- else {
103
- res.set('Content-Type', 'text/html');
104
- return res.status(500).send(sanitizeHtml(res, req, defaultHtmlSrc500));
105
- }
106
- },
107
- error400: function (req, res, next) {
108
- // if /<path>/home redirect to /<path>
109
- const homeRedirectPath = `${path === '/' ? '' : path}/home`;
110
- if (req.url.startsWith(homeRedirectPath)) {
111
- const redirectUrl = req.url.replace('/home', '');
112
- return res.redirect(redirectUrl.startsWith('/') ? redirectUrl : `/${redirectUrl}`);
113
- }
114
-
115
- if (page404) return res.status(404).redirect(page404);
116
- else {
117
- res.set('Content-Type', 'text/html');
118
- return res.status(404).send(sanitizeHtml(res, req, defaultHtmlSrc404));
119
- }
120
- },
121
- };
122
- };
62
+ const ssrMiddlewareFactory = async ({ path = '/' } = {}) => ({
63
+ error500: function (err, req, res, next) {
64
+ logger.error(err, err.stack);
65
+ return res.sendStatus(500);
66
+ },
67
+ error400: function (req, res, next) {
68
+ // `/<path>/home` is an alias of `/<path>`, not a missing route.
69
+ const homeRedirectPath = `${path === '/' ? '' : path}/home`;
70
+ if (req.url.startsWith(homeRedirectPath)) {
71
+ const redirectUrl = req.url.replace('/home', '');
72
+ return res.redirect(redirectUrl.startsWith('/') ? redirectUrl : `/${redirectUrl}`);
73
+ }
74
+ return res.sendStatus(404);
75
+ },
76
+ });
123
77
 
124
78
  export { ssrMiddlewareFactory, ssrFactory, sanitizeHtml };