underpost 3.2.9 → 3.2.10

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 (81) hide show
  1. package/.github/workflows/npmpkg.ci.yml +1 -0
  2. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  3. package/.github/workflows/release.cd.yml +1 -0
  4. package/.vscode/settings.json +10 -5
  5. package/CHANGELOG.md +122 -1
  6. package/CLI-HELP.md +22 -7
  7. package/README.md +37 -8
  8. package/bin/build.js +26 -9
  9. package/bin/deploy.js +20 -21
  10. package/bin/file.js +31 -13
  11. package/bin/index.js +2 -1
  12. package/bin/vs.js +1 -1
  13. package/bump.config.js +26 -0
  14. package/conf.js +20 -4
  15. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  16. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  17. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  18. package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
  19. package/manifests/kind-config-dev.yaml +8 -0
  20. package/manifests/mongodb/pv-pvc.yaml +44 -8
  21. package/manifests/mongodb/statefulset.yaml +55 -68
  22. package/package.json +27 -12
  23. package/scripts/k3s-node-setup.sh +28 -9
  24. package/src/api/core/core.router.js +19 -14
  25. package/src/api/core/core.service.js +5 -5
  26. package/src/api/default/default.router.js +22 -18
  27. package/src/api/default/default.service.js +5 -5
  28. package/src/api/document/document.router.js +28 -23
  29. package/src/api/document/document.service.js +100 -23
  30. package/src/api/file/file.router.js +19 -13
  31. package/src/api/file/file.service.js +9 -7
  32. package/src/api/test/test.router.js +17 -12
  33. package/src/api/types.js +24 -0
  34. package/src/api/user/guest.service.js +5 -4
  35. package/src/api/user/user.router.js +297 -288
  36. package/src/api/user/user.service.js +100 -35
  37. package/src/cli/baremetal.js +20 -11
  38. package/src/cli/cluster.js +196 -55
  39. package/src/cli/db.js +59 -60
  40. package/src/cli/deploy.js +273 -159
  41. package/src/cli/fs.js +3 -1
  42. package/src/cli/index.js +16 -9
  43. package/src/cli/ipfs.js +4 -6
  44. package/src/cli/kubectl.js +4 -1
  45. package/src/cli/lxd.js +217 -135
  46. package/src/cli/release.js +289 -131
  47. package/src/cli/repository.js +58 -7
  48. package/src/cli/run.js +152 -25
  49. package/src/cli/test.js +9 -3
  50. package/src/client/Default.index.js +9 -3
  51. package/src/client/components/core/Auth.js +4 -0
  52. package/src/client/components/core/PanelForm.js +56 -52
  53. package/src/client/components/core/Worker.js +162 -363
  54. package/src/client/sw/core.sw.js +174 -112
  55. package/src/db/DataBaseProvider.js +120 -20
  56. package/src/db/mongo/MongoBootstrap.js +587 -0
  57. package/src/db/mongo/MongooseDB.js +126 -22
  58. package/src/index.js +1 -1
  59. package/src/runtime/express/Express.js +2 -2
  60. package/src/runtime/wp/Wp.js +8 -5
  61. package/src/server/auth.js +2 -2
  62. package/src/server/client-build-docs.js +1 -1
  63. package/src/server/client-build.js +94 -129
  64. package/src/server/conf.js +20 -65
  65. package/src/server/process.js +180 -19
  66. package/src/server/runtime.js +1 -1
  67. package/src/server/start.js +12 -4
  68. package/src/ws/IoInterface.js +16 -16
  69. package/src/ws/core/channels/core.ws.chat.js +11 -11
  70. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  71. package/src/ws/core/channels/core.ws.stream.js +19 -19
  72. package/src/ws/core/core.ws.connection.js +8 -8
  73. package/src/ws/core/core.ws.server.js +6 -5
  74. package/src/ws/default/channels/default.ws.main.js +10 -10
  75. package/src/ws/default/default.ws.connection.js +4 -4
  76. package/src/ws/default/default.ws.server.js +4 -3
  77. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  78. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  79. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  80. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  81. /package/src/client/ssr/{pages → views}/Test.js +0 -0
