underpost 3.2.22 → 3.2.30

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 (57) hide show
  1. package/.github/workflows/ghpkg.ci.yml +1 -1
  2. package/.github/workflows/gitlab.ci.yml +1 -1
  3. package/.github/workflows/npmpkg.ci.yml +1 -1
  4. package/.github/workflows/publish.ci.yml +2 -2
  5. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -1
  6. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  7. package/CHANGELOG.md +181 -1
  8. package/CLI-HELP.md +61 -2
  9. package/README.md +3 -2
  10. package/baremetal/commission-workflows.json +1 -0
  11. package/bin/build.js +1 -0
  12. package/docker-compose.yml +224 -0
  13. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  14. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  15. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  16. package/package.json +27 -15
  17. package/scripts/kubeadm-node-setup.sh +317 -0
  18. package/scripts/rocky-kickstart.sh +877 -185
  19. package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
  20. package/scripts/test-monitor.sh +3 -5
  21. package/src/cli/baremetal.js +717 -16
  22. package/src/cli/cluster.js +3 -1
  23. package/src/cli/deploy.js +118 -9
  24. package/src/cli/docker-compose.js +591 -0
  25. package/src/cli/env.js +11 -2
  26. package/src/cli/fs.js +35 -11
  27. package/src/cli/index.js +89 -0
  28. package/src/cli/kickstart.js +142 -20
  29. package/src/cli/repository.js +153 -11
  30. package/src/cli/run.js +285 -8
  31. package/src/cli/secrets.js +7 -2
  32. package/src/cli/ssh.js +234 -0
  33. package/src/cli/static.js +2 -2
  34. package/src/{server → client-builder}/client-build-docs.js +15 -5
  35. package/src/{server → client-builder}/client-build-live.js +3 -3
  36. package/src/{server → client-builder}/client-build.js +25 -22
  37. package/src/{server → client-builder}/client-dev-server.js +3 -3
  38. package/src/{server → client-builder}/client-icons.js +2 -2
  39. package/src/{server → client-builder}/ssr.js +5 -5
  40. package/src/client.build.js +1 -3
  41. package/src/client.dev.js +1 -1
  42. package/src/db/mongo/MongoBootstrap.js +12 -12
  43. package/src/index.js +12 -1
  44. package/src/mailer/EmailRender.js +1 -1
  45. package/src/{server → projects/underpost}/catalog-underpost.js +1 -2
  46. package/src/runtime/express/Express.js +2 -2
  47. package/src/runtime/nginx/Nginx.js +250 -0
  48. package/src/server/catalog.js +7 -14
  49. package/src/server/conf.js +3 -3
  50. package/src/server/runtime-status.js +18 -1
  51. package/src/server/start.js +12 -2
  52. package/src/server.js +6 -2
  53. package/test/deploy-monitor.test.js +26 -10
  54. package/typedoc.json +3 -1
  55. package/src/server/ipfs-client.js +0 -599
  56. /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
  57. /package/src/{server → client-builder}/client-formatted.js +0 -0
@@ -2,13 +2,13 @@
2
2
 
3
3
  /**
4
4
  * Module for building project documentation (JSDoc, Swagger, Coverage).
5
- * @module src/server/client-build-docs.js
5
+ * @module src/client-builder/client-build-docs.js
6
6
  * @namespace clientBuildDocs
7
7
  */
8
8
 
9
9
  import fs from 'fs-extra';
10
- import { shellExec } from './process.js';
11
- import { loggerFactory } from './logger.js';
10
+ import { shellExec } from '../server/process.js';
11
+ import { loggerFactory } from '../server/logger.js';
12
12
  import { JSONweb } from './client-formatted.js';
13
13
  import { ssrFactory } from './ssr.js';
14
14
 
