underpost 3.2.5 → 3.2.8

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 (138) hide show
  1. package/.github/workflows/release.cd.yml +1 -2
  2. package/CHANGELOG.md +251 -1
  3. package/CLI-HELP.md +26 -13
  4. package/Dockerfile +0 -4
  5. package/README.md +3 -3
  6. package/bin/build.js +13 -3
  7. package/bin/deploy.js +570 -1
  8. package/bin/file.js +5 -0
  9. package/conf.js +11 -2
  10. package/jsconfig.json +1 -1
  11. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  12. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  13. package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
  14. package/manifests/deployment/dd-test-development/deployment.yaml +136 -66
  15. package/manifests/deployment/dd-test-development/proxy.yaml +41 -5
  16. package/package.json +20 -11
  17. package/src/api/core/core.controller.js +10 -10
  18. package/src/api/core/core.service.js +10 -10
  19. package/src/api/default/default.controller.js +10 -10
  20. package/src/api/default/default.service.js +10 -10
  21. package/src/api/document/document.controller.js +12 -12
  22. package/src/api/document/document.model.js +10 -16
  23. package/src/api/file/file.controller.js +8 -8
  24. package/src/api/file/file.model.js +10 -10
  25. package/src/api/file/file.service.js +36 -36
  26. package/src/api/test/test.controller.js +8 -8
  27. package/src/api/test/test.service.js +8 -8
  28. package/src/api/user/guest.service.js +99 -0
  29. package/src/api/user/user.controller.js +6 -6
  30. package/src/api/user/user.model.js +8 -13
  31. package/src/api/user/user.service.js +3 -20
  32. package/src/cli/deploy.js +33 -30
  33. package/src/cli/fs.js +62 -5
  34. package/src/cli/image.js +43 -1
  35. package/src/cli/index.js +5 -1
  36. package/src/cli/release.js +57 -1
  37. package/src/cli/repository.js +35 -3
  38. package/src/cli/run.js +300 -35
  39. package/src/cli/ssh.js +1 -1
  40. package/src/cli/static.js +43 -115
  41. package/src/client/Default.index.js +21 -33
  42. package/src/client/components/core/404.js +4 -4
  43. package/src/client/components/core/500.js +4 -4
  44. package/src/client/components/core/Account.js +73 -60
  45. package/src/client/components/core/AgGrid.js +23 -33
  46. package/src/client/components/core/Alert.js +12 -13
  47. package/src/client/components/core/AppStore.js +1 -1
  48. package/src/client/components/core/Auth.js +20 -32
  49. package/src/client/components/core/Badge.js +7 -13
  50. package/src/client/components/core/BtnIcon.js +15 -17
  51. package/src/client/components/core/CalendarCore.js +42 -63
  52. package/src/client/components/core/Chat.js +13 -15
  53. package/src/client/components/core/ClientEvents.js +87 -0
  54. package/src/client/components/core/ColorPaletteElement.js +309 -0
  55. package/src/client/components/core/Content.js +17 -14
  56. package/src/client/components/core/Css.js +15 -71
  57. package/src/client/components/core/CssCore.js +12 -16
  58. package/src/client/components/core/D3Chart.js +4 -4
  59. package/src/client/components/core/Docs.js +60 -59
  60. package/src/client/components/core/DropDown.js +69 -91
  61. package/src/client/components/core/EventBus.js +92 -0
  62. package/src/client/components/core/EventsUI.js +14 -17
  63. package/src/client/components/core/FileExplorer.js +102 -234
  64. package/src/client/components/core/FullScreen.js +47 -75
  65. package/src/client/components/core/Input.js +24 -69
  66. package/src/client/components/core/Keyboard.js +25 -18
  67. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  68. package/src/client/components/core/LoadingAnimation.js +25 -31
  69. package/src/client/components/core/LogIn.js +41 -41
  70. package/src/client/components/core/LogOut.js +23 -14
  71. package/src/client/components/core/Modal.js +397 -176
  72. package/src/client/components/core/NotificationManager.js +14 -18
  73. package/src/client/components/core/Panel.js +54 -50
  74. package/src/client/components/core/PanelForm.js +25 -125
  75. package/src/client/components/core/Polyhedron.js +110 -214
  76. package/src/client/components/core/PublicProfile.js +39 -32
  77. package/src/client/components/core/Recover.js +52 -48
  78. package/src/client/components/core/Responsive.js +88 -32
  79. package/src/client/components/core/RichText.js +9 -18
  80. package/src/client/components/core/Router.js +24 -3
  81. package/src/client/components/core/SearchBox.js +37 -37
  82. package/src/client/components/core/SignUp.js +39 -30
  83. package/src/client/components/core/SocketIo.js +31 -2
  84. package/src/client/components/core/SocketIoHandler.js +6 -6
  85. package/src/client/components/core/ToggleSwitch.js +8 -20
  86. package/src/client/components/core/ToolTip.js +5 -17
  87. package/src/client/components/core/Translate.js +56 -59
  88. package/src/client/components/core/Validator.js +26 -16
  89. package/src/client/components/core/Wallet.js +15 -26
  90. package/src/client/components/core/Worker.js +140 -25
  91. package/src/client/components/core/windowGetDimensions.js +7 -7
  92. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +87 -87
  93. package/src/client/components/default/CssDefault.js +12 -12
  94. package/src/client/components/default/LogInDefault.js +6 -4
  95. package/src/client/components/default/LogOutDefault.js +6 -4
  96. package/src/client/components/default/RouterDefault.js +47 -0
  97. package/src/client/components/default/SettingsDefault.js +4 -4
  98. package/src/client/components/default/SignUpDefault.js +6 -4
  99. package/src/client/components/default/TranslateDefault.js +3 -3
  100. package/src/client/services/core/core.service.js +17 -49
  101. package/src/client/services/default/default.management.js +139 -242
  102. package/src/client/services/default/default.service.js +10 -16
  103. package/src/client/services/document/document.service.js +14 -19
  104. package/src/client/services/file/file.service.js +8 -13
  105. package/src/client/services/test/test.service.js +8 -13
  106. package/src/client/services/user/guest.service.js +79 -0
  107. package/src/client/services/user/user.management.js +5 -5
  108. package/src/client/services/user/user.service.js +14 -20
  109. package/src/client/ssr/body/404.js +3 -3
  110. package/src/client/ssr/body/500.js +3 -3
  111. package/src/client/ssr/body/CacheControl.js +5 -2
  112. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  113. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  114. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  115. package/src/client/ssr/offline/Maintenance.js +12 -11
  116. package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
  117. package/src/client/ssr/pages/Test.js +2 -2
  118. package/src/client/sw/core.sw.js +212 -0
  119. package/src/index.js +1 -1
  120. package/src/runtime/express/Dockerfile +4 -4
  121. package/src/runtime/lampp/Dockerfile +8 -7
  122. package/src/runtime/wp/Dockerfile +11 -17
  123. package/src/server/client-build-docs.js +45 -46
  124. package/src/server/client-build.js +334 -60
  125. package/src/server/client-formatted.js +47 -16
  126. package/src/server/conf.js +5 -4
  127. package/src/server/ipfs-client.js +232 -91
  128. package/src/server/process.js +13 -27
  129. package/src/server/start.js +6 -3
  130. package/src/server/valkey.js +134 -235
  131. package/tsconfig.docs.json +15 -0
  132. package/typedoc.json +20 -0
  133. package/jsdoc.json +0 -52
  134. package/src/client/components/core/ColorPalette.js +0 -5267
  135. package/src/client/components/core/JoyStick.js +0 -80
  136. package/src/client/components/default/RoutesDefault.js +0 -49
  137. package/src/client/sw/default.sw.js +0 -127
  138. package/src/client/sw/template.sw.js +0 -84
