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