@@ -396,10 +396,20 @@ const buildCoverage = async ({ docs, docsDestination }) => {
396
396
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
397
397
  if (pkg.scripts && pkg.scripts.coverage) {
398
398
  logger.info('generating coverage report', coveragePath);
399
- shellExec(`cd ${coveragePath} && npm run coverage`, { silent: true });
399
+ try {
400
+ await shellExec(`cd ${coveragePath} && npm run coverage`, { silent: true });
401
+ } catch (err) {
402
+ logger.warn('coverage generation failed (non-fatal), skipping:', err.message);
403
+ return;
404
+ }
400
405
  } else if (pkg.scripts && pkg.scripts.test) {
401
406
  logger.info('generating coverage via test', coveragePath);
402
- shellExec(`cd ${coveragePath} && npm test`, { silent: true, silentOnError: true });
407
+ try {
408
+ await shellExec(`cd ${coveragePath} && npm test`, { silent: true, silentOnError: true });
409
+ } catch (err) {
410
+ logger.warn('coverage generation failed (non-fatal), skipping:', err.message);
411
+ return;
412
+ }
403
413
  }
404
414
  }
405
415
  }
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Module for live building client-side code
3
- * @module src/server/client-build-live.js
3
+ * @module src/client-builder/client-build-live.js
4
4
  * @namespace clientLiveBuild
5
5
  */
6
6
 
7
7
  import fs from 'fs-extra';
8
- import { Config, loadConf, readConfJson } from './conf.js';
9
- import { loggerFactory } from './logger.js';
8
+ import { Config, loadConf, readConfJson } from '../server/conf.js';
9
+ import { loggerFactory } from '../server/logger.js';
10
10
  import { buildClient } from './client-build.js';
11
11
 
12
12
  const logger = loggerFactory(import.meta);
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Manages the client-side build process, including full builds and incremental builds.
3
- * @module server/client-build.js
3
+ * @module src/client-builder/client-build.js
4
4
  * @namespace clientBuild
5
5
  */
6
6
 
@@ -8,18 +8,18 @@
8
8
 
9
9
  import fs from 'fs-extra';
10
10
  import { transformClientJs, JSONweb } from './client-formatted.js';
11
- import { loggerFactory } from './logger.js';
11
+ import { loggerFactory } from '../server/logger.js';
12
12
  import {
13
13
  getCapVariableName,
14
14
  newInstance,
15
15
  orderArrayFromAttrInt,
16
16
  uniqueArray,
17
17
  } from '../client/components/core/CommonJs.js';
18
- import { readConfJson } from './conf.js';
18
+ import { readConfJson } from '../server/conf.js';
19
19
  import { minify } from 'html-minifier-terser';
20
20
  import AdmZip from 'adm-zip';
21
21
  import * as dir from 'path';
22
- import { shellExec } from './process.js';
22
+ 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';
@@ -751,8 +751,9 @@ const buildClient = async (
751
751
  )
752
752
  )
753
753
  for (const view of views) {
754
- const buildPath = `${rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
755
- }${view.path === '/' ? view.path : `${view.path}/`}`;
754
+ const buildPath = `${
755
+ rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
756
+ }${view.path === '/' ? view.path : `${view.path}/`}`;
756
757
 
757
758
  if (!fs.existsSync(buildPath)) fs.mkdirSync(buildPath, { recursive: true });
758
759
 
@@ -768,8 +769,9 @@ const buildClient = async (
768
769
  fs.writeFileSync(`${buildPath}${buildId}.js`, jsSrc, 'utf8');
769
770
  const title = metadata.title ? metadata.title : title;
770
771
 
771
- const canonicalURL = `https://${host}${path}${view.path === '/' ? (path === '/' ? '' : '/') : path === '/' ? `${view.path.slice(1)}/` : `${view.path}/`
772
- }`;
772
+ const canonicalURL = `https://${host}${path}${
773
+ view.path === '/' ? (path === '/' ? '' : '/') : path === '/' ? `${view.path.slice(1)}/` : `${view.path}/`
774
+ }`;
773
775
 
774
776
  let ssrHeadComponents = ``;
775
777
  let ssrBodyComponents = ``;
@@ -903,12 +905,12 @@ const buildClient = async (
903
905
  `${buildPath}index.html`,
904
906
  minifyBuild
905
907
  ? await minify(htmlSrc, {
906
- minifyCSS: true,
907
- minifyJS: true,
908
- collapseBooleanAttributes: true,
909
- collapseInlineTagWhitespace: true,
910
- collapseWhitespace: true,
911
- })
908
+ minifyCSS: true,
909
+ minifyJS: true,
910
+ collapseBooleanAttributes: true,
911
+ collapseInlineTagWhitespace: true,
912
+ collapseWhitespace: true,
913
+ })
912
914
  : htmlSrc,
913
915
  'utf8',
914
916
  );
