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.
- 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 +48 -36
- package/bin/deploy.js +40 -0
- package/cli.md +89 -86
- package/conf.js +28 -3
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- 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 +125 -0
- package/src/api/file/file.controller.js +15 -1
- package/src/api/user/user.router.js +4 -3
- package/src/cli/deploy.js +1 -1
- package/src/cli/index.js +3 -0
- package/src/cli/repository.js +2 -2
- package/src/cli/run.js +29 -1
- package/src/client/Default.index.js +42 -1
- package/src/client/components/core/Account.js +8 -1
- package/src/client/components/core/AgGrid.js +18 -9
- package/src/client/components/core/BtnIcon.js +3 -2
- package/src/client/components/core/Content.js +13 -11
- package/src/client/components/core/CssCore.js +4 -0
- package/src/client/components/core/Docs.js +0 -3
- package/src/client/components/core/Input.js +34 -19
- package/src/client/components/core/Modal.js +29 -7
- package/src/client/components/core/ObjectLayerEngine.js +370 -0
- package/src/client/components/core/ObjectLayerEngineModal.js +1 -0
- package/src/client/components/core/Panel.js +7 -2
- package/src/client/components/core/PanelForm.js +187 -63
- package/src/client/components/core/VanillaJs.js +3 -0
- package/src/client/components/default/MenuDefault.js +94 -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/services/user/user.service.js +1 -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 +4 -3
- package/src/server/client-build.js +2 -3
- package/src/server/client-formatted.js +40 -12
- package/src/server/conf.js +42 -3
- package/src/server/object-layer.js +196 -0
- package/src/server/runtime.js +18 -21
- package/src/server/ssr.js +52 -10
- package/src/server/valkey.js +89 -1
package/src/index.js
CHANGED
package/src/server/auth.js
CHANGED
|
@@ -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'",
|
|
534
|
+
fontSrc: ["'self'", httpDirective, 'data:'],
|
|
534
535
|
frameAncestors: frameAncestors,
|
|
535
|
-
imgSrc: ["'self'", 'data:',
|
|
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'",
|
|
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,
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Module for formatting client-side code
|
|
3
|
+
* @module src/server/client-formatted.js
|
|
4
|
+
* @namespace clientFormatted
|
|
5
|
+
*/
|
|
2
6
|
|
|
3
|
-
|
|
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
|
-
|
|
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 };
|
package/src/server/conf.js
CHANGED
|
@@ -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(
|
|
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 =
|
|
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
|
+
};
|
package/src/server/runtime.js
CHANGED
|
@@ -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 {
|
|
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 };
|