@@ -38,6 +38,8 @@ import { ssrFactory } from './ssr.js';
38
38
  * @memberof clientBuild
39
39
  */
40
40
  const copyNonExistingFiles = (src, dest) => {
41
+ if (dir.basename(src) === '.git') return;
42
+
41
43
  // Ensure source exists
42
44
  if (!fs.existsSync(src)) {
43
45
  throw new Error(`Source directory does not exist: ${src}`);
@@ -74,6 +76,224 @@ const copyNonExistingFiles = (src, dest) => {
74
76
  }
75
77
  };
76
78
 
79
+ const splitFileByMb = ({ filePath, partSizeMb, logger }) => {
80
+ const partSizeBytes = Math.floor(Number(partSizeMb) * 1024 * 1024);
81
+ if (!Number.isFinite(partSizeBytes) || partSizeBytes <= 0) {
82
+ throw new Error(`Invalid --split value: ${partSizeMb}`);
83
+ }
84
+
85
+ // Clean ALL stale part files (any naming variant) before writing new ones
86
+ const zipDir = dir.dirname(filePath);
87
+ const zipBase = dir.basename(filePath);
88
+ if (fs.existsSync(zipDir)) {
89
+ fs.readdirSync(zipDir)
90
+ .filter((name) => name.startsWith(`${zipBase}.part`) || name.startsWith(`${zipBase}-part`))
91
+ .forEach((name) => fs.removeSync(dir.join(zipDir, name)));
92
+ }
93
+
94
+ const fileBuffer = fs.readFileSync(filePath);
95
+ const partPaths = [];
96
+
97
+ for (let offset = 0, partIndex = 0; offset < fileBuffer.length; offset += partSizeBytes, partIndex++) {
98
+ const partBuffer = fileBuffer.subarray(offset, offset + partSizeBytes);
99
+ const partPath = `${filePath}.part${String(partIndex + 1).padStart(3, '0')}`;
100
+ fs.writeFileSync(partPath, partBuffer);
101
+ partPaths.push(partPath);
102
+ }
103
+
104
+ logger.warn('split zip', {
105
+ filePath,
106
+ partSizeMb: Number(partSizeMb),
107
+ parts: partPaths.length,
108
+ });
109
+
110
+ return partPaths;
111
+ };
112
+
113
+ const getZipPartPaths = (zipPath) => {
114
+ const zipDir = dir.dirname(zipPath);
115
+ const zipBase = dir.basename(zipPath);
116
+ const partPrefixDot = `${zipBase}.part`;
117
+ const partPrefixDash = `${zipBase}-part`;
118
+
119
+ const parsePartIndex = (rawSuffix) => {
120
+ // Strip optional .zip suffix added by pull/download (e.g. '001.zip' → '001')
121
+ const digits = rawSuffix.replace(/\.zip$/i, '');
122
+ return /^\d+$/.test(digits) ? Number(digits) : NaN;
123
+ };
124
+
125
+ const getPartIndex = (name) => {
126
+ if (name.startsWith(partPrefixDot)) return parsePartIndex(name.slice(partPrefixDot.length));
127
+ if (name.startsWith(partPrefixDash)) return parsePartIndex(name.slice(partPrefixDash.length));
128
+ return NaN;
129
+ };
130
+
131
+ return fs
132
+ .readdirSync(zipDir)
133
+ .filter((name) => Number.isFinite(getPartIndex(name)))
134
+ .sort((a, b) => getPartIndex(a) - getPartIndex(b))
135
+ .map((name) => dir.join(zipDir, name));
136
+ };
137
+
138
+ const resolveClientBuildZip = (buildPrefix) => {
139
+ const normalizedPrefix = buildPrefix.replace(/\.zip(?:[.-]part\d+|[.-]part\*)?$/, '').replace(/[.-]part\*$/, '');
140
+ const candidatePrefixes = uniqueArray([
141
+ normalizedPrefix,
142
+ normalizedPrefix.endsWith('-') ? normalizedPrefix : `${normalizedPrefix}-`,
143
+ ]);
144
+
145
+ for (const prefix of candidatePrefixes) {
146
+ const zipPath = `${prefix}.zip`;
147
+ if (fs.existsSync(zipPath)) {
148
+ return {
149
+ buildPrefix: prefix,
150
+ zipPath,
151
+ partPaths: [],
152
+ };
153
+ }
154
+
155
+ const partPaths = fs.existsSync(dir.dirname(zipPath)) ? getZipPartPaths(zipPath) : [];
156
+ if (partPaths.length > 0) {
157
+ return {
158
+ buildPrefix: prefix,
159
+ zipPath,
160
+ partPaths,
161
+ };
162
+ }
163
+ }
164
+
165
+ const searchDir = dir.dirname(normalizedPrefix);
166
+ const prefixBase = dir.basename(normalizedPrefix);
167
+ if (!fs.existsSync(searchDir)) {
168
+ throw new Error(`Build directory not found: ${searchDir}`);
169
+ }
170
+
171
+ const matches = uniqueArray(
172
+ fs
173
+ .readdirSync(searchDir)
174
+ .filter((name) => name.startsWith(prefixBase) && /\.zip(?:[.-]part\d+)?$/.test(name))
175
+ .map((name) => name.replace(/[.-]part\d+$/, '')),
176
+ );
177
+
178
+ if (matches.length === 1) {
179
+ const zipPath = dir.join(searchDir, matches[0]);
180
+ const partPaths = getZipPartPaths(zipPath);
181
+ return {
182
+ buildPrefix: zipPath.replace(/\.zip$/, ''),
183
+ zipPath,
184
+ partPaths,
185
+ };
186
+ }
187
+
188
+ if (matches.length > 1) {
189
+ throw new Error(
190
+ `Multiple build zip matches found for '${buildPrefix}': ${matches.join(', ')}. Use a more specific --unzip path.`,
191
+ );
192
+ }
193
+
194
+ throw new Error(`No build zip or split parts found for: ${buildPrefix}`);
195
+ };
196
+
197
+ /**
198
+ * Merges split ZIP parts back into a single ZIP file.
199
+ * @param {object} options
200
+ * @param {string} options.buildPrefix - The build prefix path (e.g. build/underpost.net/underpost.net-).
201
+ * @param {object} options.logger - Logger instance.
202
+ * @returns {{ zipPath: string, partPaths: string[], mergedBytes: number }}
203
+ */
204
+ const mergeClientBuildZip = ({ buildPrefix, logger }) => {
205
+ // Normalize to get the zip path, then look for parts directly (bypassing resolveClientBuildZip
206
+ // which prefers an existing monolithic zip over parts).
207
+ const normalizedPrefix = buildPrefix.replace(/\.zip(?:[.-]part\d+)?$/, '').replace(/[-.]$/, '') + '-';
208
+ const candidatePrefixes = uniqueArray([buildPrefix, buildPrefix.endsWith('-') ? buildPrefix : `${buildPrefix}-`]);
209
+
210
+ let zipPath;
211
+ let partPaths = [];
212
+
213
+ for (const prefix of candidatePrefixes) {
214
+ const candidate = prefix.endsWith('.zip') ? prefix : `${prefix}.zip`;
215
+ const parts = getZipPartPaths(candidate);
216
+ if (parts.length > 0) {
217
+ zipPath = candidate;
218
+ partPaths = parts;
219
+ break;
220
+ }
221
+ }
222
+
223
+ if (partPaths.length === 0) {
224
+ // Fall back to resolveClientBuildZip for the zipPath
225
+ const resolved = resolveClientBuildZip(buildPrefix);
226
+ zipPath = resolved.zipPath;
227
+ logger.warn('merge-zip: no split parts found, nothing to merge', { buildPrefix, zipPath });
228
+ return { zipPath, partPaths, mergedBytes: 0 };
229
+ }
230
+
231
+ // For each part, extract raw bytes: if the part file is a Cloudinary wrapper zip
232
+ // (downloaded via pull without --omit-unzip or with --omit-unzip keeping the .zip),
233
+ // extract the inner entry rather than using the wrapper bytes.
234
+ const readPartBytes = (partPath) => {
235
+ const rawBytes = fs.readFileSync(partPath);
236
+ // Check for ZIP magic bytes (PK\x03\x04)
237
+ if (rawBytes[0] === 0x50 && rawBytes[1] === 0x4b && rawBytes[2] === 0x03 && rawBytes[3] === 0x04) {
238
+ try {
239
+ const wrapperZip = new AdmZip(rawBytes);
240
+ const entries = wrapperZip.getEntries();
241
+ // The inner entry is the original part file (without the outer .zip wrapper)
242
+ const partBase = dir.basename(partPath).replace(/\.zip$/i, '');
243
+ const entry = entries.find((e) => e.entryName === partBase || e.entryName.endsWith('/' + partBase));
244
+ if (entry) {
245
+ return entry.getData();
246
+ }
247
+ // Fallback: single-entry archive
248
+ if (entries.length === 1) {
249
+ return entries[0].getData();
250
+ }
251
+ } catch (_) {
252
+ // Not a valid zip or extraction failed — use raw bytes
253
+ }
254
+ }
255
+ return rawBytes;
256
+ };
257
+
258
+ const mergedBuffer = Buffer.concat(partPaths.map(readPartBytes));
259
+ fs.writeFileSync(zipPath, mergedBuffer);
260
+
261
+ logger.warn('merge-zip: merged split parts into zip', {
262
+ zipPath,
263
+ parts: partPaths.length,
264
+ mergedBytes: mergedBuffer.length,
265
+ });
266
+
267
+ return { zipPath, partPaths, mergedBytes: mergedBuffer.length };
268
+ };
269
+
270
+ const unzipClientBuild = ({ buildPrefix, logger }) => {
271
+ const { zipPath, partPaths, buildPrefix: resolvedBuildPrefix } = resolveClientBuildZip(buildPrefix);
272
+ const outputPath = resolvedBuildPrefix.replace(/-$/, '');
273
+
274
+ fs.removeSync(outputPath);
275
+ fs.mkdirSync(outputPath, { recursive: true });
276
+
277
+ const zip =
278
+ partPaths.length > 0
279
+ ? new AdmZip(Buffer.concat(partPaths.map((partPath) => fs.readFileSync(partPath))))
280
+ : new AdmZip(zipPath);
281
+
282
+ zip.extractAllTo(outputPath, true);
283
+
284
+ logger.warn('unzip build', {
285
+ source: partPaths.length > 0 ? partPaths : [zipPath],
286
+ outputPath,
287
+ splitParts: partPaths.length,
288
+ });
289
+
290
+ return {
291
+ outputPath,
292
+ zipPath,
293
+ partPaths,
294
+ };
295
+ };
296
+
77
297
  /** @type {string} Default XSL sitemap template used when no `sitemap` source file exists in the public directory. */
78
298
  const defaultSitemapXsl = `<?xml version="1.0" encoding="UTF-8"?>
79
299
  <xsl:stylesheet version="1.0"
@@ -233,6 +453,7 @@ const defaultSitemapXsl = `<?xml version="1.0" encoding="UTF-8"?>
233
453
  * @param {Array} options.liveClientBuildPaths - List of paths to build incrementally.
234
454
  * @param {Array} options.instances - List of instances to build.
235
455
  * @param {boolean} options.buildZip - Whether to create zip files of the builds.
456
+ * @param {string|number} options.split - Optional zip split size in MB.
236
457
  * @param {boolean} options.fullBuild - Whether to perform a full build.
237
458
  * @param {boolean} options.iconsBuild - Whether to build icons.
238
459
  * @returns {Promise<void>} - Promise that resolves when the build is complete.
@@ -245,6 +466,7 @@ const buildClient = async (
245
466
  liveClientBuildPaths: [],
246
467
  instances: [],
247
468
  buildZip: false,
469
+ split: '',
248
470
  fullBuild: false,
249
471
  iconsBuild: false,
250
472
  },
@@ -311,35 +533,18 @@ const buildClient = async (
311
533
 
312
534
  buildAcmeChallengePath(acmeChallengeFullPath);
313
535
 
314
- if (publicClientId && publicClientId.startsWith('html-website-templates')) {
315
- if (!fs.existsSync(`/home/dd/html-website-templates/`))
316
- shellExec(`cd /home/dd && git clone https://github.com/designmodo/html-website-templates.git`);
317
- if (!fs.existsSync(`${rootClientPath}/index.php`)) {
318
- fs.copySync(`/home/dd/html-website-templates/${publicClientId.split('-publicClientId-')[1]}`, rootClientPath);
319
- Underpost.repo.initLocalRepo({ path: rootClientPath });
320
- shellExec(`cd ${rootClientPath} && git add . && git commit -m "Base template implementation"`);
321
- // git remote add origin git@github.com:<username>/<repo>.git
322
- fs.writeFileSync(`${rootClientPath}/.git/.htaccess`, `Deny from all`, 'utf8');
323
- }
324
- return;
325
- }
326
-
327
536
  fs.removeSync(rootClientPath);
328
537
 
329
538
  if (fs.existsSync(`./src/client/public/${publicClientId}`)) {
330
539
  if (iconsBuild === true) await buildIcons({ publicClientId, metadata });
331
540
 
332
- fs.copySync(
333
- `./src/client/public/${publicClientId}`,
334
- rootClientPath /* {
335
- filter: function (name) {
336
- console.log(name);
337
- return true;
338
- },
339
- } */,
340
- );
541
+ fs.copySync(`./src/client/public/${publicClientId}`, rootClientPath, {
542
+ filter: (sourcePath) => !sourcePath.split(dir.sep).includes('.git'),
543
+ });
341
544
  } else if (fs.existsSync(`./engine-private/src/client/public/${publicClientId}`)) {
342
- fs.copySync(`./engine-private/src/client/public/${publicClientId}`, rootClientPath);
545
+ fs.copySync(`./engine-private/src/client/public/${publicClientId}`, rootClientPath, {
546
+ filter: (sourcePath) => !sourcePath.split(dir.sep).includes('.git'),
547
+ });
343
548
  }
344
549
  if (dists)
345
550
  for (const dist of dists) {
@@ -463,28 +668,18 @@ const buildClient = async (
463
668
  for (const module of services) {
464
669
  if (!fs.existsSync(`${rootClientPath}/services/${module}`))
465
670
  fs.mkdirSync(`${rootClientPath}/services/${module}`, { recursive: true });
671
+ const moduleDir = `./src/client/services/${module}`;
672
+ if (!fs.existsSync(moduleDir)) continue;
466
673
 
467
- if (fs.existsSync(`./src/client/services/${module}/${module}.service.js`)) {
468
- const jsSrcPath = `./src/client/services/${module}/${module}.service.js`;
469
- const jsPublicPath = `${rootClientPath}/services/${module}/${module}.service.js`;
470
- if (enableLiveRebuild && !options.liveClientBuildPaths.find((p) => p.srcBuildPath === jsSrcPath)) continue;
674
+ const serviceFiles = fs
675
+ .readdirSync(moduleDir)
676
+ .filter((name) => name.endsWith('.service.js') || name.endsWith('.management.js'))
677
+ .sort();
471
678
 
472
- const jsSrc = await transformClientJs(jsSrcPath, {
473
- dists,
474
- proxyPath: path,
475
- basePath: 'services',
476
- module,
477
- baseHost,
478
- minify: minifyBuild,
479
- });
480
- fs.writeFileSync(jsPublicPath, jsSrc, 'utf8');
481
- }
482
- }
679
+ for (const serviceFile of serviceFiles) {
680
+ const jsSrcPath = `${moduleDir}/${serviceFile}`;
681
+ const jsPublicPath = `${rootClientPath}/services/${module}/${serviceFile}`;
483
682
 
484
- for (const module of services) {
485
- if (fs.existsSync(`./src/client/services/${module}/${module}.management.js`)) {
486
- const jsSrcPath = `./src/client/services/${module}/${module}.management.js`;
487
- const jsPublicPath = `${rootClientPath}/services/${module}/${module}.management.js`;
488
683
  if (enableLiveRebuild && !options.liveClientBuildPaths.find((p) => p.srcBuildPath === jsSrcPath)) continue;
489
684
 
490
685
  const jsSrc = await transformClientJs(jsSrcPath, {
@@ -497,6 +692,30 @@ const buildClient = async (
497
692
  });
498
693
  fs.writeFileSync(jsPublicPath, jsSrc, 'utf8');
499
694
  }
695
+
696
+ // Auto-build guest module files when user module is processed
697
+ if (module === 'user') {
698
+ const guestModuleDir = './src/client/services/user';
699
+ const guestServicePath = `${guestModuleDir}/guest.service.js`;
700
+ if (fs.existsSync(guestServicePath)) {
701
+ if (!fs.existsSync(`${rootClientPath}/services/user`))
702
+ fs.mkdirSync(`${rootClientPath}/services/user`, { recursive: true });
703
+
704
+ const guestJsPublicPath = `${rootClientPath}/services/user/guest.service.js`;
705
+
706
+ if (!enableLiveRebuild || options.liveClientBuildPaths.find((p) => p.srcBuildPath === guestServicePath)) {
707
+ const guestJsSrc = await transformClientJs(guestServicePath, {
708
+ dists,
709
+ proxyPath: path,
710
+ basePath: 'services',
711
+ module: 'user',
712
+ baseHost,
713
+ minify: minifyBuild,
714
+ });
715
+ fs.writeFileSync(guestJsPublicPath, guestJsSrc, 'utf8');
716
+ }
717
+ }
718
+ }
500
719
  }
501
720
  }
502
721
 
@@ -506,14 +725,18 @@ const buildClient = async (
506
725
  const Render = await ssrFactory();
507
726
 
508
727
  if (views) {
509
- const jsSrcPath = fs.existsSync(`./src/client/sw/${publicClientId}.sw.js`)
510
- ? `./src/client/sw/${publicClientId}.sw.js`
511
- : `./src/client/sw/default.sw.js`;
728
+ const jsSrcPath = `./src/client/sw/core.sw.js`;
512
729
 
513
730
  const jsPublicPath = `${rootClientPath}/sw.js`;
514
731
 
515
732
  if (!(enableLiveRebuild && !options.liveClientBuildPaths.find((p) => p.srcBuildPath === jsSrcPath))) {
516
- const jsSrc = await transformClientJs(jsSrcPath, { dists, proxyPath: path, baseHost, minify: minifyBuild });
733
+ const jsSrc = await transformClientJs(jsSrcPath, {
734
+ dists,
735
+ proxyPath: path,
736
+ baseHost,
737
+ minify: minifyBuild,
738
+ externalizeBareImports: false,
739
+ });
517
740
 
518
741
  fs.writeFileSync(jsPublicPath, jsSrc, 'utf8');
519
742
  }
@@ -746,16 +969,18 @@ Sitemap: ${sitemapBaseUrl}/sitemap.xml`,
746
969
  if (client) {
747
970
  let PRE_CACHED_RESOURCES = [];
748
971
 
749
- if (views && fs.existsSync(`${rootClientPath}/sw.js`)) {
750
- PRE_CACHED_RESOURCES = await fs.readdir(rootClientPath, { recursive: true });
751
- PRE_CACHED_RESOURCES = views
752
- .map((view) => `${path === '/' ? '' : path}${view.path}`)
753
- .concat(
754
- PRE_CACHED_RESOURCES.map((p) => `/${p}`).filter(
755
- (p) => p[1] !== '.' && !fs.statSync(`${rootClientPath}${p}`).isDirectory(),
756
- ),
757
- );
758
- }
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
+ };
759
984
 
760
985
  for (const pageType of ['offline', 'pages']) {
761
986
  if (confSSR[getCapVariableName(client)] && confSSR[getCapVariableName(client)][pageType]) {
@@ -783,7 +1008,11 @@ Sitemap: ${sitemapBaseUrl}/sitemap.xml`,
783
1008
  rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
784
1009
  }${page.path === '/' ? page.path : `${page.path}/`}`;
785
1010
 
786
- PRE_CACHED_RESOURCES.push(`${path === '/' ? '' : path}${page.path === '/' ? '' : page.path}/index.html`);
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
+ }
787
1016
 
788
1017
  if (!fs.existsSync(buildPath)) fs.mkdirSync(buildPath, { recursive: true });
789
1018
 
@@ -809,13 +1038,47 @@ Sitemap: ${sitemapBaseUrl}/sitemap.xml`,
809
1038
  }
810
1039
 
811
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
+ };
1053
+
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];
1060
+
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];
1067
+
1068
+ const offlinePath = normalizeSsrRoutePath(offlineSsrPage?.path, '/offline');
1069
+ const maintenancePath = normalizeSsrRoutePath(maintenanceSsrPage?.path, '/maintenance');
1070
+
812
1071
  const renderPayload = {
813
1072
  PRE_CACHED_RESOURCES: uniqueArray(PRE_CACHED_RESOURCES),
814
1073
  PROXY_PATH: path,
1074
+ CACHE_PREFIX: `engine-core-v3-${cacheScope}`,
1075
+ OFFLINE_PATH: offlinePath,
1076
+ MAINTENANCE_PATH: maintenancePath,
815
1077
  };
816
1078
  fs.writeFileSync(
817
1079
  `${rootClientPath}/sw.js`,
818
1080
  `self.renderPayload = ${JSONweb(renderPayload)};
1081
+ self.__WB_DISABLE_DEV_LOGS = true;
819
1082
  ${fs.readFileSync(`${rootClientPath}/sw.js`, 'utf8')}`,
820
1083
  'utf8',
821
1084
  );
@@ -838,13 +1101,24 @@ ${fs.readFileSync(`${rootClientPath}/sw.js`, 'utf8')}`,
838
1101
  }
839
1102
 
840
1103
  const buildId = `${host}-${path.replaceAll('/', '')}`;
1104
+ const zipPath = `./build/${buildId}.zip`;
841
1105
 
842
- logger.warn('write zip', `./build/${buildId}.zip`);
1106
+ logger.warn('write zip', zipPath);
843
1107
 
844
- zip.writeZip(`./build/${buildId}.zip`);
1108
+ zip.writeZip(zipPath);
1109
+
1110
+ if (options.split) {
1111
+ splitFileByMb({
1112
+ filePath: zipPath,
1113
+ partSizeMb: options.split,
1114
+ logger,
1115
+ });
1116
+ fs.removeSync(zipPath);
1117
+ logger.warn('removed original zip after split', { zipPath });
1118
+ }
845
1119
  }
846
1120
  }
847
1121
  }
848
1122
  };
849
1123
 
850
- export { buildClient, copyNonExistingFiles };
1124
+ export { buildClient, copyNonExistingFiles, unzipClientBuild, mergeClientBuildZip };
@@ -27,6 +27,13 @@ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
27
27
  */
28
28
  const srcFormatted = (src) => src.replace(/(?<=[\s({[,;=+!?:^])(html|css)`/g, '`');
29
29
 
30
+ const resolveBrowserImportPath = (basePrefix, relativePath) => {
31
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(basePrefix)) {
32
+ return new URL(relativePath, basePrefix.endsWith('/') ? basePrefix : `${basePrefix}/`).toString();
33
+ }
34
+ return path.posix.normalize(`${basePrefix}${relativePath}`);
35
+ };
36
+
30
37
  /**
31
38
  * Converts a JavaScript object into a string that can be embedded in client-side code
32
39
  * and parsed back into an object (e.g., 'JSON.parse(`{...}`)').
@@ -52,7 +59,14 @@ const JSONweb = (data) => {
52
59
  * @returns {import('esbuild').Plugin}
53
60
  * @memberof clientFormatted
54
61
  */
