underpost 2.8.877 → 2.8.881

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 (54) hide show
  1. package/.env.development +35 -3
  2. package/.env.production +40 -3
  3. package/.env.test +35 -3
  4. package/.github/workflows/release.cd.yml +3 -3
  5. package/README.md +48 -36
  6. package/bin/deploy.js +40 -0
  7. package/cli.md +89 -86
  8. package/conf.js +28 -3
  9. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  10. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  11. package/package.json +1 -2
  12. package/src/api/document/document.controller.js +66 -0
  13. package/src/api/document/document.model.js +51 -0
  14. package/src/api/document/document.router.js +24 -0
  15. package/src/api/document/document.service.js +125 -0
  16. package/src/api/file/file.controller.js +15 -1
  17. package/src/api/user/user.router.js +4 -3
  18. package/src/cli/deploy.js +1 -1
  19. package/src/cli/index.js +3 -0
  20. package/src/cli/repository.js +2 -2
  21. package/src/cli/run.js +29 -1
  22. package/src/client/Default.index.js +42 -1
  23. package/src/client/components/core/Account.js +8 -1
  24. package/src/client/components/core/AgGrid.js +18 -9
  25. package/src/client/components/core/BtnIcon.js +3 -2
  26. package/src/client/components/core/Content.js +13 -11
  27. package/src/client/components/core/CssCore.js +4 -0
  28. package/src/client/components/core/Docs.js +0 -3
  29. package/src/client/components/core/Input.js +34 -19
  30. package/src/client/components/core/Modal.js +29 -7
  31. package/src/client/components/core/ObjectLayerEngine.js +370 -0
  32. package/src/client/components/core/ObjectLayerEngineModal.js +1 -0
  33. package/src/client/components/core/Panel.js +7 -2
  34. package/src/client/components/core/PanelForm.js +187 -63
  35. package/src/client/components/core/VanillaJs.js +3 -0
  36. package/src/client/components/default/MenuDefault.js +94 -41
  37. package/src/client/components/default/RoutesDefault.js +2 -0
  38. package/src/client/services/default/default.management.js +1 -0
  39. package/src/client/services/document/document.service.js +97 -0
  40. package/src/client/services/file/file.service.js +2 -0
  41. package/src/client/services/user/user.service.js +1 -0
  42. package/src/client/ssr/Render.js +1 -1
  43. package/src/client/ssr/head/DefaultScripts.js +2 -0
  44. package/src/client/ssr/head/Seo.js +1 -0
  45. package/src/index.js +1 -1
  46. package/src/mailer/EmailRender.js +1 -1
  47. package/src/server/auth.js +4 -3
  48. package/src/server/client-build.js +2 -3
  49. package/src/server/client-formatted.js +40 -12
  50. package/src/server/conf.js +42 -3
  51. package/src/server/object-layer.js +196 -0
  52. package/src/server/runtime.js +18 -21
  53. package/src/server/ssr.js +52 -10
  54. package/src/server/valkey.js +89 -1