@@ -997,8 +999,9 @@ Sitemap: ${sitemapBaseUrl}/sitemap.xml`,
997
999
  renderApi: { JSONweb },
998
1000
  });
999
1001
 
1000
- const buildPath = `${rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
1001
- }${view.path === '/' ? view.path : `${view.path}/`}`;
1002
+ const buildPath = `${
1003
+ rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
1004
+ }${view.path === '/' ? view.path : `${view.path}/`}`;
1002
1005
 
1003
1006
  const indexUrl = buildIndexUrl(view.path);
1004
1007
  if (view.offlineDefault) {
@@ -1018,12 +1021,12 @@ Sitemap: ${sitemapBaseUrl}/sitemap.xml`,
1018
1021
  buildHtmlPath,
1019
1022
  minifyBuild
1020
1023
  ? await minify(htmlSrc, {
1021
- minifyCSS: true,
1022
- minifyJS: true,
1023
- collapseBooleanAttributes: true,
1024
- collapseInlineTagWhitespace: true,
1025
- collapseWhitespace: true,
1026
- })
1024
+ minifyCSS: true,
1025
+ minifyJS: true,
1026
+ collapseBooleanAttributes: true,
1027
+ collapseInlineTagWhitespace: true,
1028
+ collapseWhitespace: true,
1029
+ })
1027
1030
  : htmlSrc,
1028
1031
  'utf8',
1029
1032
  );
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Module for creating a client-side development server
3
- * @module src/server/client-dev-server.js
3
+ * @module src/client-builder/client-dev-server.js
4
4
  * @namespace clientDevServer
5
5
  */
6
6
  import fs from 'fs-extra';
7
7
  import nodemon from 'nodemon';
8
8
  import dotenv from 'dotenv';
9
- import { shellExec } from './process.js';
10
- import { loggerFactory } from './logger.js';
9
+ import { shellExec } from '../server/process.js';
10
+ import { loggerFactory } from '../server/logger.js';
11
11
  import Underpost from '../index.js';
12
12
 
13
13
  const logger = loggerFactory(import.meta);
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Module for building client-side icons
3
- * @module src/server/client-icons.js
3
+ * @module src/client-builder/client-icons.js
4
4
  * @namespace clientIcons
5
5
  */
6
6
  import { favicons } from 'favicons';
7
- import { loggerFactory } from './logger.js';
7
+ import { loggerFactory } from '../server/logger.js';
8
8
  import fs from 'fs-extra';
9
9
  import { getCapVariableName } from '../client/components/core/CommonJs.js';
10
10
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Module for managing server side rendering
3
- * @module src/server/ssr.js
3
+ * @module src/client-builder/ssr.js
4
4
  * @namespace ServerSideRendering
5
5
  */
6
6
 
@@ -10,19 +10,19 @@ import vm from 'node:vm';
10
10
  import Underpost from '../index.js';
11
11
 
12
12
  import { srcFormatted, JSONweb } from './client-formatted.js';
13
- import { loggerFactory } from './logger.js';
14
- import { getRootDirectory } from './process.js';
13
+ import { loggerFactory } from '../server/logger.js';
14
+ import { getRootDirectory } from '../server/process.js';
15
15
 
