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.
- package/.github/workflows/publish.ci.yml +3 -3
- package/.github/workflows/release.cd.yml +1 -1
- package/CHANGELOG.md +1358 -1038
- package/CLI-HELP.md +39 -16
- package/README.md +3 -3
- package/bin/build.js +10 -4
- package/bin/deploy.js +18 -16
- package/docker-compose.yml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/playwright/deployment.yaml +1 -1
- package/manifests/mongodb/kustomization.yaml +4 -1
- package/manifests/mongodb/statefulset.yaml +4 -0
- package/manifests/mongodb/storage-class.yaml +9 -2
- package/package.json +20 -20
- package/scripts/nat-iptables.sh +10 -4
- package/scripts/test-monitor.sh +4 -3
- package/src/api/core/core.controller.js +4 -65
- package/src/api/core/core.router.js +8 -14
- package/src/api/default/default.controller.js +2 -70
- package/src/api/default/default.router.js +7 -17
- package/src/api/document/document.controller.js +5 -77
- package/src/api/document/document.router.js +9 -13
- package/src/api/file/file.controller.js +9 -53
- package/src/api/file/file.router.js +14 -6
- package/src/api/test/test.controller.js +8 -53
- package/src/api/test/test.router.js +1 -4
- package/src/cli/cluster.js +771 -66
- package/src/cli/db.js +6 -4
- package/src/cli/deploy.js +1715 -168
- package/src/cli/docker-compose.js +19 -24
- package/src/cli/fs.js +0 -1
- package/src/cli/image.js +40 -13
- package/src/cli/index.js +129 -35
- package/src/cli/ipfs.js +82 -11
- package/src/cli/monitor.js +1 -1
- package/src/cli/release.js +4 -0
- package/src/cli/repository.js +14 -3
- package/src/cli/run.js +2253 -439
- package/src/cli/secrets.js +969 -0
- package/src/cli/ssh.js +38 -39
- package/src/client/components/core/Modal.js +38 -4
- package/src/client-builder/client-build.js +94 -11
- package/src/client-builder/ssr.js +27 -73
- package/src/db/mongo/MongoBootstrap.js +295 -54
- package/src/db/mongo/MongooseDB.js +47 -32
- package/src/index.js +1 -1
- package/src/server/conf.js +1307 -6
- package/src/server/cri.js +70 -0
- package/src/server/downloader.js +3 -3
- package/src/server/middlewares.js +152 -0
- package/src/server/underpost-gateway.js +1073 -0
- package/src/server/underpost-ingress.js +364 -0
- package/test/cluster-instances.test.js +435 -0
- package/test/deploy-node-placement.test.js +45 -0
- package/test/instance-traffic-plan.test.js +710 -0
- package/test/sops-secret-store.test.js +612 -0
- package/test/underpost-gateway.test.js +469 -0
- 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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
* @
|
|
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:
|
|
605
|
-
|
|
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
|
-
* @
|
|
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:
|
|
633
|
-
|
|
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(
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
429
|
-
|
|
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
|
-
|
|
453
|
-
|
|
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
|
-
|
|
748
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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 ({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 };
|