package/src/index.js CHANGED
@@ -35,7 +35,7 @@ class Underpost {
35
35
  * @type {String}
36
36
  * @memberof Underpost
37
37
  */
38
- static version = 'v2.8.877';
38
+ static version = 'v2.8.881';
39
39
  /**
40
40
  * Repository cli API
41
41
  * @static
@@ -1,4 +1,4 @@
1
- import { ssrFactory } from '../server/client-formatted.js';
1
+ import { ssrFactory } from '../server/ssr.js';
2
2
 
3
3
  const EmailRender = {
4
4
  style: {
@@ -523,6 +523,7 @@ function applySecurity(app, opts = {}) {
523
523
 
524
524
  // Content-Security-Policy: include nonce from res.locals
525
525
  // Note: We avoid 'unsafe-inline' on script/style. Use nonces or hashes.
526
+ const httpDirective = process.env.NODE_ENV === 'production' ? 'https:' : 'http:';
526
527
  app.use(
527
528
  helmet.contentSecurityPolicy({
528
529
  useDefaults: true,
@@ -530,16 +531,16 @@ function applySecurity(app, opts = {}) {
530
531
  defaultSrc: ["'self'"],
531
532
  baseUri: ["'self'"],
532
533
  blockAllMixedContent: [],
533
- fontSrc: ["'self'", 'https:', 'data:'],
534
+ fontSrc: ["'self'", httpDirective, 'data:'],
534
535
  frameAncestors: frameAncestors,
535
- imgSrc: ["'self'", 'data:', 'https:'],
536
+ imgSrc: ["'self'", 'data:', httpDirective],
536
537
  objectSrc: ["'none'"],
537
538
  // script-src and script-src-elem include dynamic nonce
538
539
  scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
539
540
  scriptSrcElem: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
540
541
  // style-src: avoid 'unsafe-inline' when possible; if you must inline styles,
541
542
  // use a nonce for them too (or hash).
542
- styleSrc: ["'self'", 'https:', (req, res) => `'nonce-${res.locals.nonce}'`],
543
+ styleSrc: ["'self'", httpDirective, (req, res) => `'nonce-${res.locals.nonce}'`],
543
544
  // deny plugins
544
545
  objectSrc: ["'none'"],
545
546
  },
@@ -1,14 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  import fs from 'fs-extra';
4
- import { srcFormatted, componentFormatted, viewFormatted, ssrFactory, JSONweb } from './client-formatted.js';
4
+ import { srcFormatted, componentFormatted, viewFormatted, JSONweb } from './client-formatted.js';
5
5
  import { loggerFactory } from './logger.js';
6
6
  import {
7
- cap,
8
7
  getCapVariableName,
9
8
  newInstance,
10
9
  orderArrayFromAttrInt,
11
- titleFormatted,
12
10
  uniqueArray,
13
11
  } from '../client/components/core/CommonJs.js';
14
12
  import UglifyJS from 'uglify-js';
@@ -22,6 +20,7 @@ import { Readable } from 'stream';
22
20
  import { buildIcons } from './client-icons.js';
23
21
  import Underpost from '../index.js';
24
22
  import { buildDocs } from './client-build-docs.js';
23
+ import { ssrFactory } from './ssr.js';
25
24
 
26
25
  dotenv.config();
27
26
 
@@ -1,9 +1,17 @@
1
- 'use strict';
1
+ /**
2
+ * Module for formatting client-side code
3
+ * @module src/server/client-formatted.js
4
+ * @namespace clientFormatted
5
+ */
2
6
 
3
- import fs from 'fs-extra';
4
- import vm from 'node:vm';
5
- import Underpost from '../index.js';
7
+ 'use strict';
6
8
 
9
+ /**
10
+ * Formats a source code string by removing 'html`' and 'css`' tags from template literals.
11
+ * @param {string} src - The source code string.
12
+ * @returns {string} The formatted source code.
13
+ * @memberof clientFormatted
14
+ */
7
15
  const srcFormatted = (src) =>
8
16
  src
9
17
  .replaceAll(' html`', '`')
@@ -15,8 +23,26 @@ const srcFormatted = (src) =>
15
23
  .replaceAll('[html`', '[`')
16
24
  .replaceAll('[css`', '[`');
17
25
 
26
+ /**
27
+ * Converts a JavaScript object into a string that can be embedded in client-side code
28
+ * and parsed back into an object (e.g., 'JSON.parse(`{...}`)').
29
+ * @param {*} data - The data to be stringified.
30
+ * @returns {string} A string representing the code to parse the JSON data.
31
+ * @memberof clientFormatted
32
+ */
18
33
  const JSONweb = (data) => 'JSON.parse(`' + JSON.stringify(data) + '`)';
19
34
 
35
+ /**
36
+ * Formats a component's source code by rewriting its import paths to be absolute for browser consumption.
37
+ * @param {string} src - The source code of the component.
38
+ * @param {string} module - The name of the module/component.
39
+ * @param {Array<object>} dists - An array of distribution objects with import names.
40
+ * @param {string} proxyPath - The proxy path for the application.
41
+ * @param {string} [componentBasePath=''] - The base path for components.
42
+ * @param {string} [baseHost=''] - The base host URL.
43
+ * @returns {string} The formatted source code with updated import paths.
44
+ * @memberof clientFormatted
45
+ */
20
46
  const componentFormatted = (src, module, dists, proxyPath, componentBasePath = '', baseHost = '') => {
21
47
  dists.map(
22
48
  (dist) =>
@@ -40,6 +66,15 @@ const componentFormatted = (src, module, dists, proxyPath, componentBasePath = '
40
66
  );
41
67
  };
42
68
 
69
+ /**
70
+ * Formats a view's source code by rewriting its import paths.
71
+ * @param {string} src - The source code of the view.
72
+ * @param {Array<object>} dists - An array of distribution objects with import names.
73
+ * @param {string} proxyPath - The proxy path for the application.
74
+ * @param {string} [baseHost=''] - The base host URL.
75
+ * @returns {string} The formatted source code with updated import paths.
76
+ * @memberof clientFormatted
77
+ */
43
78
  const viewFormatted = (src, dists, proxyPath, baseHost = '') => {
44
79
  dists.map(
45
80
  (dist) =>
@@ -49,11 +84,4 @@ const viewFormatted = (src, dists, proxyPath, baseHost = '') => {
49
84
  return src.replaceAll(`from './`, componentFromFormatted).replaceAll(`from '../`, componentFromFormatted);
50
85
  };
51
86
 
52
- const ssrFactory = async (componentPath = `./src/client/ssr/Render.js`) => {
53
- const context = { SrrComponent: () => {}, npm_package_version: Underpost.version };
54
- vm.createContext(context);
55
- vm.runInContext(await srcFormatted(fs.readFileSync(componentPath, 'utf8')), context);
56
- return context.SrrComponent;
57
- };
58
-
59
- export { srcFormatted, JSONweb, componentFormatted, viewFormatted, ssrFactory };
87
+ export { srcFormatted, JSONweb, componentFormatted, viewFormatted };
@@ -33,12 +33,13 @@ const Config = {
33
33
  else if (deployContext.startsWith('dd-')) return loadConf(deployContext, process.env.NODE_ENV, subConf);
34
34
  if (deployContext === 'proxy') Config.buildProxy(deployContext, deployList, subConf);
35
35
  },
36
- deployIdFactory: function (deployId = 'dd-default') {
36
+ deployIdFactory: function (deployId = 'dd-default', options = { cluster: false }) {
37
37
  if (!deployId.startsWith('dd-')) deployId = `dd-${deployId}`;
38
38
 
39
39
  logger.info('Build deployId', deployId);
40
40
 
41
41
  const folder = `./engine-private/conf/${deployId}`;
42
+ const repoName = `engine-${deployId.split('dd-')[1]}`;
42
43
 
43
44
  if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
44
45
  fs.writeFileSync(
@@ -56,10 +57,48 @@ const Config = {
56
57
  fs.readFileSync('./.env.test', 'utf8').replaceAll('dd-default', deployId),
57
58
  'utf8',
58
59
  );
59
- fs.writeFileSync(`${folder}/package.json`, fs.readFileSync('./package.json', 'utf8'), 'utf8');
60
+ fs.writeFileSync(
61
+ `${folder}/package.json`,
62
+ fs.readFileSync('./package.json', 'utf8').replaceAll('dd-default', deployId),
63
+ 'utf8',
64
+ );
60
65
 
61
66
  this.buildTmpConf(folder);
62
67
 
68
+ if (options.cluster === true) {
69
+ fs.writeFileSync(
70
+ `./.github/workflows/${repoName}.cd.yml`,
71
+ fs.readFileSync(`./.github/workflows/engine-test.cd.yml`, 'utf8').replaceAll('test', deployId.split('dd-')[1]),
72
+ 'utf8',
73
+ );
74
+ fs.writeFileSync(
75
+ `./.github/workflows/${repoName}.ci.yml`,
76
+ fs.readFileSync(`./.github/workflows/engine-test.ci.yml`, 'utf8').replaceAll('test', deployId.split('dd-')[1]),
77
+ 'utf8',
78
+ );
79
+ shellExec(`node bin/deploy update-default-conf ${deployId}`);
80
+
81
+ if (!fs.existsSync(`./engine-private/deploy/dd.router`))
82
+ fs.writeFileSync(`./engine-private/deploy/dd.router`, '', 'utf8');
83
+
84
+ fs.writeFileSync(
85
+ `./engine-private/deploy/dd.router`,
86
+ fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').trim() + `,${deployId}`,
87
+ 'utf8',
88
+ );
89
+ const updateRepo = (stage = 1) => {
90
+ shellExec(`git add . && git commit -m "Add base deployId ${deployId} cluster files stage:${stage}"`);
91
+ shellExec(
92
+ `cd engine-private && git add . && git commit -m "Add base deployId ${deployId} cluster files stage:${stage}"`,
93
+ );
94
+ };
95
+ updateRepo(1);
96
+ shellExec(`node bin run --build --dev sync`);
97
+ updateRepo(2);
98
+ shellExec(`node bin run --build sync`);
99
+ updateRepo(3);
100
+ }
101
+
63
102
  return { deployIdFolder: folder, deployId };
64
103
  },
65
104
  buildTmpConf: function (folder = './conf') {
@@ -657,7 +696,7 @@ const validateTemplatePath = (absolutePath = '') => {
657
696
  const confServer = DefaultConf.server[host][path];
658
697
  const confClient = DefaultConf.client[client];
659
698
  const confSsr = DefaultConf.ssr[ssr];
660
- const clients = Object.keys(confClient).concat(['core', 'test', 'default', 'user']);
699
+ const clients = DefaultConf.client.default.services;
661
700
 
662
701
  if (absolutePath.match('src/api') && !confServer.apis.find((p) => absolutePath.match(`src/api/${p}/`))) {
663
702
  return false;
@@ -0,0 +1,196 @@
1
+ import dotenv from 'dotenv';
2
+
3
+ import fs from 'fs-extra';
4
+ import { PNG } from 'pngjs';
5
+ import sharp from 'sharp';
6
+ import Jimp from 'jimp';
7
+
8
+ import { range } from '../client/components/core/CommonJs.js';
9
+ import { random } from '../client/components/core/CommonJs.js';
10
+
11
+ dotenv.config({ path: `./engine-private/conf/dd-cyberia/.env.production`, override: true });
12
+
13
+ const pngDirectoryIteratorByObjectLayerType = async (
14
+ objectLayerType = 'skin',
15
+ callback = ({ path, objectLayerType, objectLayerId, direction, frame }) => {},
16
+ ) => {
17
+ for (const objectLayerId of await fs.readdir(`./src/client/public/cyberia/assets/${objectLayerType}`)) {
18
+ for (const direction of await fs.readdir(
19
+ `./src/client/public/cyberia/assets/${objectLayerType}/${objectLayerId}`,
20
+ )) {
21
+ const dirFolder = `./src/client/public/cyberia/assets/${objectLayerType}/${objectLayerId}/${direction}`;
22
+ if (!fs.statSync(dirFolder).isDirectory()) continue;
23
+ for (const frame of await fs.readdir(dirFolder)) {
24
+ const imageFilePath = `./src/client/public/cyberia/assets/${objectLayerType}/${objectLayerId}/${direction}/${frame}`;
25
+ await callback({ path: imageFilePath, objectLayerType, objectLayerId, direction, frame });
26
+ }
27
+ }
28
+ }
29
+ };
30
+
31
+ const readPngAsync = (filePath) => {
32
+ return new Promise((resolve, reject) => {
33
+ fs.createReadStream(filePath)
34
+ .pipe(new PNG())
35
+ .on('parsed', function () {
36
+ resolve({
37
+ width: this.width,
38
+ height: this.height,
39
+ data: Buffer.from(this.data),
40
+ });
41
+ })
42
+ .on('error', (err) => {
43
+ reject(err);
44
+ });
45
+ });
46
+ };
47
+
48
+ const frameFactory = async (path, colors = []) => {
49
+ const frame = [];
50
+ try {
51
+ let image;
52
+
53
+ if (path.endsWith('.gif')) {
54
+ image = await Jimp.read(path);
55
+ // remove gif file
56
+ fs.removeSync(path);
57
+ // save image replacing gif for png
58
+ const pngPath = path.replace('.gif', '.png');
59
+ await image.writeAsync(pngPath);
60
+ } else {
61
+ const png = await readPngAsync(path);
62
+ image = new Jimp(png.width, png.height);
63
+ image.bitmap = png;
64
+ }
65
+
66
+ const mazeFactor = parseInt(image.bitmap.height / 24);
67
+ let _y = -1;
68
+ for (const y of range(0, image.bitmap.height - 1)) {
69
+ if (y % mazeFactor === 0) {
70
+ _y++;
71
+ if (!frame[_y]) frame[_y] = [];
72
+ }
73
+ let _x = -1;
74
+ for (const x of range(0, image.bitmap.width - 1)) {
75
+ const rgba = Object.values(Jimp.intToRGBA(image.getPixelColor(x, y)));
76
+ if (y % mazeFactor === 0 && x % mazeFactor === 0) {
77
+ _x++;
78
+ const indexColor = colors.findIndex(
79
+ (c) => c[0] === rgba[0] && c[1] === rgba[1] && c[2] === rgba[2] && c[3] === rgba[3],
80
+ );
81
+ if (indexColor === -1) {
82
+ colors.push(rgba);
83
+ frame[_y][_x] = colors.length - 1;
84
+ } else {
85
+ frame[_y][_x] = indexColor;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ } catch (error) {
91
+ logger.error(`Failed to process image ${path}:`, error);
92
+ }
93
+ return { frame, colors };
94
+ };
95
+
96
+ const getKeyFramesDirectionsFromNumberFolderDirection = (direction) => {
97
+ let objectLayerFrameDirections = [];
98
+
99
+ switch (direction) {
100
+ case '08':
101
+ objectLayerFrameDirections = ['down_idle', 'none_idle', 'default_idle'];
102
+ break;
103
+ case '18':
104
+ objectLayerFrameDirections = ['down_walking'];
105
+ break;
106
+ case '02':
107
+ objectLayerFrameDirections = ['up_idle'];
108
+ break;
109
+ case '12':
110
+ objectLayerFrameDirections = ['up_walking'];
111
+ break;
112
+ case '04':
113
+ objectLayerFrameDirections = ['left_idle', 'up_left_idle', 'down_left_idle'];
114
+ break;
115
+ case '14':
116
+ objectLayerFrameDirections = ['left_walking', 'up_left_walking', 'down_left_walking'];
117
+ break;
118
+ case '06':
119
+ objectLayerFrameDirections = ['right_idle', 'up_right_idle', 'down_right_idle'];
120
+ break;
121
+ case '16':
122
+ objectLayerFrameDirections = ['right_walking', 'up_right_walking', 'down_right_walking'];
123
+ break;
124
+ }
125
+
126
+ return objectLayerFrameDirections;
127
+ };
128
+
129
+ const buildImgFromTile = async (
130
+ options = {
131
+ tile: { map_color: null, frame_matrix: null },
132
+ imagePath: '',
133
+ cellPixelDim: 20,
134
+ opacityFilter: (x, y, color) => 255,
135
+ },
136
+ ) => {
137
+ const { tile, imagePath, cellPixelDim, opacityFilter } = options;
138
+ const mainMatrix = tile.frame_matrix;
139
+ const sharpOptions = {
140
+ create: {
141
+ width: cellPixelDim * mainMatrix[0].length,
142
+ height: cellPixelDim * mainMatrix.length,
143
+ channels: 4,
144
+ background: { r: 0, g: 0, b: 0, alpha: 0 }, // transparent background
145
+ },
146
+ };
147
+
148
+ let image = await sharp(sharpOptions).png().toBuffer();
149
+ fs.writeFileSync(imagePath, image);
150
+ image = await Jimp.read(imagePath);
151
+
152
+ for (let y = 0; y < mainMatrix.length; y++) {
153
+ for (let x = 0; x < mainMatrix[y].length; x++) {
154
+ const colorIndex = mainMatrix[y][x];
155
+ if (colorIndex === null || colorIndex === undefined) continue;
156
+
157
+ const color = tile.map_color[colorIndex];
158
+ if (!color) continue;
159
+
160
+ const rgbaColor = color.length === 4 ? color : [...color, 255]; // Ensure alpha channel
161
+
162
+ for (let dy = 0; dy < cellPixelDim; dy++) {
163
+ for (let dx = 0; dx < cellPixelDim; dx++) {
164
+ const pixelX = x * cellPixelDim + dx;
165
+ const pixelY = y * cellPixelDim + dy;
166
+ image.setPixelColor(Jimp.rgbaToInt(...rgbaColor), pixelX, pixelY);
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ await image.writeAsync(imagePath);
173
+ };
174
+
175
+ const generateRandomStats = () => {
176
+ return {
177
+ effect: random(0, 10),
178
+ resistance: random(0, 10),
179
+ agility: random(0, 10),
180
+ range: random(0, 10),
181
+ intelligence: random(0, 10),
182
+ utility: random(0, 10),
183
+ };
184
+ };
185
+
186
+ const zIndexPriority = { floor: 0, skin: 1, weapon: 2, skill: 3, coin: 4 };
187
+
188
+ export {
189
+ pngDirectoryIteratorByObjectLayerType,
190
+ readPngAsync,
191
+ frameFactory,
192
+ getKeyFramesDirectionsFromNumberFolderDirection,
193
+ buildImgFromTile,
194
+ generateRandomStats,
195
+ zIndexPriority,
196
+ };
@@ -117,9 +117,6 @@ const buildRuntime = async () => {
117
117
  // set logger
118
118
  app.use(loggerMiddleware(import.meta));
119
119
 
120
- // instance public static
121
- app.use('/', express.static(directory ? directory : `.${rootHostPath}`));
122
-
123
120
  // js src compression
124
121
  app.use(compression({ filter: shouldCompress }));
125
122
  function shouldCompress(req, res) {
@@ -132,17 +129,6 @@ const buildRuntime = async () => {
132
129
  return compression.filter(req, res);
133
130
  }
134
131
 
135
- if (process.argv.includes('static')) {
136
- logger.info('Build static server runtime', `${host}${path}`);
137
- currentPort += 2;
138
- const staticPort = newInstance(currentPort);
139
-
140
- await UnderpostStartUp.API.listenPortController(app, staticPort, runningData);
141
- currentPort++;
142
- continue;
143
- }
144
- logger.info('Build api server runtime', `${host}${path}`);
145
-
146
132
  // parse requests of content-type - application/json
147
133
  app.use(express.json({ limit: '100MB' }));
148
134
 
@@ -164,6 +150,17 @@ const buildRuntime = async () => {
164
150
  return next();
165
151
  });
166
152
 
153
+ // instance public static
154
+ app.use('/', express.static(directory ? directory : `.${rootHostPath}`));
155
+ if (process.argv.includes('static')) {
156
+ logger.info('Build static server runtime', `${host}${path}`);
157
+ currentPort += 2;
158
+ const staticPort = newInstance(currentPort);
159
+ await UnderpostStartUp.API.listenPortController(app, staticPort, runningData);
160
+ currentPort++;
161
+ continue;
162
+ }
163
+
167
164
  // security
168
165
  applySecurity(app, {
169
166
  origin: origins.concat(
@@ -228,24 +225,20 @@ const buildRuntime = async () => {
228
225
  templates: mailerSsrConf ? mailerSsrConf.mailer : {},
229
226
  });
230
227
  }
231
- if (apis) {
228
+ if (apis && apis.length > 0) {
232
229
  const authMiddleware = authMiddlewareFactory({ host, path });
233
-
234
230
  const apiPath = `${path === '/' ? '' : path}/${process.env.BASE_API}`;
235
231
  for (const api of apis)
236
232
  await (async () => {
233
+ logger.info(`Build api server`, `${host}${apiPath}/${api}`);
237
234
  const { ApiRouter } = await import(`../api/${api}/${api}.router.js`);
238
- const router = ApiRouter({ host, path, apiPath, mailer, db, authMiddleware });
235
+ const router = ApiRouter({ host, path, apiPath, mailer, db, authMiddleware, origins });
239
236
  // router.use(cors({ origin: origins }));
240
237
  // logger.info('Load api router', { host, path: apiPath, api });
241
238
  app.use(`${apiPath}/${api}`, router);
242
239
  })();
243
240
  }
244
241
 
245
- // load ssr
246
- const ssr = await ssrMiddlewareFactory({ app, directory, rootHostPath, path });
247
- for (const [_, ssrMiddleware] of Object.entries(ssr)) app.use(ssrMiddleware);
248
-
249
242
  if (ws)
250
243
  await (async () => {
251
244
  const { createIoServer } = await import(`../ws/${ws}/${ws}.ws.server.js`);
@@ -287,6 +280,10 @@ const buildRuntime = async () => {
287
280
  }
288
281
  }
289
282
 
283
+ // load ssr
284
+ const ssr = await ssrMiddlewareFactory({ app, directory, rootHostPath, path });
285
+ for (const [_, ssrMiddleware] of Object.entries(ssr)) app.use(ssrMiddleware);
286
+
290
287
  await UnderpostStartUp.API.listenPortController(server, port, runningData);
291
288
 
292
289
  break;
package/src/server/ssr.js CHANGED
@@ -1,8 +1,16 @@
1
+ /**
2
+ * Module for managing server side rendering
3
+ * @module src/server/ssr.js
4
+ * @namespace SSR
5
+ */
6
+
1
7
  import fs from 'fs-extra';
2
8
  import dotenv from 'dotenv';
9
+ import vm from 'node:vm';
10
+
3
11
  import Underpost from '../index.js';
4
12
 
5
- import { ssrFactory, JSONweb } from './client-formatted.js';
13
+ import { srcFormatted, JSONweb } from './client-formatted.js';
6
14
  import { loggerFactory } from './logger.js';
7
15
  import { getRootDirectory } from './process.js';
8
16
 
@@ -10,6 +18,48 @@ dotenv.config();
10
18
 
11
19
  const logger = loggerFactory(import.meta);
12
20
 
21
+ /**
22
+ * Creates a server-side rendering component function from a given file path.
23
+ * It reads the component file, formats it, and executes it in a sandboxed Node.js VM context to extract the component.
24
+ * @param {string} [componentPath='./src/client/ssr/Render.js'] - The path to the SSR component file.
25
+ * @returns {Promise<Function>} A promise that resolves to the SSR component function.
26
+ * @memberof SSR
27
+ */
28
+ const ssrFactory = async (componentPath = `./src/client/ssr/Render.js`) => {
29
+ const context = { SrrComponent: () => {}, npm_package_version: Underpost.version };
30
+ vm.createContext(context);
31
+ vm.runInContext(await srcFormatted(fs.readFileSync(componentPath, 'utf8')), context);
32
+ return context.SrrComponent;
33
+ };
34
+
35
+ /**
36
+ * Sanitizes an HTML string by adding a nonce to all script and style tags for Content Security Policy (CSP).
37
+ * The nonce is retrieved from `res.locals.nonce`.
38
+ * @param {object} res - The Express response object.
39
+ * @param {object} req - The Express request object.
40
+ * @param {string} html - The HTML string to sanitize.
41
+ * @returns {string} The sanitized HTML string with nonces.
42
+ * @memberof SSR
43
+ */
44
+ const sanitizeHtml = (res, req, html) => {
45
+ const nonce = res.locals.nonce;
46
+
47
+ return html
48
+ .replace(/<script(?=\s|>)/gi, `<script nonce="${nonce}"`)
49
+ .replace(/<style(?=\s|>)/gi, `<style nonce="${nonce}"`);
50
+ };
51
+
52
+ /**
53
+ * Factory function to create Express middleware for handling 404 and 500 errors.
54
+ * It generates server-side rendered HTML for these error pages. If static error pages exist, it redirects to them.
55
+ * @param {object} options - The options for creating the middleware.
56
+ * @param {object} options.app - The Express app instance.
57
+ * @param {string} options.directory - The directory for the instance's static files.
58
+ * @param {string} options.rootHostPath - The root path for the host's public files.
59
+ * @param {string} options.path - The base path for the instance.
60
+ * @returns {Promise<{error500: Function, error400: Function}>} A promise that resolves to an object containing the 500 and 404 error handling middleware.
61
+ * @memberof SSR
62
+ */
13
63
  const ssrMiddlewareFactory = async ({ app, directory, rootHostPath, path }) => {
14
64
  const Render = await ssrFactory();
15
65
  const ssrPath = path === '/' ? path : `${path}/`;
@@ -48,14 +98,6 @@ const ssrMiddlewareFactory = async ({ app, directory, rootHostPath, path }) => {
48
98
  const path500 = `${directory ? directory : `${getRootDirectory()}${rootHostPath}`}/500/index.html`;
49
99
  const page500 = fs.existsSync(path500) ? `${path === '/' ? '' : path}/500` : undefined;
50
100
 
51
- const sanitizeHtml = (res, req, html) => {
52
- const nonce = res.locals.nonce;
53
-
54
- return html
55
- .replace(/<script(?=\s|>)/gi, `<script nonce="${nonce}"`)
56
- .replace(/<style(?=\s|>)/gi, `<style nonce="${nonce}"`);
57
- };
58
-
59
101
  return {
60
102
  error500: function (err, req, res, next) {
61
103
  logger.error(err, err.stack);
@@ -82,4 +124,4 @@ const ssrMiddlewareFactory = async ({ app, directory, rootHostPath, path }) => {
82
124
  };
83
125
  };
84
126
 
85
- export { ssrMiddlewareFactory };
127
+ export { ssrMiddlewareFactory, ssrFactory, sanitizeHtml };