underpost 2.8.878 → 2.8.882
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.
- package/.env.development +35 -3
- package/.env.production +40 -3
- package/.env.test +35 -3
- package/.github/workflows/release.cd.yml +3 -3
- package/README.md +20 -2
- package/bin/deploy.js +40 -0
- package/cli.md +3 -1
- package/conf.js +29 -3
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +6 -6
- package/package.json +1 -2
- package/src/api/document/document.controller.js +66 -0
- package/src/api/document/document.model.js +51 -0
- package/src/api/document/document.router.js +24 -0
- package/src/api/document/document.service.js +133 -0
- package/src/cli/deploy.js +1 -1
- package/src/cli/index.js +2 -0
- package/src/cli/repository.js +2 -0
- package/src/cli/run.js +27 -1
- package/src/client/Default.index.js +46 -1
- package/src/client/components/core/Account.js +8 -1
- package/src/client/components/core/AgGrid.js +18 -9
- package/src/client/components/core/Auth.js +258 -89
- package/src/client/components/core/BtnIcon.js +13 -3
- package/src/client/components/core/Content.js +2 -1
- package/src/client/components/core/CssCore.js +40 -27
- package/src/client/components/core/Docs.js +189 -88
- package/src/client/components/core/Input.js +34 -19
- package/src/client/components/core/LoadingAnimation.js +5 -10
- package/src/client/components/core/Modal.js +280 -123
- package/src/client/components/core/ObjectLayerEngine.js +470 -104
- package/src/client/components/core/ObjectLayerEngineModal.js +1 -0
- package/src/client/components/core/Panel.js +9 -2
- package/src/client/components/core/PanelForm.js +234 -76
- package/src/client/components/core/Router.js +15 -15
- package/src/client/components/core/ToolTip.js +83 -19
- package/src/client/components/core/Translate.js +1 -1
- package/src/client/components/core/VanillaJs.js +7 -3
- package/src/client/components/core/windowGetDimensions.js +202 -0
- package/src/client/components/default/MenuDefault.js +105 -41
- package/src/client/components/default/RoutesDefault.js +2 -0
- package/src/client/services/default/default.management.js +1 -0
- package/src/client/services/document/document.service.js +97 -0
- package/src/client/services/file/file.service.js +2 -0
- package/src/client/ssr/Render.js +1 -1
- package/src/client/ssr/head/DefaultScripts.js +2 -0
- package/src/client/ssr/head/Seo.js +1 -0
- package/src/index.js +1 -1
- package/src/mailer/EmailRender.js +1 -1
- package/src/server/auth.js +68 -17
- package/src/server/client-build.js +2 -3
- package/src/server/client-formatted.js +40 -12
- package/src/server/conf.js +5 -1
- package/src/server/crypto.js +195 -76
- package/src/server/object-layer.js +196 -0
- package/src/server/peer.js +47 -5
- package/src/server/process.js +85 -1
- package/src/server/runtime.js +23 -23
- package/src/server/ssr.js +52 -10
- package/src/server/valkey.js +89 -1
- package/test/crypto.test.js +117 -0
|
@@ -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
|
+
};
|
package/src/server/peer.js
CHANGED
|
@@ -1,30 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module for peerjs server management.
|
|
3
|
+
* Initializes and configures the PeerJS server instance, typically running
|
|
4
|
+
* alongside a main Node.js application.
|
|
5
|
+
*
|
|
6
|
+
* @module src/server/peer.js
|
|
7
|
+
* @namespace Peer
|
|
8
|
+
*/
|
|
9
|
+
|
|
1
10
|
import { PeerServer } from 'peer';
|
|
2
11
|
import dotenv from 'dotenv';
|
|
3
12
|
import { loggerFactory } from './logger.js';
|
|
4
|
-
import fs from 'fs-extra';
|
|
5
13
|
import UnderpostStartUp from './start.js';
|
|
6
14
|
|
|
7
15
|
dotenv.config();
|
|
8
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Logger instance for this module, utilizing the framework's factory.
|
|
19
|
+
* @type {function(*): void}
|
|
20
|
+
* @memberof Peer
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
9
23
|
const logger = loggerFactory(import.meta);
|
|
10
24
|
|
|
25
|
+
// Documentation references:
|
|
11
26
|
// https://github.com/peers/peerjs
|
|
12
27
|
// https://github.com/peers/peerjs-server
|
|
13
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Creates and starts a configured PeerJS server instance.
|
|
31
|
+
*
|
|
32
|
+
* This function handles port configuration, CORS origins, and paths, then uses
|
|
33
|
+
* a listener factory to start the server.
|
|
34
|
+
*
|
|
35
|
+
* @async
|
|
36
|
+
* @function createPeerServer
|
|
37
|
+
* @memberof Peer
|
|
38
|
+
* @param {object} config - Configuration object for the PeerJS server setup.
|
|
39
|
+
* @param {number} config.port - The primary port on which the PeerJS server will listen.
|
|
40
|
+
* @param {number} [config.devPort] - Optional development port. If provided and in 'development' NODE_ENV, 'http://localhost:${devPort}' is added to allowed origins.
|
|
41
|
+
* @param {string[]} config.origins - An array of allowed domain origins for Cross-Origin Resource Sharing (CORS).
|
|
42
|
+
* @param {string} config.host - The host address the server is bound to (used internally for configuration).
|
|
43
|
+
* @param {string} config.path - The base path for the API. The PeerJS path ('/peer') will be appended to this.
|
|
44
|
+
* @returns {Promise<object>} A promise that resolves to an object containing the final configuration and the server instance.
|
|
45
|
+
* @returns {import('peer').IConfig} return.options - The final options object used to create the PeerServer.
|
|
46
|
+
* @returns {import('peer').Server} return.peerServer - The created and listening PeerServer instance (wrapped by the listening server factory).
|
|
47
|
+
* @returns {object} return.meta - The module's import meta object (`import.meta`).
|
|
48
|
+
*/
|
|
14
49
|
const createPeerServer = async ({ port, devPort, origins, host, path }) => {
|
|
15
|
-
if (process.env.NODE_ENV === 'development' && devPort)
|
|
50
|
+
if (process.env.NODE_ENV === 'development' && devPort) {
|
|
51
|
+
logger.warn(`Adding development origin: http://localhost:${devPort}`);
|
|
52
|
+
origins.push(`http://localhost:${devPort}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
16
55
|
/** @type {import('peer').IConfig} */
|
|
17
56
|
const options = {
|
|
18
57
|
port,
|
|
58
|
+
// Ensure the path is correctly formatted, handling the root path case
|
|
19
59
|
path: `${path === '/' ? '' : path}/peer`,
|
|
20
60
|
corsOptions: {
|
|
21
61
|
origin: origins,
|
|
22
62
|
},
|
|
23
63
|
proxied: true,
|
|
24
|
-
// key: fs.readFileSync(''),
|
|
25
|
-
// cert: fs.readFileSync(''),
|
|
26
|
-
// ca: fs.readFileSync(''),
|
|
64
|
+
// key: fs.readFileSync(''), // Example for HTTPS/SSL
|
|
65
|
+
// cert: fs.readFileSync(''), // Example for HTTPS/SSL
|
|
66
|
+
// ca: fs.readFileSync(''), // Example for HTTPS/SSL
|
|
27
67
|
};
|
|
68
|
+
|
|
69
|
+
// Use the framework's factory to listen on the server, ensuring graceful startup/shutdown
|
|
28
70
|
const peerServer = UnderpostStartUp.API.listenServerFactory(() => PeerServer(options));
|
|
29
71
|
|
|
30
72
|
return { options, peerServer, meta: import.meta };
|
package/src/server/process.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module for process and shell command management.
|
|
3
|
+
* Provides utilities for executing shell commands, managing signals, and handling environment details.
|
|
4
|
+
* @module src/server/process.js
|
|
5
|
+
* @namespace Process
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
// https://nodejs.org/api/process
|
|
2
9
|
|
|
3
10
|
import shell from 'shelljs';
|
|
4
11
|
import dotenv from 'dotenv';
|
|
5
|
-
import fs from 'fs-extra';
|
|
6
12
|
import { loggerFactory } from './logger.js';
|
|
7
13
|
import clipboard from 'clipboardy';
|
|
8
14
|
import UnderpostRootEnv from '../cli/env.js';
|
|
@@ -11,9 +17,23 @@ dotenv.config();
|
|
|
11
17
|
|
|
12
18
|
const logger = loggerFactory(import.meta);
|
|
13
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Gets the current working directory, replacing backslashes with forward slashes for consistency.
|
|
22
|
+
* @memberof Process
|
|
23
|
+
* @returns {string} The root directory path.
|
|
24
|
+
*/
|
|
14
25
|
const getRootDirectory = () => process.cwd().replace(/\\/g, '/');
|
|
15
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Controls and manages process-level events and signals.
|
|
29
|
+
* @namespace ProcessController
|
|
30
|
+
*/
|
|
16
31
|
const ProcessController = {
|
|
32
|
+
/**
|
|
33
|
+
* List of signals to listen for for graceful shutdown/handling.
|
|
34
|
+
* @memberof ProcessController
|
|
35
|
+
* @type {string[]}
|
|
36
|
+
*/
|
|
17
37
|
SIG: [
|
|
18
38
|
'SIGPIPE',
|
|
19
39
|
'SIGHUP',
|
|
@@ -28,6 +48,13 @@ const ProcessController = {
|
|
|
28
48
|
'SIGSEGV',
|
|
29
49
|
'SIGILL',
|
|
30
50
|
],
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sets up listeners for various process signals defined in {@link ProcessController.SIG}.
|
|
54
|
+
* Handles graceful exit on 'SIGINT' (Ctrl+C).
|
|
55
|
+
* @memberof ProcessController
|
|
56
|
+
* @returns {Array<process.Process>} An array of process listener handles.
|
|
57
|
+
*/
|
|
31
58
|
onSigListen: function () {
|
|
32
59
|
return this.SIG.map((sig) =>
|
|
33
60
|
process.on(sig, (...args) => {
|
|
@@ -42,6 +69,14 @@ const ProcessController = {
|
|
|
42
69
|
}),
|
|
43
70
|
);
|
|
44
71
|
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Initializes the ProcessController.
|
|
75
|
+
* Sets up signal listeners, registers a listener for the 'exit' event, and cleans up temporary deployment environment variables.
|
|
76
|
+
* @memberof ProcessController
|
|
77
|
+
* @param {Object} logger - The logger instance to use for internal logging.
|
|
78
|
+
* @returns {void}
|
|
79
|
+
*/
|
|
45
80
|
init: function (logger) {
|
|
46
81
|
this.logger = logger;
|
|
47
82
|
process.on('exit', (...args) => {
|
|
@@ -52,33 +87,82 @@ const ProcessController = {
|
|
|
52
87
|
},
|
|
53
88
|
};
|
|
54
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Executes a shell command using shelljs.
|
|
92
|
+
* @memberof Process
|
|
93
|
+
* @param {string} cmd - The command string to execute.
|
|
94
|
+
* @param {Object} [options] - Options for execution.
|
|
95
|
+
* @param {boolean} [options.silent=false] - Suppress output from shell commands.
|
|
96
|
+
* @param {boolean} [options.async=false] - Run command asynchronously.
|
|
97
|
+
* @param {boolean} [options.stdout=false] - Return stdout content (string) instead of shelljs result object.
|
|
98
|
+
* @param {boolean} [options.disableLog=false] - Prevent logging of the command.
|
|
99
|
+
* @returns {string|shelljs.ShellString} The result of the shell command (string if `stdout: true`, otherwise a ShellString object).
|
|
100
|
+
*/
|
|
55
101
|
const shellExec = (cmd, options = { silent: false, async: false, stdout: false, disableLog: false }) => {
|
|
56
102
|
if (!options.disableLog) logger.info(`cmd`, cmd);
|
|
57
103
|
return options.stdout ? shell.exec(cmd, options).stdout : shell.exec(cmd, options);
|
|
58
104
|
};
|
|
59
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Changes the current working directory using shelljs.
|
|
108
|
+
* @memberof Process
|
|
109
|
+
* @param {string} cd - The path to change the directory to.
|
|
110
|
+
* @param {Object} [options] - Options for the CD operation.
|
|
111
|
+
* @param {boolean} [options.disableLog=false] - Prevent logging of the CD command.
|
|
112
|
+
* @returns {shelljs.ShellString} The result of the shelljs cd command.
|
|
113
|
+
*/
|
|
60
114
|
const shellCd = (cd, options = { disableLog: false }) => {
|
|
61
115
|
if (!options.disableLog) logger.info(`cd`, cd);
|
|
62
116
|
return shell.cd(cd);
|
|
63
117
|
};
|
|
64
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Opens a new GNOME terminal and executes a command.
|
|
121
|
+
* Note: This function is environment-specific (GNOME/Linux).
|
|
122
|
+
* @memberof Process
|
|
123
|
+
* @param {string} cmd - The command to execute in the new terminal.
|
|
124
|
+
* @param {Object} [options] - Options for the terminal opening.
|
|
125
|
+
* @param {boolean} [options.single=false] - If true, execute as a single session process using `setsid`.
|
|
126
|
+
* @returns {void}
|
|
127
|
+
*/
|
|
65
128
|
const openTerminal = (cmd, options = { single: false }) => {
|
|
66
129
|
if (options.single === true) {
|
|
130
|
+
// Run as a single session process
|
|
67
131
|
shellExec(`setsid gnome-terminal -- bash -ic "${cmd}; exec bash" >/dev/null 2>&1 &`);
|
|
68
132
|
return;
|
|
69
133
|
}
|
|
134
|
+
// Run asynchronously and disown
|
|
70
135
|
shellExec(`gnome-terminal -- bash -c "${cmd}; exec bash" & disown`, {
|
|
71
136
|
async: true,
|
|
72
137
|
stdout: true,
|
|
73
138
|
});
|
|
74
139
|
};
|
|
75
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Wraps a command to run it as a daemon process in a shell (keeping the process alive/terminal open).
|
|
143
|
+
* @memberof Process
|
|
144
|
+
* @param {string} cmd - The command to daemonize.
|
|
145
|
+
* @returns {string} The shell command string for the daemon process.
|
|
146
|
+
*/
|
|
76
147
|
const daemonProcess = (cmd) => `exec bash -c '${cmd}; exec tail -f /dev/null'`;
|
|
77
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Retrieves the process ID (PID) of the most recently created gnome-terminal instance.
|
|
151
|
+
* Note: This function is environment-specific (GNOME/Linux) and uses `pgrep -n`.
|
|
152
|
+
* @memberof Process
|
|
153
|
+
* @returns {number} The PID of the last gnome-terminal process.
|
|
154
|
+
*/
|
|
78
155
|
// list all terminals: pgrep gnome-terminal
|
|
79
156
|
// list last terminal: pgrep -n gnome-terminal
|
|
80
157
|
const getTerminalPid = () => JSON.parse(shellExec(`pgrep -n gnome-terminal`, { stdout: true, silent: true }));
|
|
81
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Copies text content to the system clipboard using clipboardy.
|
|
161
|
+
* Logs the copied content for confirmation.
|
|
162
|
+
* @memberof Process
|
|
163
|
+
* @param {string} [data='🦄'] - The data to copy. Defaults to '🦄'.
|
|
164
|
+
* @returns {void}
|
|
165
|
+
*/
|
|
82
166
|
function pbcopy(data) {
|
|
83
167
|
clipboard.writeSync(data || '🦄');
|
|
84
168
|
logger.info(`copied to clipboard`, clipboard.readSync());
|
package/src/server/runtime.js
CHANGED
|
@@ -99,6 +99,8 @@ const buildRuntime = async () => {
|
|
|
99
99
|
return next();
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
if (process.env.NODE_ENV === 'production') app.set('trust proxy', true);
|
|
103
|
+
|
|
102
104
|
app.use((req, res, next) => {
|
|
103
105
|
requestCounter.inc({
|
|
104
106
|
instance: `${host}:${port}${path}`,
|
|
@@ -129,17 +131,6 @@ const buildRuntime = async () => {
|
|
|
129
131
|
return compression.filter(req, res);
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
if (process.argv.includes('static')) {
|
|
133
|
-
logger.info('Build static server runtime', `${host}${path}`);
|
|
134
|
-
currentPort += 2;
|
|
135
|
-
const staticPort = newInstance(currentPort);
|
|
136
|
-
|
|
137
|
-
await UnderpostStartUp.API.listenPortController(app, staticPort, runningData);
|
|
138
|
-
currentPort++;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
logger.info('Build api server runtime', `${host}${path}`);
|
|
142
|
-
|
|
143
134
|
// parse requests of content-type - application/json
|
|
144
135
|
app.use(express.json({ limit: '100MB' }));
|
|
145
136
|
|
|
@@ -163,6 +154,23 @@ const buildRuntime = async () => {
|
|
|
163
154
|
|
|
164
155
|
// instance public static
|
|
165
156
|
app.use('/', express.static(directory ? directory : `.${rootHostPath}`));
|
|
157
|
+
if (process.argv.includes('static')) {
|
|
158
|
+
logger.info('Build static server runtime', `${host}${path}`);
|
|
159
|
+
currentPort += 2;
|
|
160
|
+
const staticPort = newInstance(currentPort);
|
|
161
|
+
await UnderpostStartUp.API.listenPortController(app, staticPort, runningData);
|
|
162
|
+
currentPort++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Flag swagger requests before security middleware is applied
|
|
167
|
+
const swaggerJsonPath = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
|
|
168
|
+
const swaggerPath = `${path === '/' ? `/api-docs` : `${path}/api-docs`}`;
|
|
169
|
+
if (fs.existsSync(swaggerJsonPath))
|
|
170
|
+
app.use(swaggerPath, (req, res, next) => {
|
|
171
|
+
res.locals.isSwagger = true;
|
|
172
|
+
next();
|
|
173
|
+
});
|
|
166
174
|
|
|
167
175
|
// security
|
|
168
176
|
applySecurity(app, {
|
|
@@ -194,22 +202,14 @@ const buildRuntime = async () => {
|
|
|
194
202
|
if (peer) currentPort++;
|
|
195
203
|
|
|
196
204
|
if (!apiBaseHost) {
|
|
197
|
-
const swaggerJsonPath = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
|
|
198
205
|
if (fs.existsSync(swaggerJsonPath)) {
|
|
199
|
-
// logger.info('Build swagger serve', swaggerJsonPath);
|
|
200
|
-
|
|
201
206
|
const swaggerInstance =
|
|
202
207
|
(swaggerDoc) =>
|
|
203
208
|
(...args) =>
|
|
204
209
|
swaggerUi.setup(swaggerDoc)(...args);
|
|
205
|
-
|
|
206
210
|
const swaggerDoc = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf8'));
|
|
207
|
-
|
|
208
|
-
app.use(
|
|
209
|
-
`${path === '/' ? `/api-docs` : `${path}/api-docs`}`,
|
|
210
|
-
swaggerUi.serve,
|
|
211
|
-
swaggerInstance(swaggerDoc),
|
|
212
|
-
);
|
|
211
|
+
const swaggerPath = `${path === '/' ? `/api-docs` : `${path}/api-docs`}`;
|
|
212
|
+
app.use(swaggerPath, swaggerUi.serve, swaggerInstance(swaggerDoc));
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
if (db && apis) await DataBaseProvider.load({ apis, host, path, db });
|
|
@@ -228,12 +228,12 @@ const buildRuntime = async () => {
|
|
|
228
228
|
templates: mailerSsrConf ? mailerSsrConf.mailer : {},
|
|
229
229
|
});
|
|
230
230
|
}
|
|
231
|
-
if (apis) {
|
|
231
|
+
if (apis && apis.length > 0) {
|
|
232
232
|
const authMiddleware = authMiddlewareFactory({ host, path });
|
|
233
|
-
|
|
234
233
|
const apiPath = `${path === '/' ? '' : path}/${process.env.BASE_API}`;
|
|
235
234
|
for (const api of apis)
|
|
236
235
|
await (async () => {
|
|
236
|
+
logger.info(`Build api server`, `${host}${apiPath}/${api}`);
|
|
237
237
|
const { ApiRouter } = await import(`../api/${api}/${api}.router.js`);
|
|
238
238
|
const router = ApiRouter({ host, path, apiPath, mailer, db, authMiddleware, origins });
|
|
239
239
|
// router.use(cors({ origin: origins }));
|
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 {
|
|
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 };
|