@@ -1,5 +1,4 @@
1
1
  import mongoose from 'mongoose';
2
- import { loggerFactory } from '../../server/logger.js';
3
2
  import { getCapVariableName } from '../../client/components/core/CommonJs.js';
4
3
 
5
4
  /**
@@ -8,7 +7,33 @@ import { getCapVariableName } from '../../client/components/core/CommonJs.js';
8
7
  * @namespace MongooseDBService
9
8
  */
10
9
 
11
- const logger = loggerFactory(import.meta);
10
+ const MONGODB_SERVICE_NAME = 'mongodb-service';
11
+ const MONGODB_STATEFULSET_NAME = 'mongodb';
12
+ const MONGODB_DEFAULT_AUTH_SOURCE = 'admin';
13
+ const MONGODB_DEFAULT_REPLICA_SET = 'rs0';
14
+ const MONGODB_DEFAULT_REPLICA_COUNT = 3;
15
+
16
+
17
+
18
+ /**
19
+ * Resolves MongoDB replica hosts from explicit input or StatefulSet defaults.
20
+ * @param {{hostList?: string, replicaCount?: number}} [options] - Host resolution options.
21
+ * @returns {Array<string>} Normalized host:port entries.
22
+ */
23
+ const resolveMongoReplicaHosts = ({ hostList = '', replicaCount = MONGODB_DEFAULT_REPLICA_COUNT }) => {
24
+ if (hostList) {
25
+ return hostList
26
+ .split(',')
27
+ .map((host) => host.trim())
28
+ .filter(Boolean)
29
+ .map((host) => (host.includes(':') ? host : `${host}:27017`));
30
+ }
31
+
32
+ return Array.from(
33
+ { length: replicaCount },
34
+ (_, index) => `${MONGODB_STATEFULSET_NAME}-${index}.${MONGODB_SERVICE_NAME}:27017`,
35
+ );
36
+ };
12
37
 
13
38
  /**
14
39
  * @class
@@ -23,35 +48,106 @@ const logger = loggerFactory(import.meta);
23
48
  * 3. No built-in defaults — both `host` and `name` are required from the caller or environment.
24
49
  */
