@underpostnet/underpost 3.0.2 → 3.1.0

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 (85) hide show
  1. package/{.env.production → .env.example} +20 -2
  2. package/.github/workflows/ghpkg.ci.yml +1 -1
  3. package/.github/workflows/gitlab.ci.yml +1 -1
  4. package/.github/workflows/npmpkg.ci.yml +22 -7
  5. package/.github/workflows/publish.ci.yml +5 -5
  6. package/.github/workflows/pwa-microservices-template-page.cd.yml +3 -3
  7. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  8. package/.github/workflows/release.cd.yml +3 -2
  9. package/.vscode/extensions.json +9 -8
  10. package/.vscode/settings.json +3 -2
  11. package/CHANGELOG.md +468 -290
  12. package/CLI-HELP.md +72 -52
  13. package/README.md +2 -2
  14. package/bin/build.js +4 -2
  15. package/bin/deploy.js +150 -208
  16. package/bin/file.js +2 -1
  17. package/bin/vs.js +3 -3
  18. package/conf.js +30 -13
  19. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  20. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  21. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  22. package/manifests/deployment/dd-test-development/deployment.yaml +52 -52
  23. package/manifests/deployment/dd-test-development/proxy.yaml +4 -4
  24. package/manifests/pv-pvc-dd.yaml +1 -1
  25. package/package.json +53 -44
  26. package/scripts/k3s-node-setup.sh +1 -1
  27. package/src/api/document/document.service.js +1 -1
  28. package/src/api/file/file.controller.js +3 -1
  29. package/src/api/file/file.service.js +28 -5
  30. package/src/api/user/user.router.js +10 -5
  31. package/src/api/user/user.service.js +7 -7
  32. package/src/cli/baremetal.js +6 -10
  33. package/src/cli/cloud-init.js +0 -3
  34. package/src/cli/db.js +54 -71
  35. package/src/cli/deploy.js +64 -12
  36. package/src/cli/env.js +4 -4
  37. package/src/cli/fs.js +0 -2
  38. package/src/cli/image.js +0 -3
  39. package/src/cli/index.js +33 -13
  40. package/src/cli/monitor.js +5 -6
  41. package/src/cli/repository.js +322 -35
  42. package/src/cli/run.js +148 -71
  43. package/src/cli/secrets.js +0 -3
  44. package/src/cli/ssh.js +1 -1
  45. package/src/client/components/core/AgGrid.js +20 -5
  46. package/src/client/components/core/Content.js +22 -3
  47. package/src/client/components/core/Docs.js +21 -4
  48. package/src/client/components/core/FileExplorer.js +71 -4
  49. package/src/client/components/core/Input.js +1 -1
  50. package/src/client/components/core/Modal.js +22 -6
  51. package/src/client/components/core/PublicProfile.js +3 -3
  52. package/src/client/components/core/Router.js +34 -1
  53. package/src/client/components/core/Worker.js +1 -1
  54. package/src/client/public/default/sitemap +3 -3
  55. package/src/client/public/test/sitemap +3 -3
  56. package/src/client.build.js +0 -3
  57. package/src/client.dev.js +0 -3
  58. package/src/db/DataBaseProvider.js +17 -2
  59. package/src/db/mariadb/MariaDB.js +14 -9
  60. package/src/db/mongo/MongooseDB.js +17 -1
  61. package/src/index.js +1 -1
  62. package/src/proxy.js +0 -3
  63. package/src/runtime/express/Express.js +7 -1
  64. package/src/runtime/lampp/Lampp.js +6 -13
  65. package/src/server/auth.js +6 -9
  66. package/src/server/backup.js +2 -3
  67. package/src/server/client-build-docs.js +178 -3
  68. package/src/server/client-build-live.js +9 -18
  69. package/src/server/client-build.js +175 -38
  70. package/src/server/client-dev-server.js +14 -13
  71. package/src/server/conf.js +357 -149
  72. package/src/server/cron.js +2 -1
  73. package/src/server/dns.js +28 -12
  74. package/src/server/downloader.js +0 -2
  75. package/src/server/logger.js +27 -9
  76. package/src/server/peer.js +0 -2
  77. package/src/server/process.js +1 -50
  78. package/src/server/proxy.js +4 -8
  79. package/src/server/runtime.js +5 -8
  80. package/src/server/ssr.js +0 -3
  81. package/src/server/start.js +5 -5
  82. package/src/server/tls.js +0 -2
  83. package/src/server.js +0 -4
  84. package/.env.development +0 -43
  85. package/.env.test +0 -43
@@ -26,10 +26,235 @@ import Underpost from '../index.js';
26
26
 
27
27
  colors.enable();
28
28
 
29
- dotenv.config();
30
-
31
29
  const logger = loggerFactory(import.meta);
32
30
 