55
- const importRewritePlugin = ({ dists = [], proxyPath, basePath = '', module = '', baseHost = '' }) => ({
62
+ const importRewritePlugin = ({
63
+ dists = [],
64
+ proxyPath,
65
+ basePath = '',
66
+ module = '',
67
+ baseHost = '',
68
+ externalizeBareImports = true,
69
+ }) => ({
56
70
  name: 'import-rewrite',
57
71
  setup(build) {
58
72
  const prefix = `${baseHost}${proxyPath !== '/' ? `${proxyPath}/` : '/'}`;
@@ -69,26 +83,35 @@ const importRewritePlugin = ({ dists = [], proxyPath, basePath = '', module = ''
69
83
  }
70
84
  }
71
85
 
72
- // Rewrite relative imports to absolute paths based on proxy path and module
86
+ // Rewrite app-relative imports to absolute paths based on proxy path and module.
87
+ // Do not touch node_modules relative imports so esbuild can bundle package internals.
73
88
  build.onResolve({ filter: /^\.\.?\// }, (args) => {
74
- const basePrefix = `${prefix}${basePath ? `${basePath}/` : ''}`;
75
- if (args.path.startsWith('./')) {
76
- return {
77
- path: `${basePrefix}${module ? `${module}/` : ''}${args.path.slice(2)}`,
78
- external: true,
79
- };
89
+ const normalizedImporter = (args.importer || '').replace(/\\/g, '/');
90
+ if (!normalizedImporter.includes('/src/client/')) {
91
+ return;
80
92
  }
81
- if (args.path.startsWith('../')) {
82
- return {
83
- path: `${basePrefix}${args.path.slice(3)}`,
84
- external: true,
85
- };
93
+
94
+ // Extract the path relative to /src/client/
95
+ // Handle cases where the path might have duplicates or be in various formats
96
+ const srcClientIndex = normalizedImporter.lastIndexOf('/src/client/');
97
+ if (srcClientIndex === -1) {
98
+ return;
86
99
  }
100
+ const importerFromClientRoot = normalizedImporter.substring(srcClientIndex + '/src/client/'.length);
101
+ const importerDir = path.posix.dirname(importerFromClientRoot);
102
+ const resolvedFromClientRoot = path.posix.normalize(path.posix.join(importerDir, args.path));
103
+ const result = resolveBrowserImportPath(prefix, resolvedFromClientRoot);
104
+
105
+ return {
106
+ path: result,
107
+ external: true,
108
+ };
87
109
  });
88
110
 
89
- // Mark any remaining imports as external
111
+ // For client app modules we externalize bare imports; for SW builds we let esbuild bundle them.
90
112
  build.onResolve({ filter: /.*/ }, (args) => {
91
113
  if (args.kind === 'entry-point') return;
114
+ if (!externalizeBareImports) return;
92
115
  return { path: args.path, external: true };
93
116
  });
94
117
  },
@@ -111,7 +134,15 @@ const importRewritePlugin = ({ dists = [], proxyPath, basePath = '', module = ''
111
134
  */
112
135
  const transformClientJs = async (
113
136
  srcPath,
114
- { dists = [], proxyPath, basePath = '', module = '', baseHost = '', minify: shouldMinify = false } = {},
137
+ {
138
+ dists = [],
139
+ proxyPath,
140
+ basePath = '',
141
+ module = '',
142
+ baseHost = '',
143
+ minify: shouldMinify = false,
144
+ externalizeBareImports = true,
145
+ } = {},
115
146
  ) => {
116
147
  const src = fs.readFileSync(srcPath, 'utf8');
117
148
  const stripped = srcFormatted(src);
@@ -130,7 +161,7 @@ const transformClientJs = async (
130
161
  target: 'esnext',
131
162
  minify: shouldMinify,
132
163
  logLevel: 'warning',
133
- plugins: [importRewritePlugin({ dists, proxyPath, basePath, module, baseHost })],
164
+ plugins: [importRewritePlugin({ dists, proxyPath, basePath, module, baseHost, externalizeBareImports })],
134
165
  });
135
166
 
136
167
  return result.outputFiles[0].text;
@@ -457,7 +457,7 @@ const loadConf = (deployId = DEFAULT_DEPLOY_ID, subConf) => {
457
457
  fs.removeSync(`${path}/.env.production`);
458
458
  fs.removeSync(`${path}/.env.development`);
459
459
  fs.removeSync(`${path}/.env.test`);
460
- if (fs.existsSync(`${path}/jsdoc.json`)) shellExec(`git checkout ${path}/jsdoc.json`);
460
+ if (fs.existsSync(`${path}/typedoc.json`)) shellExec(`git checkout ${path}/typedoc.json`);
461
461
  shellExec(`git checkout ${path}/package.json`);
462
462
  shellExec(`git checkout ${path}/package-lock.json`);
463
463
  return;
@@ -1156,6 +1156,7 @@ const getDataDeploy = async (
1156
1156
 
1157
1157
  let buildDataDeploy = [];
1158
1158
  for (const deployObj of dataDeploy) {
1159
+ const isReplicaDeploy = fs.existsSync(`./engine-private/replica/${deployObj.deployId}`);
1159
1160
  const serverConf = loadReplicas(
1160
1161
  deployObj.deployId,
1161
1162
  loadConfServerJson(`./engine-private/conf/${deployObj.deployId}/conf.server.json`),
@@ -1163,7 +1164,7 @@ const getDataDeploy = async (
1163
1164
  let replicaDataDeploy = [];
1164
1165
  for (const host of Object.keys(serverConf))
1165
1166
  for (const path of Object.keys(serverConf[host])) {
1166
- if (serverConf[host][path].replicas && serverConf[host][path].singleReplica) {
1167
+ if (!isReplicaDeploy && serverConf[host][path].replicas && serverConf[host][path].singleReplica) {
1167
1168
  if (options && options.buildSingleReplica)
1168
1169
  await Underpost.repo.client(deployObj.deployId, '', host, path, {
1169
1170
  singleReplica: true,
@@ -1211,7 +1212,7 @@ const validateTemplatePath = (absolutePath = '') => {
1211
1212
  return false;
1212
1213
  }
1213
1214
  if (absolutePath.match('conf.dd-') && absolutePath.match('.js')) return false;
1214
- if (absolutePath.match('jsdoc.dd-') && absolutePath.match('.json')) return false;
1215
+ if (absolutePath.match('typedoc.dd-') && absolutePath.match('.json')) return false;
1215
1216
  if (
1216
1217
  absolutePath.match('src/client/services/') &&
1217
1218
  !clients.find((p) => absolutePath.match(`src/client/services/${p}/`))
@@ -1227,7 +1228,7 @@ const validateTemplatePath = (absolutePath = '') => {
1227
1228
  ) {
1228
1229
  return false;
1229
1230
  }
1230
- if (absolutePath.match('src/client/sw/') && !clients.find((p) => absolutePath.match(`src/client/sw/${p}.sw.js`))) {
1231
+ if (absolutePath.match('src/client/sw/') && !absolutePath.match('src/client/sw/core.sw.js')) {
1231
1232
  return false;
1232
1233
  }
1233
1234
  if (