16
16
  const logger = loggerFactory(import.meta);
17
17
 
18
18
  /**
19
19
  * Creates a server-side rendering component function from a given file path.
20
20
  * It reads the component file, formats it, and executes it in a sandboxed Node.js VM context to extract the component.
21
- * @param {string} [componentPath='./src/client/ssr/Render.js'] - The path to the SSR component file.
21
+ * @param {string} [componentPath='./src/client/ssr/RootDocument.js'] - The path to the SSR component file.
22
22
  * @returns {Promise<Function>} A promise that resolves to the SSR component function.
23
23
  * @memberof ServerSideRendering
24
24
  */
25
- const ssrFactory = async (componentPath = `./src/client/ssr/Render.js`) => {
25
+ const ssrFactory = async (componentPath = `./src/client/ssr/RootDocument.js`) => {
26
26
  const context = { SrrComponent: () => {}, npm_package_version: Underpost.version };
27
27
  vm.createContext(context);
28
28
  vm.runInContext(await srcFormatted(fs.readFileSync(componentPath, 'utf8')), context);
@@ -6,7 +6,7 @@
6
6
  import { loggerFactory } from './server/logger.js';
7
7
  import { Config } from './server/conf.js';
8
8
  import { ProcessController } from './server/process.js';
9
- import { clientLiveBuild } from './server/client-build-live.js';
9
+ import { clientLiveBuild } from './client-builder/client-build-live.js';
10
10
 
11
11
  await Config.build();
12
12
 
@@ -15,5 +15,3 @@ const logger = loggerFactory(import.meta);
15
15
  await logger.setUpInfo();
16
16
 
17
17
  await clientLiveBuild();
18
-
19
- // ProcessController.init(logger);
package/src/client.dev.js CHANGED
@@ -6,7 +6,7 @@
6
6
  import { loggerFactory } from './server/logger.js';
7
7
  import { ProcessController } from './server/process.js';
8
8
  import { Config, buildClientStaticConf } from './server/conf.js';
9
- import { createClientDevServer } from './server/client-dev-server.js';
9
+ import { createClientDevServer } from './client-builder/client-dev-server.js';
10
10
 
11
11
  const logger = loggerFactory(import.meta);
12
12
 
@@ -290,19 +290,16 @@ class MongoBootstrap {
290
290
  * @returns {Promise<number>} Number of pods that failed to become ready (0 = all good).
291
291
  */
292
292
  static async waitForPods(namespace, replicaCount) {
293
- const { default: Underpost } = await import('../../index.js');
294
- const results = await Promise.all(
295
- Array.from({ length: replicaCount }, async (_, i) => {
296
- const podName = `${MONGODB_STATEFULSET_NAME}-${i}`;
297
- const ready = await Underpost.test.statusMonitor(podName, 'Running', 'pods', 1000, 60 * 10);
298
- return { index: i, ready };
299
- }),
300
- );
301
- const failed = results.filter((r) => !r.ready).map((r) => `${MONGODB_STATEFULSET_NAME}-${r.index}`);
302
- if (failed.length > 0) {
303
- logger.error('MongoDB pods did not become ready', { failed });
293
+ let failedCount = 0;
294
+ for (let i = 0; i < replicaCount; i++) {
295
+ const podName = `${MONGODB_STATEFULSET_NAME}-${i}`;
296
+ const result = shellExec(`kubectl wait --for=condition=Ready pod/${podName} -n ${namespace} --timeout=60s`);
297
+ if (result.code !== 0) {
298
+ logger.error(`Pod ${podName} did not become ready`);
299
+ failedCount++;
300
+ }
304
301
  }
305
- return failed.length;
302
+ return failedCount;
306
303
  }
307
304
 
308
305
  /**
@@ -620,6 +617,9 @@ class MongoBootstrap {
620
617
  process.env.DB_PASSWORD ||
621
618
  readTrimmedFile('./engine-private/mongodb-password');
622
619
 
620
+ // Ensure the pod is ready before querying
621
+ shellExec(`kubectl wait --for=condition=Ready pod/${podName} -n ${namespace} --timeout=60s`);
622
+
623
623
  const evalExpr = 'rs.status().members.filter(m=>m.stateStr=="PRIMARY").map(m=>m.name)';
624
624
 
625
625
  const cli = disableAuth ? 'mongo' : 'mongosh';
package/src/index.js CHANGED
@@ -10,6 +10,7 @@ import UnderpostKickStart from './cli/kickstart.js';
10
10
  import UnderpostCluster from './cli/cluster.js';
11
11
  import UnderpostDB from './cli/db.js';
12
12
  import UnderpostDeploy from './cli/deploy.js';
13
+ import UnderpostDockerCompose from './cli/docker-compose.js';
13
14
  import UnderpostKubectl from './cli/kubectl.js';
14
15
  import UnderpostRootEnv from './cli/env.js';
15
16
  import UnderpostFileStorage from './cli/fs.js';
@@ -44,7 +45,7 @@ class Underpost {
44
45
  * @type {String}
45
46
  * @memberof Underpost
46
47
  */
47
- static version = 'v3.2.22';
48
+ static version = 'v3.2.30';
48
49
 
49
50
  /**
50
51
  * Required Node.js major version
@@ -146,6 +147,15 @@ class Underpost {
146
147
  static get deploy() {
147
148
  return UnderpostDeploy.API;
148
149
  }
150
+ /**
151
+ * Docker Compose pipeline cli API
152
+ * @static
153
+ * @type {UnderpostDockerCompose.API}
154
+ * @memberof Underpost
155
+ */
156
+ static get dockerCompose() {
157
+ return UnderpostDockerCompose.API;
158
+ }
149
159
  /**
150
160
  * File Storage cli API
151
161
  * @static
@@ -321,6 +331,7 @@ export {
321
331
  UnderpostCluster,
322
332
  UnderpostDB,
323
333
  UnderpostDeploy,
334
+ UnderpostDockerCompose,
324
335
  UnderpostKubectl,
325
336
  UnderpostRootEnv,
326
337
  UnderpostFileStorage,
@@ -1,4 +1,4 @@
1
- import { ssrFactory } from '../server/ssr.js';
1
+ import { ssrFactory } from '../client-builder/ssr.js';
2
2
 
3
3
  /**
4
4
  * Module for handling the rendering and styling of HTML emails using SSR components.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Underpost platform content catalog — the base `pwa-microservices-template`.
3
3
  *
4
- * @module src/server/catalog-underpost.js
4
+ * @module src/projects/underpost/catalog-underpost.js
5
5
  * @namespace UnderpostCatalog
6
6
  */
7
7
 
@@ -11,7 +11,6 @@
11
11
  * @memberof UnderpostCatalog
12
12
  */
13
13
  const TEMPLATE_RESTORE_PATHS = [
14
- `./src/server/catalog-underpost.js`,
15
14
  `./.github/workflows/pwa-microservices-template-page.cd.yml`,
16
15
  `./.github/workflows/pwa-microservices-template-test.ci.yml`,
17
16
  `./.github/workflows/npmpkg.ci.yml`,
@@ -19,8 +19,8 @@ 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';
22
- import { ssrMiddlewareFactory } from '../../server/ssr.js';
23
- import { buildSwaggerUiOptions } from '../../server/client-build-docs.js';
22
+ import { ssrMiddlewareFactory } from '../../client-builder/ssr.js';
23
+ import { buildSwaggerUiOptions } from '../../client-builder/client-build-docs.js';
24
24
 
25
25
  import { shellExec } from '../../server/process.js';
26
26
  import { devProxyHostFactory, isDevProxyContext, isTlsDevProxy } from '../../server/conf.js';
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Exported singleton instance of the NginxService class.
3
+ * Manages dynamic generation of nginx reverse-proxy router configuration used
4
+ * by the Docker Compose development stack. Mirrors the conventions of
5
+ * {@link module:src/runtime/lampp/Lampp.js LamppService}.
6
+ * @module src/runtime/nginx/Nginx.js
7
+ * @namespace NginxService
8
+ */
9
+
10
+ import fs from 'fs-extra';
11
+ import path from 'path';
12
+ import { loggerFactory } from '../../server/logger.js';
13
+
14
+ const logger = loggerFactory(import.meta);
15
+
16
+ /**
17
+ * @class NginxService
18
+ * @description Builds nginx `server` blocks (the router) for fronting upstream
19
+ * application services and writes the rendered configuration to disk. The
20
+ * router is accumulated in memory via {@link NginxService#createApp} and
21
+ * flushed with {@link NginxService#writeConf}, keeping config generation
22
+ * decoupled from the filesystem location it is written to.
23
+ * @memberof NginxService
24
+ */
25
+ class NginxService {
26
+ /**
27
+ * @type {string}
28
+ * @description Accumulated nginx `server { ... }` blocks (the router definition).
29
+ * @memberof NginxService
30
+ */
31
+ router = '';
32
+
33
+ /**
34
+ * @type {Set<string>}
35
+ * @description Upstream blocks keyed by upstream name to avoid duplicates.
36
+ * @memberof NginxService
37
+ */
38
+ upstreams;
39
+
40
+ /**
41
+ * @type {boolean}
42
+ * @description Whether a default_server catch-all block has been emitted.
43
+ * @memberof NginxService
44
+ */
45
+ hasDefaultServer;
46
+
47
+ constructor() {
48
+ this.reset();
49
+ }
50
+
51
+ /**
52
+ * Resets the in-memory router, upstreams, and default-server flag.
53
+ * @method reset
54
+ * @returns {void}
55
+ * @memberof NginxService
56
+ */
57
+ reset() {
58
+ this.router = '';
59
+ this.upstreams = new Map();
60
+ this.hasDefaultServer = false;
61
+ }
62
+
63
+ /**
64
+ * Appends a raw render fragment to the router string.
65
+ * @method appendRouter
66
+ * @param {string} render - The configuration fragment to append.
67
+ * @returns {string} The complete, updated router configuration string.
68
+ * @memberof NginxService
69
+ */
70
+ appendRouter(render) {
71
+ if (!this.router) return (this.router = render);
72
+ return (this.router += render);
73
+ }
74
+
75
+ /**
76
+ * Clears the in-memory router configuration.
77
+ * @method removeRouter
78
+ * @returns {void}
79
+ * @memberof NginxService
80
+ */
81
+ removeRouter() {
82
+ this.reset();
83
+ }
84
+
85
+ /**
86
+ * Registers a named upstream pointing at a container service:port.
87
+ * Idempotent: repeated names with the same target are collapsed.
88
+ * @method addUpstream
89
+ * @param {string} name - The upstream identifier.
90
+ * @param {string} service - The Docker service name (resolved via service discovery).
91
+ * @param {number} port - The upstream container port.
92
+ * @returns {string} The upstream name.
93
+ * @memberof NginxService
94
+ */
95
+ addUpstream(name, service, port) {
96
+ this.upstreams.set(name, `upstream ${name} { server ${service}:${port}; }`);
97
+ return name;
98
+ }
99
+
100
+ /**
101
+ * Renders the standard proxy directive block shared by all locations,
102
+ * including websocket upgrade headers and forwarded headers.
103
+ * @method proxyLocation
104
+ * @param {string} location - The location path (e.g. '/', '/peer').
105
+ * @param {string} upstream - The upstream name to proxy to.
106
+ * @returns {string} The rendered `location { ... }` block.
107
+ * @memberof NginxService
108
+ */
109
+ proxyLocation(location, upstream) {
110
+ return `
111
+ location ${location} {
112
+ proxy_pass http://${upstream};
113
+ proxy_set_header Host $host;
114
+ proxy_set_header X-Real-IP $remote_addr;
115
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
116
+ proxy_set_header X-Forwarded-Proto $scheme;
117
+ proxy_set_header Upgrade $http_upgrade;
118
+ proxy_set_header Connection $connection_upgrade;
119
+ proxy_read_timeout 3600s;
120
+ }
121
+ `;
122
+ }
123
+
124
+ /**
125
+ * Renders the `/healthz` location used by the proxy container healthcheck.
126
+ * @method healthLocation
127
+ * @returns {string} The rendered health-check location block.
128
+ * @memberof NginxService
129
+ */
130
+ healthLocation() {
131
+ return `
132
+ location = /healthz {
133
+ access_log off;
134
+ return 200 "ok\\n";
135
+ add_header Content-Type text/plain;
136
+ }
137
+ `;
138
+ }
139
+
140
+ /**
141
+ * Creates an nginx virtual-host (`server`) entry for a host and appends it to
142
+ * the router. Each route maps a URL prefix to an upstream service:port with
143
+ * websocket support, mirroring the Contour HTTPProxy route model.
144
+ *
145
+ * @method createApp
146
+ * @param {object} options - Virtual host options.
147
+ * @param {string} options.host - The `server_name` (e.g. 'default.net').
148
+ * @param {number} [options.listen=80] - The listen port.
149
+ * @param {Array<{location: string, service: string, port: number}>} options.routes
150
+ * - Route table. Longer prefixes should precede '/' for correct matching.
151
+ * @param {boolean} [options.resetRouter] - Clear the router before appending.
152
+ * @returns {string} The complete, updated router configuration string.
153
+ * @memberof NginxService
154
+ */
155
+ createApp({ host, listen = 80, routes = [], resetRouter = false }) {
156
+ if (resetRouter) this.removeRouter();
157
+
158
+ const safeHost = host.replace(/[^a-zA-Z0-9]/g, '_');
159
+ const locationBlocks = routes
160
+ .map(({ location, service, port }) => {
161
+ const upstreamName = this.addUpstream(`up_${safeHost}_${port}`, service, port);
162
+ return this.proxyLocation(location, upstreamName);
163
+ })
164
+ .join('');
165
+
166
+ this.appendRouter(`
167
+ server {
168
+ listen ${listen};
169
+ server_name ${host};
170
+ ${this.healthLocation()}${locationBlocks}}
171
+ `);
172
+
173
+ return this.router;
174
+ }
175
+
176
+ /**
177
+ * Emits a `default_server` catch-all that forwards to the given upstream and
178
+ * answers the health probe for unmatched Host headers. Safe to call once.
179
+ *
180
+ * @method createDefaultServer
181
+ * @param {object} options - Default server options.
182
+ * @param {string} options.service - The Docker service name to fall back to.
183
+ * @param {number} options.port - The upstream container port.
184
+ * @param {number} [options.listen=80] - The listen port.
185
+ * @returns {string} The complete, updated router configuration string.
186
+ * @memberof NginxService
187
+ */
188
+ createDefaultServer({ service, port, listen = 80 }) {
189
+ if (this.hasDefaultServer) return this.router;
190
+ this.hasDefaultServer = true;
191
+ const upstreamName = this.addUpstream('up_default', service, port);
192
+ this.appendRouter(`
193
+ server {
194
+ listen ${listen} default_server;
195
+ server_name _;
196
+ ${this.healthLocation()}${this.proxyLocation('/', upstreamName)}}
197
+ `);
198
+ return this.router;
199
+ }
200
+
201
+ /**
202
+ * Renders the full nginx config document: the websocket connection map, all
203
+ * upstream blocks, the global `proxy_http_version`, and the accumulated router.
204
+ * @method render
205
+ * @returns {string} The complete nginx configuration file content.
206
+ * @memberof NginxService
207
+ */
208
+ render() {
209
+ const upstreamBlocks = Array.from(this.upstreams.values()).join('\n');
210
+ return `# Generated by src/runtime/nginx/Nginx.js — do not hand-edit.
211
+ # Reverse proxy derived from manifests/deployment/*/proxy.yaml (Contour HTTPProxy).
212
+ # Upstreams resolve container services via the Docker internal network.
213
+
214
+ map $http_upgrade $connection_upgrade {
215
+ default upgrade;
216
+ '' close;
217
+ }
218
+
219
+ ${upstreamBlocks}
220
+
221
+ proxy_http_version 1.1;
222
+ ${this.router}`;
223
+ }
224
+
225
+ /**
226
+ * Writes the rendered configuration to the given path, creating parent
227
+ * directories as needed. Idempotent and safe to rerun.
228
+ * @method writeConf
229
+ * @param {string} confPath - Absolute or relative destination path.
230
+ * @returns {string} The path written.
231
+ * @memberof NginxService
232
+ */
233
+ writeConf(confPath) {
234
+ const target = path.resolve(confPath);
235
+ fs.mkdirpSync(path.dirname(target));
236
+ fs.writeFileSync(target, this.render(), 'utf8');
237
+ logger.info(`nginx config written`, { path: target, upstreams: this.upstreams.size });
238
+ return target;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * @description Exported singleton instance of the NginxService class.
244
+ * @type {NginxService}
245
+ * @memberof NginxService
246
+ */
247
+ const Nginx = new NginxService();
248
+
249
+ export { Nginx, NginxService };
250
+ export default Nginx;
@@ -18,8 +18,6 @@ import fs from 'fs-extra';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import * as path from 'node:path';
20
20
 
21
- const catalogDir = path.dirname(fileURLToPath(import.meta.url));
22
-
23
21
  /** Empty product catalog returned for deploy ids without a dedicated module. */
24
22
  const EMPTY_CATALOG = {
25
23
  sourceMoves: [],
@@ -43,12 +41,11 @@ const EMPTY_CATALOG = {
43
41
  const loadDeployCatalog = async (deployId) => {
44
42
  const suffix = (deployId ?? '').split('dd-')[1];
45
43
  if (!suffix) return EMPTY_CATALOG;
46
- try {
47
- const mod = await import(`./catalog-${suffix}.js`);
44
+ if (fs.existsSync(`./src/projects/${suffix}/catalog-${suffix}.js`)) {
45
+ const mod = await import(`../projects/${suffix}/catalog-${suffix}.js`);
48
46
  return { ...EMPTY_CATALOG, ...(mod.default ?? {}) };
49
- } catch {
50
- return EMPTY_CATALOG;
51
47
  }
48
+ return EMPTY_CATALOG;
52
49
  };
53
50
 
54
51
  /**
@@ -62,14 +59,10 @@ const loadDeployCatalog = async (deployId) => {
62
59
  */
63
60
  const loadProductCatalogs = async () => {
64
61
  const catalogs = [];
65
- for (const file of fs.readdirSync(catalogDir)) {
66
- if (!/^catalog-.+\.js$/.test(file) || file === 'catalog-underpost.js') continue;
67
- try {
68
- const mod = await import(`./${file}`);
69
- if (mod.default) catalogs.push({ ...EMPTY_CATALOG, ...mod.default });
70
- } catch {
71
- /* a malformed/removed product catalog must not break the base build */
72
- }
62
+ for (const file of await fs.readdir('./src/projects')) {
63
+ if (file === 'underpost') continue;
64
+ const mod = await import(`../projects/${file}/catalog-${file}.js`);
65
+ if (mod.default) catalogs.push({ ...EMPTY_CATALOG, ...mod.default });
73
66
  }
74
67
  return catalogs;
75
68
  };