31
+ /**
32
+ * Prefix used in JSON configuration files to denote an environment variable reference.
33
+ * Any string value in a config object that starts with this prefix will be resolved
34
+ * to the corresponding `process.env` value at runtime.
35
+ *
36
+ * @constant {string}
37
+ * @memberof ServerConfBuilder
38
+ * @example
39
+ * // In conf.server.json:
40
+ * { "db": { "password": "env:MARIADB_PASSWORD" } }
41
+ */
42
+ const ENV_REF_PREFIX = 'env:';
43
+
44
+ /**
45
+ * Recursively walks a configuration object and replaces every string value that
46
+ * starts with {@link ENV_REF_PREFIX} (`"env:"`) with the corresponding
47
+ * `process.env[VAR_NAME]` value.
48
+ *
49
+ * Non-string values and strings that do not start with the prefix are left untouched.
50
+ *
51
+ * Supports three reference formats:
52
+ * - `"env:VAR_NAME"` — resolves to `process.env.VAR_NAME`, returns `''` if unset.
53
+ * - `"env:VAR_NAME:default_value"` — resolves to `process.env.VAR_NAME`, falls back to `default_value` if unset.
54
+ * - Type-coerced defaults:
55
+ * - `"env:VAR_NAME:int:465"` — resolved value is parsed as integer (`parseInt`), falls back to `465`.
56
+ * - `"env:VAR_NAME:bool:true"` — resolved value is coerced to boolean (`value !== 'false'`), falls back to `true`.
57
+ *
58
+ * @method resolveConfSecrets
59
+ * @param {any} obj - The configuration object (or value) to resolve.
60
+ * @returns {any} A **new** object with all `env:` references replaced by their runtime values.
61
+ * @memberof ServerConfBuilder
62
+ *
63
+ * @example
64
+ * // Given process.env.MARIADB_PASSWORD = 'supersecret'
65
+ * resolveConfSecrets({ db: { password: 'env:MARIADB_PASSWORD' } });
66
+ * // => { db: { password: 'supersecret' } }
67
+ *
68
+ * @example
69
+ * // With default value fallback (env var not set)
70
+ * resolveConfSecrets({ db: { provider: 'env:DB_PROVIDER:mongoose' } });
71
+ * // => { db: { provider: 'mongoose' } }
72
+ *
73
+ * @example
74
+ * // With int type coercion
75
+ * resolveConfSecrets({ port: 'env:SMTP_PORT:int:465' });
76
+ * // => { port: 465 }
77
+ *
78
+ * @example
79
+ * // With bool type coercion
80
+ * resolveConfSecrets({ secure: 'env:SMTP_SECURE:bool:true' });
81
+ * // => { secure: true }
82
+ */
83
+ const resolveConfSecrets = (obj) => {
84
+ if (obj === null || obj === undefined) return obj;
85
+ if (typeof obj === 'string') {
86
+ if (obj.startsWith(ENV_REF_PREFIX)) {
87
+ const ref = obj.slice(ENV_REF_PREFIX.length);
88
+ // Support env:VAR_NAME:default_value syntax (first colon separates key from default)
89
+ const colonIdx = ref.indexOf(':');
90
+ const envKey = colonIdx !== -1 ? ref.slice(0, colonIdx) : ref;
91
+ const defaultValue = colonIdx !== -1 ? ref.slice(colonIdx + 1) : undefined;
92
+ const envValue = process.env[envKey];
93
+
94
+ let resolved;
95
+ if (envValue !== undefined) {
96
+ resolved = envValue;
97
+ } else if (defaultValue !== undefined) {
98
+ resolved = defaultValue;
99
+ } else {
100
+ logger.warn(`resolveConfSecrets: environment variable "${envKey}" is not set (referenced as "${obj}")`);
101
+ return '';
102
+ }
103
+
104
+ // Type coercion via prefix in default value: int:N or bool:B
105
+ // Also apply coercion when an env value is present and a typed default is declared
106
+ if (defaultValue !== undefined) {
107
+ if (defaultValue.startsWith('int:')) {
108
+ return parseInt(resolved, 10) || parseInt(defaultValue.slice(4), 10) || 0;
109
+ }
110
+ if (defaultValue.startsWith('bool:')) {
111
+ const boolDefault = defaultValue.slice(5);
112
+ if (envValue !== undefined) return envValue !== 'false';
113
+ return boolDefault !== 'false';
114
+ }
115
+ }
116
+
117
+ return resolved;
118
+ }
119
+ return obj;
120
+ }
121
+ if (Array.isArray(obj)) return obj.map((item) => resolveConfSecrets(item));
122
+ if (typeof obj === 'object') {
123
+ const resolved = {};
124
+ for (const [key, value] of Object.entries(obj)) {
125
+ resolved[key] = resolveConfSecrets(value);
126
+ }
127
+ return resolved;
128
+ }
129
+ return obj;
130
+ };
131
+
132
+ /**
133
+ * Returns the private configuration folder for a given deploy ID.
134
+ * Checks for a replica folder first, then falls back to the standard conf folder.
135
+ *
136
+ * @method getConfFolder
137
+ * @param {string} deployId - The deploy ID.
138
+ * @returns {string} The path to the private configuration folder.
139
+ * @memberof ServerConfBuilder
140
+ *
141
+ * @example
142
+ * getConfFolder('dd-myapp');
143
+ * // => './engine-private/conf/dd-myapp' (or './engine-private/replica/dd-myapp' if it exists)
144
+ */
145
+ const getConfFolder = (deployId) => {
146
+ return fs.existsSync(`./engine-private/replica/${deployId}`)
147
+ ? `./engine-private/replica/${deployId}`
148
+ : `./engine-private/conf/${deployId}`;
149
+ };
150
+
151
+ /**
152
+ * Loads the deployment-specific `.env` file referenced by `engine-private/deploy/dd.cron`
153
+ * into `process.env`. Uses `NODE_ENV` to select the environment variant
154
+ * (defaults to `production`).
155
+ *
156
+ * Safe to call multiple times; subsequent calls are no-ops once the env is loaded.
157
+ *
158
+ * @method loadCronDeployEnv
159
+ * @memberof ServerConfBuilder
160
+ */
161
+ function loadCronDeployEnv() {
162
+ const envName = process.env.NODE_ENV || 'production';
163
+
164
+ // 1) Load dd.cron env (takes full precedence)
165
+ const cronDeployFile = './engine-private/deploy/dd.cron';
166
+ if (fs.existsSync(cronDeployFile)) {
167
+ const cronDeployId = fs.readFileSync(cronDeployFile, 'utf8').trim();
168
+ if (cronDeployId) {
169
+ const cronEnvPath = `./engine-private/conf/${cronDeployId}/.env.${envName}`;
170
+ if (fs.existsSync(cronEnvPath)) {
171
+ const cronEnv = dotenv.parse(fs.readFileSync(cronEnvPath, 'utf8'));
172
+ process.env = { ...process.env, ...cronEnv };
173
+ }
174
+ }
175
+ }
176
+
177
+ // 2) Load dd.router envs — only keys not already present
178
+ const routerDeployFile = './engine-private/deploy/dd.router';
179
+ if (fs.existsSync(routerDeployFile)) {
180
+ const routerIds = fs.readFileSync(routerDeployFile, 'utf8').trim().split(',');
181
+ for (const deployId of routerIds) {
182
+ const id = deployId.trim();
183
+ if (!id) continue;
184
+ const envPath = `./engine-private/conf/${id}/.env.${envName}`;
185
+ if (!fs.existsSync(envPath)) continue;
186
+ const env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
187
+ for (const key of Object.keys(env)) {
188
+ if (!(key in process.env)) process.env[key] = env[key];
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Resolves the full path to a specific configuration JSON file for a deploy ID.
196
+ * For `server` configs in development mode with a subConf, it will prefer the
197
+ * dev-specific variant if it exists.
198
+ *
199
+ * @method getConfFilePath
200
+ * @param {string} deployId - The deploy ID.
201
+ * @param {string} confType - The configuration type (e.g. 'server', 'client', 'cron', 'ssr').
202
+ * @param {string} [subConf=''] - Optional sub-configuration identifier (used for dev server variants).
203
+ * @returns {string} The resolved path to the configuration JSON file.
204
+ * @memberof ServerConfBuilder
205
+ *
206
+ * @example
207
+ * getConfFilePath('dd-myapp', 'server');
208
+ * // => './engine-private/conf/dd-myapp/conf.server.json'
209
+ *
210
+ * @example
211
+ * // In development with subConf 'local':
212
+ * getConfFilePath('dd-myapp', 'server', 'local');
213
+ * // => './engine-private/conf/dd-myapp/conf.server.dev.local.json' (if it exists)
214
+ */
215
+ const getConfFilePath = (deployId, confType, subConf = '') => {
216
+ const folder = getConfFolder(deployId);
217
+ // When no explicit subConf is given, fall back to the env var set by loadConf()
218
+ const effectiveSubConf = subConf || process.env.DEPLOY_SUB_CONF || '';
219
+ if (confType === 'server' && effectiveSubConf) {
220
+ const devConfPath = `${folder}/conf.${confType}.dev.${effectiveSubConf}.json`;
221
+ if (fs.existsSync(devConfPath)) return devConfPath;
222
+ }
223
+ return `${folder}/conf.${confType}.json`;
224
+ };
225
+
226
+ /**
227
+ * Reads and parses a configuration JSON file for a given deploy ID and config type.
228
+ * Optionally resolves `env:` secret references and/or applies replica expansion.
229
+ *
230
+ * @method readConfJson
231
+ * @param {string} deployId - The deploy ID.
232
+ * @param {string} confType - The configuration type (e.g. 'server', 'client', 'cron', 'ssr').
233
+ * @param {object} [options={}] - Options.
234
+ * @param {string} [options.subConf=''] - Sub-configuration identifier for dev variants.
235
+ * @param {boolean} [options.resolve=false] - Whether to resolve `env:` references.
236
+ * @param {boolean} [options.loadReplicas=false] - Whether to expand replica entries (server configs).
237
+ * @returns {object} The parsed (and optionally resolved) configuration object.
238
+ * @memberof ServerConfBuilder
239
+ */
240
+ const readConfJson = (deployId, confType, options = {}) => {
241
+ const filePath = getConfFilePath(deployId, confType, options.subConf || '');
242
+ if (!fs.existsSync(filePath)) {
243
+ throw new Error(`readConfJson: configuration file not found: ${filePath}`);
244
+ }
245
+ let parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
246
+ if (options.loadReplicas && confType === 'server') parsed = loadReplicas(deployId, parsed);
247
+ if (options.resolve) parsed = resolveConfSecrets(parsed);
248
+ return parsed;
249
+ };
250
+
251
+ /**
252
+ * Default deploy ID used when no deploy ID is specified.
253
+ * @constant {string}
254
+ * @memberof ServerConfBuilder
255
+ */
256
+ const DEFAULT_DEPLOY_ID = 'dd-default';
257
+
33
258
  /**
34
259
  * @class Config
35
260
  * @description Manages the configuration of the server.
@@ -53,12 +278,13 @@ const Config = {
53
278
  * @param {string} [subConf=''] - The sub configuration.
54
279
  * @memberof ServerConfBuilder
55
280
  */
56
- build: async function (deployContext = 'dd-default', deployList, subConf) {
281
+ build: async function (deployContext = DEFAULT_DEPLOY_ID, deployList, subConf) {
57
282
  if (process.argv[2] && typeof process.argv[2] === 'string' && process.argv[2].startsWith('dd-'))
58
283
  deployContext = process.argv[2];
284
+ else if (deployContext !== 'proxy' && process.env.DEPLOY_ID && process.env.DEPLOY_ID.startsWith('dd-'))
285
+ deployContext = process.env.DEPLOY_ID;
59
286
  if (!subConf && process.argv[3] && typeof process.argv[3] === 'string') subConf = process.argv[3];
60
- if (!fs.existsSync(`./tmp`)) fs.mkdirSync(`./tmp`);
61
- if (!fs.existsSync(`./conf`)) fs.mkdirSync(`./conf`);
287
+
62
288
  Underpost.env.set('await-deploy', new Date().toISOString());
63
289
  if (deployContext.startsWith('dd-')) loadConf(deployContext, subConf);
64
290
  if (deployContext === 'proxy') await Config.buildProxy(deployList, subConf);
@@ -70,7 +296,7 @@ const Config = {
70
296
  * @param {object} [options={ subConf: '', cluster: false }] - The options.
71
297
  * @memberof ServerConfBuilder
72
298
  */
73
- deployIdFactory: function (deployId = 'dd-default', options = { subConf: '', cluster: false }) {
299
+ deployIdFactory: function (deployId = DEFAULT_DEPLOY_ID, options = { subConf: '', cluster: false }) {
74
300
  if (!deployId.startsWith('dd-')) deployId = `dd-${deployId}`;
75
301
 
76
302
  logger.info('Build deployId', deployId);
@@ -79,28 +305,39 @@ const Config = {
79
305
  const repoName = `engine-${deployId.split('dd-')[1]}`;
80
306
 
81
307
  if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
82
- fs.writeFileSync(
83
- `${folder}/.env.production`,
84
- fs.readFileSync('./.env.production', 'utf8').replaceAll('dd-default', deployId),
85
- 'utf8',
86
- );
87
- fs.writeFileSync(
88
- `${folder}/.env.development`,
89
- fs.readFileSync('./.env.development', 'utf8').replaceAll('dd-default', deployId),
90
- 'utf8',
91
- );
92
- fs.writeFileSync(
93
- `${folder}/.env.test`,
94
- fs.readFileSync('./.env.test', 'utf8').replaceAll('dd-default', deployId),
95
- 'utf8',
96
- );
308
+
309
+ const envTemplate = fs.existsSync('./.env.example')
310
+ ? fs.readFileSync('./.env.example', 'utf8')
311
+ : fs.existsSync('./.env.production')
312
+ ? fs.readFileSync('./.env.production', 'utf8')
313
+ : '';
314
+
315
+ if (envTemplate) {
316
+ const prodEnv = envTemplate.replaceAll('dd-default', deployId);
317
+ fs.writeFileSync(`${folder}/.env.production`, prodEnv, 'utf8');
318
+ fs.writeFileSync(
319
+ `${folder}/.env.development`,
320
+ prodEnv.replace('NODE_ENV=production', 'NODE_ENV=development').replace('PORT=3000', 'PORT=4000'),
321
+ 'utf8',
322
+ );
323
+ fs.writeFileSync(
324
+ `${folder}/.env.test`,
325
+ prodEnv.replace('NODE_ENV=production', 'NODE_ENV=test').replace('PORT=3000', 'PORT=5000'),
326
+ 'utf8',
327
+ );
328
+ }
329
+
97
330
  fs.writeFileSync(
98
331
  `${folder}/package.json`,
99
332
  fs.readFileSync('./package.json', 'utf8').replaceAll('dd-default', deployId),
100
333
  'utf8',
101
334
  );
102
335
 
103
- this.buildTmpConf(folder);
336
+ // Write default conf JSON files if they don't exist
337
+ for (const confType of Object.keys(this.default)) {
338
+ const confPath = `${folder}/conf.${confType}.json`;
339
+ if (!fs.existsSync(confPath)) fs.writeFileSync(confPath, JSON.stringify(this.default[confType], null, 4), 'utf8');
340
+ }
104
341
 
105
342
  if (options.subConf) {
106
343
  logger.info('Creating sub conf', {
@@ -127,38 +364,17 @@ const Config = {
127
364
  shellExec(`node bin new --default-conf --deploy-id ${deployId}`);
128
365
 
129
366
  if (!fs.existsSync(`./engine-private/deploy/dd.router`))
130
- fs.writeFileSync(`./engine-private/deploy/dd.router`, '', 'utf8');
131
-
132
- fs.writeFileSync(
133
- `./engine-private/deploy/dd.router`,
134
- fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').trim() + `,${deployId}`,
135
- 'utf8',
136
- );
137
- const updateRepo = (stage = 1) => {
138
- shellExec(`git add . && git commit -m "Add base deployId ${deployId} cluster files stage:${stage}"`);
139
- shellExec(
140
- `cd engine-private && git add . && git commit -m "Add base deployId ${deployId} cluster files stage:${stage}"`,
367
+ fs.writeFileSync(`./engine-private/deploy/dd.router`, deployId, 'utf8');
368
+ else
369
+ fs.writeFileSync(
370
+ `./engine-private/deploy/dd.router`,
371
+ fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').trim() + `,${deployId}`,
372
+ 'utf8',
141
373
  );
142
- };
143
- updateRepo(1);
144
- shellExec(`node bin run --build --dev sync`);
145
- updateRepo(2);
146
- shellExec(`node bin run --build sync`);
147
- updateRepo(3);
148
374
  }
149
375
 
150
376
  return { deployIdFolder: folder, deployId };
151
377
  },
152
- /**
153
- * @method buildTmpConf
154
- * @description Builds the temporary configuration of the server.
155
- * @param {string} [folder='./conf'] - The folder.
156
- * @memberof ServerConfBuilder
157
- */
158
- buildTmpConf: function (folder = './conf') {
159
- for (const confType of Object.keys(this.default))
160
- fs.writeFileSync(`${folder}/conf.${confType}.json`, JSON.stringify(this.default[confType], null, 4), 'utf8');
161
- },
162
378
  /**
163
379
  * @method buildProxyByDeployId
164
380
  * @description Builds the proxy by deploy ID.
@@ -178,7 +394,7 @@ const Config = {
178
394
  )
179
395
  confPath = `./engine-private/conf/${deployId}/conf.server.dev.${subConf}.json`;
180
396
 
181
- const serverConf = JSON.parse(fs.readFileSync(confPath, 'utf8'));
397
+ const serverConf = loadConfServerJson(confPath);
182
398
 
183
399
  for (const host of Object.keys(loadReplicas(deployId, serverConf)))
184
400
  this.default.server[host] = {
@@ -206,7 +422,6 @@ const Config = {
206
422
  }
207
423
  }
208
424
  }
209
- this.buildTmpConf();
210
425
  },
211
426
  };
212
427
 
@@ -217,7 +432,7 @@ const Config = {
217
432
  * @param {string} [subConf=''] - The sub configuration.
218
433
  * @memberof ServerConfBuilder
219
434
  */
220
- const loadConf = (deployId = 'dd-default', subConf) => {
435
+ const loadConf = (deployId = DEFAULT_DEPLOY_ID, subConf) => {
221
436
  if (deployId === 'current') {
222
437
  console.log(process.env.DEPLOY_ID);
223
438
  return;
@@ -225,20 +440,19 @@ const loadConf = (deployId = 'dd-default', subConf) => {
225
440
  if (deployId === 'clean') {
226
441
  const path = '.';
227
442
  fs.removeSync(`${path}/.env`);
228
- shellExec(`git checkout ${path}/.env.production`);
229
- shellExec(`git checkout ${path}/.env.development`);
230
- shellExec(`git checkout ${path}/.env.test`);
443
+ fs.removeSync(`${path}/.env.production`);
444
+ fs.removeSync(`${path}/.env.development`);
445
+ fs.removeSync(`${path}/.env.test`);
231
446
  if (fs.existsSync(`${path}/jsdoc.json`)) shellExec(`git checkout ${path}/jsdoc.json`);
232
447
  shellExec(`git checkout ${path}/package.json`);
233
448
  shellExec(`git checkout ${path}/package-lock.json`);
234
449
  return;
235
450
  }
236
- const folder = fs.existsSync(`./engine-private/replica/${deployId}`)
237
- ? `./engine-private/replica/${deployId}`
238
- : `./engine-private/conf/${deployId}`;
451
+ const folder = getConfFolder(deployId);
452
+
239
453
  if (!fs.existsSync(folder)) Config.deployIdFactory(deployId);
240
- if (!fs.existsSync(`./conf`)) fs.mkdirSync(`./conf`);
241
- if (!fs.existsSync(`./tmp`)) fs.mkdirSync(`./tmp`);
454
+
455
+ if (subConf) process.env.DEPLOY_SUB_CONF = subConf;
242
456
 
243
457
  for (const typeConf of Object.keys(Config.default)) {
244
458
  let srcConf = fs.readFileSync(`${folder}/conf.${typeConf}.json`, 'utf8');
@@ -246,13 +460,14 @@ const loadConf = (deployId = 'dd-default', subConf) => {
246
460
  const devConfPath = `${folder}/conf.${typeConf}.dev${subConf ? `.${subConf}` : ''}.json`;
247
461
  if (fs.existsSync(devConfPath)) srcConf = fs.readFileSync(devConfPath, 'utf8');
248
462
  }
249
- if (typeConf === 'server') srcConf = JSON.stringify(loadReplicas(deployId, JSON.parse(srcConf)), null, 4);
250
- fs.writeFileSync(`./conf/conf.${typeConf}.json`, srcConf, 'utf8');
463
+ let parsed = JSON.parse(srcConf);
464
+ if (typeConf === 'server') parsed = loadReplicas(deployId, parsed);
465
+ Config.default[typeConf] = parsed;
251
466
  }
252
467
  fs.writeFileSync(`./.env.production`, fs.readFileSync(`${folder}/.env.production`, 'utf8'), 'utf8');
253
468
  fs.writeFileSync(`./.env.development`, fs.readFileSync(`${folder}/.env.development`, 'utf8'), 'utf8');
254
469
  fs.writeFileSync(`./.env.test`, fs.readFileSync(`${folder}/.env.test`, 'utf8'), 'utf8');
255
- const NODE_ENV = process.env.NODE_ENV;
470
+ const NODE_ENV = process.env.NODE_ENV || 'development';
256
471
  if (NODE_ENV) {
257
472
  const subPathEnv = fs.existsSync(`${folder}/.env.${NODE_ENV}.${subConf}`)
258
473
  ? `${folder}/.env.${NODE_ENV}.${subConf}`
@@ -644,7 +859,7 @@ const cloneSrcComponents = async ({ toOptions, fromOptions }) => {
644
859
  * @memberof ServerConfBuilder
645
860
  */
646
861
  const buildProxyRouter = () => {
647
- const confServer = JSON.parse(fs.readFileSync(`./conf/conf.server.json`, 'utf8'));
862
+ const confServer = newInstance(Config.default.server);
648
863
  let currentPort = parseInt(process.env.PORT) + 1;
649
864
  const proxyRouter = {};
650
865
  for (const host of Object.keys(confServer)) {
@@ -726,9 +941,7 @@ const pathPortAssignmentFactory = async (deployId, router, confServer) => {
726
941
  const singleReplicas = await fs.readdir(`./engine-private/replica`);
727
942
  for (let replica of singleReplicas) {
728
943
  if (replica.startsWith(deployId)) {
729
- const replicaServerConf = JSON.parse(
730
- fs.readFileSync(`./engine-private/replica/${replica}/conf.server.json`, 'utf8'),
731
- );
944
+ const replicaServerConf = loadConfServerJson(`./engine-private/replica/${replica}/conf.server.json`);
732
945
  for (const host of Object.keys(replicaServerConf)) {
733
946
  const pathPortAssignment = [];
734
947
  for (const path of Object.keys(replicaServerConf[host])) {
@@ -906,7 +1119,7 @@ const buildReplicaId = ({ deployId, replica }) => `${deployId}-${replica.slice(1
906
1119
  * @returns {object} - The data deploy.
907
1120
  * @memberof ServerConfBuilder
908
1121
  */
909
- const getDataDeploy = (
1122
+ const getDataDeploy = async (
910
1123
  options = {
911
1124
  buildSingleReplica: false,
912
1125
  disableSyncEnvPort: false,
@@ -931,13 +1144,16 @@ const getDataDeploy = (
931
1144
  for (const deployObj of dataDeploy) {
932
1145
  const serverConf = loadReplicas(
933
1146
  deployObj.deployId,
934
- JSON.parse(fs.readFileSync(`./engine-private/conf/${deployObj.deployId}/conf.server.json`, 'utf8')),
1147
+ loadConfServerJson(`./engine-private/conf/${deployObj.deployId}/conf.server.json`),
935
1148
  );
936
1149
  let replicaDataDeploy = [];
937
1150
  for (const host of Object.keys(serverConf))
938
1151
  for (const path of Object.keys(serverConf[host])) {
939
1152
  if (serverConf[host][path].replicas && serverConf[host][path].singleReplica) {
940
- if (options && options.buildSingleReplica) shellExec(Cmd.replica(deployObj.deployId, host, path));
1153
+ if (options && options.buildSingleReplica)
1154
+ await Underpost.repo.client(deployObj.deployId, '', host, path, {
1155
+ singleReplica: true,
1156
+ });
941
1157
  replicaDataDeploy = replicaDataDeploy.concat(
942
1158
  serverConf[host][path].replicas.map((r) => {
943
1159
  return {
@@ -952,7 +1168,8 @@ const getDataDeploy = (
952
1168
  if (replicaDataDeploy.length > 0) buildDataDeploy = buildDataDeploy.concat(replicaDataDeploy);
953
1169
  }
954
1170
 
955
- if (!options.disableSyncEnvPort && options.buildSingleReplica) shellExec(Cmd.syncPorts());
1171
+ if (!options.disableSyncEnvPort && options.buildSingleReplica)
1172
+ await Underpost.repo.client(undefined, '', '', '', { syncEnvPort: true });
956
1173
 
957
1174
  logger.info('Deployments configured', buildDataDeploy);
958
1175
 
@@ -980,6 +1197,7 @@ const validateTemplatePath = (absolutePath = '') => {
980
1197
  return false;
981
1198
  }
982
1199
  if (absolutePath.match('conf.dd-') && absolutePath.match('.js')) return false;
1200
+ if (absolutePath.match('jsdoc.dd-') && absolutePath.match('.json')) return false;
983
1201
  if (
984
1202
  absolutePath.match('src/client/services/') &&
985
1203
  !clients.find((p) => absolutePath.match(`src/client/services/${p}/`))
@@ -1102,10 +1320,7 @@ const mergeFile = async (parts = [], outputFilePath) => {
1102
1320
  * @memberof ServerConfBuilder
1103
1321
  */
1104
1322
  const rebuildConfFactory = ({ deployId, valkey, mongo }) => {
1105
- const confServer = loadReplicas(
1106
- deployId,
1107
- JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8')),
1108
- );
1323
+ const confServer = loadReplicas(deployId, loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`));
1109
1324
  const hosts = {};
1110
1325
  for (const host of Object.keys(confServer)) {
1111
1326
  hosts[host] = {};
@@ -1116,9 +1331,7 @@ const rebuildConfFactory = ({ deployId, valkey, mongo }) => {
1116
1331
  if (singleReplica) {
1117
1332
  for (const replica of replicas) {
1118
1333
  const deployIdReplica = buildReplicaId({ replica, deployId });
1119
- const confServerReplica = JSON.parse(
1120
- fs.readFileSync(`./engine-private/replica/${deployIdReplica}/conf.server.json`, 'utf8'),
1121
- );
1334
+ const confServerReplica = loadConfServerJson(`./engine-private/replica/${deployIdReplica}/conf.server.json`);
1122
1335
  for (const _host of Object.keys(confServerReplica)) {
1123
1336
  for (const _path of Object.keys(confServerReplica[_host])) {
1124
1337
  hosts[host][_path] = { replica: { host, path } };
@@ -1166,56 +1379,6 @@ const getPathsSSR = (conf) => {
1166
1379
  return paths;
1167
1380
  };
1168
1381
 
1169
- /**
1170
- * @method CmdUnderpost
1171
- * @description The command factory.
1172
- * @memberof ServerConfBuilder
1173
- * @namespace CmdUnderpost
1174
- */
1175
- const Cmd = {
1176
- /**
1177
- * @method run
1178
- * @description Runs the deploy.
1179
- * @returns {string} - The run command.
1180
- * @memberof Cmd
1181
- */
1182
- run: () => `npm start`,
1183
- /**
1184
- * @method build
1185
- * @description Builds the deploy.
1186
- * @param {string} deployId - The deploy ID.
1187
- * @returns {string} - The build command.
1188
- * @memberof CmdUnderpost
1189
- */
1190
- build: (deployId) => `node bin/deploy build-full-client ${deployId}`,
1191
- /**
1192
- * @method conf
1193
- * @description Configures the deploy.
1194
- * @param {string} deployId - The deploy ID.
1195
- * @param {string} env - The environment.
1196
- * @returns {string} - The conf command.
1197
- * @memberof CmdUnderpost
1198
- */
1199
- conf: (deployId, env) => `node bin/deploy conf ${deployId} ${env ? env : 'production'}`,
1200
- /**
1201
- * @method replica
1202
- * @description Builds the replica.
1203
- * @param {string} deployId - The deploy ID.
1204
- * @param {string} host - The host.
1205
- * @param {string} path - The path.
1206
- * @returns {string} - The replica command.
1207
- * @memberof CmdUnderpost
1208
- */
1209
- replica: (deployId, host, path) => `node bin/deploy build-single-replica ${deployId} ${host} ${path}`,
1210
- /**
1211
- * @method syncPorts
1212
- * @description Syncs the ports.
1213
- * @returns {string} - The sync ports command.
1214
- * @memberof CmdUnderpost
1215
- */
1216
- syncPorts: () => `node bin/deploy sync-env-port`,
1217
- };
1218
-
1219
1382
  /**
1220
1383
  * @method splitFileFactory
1221
1384
  * @description Splits the file factory.
@@ -1348,19 +1511,26 @@ const buildCliDoc = (program, oldVersion, newVersion) => {
1348
1511
  * @param {boolean} options.singleReplica - The single replica.
1349
1512
  * @param {Array} options.replicas - The replicas.
1350
1513
  * @param {string} options.redirect - The redirect.
1514
+ * @param {boolean} [options.peer=false] - Whether peer is enabled on the parent singleReplica path (used for port offset estimation when replica conf is not yet built).
1351
1515
  * @returns {object} - The instance context.
1352
1516
  * @memberof ServerConfBuilder
1353
1517
  */
1354
- const getInstanceContext = async (options = { deployId, singleReplica, replicas, redirect: '' }) => {
1355
- const { deployId, singleReplica, replicas, redirect } = options;
1518
+ const getInstanceContext = async (options = { deployId, singleReplica, replicas, redirect: '', peer: false }) => {
1519
+ const { deployId, singleReplica, replicas, redirect, peer } = options;
1356
1520
  let singleReplicaOffsetPortSum = 0;
1357
1521
 
1358
1522
  if (singleReplica && replicas && replicas.length > 0) {
1359
1523
  for (const replica of replicas) {
1360
1524
  const replicaDeployId = buildReplicaId({ deployId, replica });
1361
- const confReplicaServer = JSON.parse(
1362
- fs.readFileSync(`./engine-private/replica/${replicaDeployId}/conf.server.json`, 'utf8'),
1363
- );
1525
+ const replicaConfPath = `./engine-private/replica/${replicaDeployId}/conf.server.json`;
1526
+ if (!fs.existsSync(replicaConfPath)) {
1527
+ // Replica folder not built yet (e.g. dev mode without prior build);
1528
+ // estimate port offset: 1 per replica path + 1 extra if peer is enabled on the parent singleReplica config
1529
+ singleReplicaOffsetPortSum++;
1530
+ if (peer) singleReplicaOffsetPortSum++;
1531
+ continue;
1532
+ }
1533
+ const confReplicaServer = loadConfServerJson(replicaConfPath);
1364
1534
  for (const host of Object.keys(confReplicaServer)) {
1365
1535
  for (const path of Object.keys(confReplicaServer[host])) {
1366
1536
  singleReplicaOffsetPortSum++;
@@ -1480,15 +1650,7 @@ const isDeployRunnerContext = (path, options) => !options.build && path && path
1480
1650
  * @returns {boolean} - The dev proxy context.
1481
1651
  * @memberof ServerConfBuilder
1482
1652
  */
1483
- const isDevProxyContext = () => {
1484
- try {
1485
- if (process.argv[2] === 'proxy') return true;
1486
- if (!process.argv[6].startsWith('localhost')) return false;
1487
- return new URL('http://' + process.argv[6]).hostname ? true : false;
1488
- } catch {
1489
- return false;
1490
- }
1491
- };
1653
+ const isDevProxyContext = () => (process.argv.find((arg) => arg === 'proxy') ? true : false);
1492
1654
 
1493
1655
  /**
1494
1656
  * @method devProxyHostFactory
@@ -1501,10 +1663,14 @@ const isDevProxyContext = () => {
1501
1663
  * @returns {string} - The dev proxy host.
1502
1664
  * @memberof ServerConfBuilder
1503
1665
  */
1504
- const devProxyHostFactory = (options = { host: 'default.net', includeHttp: false, port: 80, tls: false }) =>
1505
- `${options.includeHttp ? (options.tls ? 'https://' : 'http://') : ''}${options.host ? options.host : 'localhost'}:${
1506
- (options.port ? options.port : options.tls ? 443 : 80) + parseInt(process.env.DEV_PROXY_PORT_OFFSET)
1507
- }`;
1666
+ const devProxyHostFactory = (options = { host: 'default.net', includeHttp: false, port: 80, tls: false }) => {
1667
+ const resolvedPort =
1668
+ (options.port ? options.port : options.tls ? 443 : 80) + parseInt(process.env.DEV_PROXY_PORT_OFFSET);
1669
+ const isDefaultPort = (options.tls && resolvedPort === 443) || (!options.tls && resolvedPort === 80);
1670
+ const protocol = options.includeHttp ? (options.tls ? 'https://' : 'http://') : '';
1671
+ const hostname = options.host ? options.host : 'localhost';
1672
+ return `${protocol}${hostname}${isDefaultPort ? '' : `:${resolvedPort}`}`;
1673
+ };
1508
1674
 
1509
1675
  /**
1510
1676
  * @method isTlsDevProxy
@@ -1512,7 +1678,7 @@ const devProxyHostFactory = (options = { host: 'default.net', includeHttp: false
1512
1678
  * @returns {boolean} - The TLS dev proxy status.
1513
1679
  * @memberof ServerConfBuilder
1514
1680
  */
1515
- const isTlsDevProxy = () => process.env.NODE_ENV !== 'production' && process.argv[7] === 'tls';
1681
+ const isTlsDevProxy = () => process.env.NODE_ENV !== 'production' && !!process.argv.find((arg) => arg === 'tls');
1516
1682
 
1517
1683
  /**
1518
1684
  * @method getTlsHosts
@@ -1524,17 +1690,52 @@ const isTlsDevProxy = () => process.env.NODE_ENV !== 'production' && process.arg
1524
1690
  const getTlsHosts = (confServer) =>
1525
1691
  Array.from(new Set(Object.keys(confServer).map((h) => new URL('https://' + h).hostname)));
1526
1692
 
1693
+ /**
1694
+ * Reads a `conf.server.json` file from disk, parses it, and resolves all `env:` secret
1695
+ * references using {@link resolveConfSecrets}.
1696
+ *
1697
+ * Reads and parses a `conf.server.json` file from disk. The `env:` secret
1698
+ * references are **preserved** by default so that build/deploy tooling never
1699
+ * accidentally strips them. Callers that need the actual secret values
1700
+ * (e.g. database or mailer modules) should explicitly wrap the result with
1701
+ * {@link resolveConfSecrets}.
1702
+ *
1703
+ * @method loadConfServerJson
1704
+ * @param {string} jsonPath - Absolute or relative path to the `conf.server.json` file.
1705
+ * @param {object} [options] - Optional settings.
1706
+ * @param {boolean} [options.resolve=false] - When `true`, resolves `env:` references
1707
+ * via {@link resolveConfSecrets} before returning.
1708
+ * @returns {object} The parsed server configuration object (secrets unresolved unless
1709
+ * `options.resolve` is `true`).
1710
+ * @throws {Error} If the file does not exist or cannot be parsed.
1711
+ * @memberof ServerConfBuilder
1712
+ *
1713
+ * @example
1714
+ * // Structure-only read (env: strings preserved)
1715
+ * const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`);
1716
+ *
1717
+ * @example
1718
+ * // Resolved read (env: strings replaced with process.env values)
1719
+ * const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`, { resolve: true });
1720
+ */
1721
+ const loadConfServerJson = (jsonPath, options) => {
1722
+ if (!fs.existsSync(jsonPath)) {
1723
+ throw new Error(`loadConfServerJson: configuration file not found: ${jsonPath}`);
1724
+ }
1725
+ const raw = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
1726
+ return options && options.resolve === true ? resolveConfSecrets(raw) : raw;
1727
+ };
1728
+
1527
1729
  export {
1528
- Cmd,
1529
1730
  Config,
1530
1731
  loadConf,
1531
1732
  loadReplicas,
1532
1733
  cloneConf,
1533
1734
  getCapVariableName,
1735
+ addClientConf,
1534
1736
  buildClientSrc,
1535
1737
  buildApiSrc,
1536
1738
  addApiConf,
1537
- addClientConf,
1538
1739
  addWsConf,
1539
1740
  buildWsSrc,
1540
1741
  cloneSrcComponents,
@@ -1564,4 +1765,11 @@ export {
1564
1765
  devProxyHostFactory,
1565
1766
  isTlsDevProxy,
1566
1767
  getTlsHosts,
1768
+ resolveConfSecrets,
1769
+ loadConfServerJson,
1770
+ getConfFolder,
1771
+ getConfFilePath,
1772
+ readConfJson,
1773
+ DEFAULT_DEPLOY_ID,
1774
+ loadCronDeployEnv,
1567
1775
  };