25
50
  class MongooseDBService {
51
+ /**
52
+ * Normalizes Mongo host inputs into plain host:port entries.
53
+ * @param {Array<string>|string} hosts - Host input as list or comma-separated string.
54
+ * @returns {Array<string>} Normalized host:port entries.
55
+ */
56
+ normalizeHosts(hosts) {
57
+ const hostEntries = Array.isArray(hosts) ? hosts : `${hosts || ''}`.split(',');
58
+
59
+ return hostEntries
60
+ .map((entry) => `${entry || ''}`.trim())
61
+ .filter(Boolean)
62
+ .map((entry) => entry.replace(/^mongodb:\/\//, '').replace(/\/.*$/, ''));
63
+ }
64
+
65
+ /**
66
+ * Normalizes connection config from object or legacy host/name signature.
67
+ * @param {object|string} configOrHost - Connection config object or host string.
68
+ * @param {string} [name] - Legacy DB name when using host string input.
69
+ * @returns {{authSource: string, dbName: string, hosts: Array<string>, password: string, replicaSet: string, user: string}} Normalized config.
70
+ */
71
+ normalizeConfig(configOrHost, name) {
72
+ const config =
73
+ typeof configOrHost === 'object' && configOrHost !== null
74
+ ? { ...configOrHost }
75
+ : { host: configOrHost, name };
76
+
77
+ const rawHosts = config.host || process.env.DB_HOST;
78
+ const hosts = this.normalizeHosts(rawHosts);
79
+ const dbName = config.name || process.env.DB_NAME;
80
+
81
+ if (!hosts.length || !dbName) {
82
+ const missing = [!hosts.length && 'host (db.host|DB_HOST)', !dbName && 'name (db.name|DB_NAME)']
83
+ .filter(Boolean)
84
+ .join(', ');
85
+ throw new Error(`MongooseDBService.connect: missing required parameter(s): ${missing}`);
86
+ }
87
+
88
+ const user = config.user || process.env.DB_USER || '';
89
+ const password = config.password || process.env.DB_PASSWORD || '';
90
+ const replicaSet =
91
+ config.replicaSet || process.env.DB_REPLICA_SET || (hosts.length > 1 ? MONGODB_DEFAULT_REPLICA_SET : '');
92
+ const authSource =
93
+ config.authSource || process.env.DB_AUTH_SOURCE || (user ? MONGODB_DEFAULT_AUTH_SOURCE : '');
94
+
95
+ return {
96
+ authSource,
97
+ dbName,
98
+ hosts,
99
+ password,
100
+ replicaSet,
101
+ user,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Builds a MongoDB URI from normalized config options.
107
+ * @param {object|string} configOrHost - Connection config object or host string.
108
+ * @param {string} [name] - Legacy DB name when using host string input.
109
+ * @returns {string} MongoDB connection URI.
110
+ */
111
+ buildUri(configOrHost, name) {
112
+ const config = this.normalizeConfig(configOrHost, name);
113
+ const credentials = config.user && config.password
114
+ ? `${encodeURIComponent(config.user)}:${encodeURIComponent(config.password)}@`
115
+ : '';
116
+ const query = new URLSearchParams();
117
+
118
+ if (config.replicaSet) query.set('replicaSet', config.replicaSet);
119
+ if (config.authSource) query.set('authSource', config.authSource);
120
+
121
+ return `mongodb://${credentials}${config.hosts.join(',')}/${config.dbName}${query.size ? `?${query.toString()}` : ''}`;
122
+ }
123
+
26
124
  /**
27
125
  * Establishes a Mongoose connection to the specified MongoDB instance.
28
126
  *
29
127
  * @async
30
- * @param {string} host - The MongoDB host URI (e.g., `'mongodb://localhost:27017'`).
31
- * Falls back to `process.env.DB_HOST` when not provided.
32
- * @param {string} name - The database name.
33
- * Falls back to `process.env.DB_NAME` when not provided.
128
+ * @param {object|string} configOrHost - Either a db config object or a legacy host string.
129
+ * @param {string} [configOrHost.host] - Legacy single host or comma-separated host list.
130
+ * @param {string} [configOrHost.name] - The database name.
131
+ * @param {string} [configOrHost.replicaSet] - The MongoDB replica set name.
132
+ * @param {string} [configOrHost.authSource] - The authentication database.
133
+ * @param {string} [configOrHost.user] - The MongoDB username.
134
+ * @param {string} [configOrHost.password] - The MongoDB password.
135
+ * @param {string} [name] - Legacy database name when a host string is passed.
34
136
  * @returns {Promise<mongoose.Connection>} A promise that resolves to the established Mongoose connection object.
35
137
  * @throws {Error} If neither the argument nor the corresponding environment variable supplies a value.
36
138
  */
37
- async connect(host, name) {
38
- host = host || process.env.DB_HOST;
39
- name = name || process.env.DB_NAME;
40
-
41
- if (!host || !name) {
42
- const missing = [!host && 'host (DB_HOST)', !name && 'name (DB_NAME)'].filter(Boolean).join(', ');
43
- throw new Error(`MongooseDBService.connect: missing required parameter(s): ${missing}`);
44
- }
45
-
46
- const uri = `${host}/${name}`;
47
- // logger.info('MongooseDB connect', { host, name, uri });
139
+ async connect(configOrHost, name) {
140
+ const uri = this.buildUri(configOrHost, name);
48
141
  return await mongoose
49
142
  .createConnection(uri, {
143
+ autoIndex: process.env.NODE_ENV !== 'production',
144
+ heartbeatFrequencyMS: 10000,
145
+ maxPoolSize: 20,
146
+ minPoolSize: 2,
147
+ retryReads: true,
148
+ retryWrites: true,
50
149
  serverSelectionTimeoutMS: 5000,
51
- // readPreference: 'primary',
52
- // directConnection: true,
53
- // useNewUrlParser: true,
54
- // useUnifiedTopology: true,
150
+ socketTimeoutMS: 45000,
55
151
  })
56
152
  .asPromise();
57
153
  }
@@ -87,4 +183,12 @@ class MongooseDBService {
87
183
  */
88
184
  const MongooseDB = new MongooseDBService();
89
185
 
90
- export { MongooseDB, MongooseDBService as MongooseDBClass };
186
+ export {
187
+ MongooseDB,
188
+ MongooseDBService as MongooseDBClass,
189
+ MONGODB_DEFAULT_REPLICA_COUNT,
190
+ MONGODB_DEFAULT_REPLICA_SET,
191
+ MONGODB_SERVICE_NAME,
192
+ MONGODB_STATEFULSET_NAME,
193
+ resolveMongoReplicaHosts
194
+ };
package/src/index.js CHANGED
@@ -44,7 +44,7 @@ class Underpost {
44
44
  * @type {String}
45
45
  * @memberof Underpost
46
46
  */
47
- static version = 'v3.2.9';
47
+ static version = 'v3.2.10';
48
48
 
49
49
  /**
50
50
  * Required Node.js major version
@@ -15,7 +15,7 @@ import { createServer } from 'http';
15
15
  import { loggerFactory, loggerMiddleware } from '../../server/logger.js';
16
16
  import { getCapVariableName, newInstance } from '../../client/components/core/CommonJs.js';
17
17
  import { MailerProvider } from '../../mailer/MailerProvider.js';
18
- import { DataBaseProvider } from '../../db/DataBaseProvider.js';
18
+ import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
19
19
  import { createPeerServer } from '../../server/peer.js';
20
20
  import { createValkeyConnection } from '../../server/valkey.js';
21
21
  import { applySecurity, authMiddlewareFactory } from '../../server/auth.js';
@@ -192,7 +192,7 @@ class ExpressService {
192
192
  }
193
193
 
194
194
  // Database and Valkey connections
195
- if (db && apis) await DataBaseProvider.load({ apis, host, path, db });
195
+ if (db && apis) await DataBaseProviderService.load({ apis, host, path, db });
196
196
 
197
197
  if (valkey) await createValkeyConnection({ host, path }, valkey);
198
198
 
@@ -56,10 +56,11 @@ class WpService {
56
56
  * to `/usr/local/bin/wp` if it is not already present.
57
57
  */
58
58
  static ensureWpCli() {
59
- const existing = shellExec(`PATH="${LAMPP_BIN}:$PATH" which wp 2>/dev/null || true`, {
59
+ const existing = shellExec(`PATH="${LAMPP_BIN}:$PATH" which wp`, {
60
60
  stdout: true,
61
61
  silent: true,
62
62
  disableLog: true,
63
+ silentOnError: true,
63
64
  });
64
65
  if (existing && existing.trim()) return;
65
66
  logger.info('WP-CLI not found — installing to /usr/local/bin/wp');
@@ -76,10 +77,11 @@ class WpService {
76
77
  */
77
78
  static ensureSendmail() {
78
79
  const sendmailPath = '/usr/sbin/sendmail';
79
- const existing = shellExec(`test -x "${sendmailPath}" && echo ok || true`, {
80
+ const existing = shellExec(`test -x "${sendmailPath}" && echo ok`, {
80
81
  stdout: true,
81
82
  silent: true,
82
83
  disableLog: true,
84
+ silentOnError: true,
83
85
  });
84
86
  if (existing && existing.trim() === 'ok') return;
85
87
  logger.info('sendmail stub missing — creating no-op at /usr/sbin/sendmail');
@@ -494,7 +496,7 @@ Thumbs.db
494
496
  * `git clone` yields a fully working site without needing a fresh install.
495
497
  *
496
498
  * Safe to call repeatedly — `git commit` is a no-op when the working tree
497
- * is clean (`|| true` prevents non-zero exit).
499
+ * is clean (`silentOnError: true` swallows the non-zero exit gracefully).
498
500
  *
499
501
  * @param {object} opts
500
502
  * @param {string} opts.siteRoot - Absolute path to the WordPress root.
@@ -514,7 +516,8 @@ Thumbs.db
514
516
 
515
517
  logger.info(`${host}: persisting site to repository`);
516
518
  shellExec(
517
- `cd "${siteRoot}" && git add -A && git commit -m "wp provision ${host} $(date -u +%Y-%m-%dT%H:%M:%SZ)" || true`,
519
+ `cd "${siteRoot}" && git add -A && git commit -m "wp provision ${host} $(date -u +%Y-%m-%dT%H:%M:%SZ)"`,
520
+ { silentOnError: true },
518
521
  );
519
522
  shellExec(`cd "${siteRoot}" && underpost push . ${githubOrg}/${repoName} -f`);
520
523
  logger.info(`${host}: initial commit pushed to ${githubOrg}/${repoName}`);
@@ -627,7 +630,7 @@ if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROT
627
630
 
628
631
  // MariaDB export is handled by the shared db.js backup flow — no duplicate dump here.
629
632
  if (fs.existsSync(path.join(siteRoot, '.git'))) {
630
- shellExec(`cd "${siteRoot}" && git add -A && git commit -m "wp backup $(date -u +%Y-%m-%dT%H:%M:%SZ)" || true`);
633
+ shellExec(`cd "${siteRoot}" && git add -A && git commit -m "wp backup $(date -u +%Y-%m-%dT%H:%M:%SZ)"`, { silentOnError: true });
631
634
  shellExec(`cd "${siteRoot}" && underpost push . ${githubOrg}/${repository.split('/').pop().split('.')[0]}`);
632
635
  logger.info(`backup: git push done for ${siteRoot}`);
633
636
  } else {
@@ -20,7 +20,7 @@ import rateLimit from 'express-rate-limit';
20
20
  import slowDown from 'express-slow-down';
21
21
  import cors from 'cors';
22
22
  import cookieParser from 'cookie-parser';
23
- import { DataBaseProvider } from '../db/DataBaseProvider.js';
23
+ import { DataBaseProviderService } from '../db/DataBaseProvider.js';
24
24
  import { isDevProxyContext } from './conf.js';
25
25
 
26
26
  const logger = loggerFactory(import.meta);
@@ -229,7 +229,7 @@ const authMiddlewareFactory = (options = { host: '', path: '' }) => {
229
229
 
230
230
  // Non-guest verify session exists
231
231
  if (payload.jwtid && payload.role !== 'guest') {
232
- const User = DataBaseProvider.instance[`${payload.host}${payload.path}`].mongoose.models.User;
232
+ const User = DataBaseProviderService.getModel('user', { host: payload.host, path: payload.path });
233
233
  const user = await User.findOne({ _id: payload._id, 'activeSessions._id': payload.jwtid }).lean();
234
234
 
235
235
  if (!user) {
@@ -399,7 +399,7 @@ const buildCoverage = async ({ docs, docsDestination }) => {
399
399
  shellExec(`cd ${coveragePath} && npm run coverage`, { silent: true });
400
400
  } else if (pkg.scripts && pkg.scripts.test) {
401
401
  logger.info('generating coverage via test', coveragePath);
402
- shellExec(`cd ${coveragePath} && npm test`, { silent: true });
402
+ shellExec(`cd ${coveragePath} && npm test`, { silent: true, silentOnError: true });
403
403
  }
404
404
  }
405
405
  }
@@ -724,23 +724,24 @@ const buildClient = async (
724
724
  const ssrPath = path === '/' ? path : `${path}/`;
725
725
  const Render = await ssrFactory();
726
726
 
727
- if (views) {
728
- const jsSrcPath = `./src/client/sw/core.sw.js`;
729
-
730
- const jsPublicPath = `${rootClientPath}/sw.js`;
731
-
732
- if (!(enableLiveRebuild && !options.liveClientBuildPaths.find((p) => p.srcBuildPath === jsSrcPath))) {
733
- const jsSrc = await transformClientJs(jsSrcPath, {
734
- dists,
735
- proxyPath: path,
736
- baseHost,
737
- minify: minifyBuild,
738
- externalizeBareImports: false,
739
- });
740
-
741
- fs.writeFileSync(jsPublicPath, jsSrc, 'utf8');
742
- }
727
+ const swSrcPath = `./src/client/sw/core.sw.js`;
728
+ const swPublicPath = `${rootClientPath}/sw.js`;
729
+ const swShouldRebuild =
730
+ views && !(enableLiveRebuild && !options.liveClientBuildPaths.find((p) => p.srcBuildPath === swSrcPath));
731
+ // Transformed SW JS is held in memory; it gets prepended with renderPayload
732
+ // and written once below, after PRE_CACHED_RESOURCES are known.
733
+ let swTransformedJs = '';
734
+ if (swShouldRebuild) {
735
+ swTransformedJs = await transformClientJs(swSrcPath, {
736
+ dists,
737
+ proxyPath: path,
738
+ baseHost,
739
+ minify: minifyBuild,
740
+ externalizeBareImports: false,
741
+ });
742
+ }
743
743
 
744
+ if (views) {
744
745
  if (
745
746
  !(
746
747
  enableLiveRebuild &&
@@ -750,9 +751,8 @@ const buildClient = async (
750
751
  )
751
752
  )
752
753
  for (const view of views) {
753
- const buildPath = `${
754
- rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
755
- }${view.path === '/' ? view.path : `${view.path}/`}`;
754
+ const buildPath = `${rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
755
+ }${view.path === '/' ? view.path : `${view.path}/`}`;
756
756
 
757
757
  if (!fs.existsSync(buildPath)) fs.mkdirSync(buildPath, { recursive: true });
758
758
 
@@ -768,9 +768,8 @@ const buildClient = async (
768
768
  fs.writeFileSync(`${buildPath}${buildId}.js`, jsSrc, 'utf8');
769
769
  const title = metadata.title ? metadata.title : title;
770
770
 
771
- const canonicalURL = `https://${host}${path}${
772
- view.path === '/' ? (path === '/' ? '' : '/') : path === '/' ? `${view.path.slice(1)}/` : `${view.path}/`
773
- }`;
771
+ const canonicalURL = `https://${host}${path}${view.path === '/' ? (path === '/' ? '' : '/') : path === '/' ? `${view.path.slice(1)}/` : `${view.path}/`
772
+ }`;
774
773
 
775
774
  let ssrHeadComponents = ``;
776
775
  let ssrBodyComponents = ``;
@@ -904,12 +903,12 @@ const buildClient = async (
904
903
  `${buildPath}index.html`,
905
904
  minifyBuild
906
905
  ? await minify(htmlSrc, {
907
- minifyCSS: true,
908
- minifyJS: true,
909
- collapseBooleanAttributes: true,
910
- collapseInlineTagWhitespace: true,
911
- collapseWhitespace: true,
912
- })
906
+ minifyCSS: true,
907
+ minifyJS: true,
908
+ collapseBooleanAttributes: true,
909
+ collapseInlineTagWhitespace: true,
910
+ collapseWhitespace: true,
911
+ })
913
912
  : htmlSrc,
914
913
  'utf8',
915
914
  );
@@ -967,119 +966,85 @@ Sitemap: ${sitemapBaseUrl}/sitemap.xml`,
967
966
  }
968
967
 
969
968
  if (client) {
970
- let PRE_CACHED_RESOURCES = [];
971
-
972
- const normalizePrecacheRoutePath = (candidatePath) => {
973
- const routePath =
974
- typeof candidatePath === 'string' && candidatePath.trim().length > 0 ? candidatePath.trim() : '/offline';
975
- const withLeadingSlash = routePath.startsWith('/') ? routePath : `/${routePath}`;
976
- const withoutTrailingSlash = withLeadingSlash.replace(/\/+$/, '');
977
- return withoutTrailingSlash.length > 0 ? withoutTrailingSlash : '/';
978
- };
979
-
980
- const toPrecacheIndexUrl = (routePath) => {
981
- const normalizedRoutePath = normalizePrecacheRoutePath(routePath);
982
- return `${path === '/' ? '' : path}${normalizedRoutePath === '/' ? '' : normalizedRoutePath}/index.html`;
983
- };
984
-
985
- for (const pageType of ['offline', 'pages']) {
986
- if (confSSR[getCapVariableName(client)] && confSSR[getCapVariableName(client)][pageType]) {
987
- for (const page of confSSR[getCapVariableName(client)][pageType]) {
988
- const SsrComponent = await ssrFactory(`./src/client/ssr/${pageType}/${page.client}.js`);
989
-
990
- const htmlSrc = Render({
991
- title: page.title,
992
- ssrPath,
993
- ssrHeadComponents: '<base target="_top">',
994
- ssrBodyComponents: SsrComponent(),
995
- renderPayload: {
996
- apiBaseProxyPath,
997
- apiBaseHost,
998
- apiBasePath: process.env.BASE_API,
999
- version: Underpost.version,
1000
- ...(isDevelopment ? { dev: true } : undefined),
1001
- },
1002
- renderApi: {
1003
- JSONweb,
1004
- },
1005
- });
1006
-
1007
- const buildPath = `${
1008
- rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
1009
- }${page.path === '/' ? page.path : `${page.path}/`}`;
1010
-
1011
- // Install-time precache is intentionally restricted to SSR offline pages.
1012
- // All other routes/assets are loaded lazily at runtime.
1013
- if (pageType === 'offline') {
1014
- PRE_CACHED_RESOURCES.push(toPrecacheIndexUrl(page.path));
1015
- }
1016
-
1017
- if (!fs.existsSync(buildPath)) fs.mkdirSync(buildPath, { recursive: true });
1018
-
1019
- const buildHtmlPath = `${buildPath}index.html`;
1020
-
1021
- logger.info('ssr page build', buildHtmlPath);
1022
-
1023
- fs.writeFileSync(
1024
- buildHtmlPath,
1025
- minifyBuild
1026
- ? await minify(htmlSrc, {
1027
- minifyCSS: true,
1028
- minifyJS: true,
1029
- collapseBooleanAttributes: true,
1030
- collapseInlineTagWhitespace: true,
1031
- collapseWhitespace: true,
1032
- })
1033
- : htmlSrc,
1034
- 'utf8',
1035
- );
1036
- }
1037
- }
1038
- }
969
+ const proxyPrefix = path === '/' ? '' : path;
970
+ const buildIndexUrl = (routePath) => `${proxyPrefix}${routePath === '/' ? '' : routePath}/index.html`;
971
+
972
+ // SSR views: a single declarative array. The role of each view (regular
973
+ // page vs. offline/maintenance fallback) is expressed by per-entry flags;
974
+ // fallback-flagged views are also precached so the SW can serve them
975
+ // when the network is unreachable.
976
+ const ssrClientConf = confSSR[getCapVariableName(client)] || {};
977
+ const ssrViews = Array.isArray(ssrClientConf.views) ? ssrClientConf.views : [];
978
+ const PRE_CACHED_RESOURCES = [];
979
+ let offlineFallbackUrl = null;
980
+ let maintenanceFallbackUrl = null;
981
+
982
+ for (const view of ssrViews) {
983
+ const SsrComponent = await ssrFactory(`./src/client/ssr/views/${view.client}.js`);
984
+
985
+ const htmlSrc = Render({
986
+ title: view.title,
987
+ ssrPath,
988
+ ssrHeadComponents: '<base target="_top">',
989
+ ssrBodyComponents: SsrComponent(),
990
+ renderPayload: {
991
+ apiBaseProxyPath,
992
+ apiBaseHost,
993
+ apiBasePath: process.env.BASE_API,
994
+ version: Underpost.version,
995
+ ...(isDevelopment ? { dev: true } : undefined),
996
+ },
997
+ renderApi: { JSONweb },
998
+ });
1039
999
 
1040
- {
1041
- const cacheScope = path === '/' ? 'root' : path.replaceAll('/', '_');
1042
- const ssrClientConf = confSSR[getCapVariableName(client)] || {};
1043
- const ssrOfflinePages = Array.isArray(ssrClientConf.offline) ? ssrClientConf.offline : [];
1044
- const normalizeSsrRoutePath = (candidatePath, fallbackPath) => {
1045
- const value =
1046
- typeof candidatePath === 'string' && candidatePath.trim().length > 0
1047
- ? candidatePath.trim()
1048
- : fallbackPath;
1049
- const withLeadingSlash = value.startsWith('/') ? value : `/${value}`;
1050
- const withoutTrailingSlash = withLeadingSlash.replace(/\/+$/, '');
1051
- return withoutTrailingSlash.length > 0 ? withoutTrailingSlash : '/';
1052
- };
1000
+ const buildPath = `${rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
1001
+ }${view.path === '/' ? view.path : `${view.path}/`}`;
1053
1002
 
1054
- const offlineSsrPage =
1055
- ssrOfflinePages.find(
1056
- (page) =>
1057
- page?.client === 'NoNetworkConnection' ||
1058
- /no\s*network|offline/i.test(`${page?.title || ''} ${page?.client || ''} ${page?.path || ''}`),
1059
- ) || ssrOfflinePages[0];
1003
+ const indexUrl = buildIndexUrl(view.path);
1004
+ if (view.offlineDefault) {
1005
+ offlineFallbackUrl = indexUrl;
1006
+ PRE_CACHED_RESOURCES.push(indexUrl);
1007
+ }
1008
+ if (view.maintenanceDefault) {
1009
+ maintenanceFallbackUrl = indexUrl;
1010
+ PRE_CACHED_RESOURCES.push(indexUrl);
1011
+ }
1060
1012
 
1061
- const maintenanceSsrPage =
1062
- ssrOfflinePages.find(
1063
- (page) =>
1064
- page?.client === 'Maintenance' ||
1065
- /maintenance/i.test(`${page?.title || ''} ${page?.client || ''} ${page?.path || ''}`),
1066
- ) || ssrOfflinePages[1];
1013
+ if (!fs.existsSync(buildPath)) fs.mkdirSync(buildPath, { recursive: true });
1014
+ const buildHtmlPath = `${buildPath}index.html`;
1015
+ logger.info('ssr view build', buildHtmlPath);
1067
1016
 
1068
- const offlinePath = normalizeSsrRoutePath(offlineSsrPage?.path, '/offline');
1069
- const maintenancePath = normalizeSsrRoutePath(maintenanceSsrPage?.path, '/maintenance');
1017
+ fs.writeFileSync(
1018
+ buildHtmlPath,
1019
+ minifyBuild
1020
+ ? await minify(htmlSrc, {
1021
+ minifyCSS: true,
1022
+ minifyJS: true,
1023
+ collapseBooleanAttributes: true,
1024
+ collapseInlineTagWhitespace: true,
1025
+ collapseWhitespace: true,
1026
+ })
1027
+ : htmlSrc,
1028
+ 'utf8',
1029
+ );
1030
+ }
1070
1031
 
1032
+ if (swShouldRebuild) {
1033
+ const cacheScope = path === '/' ? 'root' : path.replaceAll('/', '_');
1071
1034
  const renderPayload = {
1072
1035
  PRE_CACHED_RESOURCES: uniqueArray(PRE_CACHED_RESOURCES),
1073
1036
  PROXY_PATH: path,
1074
- CACHE_PREFIX: `engine-core-v3-${cacheScope}`,
1075
- OFFLINE_PATH: offlinePath,
1076
- MAINTENANCE_PATH: maintenancePath,
1037
+ CACHE_PREFIX: `engine-core-${cacheScope}`,
1038
+ OFFLINE_URL: offlineFallbackUrl || buildIndexUrl('/offline'),
1039
+ MAINTENANCE_URL: maintenanceFallbackUrl || buildIndexUrl('/maintenance'),
1077
1040
  };
1041
+
1042
+ // Single write: prepend the payload prelude to the transformed SW JS.
1078
1043
  fs.writeFileSync(
1079
- `${rootClientPath}/sw.js`,
1044
+ swPublicPath,
1080
1045
  `self.renderPayload = ${JSONweb(renderPayload)};
1081
1046
  self.__WB_DISABLE_DEV_LOGS = true;
1082
- ${fs.readFileSync(`${rootClientPath}/sw.js`, 'utf8')}`,
1047
+ ${swTransformedJs}`,
1083
1048
  'utf8',
1084
1049
  );
1085
1050
  }