Initial commit
This commit is contained in:
commit
eadd4aa007
13 changed files with 5566 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018-2024 SVR.JS
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
29
README
Normal file
29
README
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
This repository contains SVR.JS mod starter code and its build system
|
||||||
|
The mod will work for SVR.JS Nightly-GitNext.
|
||||||
|
|
||||||
|
Before doing anything, run "npm install".
|
||||||
|
To build SVR.JS mod, run "npm run build".
|
||||||
|
To check SVR.JS mod code for errors with ESLint, run "npm run lint".
|
||||||
|
To fix and beautify SVR.JS mod code with ESLint and Prettier, run "npm run lint:fix".
|
||||||
|
To perform unit tests with Jest, run "npm test".
|
||||||
|
|
||||||
|
To test the mod:
|
||||||
|
1. Clone the SVR.JS repository with "git clone https://git.svrjs.org/svrjs/svrjs.git -b next" command.
|
||||||
|
2. Change the working directory to "svrjs" using "cd svr.js".
|
||||||
|
3. Build SVR.JS by first running "npm install" and then running "npm run build".
|
||||||
|
4. Copy the mod into mods directory in the dist directory using "cp ../dist/mod.js dist/mods" (GNU/Linux, Unix, BSD) or "copy ..\dist\mod.js dist\mods" (Windows).
|
||||||
|
5. Do the necessary mod configuration if the mod requires it.
|
||||||
|
6. Run SVR.JS by running "npm start".
|
||||||
|
7. Do some requests to the endpoints covered by the mod.
|
||||||
|
|
||||||
|
Structure:
|
||||||
|
- dist - contains the built SVR.JS mod
|
||||||
|
- src - contains SVR.JS mod source code
|
||||||
|
- index.js - entry point
|
||||||
|
- utils - utility functions
|
||||||
|
- tests - Jest unit tests
|
||||||
|
- utils - unit tests for utility functions
|
||||||
|
- esbuild.config.js - the build script
|
||||||
|
- eslint.config.js - ESLint configuration
|
||||||
|
- jest.config.js - Jest configuration
|
||||||
|
- modInfo.json - SVR.JS mod name and version
|
11
esbuild.config.js
Normal file
11
esbuild.config.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
const esbuild = require("esbuild");
|
||||||
|
|
||||||
|
esbuild.build({
|
||||||
|
entryPoints: ["src/index.js"],
|
||||||
|
bundle: true,
|
||||||
|
outfile: "dist/mod.js", // Mod output file
|
||||||
|
platform: "node",
|
||||||
|
target: "es2017",
|
||||||
|
}).catch((err) => {
|
||||||
|
throw err;
|
||||||
|
});
|
30
eslint.config.js
Normal file
30
eslint.config.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
const globals = require("globals");
|
||||||
|
const pluginJs = require("@eslint/js");
|
||||||
|
const eslintPluginPrettierRecommended = require("eslint-plugin-prettier/recommended");
|
||||||
|
const jest = require("eslint-plugin-jest");
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
files: ["**/*.js"],
|
||||||
|
languageOptions: {
|
||||||
|
sourceType: "commonjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["tests/*.test.js", "tests/**/*.test.js"],
|
||||||
|
...jest.configs['flat/recommended'],
|
||||||
|
rules: {
|
||||||
|
...jest.configs['flat/recommended'].rules,
|
||||||
|
'jest/prefer-expect-assertions': 'off',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pluginJs.configs.recommended,
|
||||||
|
eslintPluginPrettierRecommended
|
||||||
|
];
|
5
jest.config.js
Normal file
5
jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testMatch: ['**/tests/**/*.test.js'],
|
||||||
|
verbose: true,
|
||||||
|
};
|
4
modInfo.json
Normal file
4
modInfo.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"name": "Example mod",
|
||||||
|
"version": "0.0.0"
|
||||||
|
}
|
5128
package-lock.json
generated
Normal file
5128
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
package.json
Normal file
22
package.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "svrjs-mod-starter",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "node esbuild.config.js",
|
||||||
|
"lint": "eslint --no-error-on-unmatched-pattern src/**/*.js src/*.js tests/**/*.test.js tests/**/*.js",
|
||||||
|
"lint:fix": "npm run lint -- --fix",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.9.1",
|
||||||
|
"esbuild": "^0.23.1",
|
||||||
|
"eslint": "^9.9.1",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-jest": "^28.8.0",
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^3.3.3"
|
||||||
|
}
|
||||||
|
}
|
68
src/index.js
Normal file
68
src/index.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
const cluster = require("./utils/clusterBunShim.js"); // Cluster shim for Bun
|
||||||
|
const { add } = require("./utils/helper.js"); // Require the addition module
|
||||||
|
const modInfo = require("../modInfo.json"); // SVR.JS mod information
|
||||||
|
|
||||||
|
// Exported SVR.JS mod callback
|
||||||
|
module.exports = (req, res, logFacilities, config, next) => {
|
||||||
|
if (req.parsedURL.pathname == "/test.svr") {
|
||||||
|
res.writeHead(200, "OK", {
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
});
|
||||||
|
res.end("2 + 2 = " + add(2,2));
|
||||||
|
} else if (req.parsedURL.pathname == "/ping.svr") {
|
||||||
|
if (!cluster.isWorker) {
|
||||||
|
// Invoke 500 Internal Server Error status code, if the process is not a worker
|
||||||
|
res.error(500, new Error("SVR.JS is running single-threaded, so this request is not supported"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping OK message listener
|
||||||
|
const pingOKListener = (message) => {
|
||||||
|
if (message == "\x14MODPINGOK") {
|
||||||
|
process.removeListener("message", pingOKListener);
|
||||||
|
res.writeHead(200, "OK", {
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
});
|
||||||
|
res.end("OK");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen to Ping OK messages
|
||||||
|
process.on("message", pingOKListener);
|
||||||
|
|
||||||
|
// Send Ping message
|
||||||
|
process.send("\x12MODPING");
|
||||||
|
} else {
|
||||||
|
next(); // Invoke other request handlers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exported command
|
||||||
|
module.exports.commands = {
|
||||||
|
somecmd: (args, log, passCommand) => {
|
||||||
|
log("Arguments: " + args.toString()); // Print arguments
|
||||||
|
passCommand(args, log) // Invoke other command handlers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Uncomment, if you want the callback to cover the forward proxy requests
|
||||||
|
//module.exports.proxySafe = true;
|
||||||
|
|
||||||
|
// Uncomment, if you want to handle proxy requests
|
||||||
|
//module.exports.proxy = (req, socket, head, logFacilities, config, next) => {
|
||||||
|
// next(); // Invoke other proxy handlers
|
||||||
|
//}
|
||||||
|
|
||||||
|
// IPC listener for main process
|
||||||
|
// Control messages received by main process begin with 0x12 control character
|
||||||
|
// Control messages sent by main process begin with 0x14 control character
|
||||||
|
process.messageEventListeners.push((worker, serverconsole) => {
|
||||||
|
return (message) => {
|
||||||
|
if (message == "\x12MODPING") {
|
||||||
|
// Ping back
|
||||||
|
worker.send("\x14MODPINGOK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.modInfo = modInfo;
|
233
src/utils/clusterBunShim.js
Normal file
233
src/utils/clusterBunShim.js
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
const net = require("net");
|
||||||
|
const os = require("os");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
let cluster = {};
|
||||||
|
|
||||||
|
if (!process.singleThreaded) {
|
||||||
|
try {
|
||||||
|
// Import cluster module
|
||||||
|
cluster = require("cluster");
|
||||||
|
} catch (err) {
|
||||||
|
// Clustering is not supported!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cluster & IPC shim for Bun
|
||||||
|
|
||||||
|
cluster.bunShim = function () {
|
||||||
|
cluster.isMaster = !process.env.NODE_UNIQUE_ID;
|
||||||
|
cluster.isPrimary = cluster.isMaster;
|
||||||
|
cluster.isWorker = !cluster.isMaster;
|
||||||
|
cluster.__shimmed__ = true;
|
||||||
|
|
||||||
|
if (cluster.isWorker) {
|
||||||
|
// Shim the cluster.worker object for worker processes
|
||||||
|
cluster.worker = {
|
||||||
|
id: parseInt(process.env.NODE_UNIQUE_ID),
|
||||||
|
process: process,
|
||||||
|
isDead: function () {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
send: function (message, b, c, d) {
|
||||||
|
process.send(message, b, c, d);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!process.send) {
|
||||||
|
// Shim the process.send function for worker processes
|
||||||
|
|
||||||
|
// Create a fake IPC server to receive messages
|
||||||
|
let fakeIPCServer = net.createServer(function (socket) {
|
||||||
|
let receivedData = "";
|
||||||
|
|
||||||
|
socket.on("data", function (data) {
|
||||||
|
receivedData += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("end", function () {
|
||||||
|
process.emit("message", receivedData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fakeIPCServer.listen(
|
||||||
|
os.platform() === "win32"
|
||||||
|
? path.join(
|
||||||
|
"\\\\?\\pipe",
|
||||||
|
process.dirname,
|
||||||
|
"temp/.W" + process.pid + ".ipc",
|
||||||
|
)
|
||||||
|
: process.dirname + "/temp/.W" + process.pid + ".ipc",
|
||||||
|
);
|
||||||
|
|
||||||
|
process.send = function (message) {
|
||||||
|
// Create a fake IPC connection to send messages
|
||||||
|
let fakeIPCConnection = net.createConnection(
|
||||||
|
os.platform() === "win32"
|
||||||
|
? path.join(
|
||||||
|
"\\\\?\\pipe",
|
||||||
|
process.dirname,
|
||||||
|
"temp/.P" + process.pid + ".ipc",
|
||||||
|
)
|
||||||
|
: process.dirname + "/temp/.P" + process.pid + ".ipc",
|
||||||
|
function () {
|
||||||
|
fakeIPCConnection.end(message);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.removeFakeIPC = function () {
|
||||||
|
// Close IPC server
|
||||||
|
process.send = function () {};
|
||||||
|
fakeIPCServer.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom implementation for cluster.fork()
|
||||||
|
cluster._workersCounter = 1;
|
||||||
|
cluster.workers = {};
|
||||||
|
cluster.fork = function (env) {
|
||||||
|
const child_process = require("child_process");
|
||||||
|
let newEnvironment = Object.assign(env ? env : process.env);
|
||||||
|
newEnvironment.NODE_UNIQUE_ID = cluster._workersCounter;
|
||||||
|
let newArguments = Object.assign(process.argv);
|
||||||
|
let command = newArguments.shift();
|
||||||
|
let newWorker = child_process.spawn(command, newArguments, {
|
||||||
|
env: newEnvironment,
|
||||||
|
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
||||||
|
});
|
||||||
|
|
||||||
|
newWorker.process = newWorker;
|
||||||
|
newWorker.isDead = function () {
|
||||||
|
return newWorker.exitCode !== null || newWorker.killed;
|
||||||
|
};
|
||||||
|
newWorker.id = newEnvironment.NODE_UNIQUE_ID;
|
||||||
|
|
||||||
|
function checkSendImplementation(worker) {
|
||||||
|
let sendImplemented = true;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
process.versions &&
|
||||||
|
process.versions.bun &&
|
||||||
|
process.versions.bun[0] != "0"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (!worker.send) {
|
||||||
|
sendImplemented = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let oldLog = console.log;
|
||||||
|
console.log = function (a, b, c, d, e, f) {
|
||||||
|
if (
|
||||||
|
a == "ChildProcess.prototype.send() - Sorry! Not implemented yet"
|
||||||
|
) {
|
||||||
|
throw new Error("NOT IMPLEMENTED");
|
||||||
|
} else {
|
||||||
|
oldLog(a, b, c, d, e, f);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
worker.send(undefined);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message === "NOT IMPLEMENTED") {
|
||||||
|
sendImplemented = false;
|
||||||
|
}
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log = oldLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendImplemented;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkSendImplementation(newWorker)) {
|
||||||
|
// Create a fake IPC server for worker process to receive messages
|
||||||
|
let fakeWorkerIPCServer = net.createServer(function (socket) {
|
||||||
|
let receivedData = "";
|
||||||
|
|
||||||
|
socket.on("data", function (data) {
|
||||||
|
receivedData += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("end", function () {
|
||||||
|
newWorker.emit("message", receivedData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fakeWorkerIPCServer.listen(
|
||||||
|
os.platform() === "win32"
|
||||||
|
? path.join(
|
||||||
|
"\\\\?\\pipe",
|
||||||
|
process.dirname,
|
||||||
|
"temp/.P" + newWorker.process.pid + ".ipc",
|
||||||
|
)
|
||||||
|
: process.dirname + "/temp/.P" + newWorker.process.pid + ".ipc",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cleanup when worker process exits
|
||||||
|
newWorker.on("exit", function () {
|
||||||
|
fakeWorkerIPCServer.close();
|
||||||
|
delete cluster.workers[newWorker.id];
|
||||||
|
});
|
||||||
|
|
||||||
|
newWorker.send = function (
|
||||||
|
message,
|
||||||
|
fakeParam2,
|
||||||
|
fakeParam3,
|
||||||
|
fakeParam4,
|
||||||
|
tries,
|
||||||
|
) {
|
||||||
|
if (!tries) tries = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a fake IPC connection to send messages to worker process
|
||||||
|
let fakeWorkerIPCConnection = net.createConnection(
|
||||||
|
os.platform() === "win32"
|
||||||
|
? path.join(
|
||||||
|
"\\\\?\\pipe",
|
||||||
|
process.dirname,
|
||||||
|
"temp/.W" + newWorker.process.pid + ".ipc",
|
||||||
|
)
|
||||||
|
: process.dirname + "/temp/.W" + newWorker.process.pid + ".ipc",
|
||||||
|
function () {
|
||||||
|
fakeWorkerIPCConnection.end(message);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (tries > 50) throw err;
|
||||||
|
newWorker.send(
|
||||||
|
message,
|
||||||
|
fakeParam2,
|
||||||
|
fakeParam3,
|
||||||
|
fakeParam4,
|
||||||
|
tries + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newWorker.on("exit", function () {
|
||||||
|
delete cluster.workers[newWorker.id];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster.workers[newWorker.id] = newWorker;
|
||||||
|
cluster._workersCounter++;
|
||||||
|
return newWorker;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
process.isBun &&
|
||||||
|
(cluster.isMaster === undefined ||
|
||||||
|
(cluster.isMaster && process.env.NODE_UNIQUE_ID))
|
||||||
|
) {
|
||||||
|
cluster.bunShim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shim cluster.isPrimary field
|
||||||
|
if (cluster.isPrimary === undefined && cluster.isMaster !== undefined)
|
||||||
|
cluster.isPrimary = cluster.isMaster;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = cluster;
|
8
src/utils/helper.js
Normal file
8
src/utils/helper.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// src/utils/helper.js
|
||||||
|
function add(a, b) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
add,
|
||||||
|
};
|
5
tests/utils/helper.test.js
Normal file
5
tests/utils/helper.test.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
const { add } = require("../../src/utils/helper");
|
||||||
|
|
||||||
|
test("adds 1 + 2 to equal 3", () => {
|
||||||
|
expect(add(1, 2)).toBe(3);
|
||||||
|
});
|
Loading…
Reference in a new issue