UNPKG

rune

Version:

CLI to upload your games to Rune

258 lines (253 loc) 10.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.validateGameFiles = exports.validateGameFilesWithEval = exports.parseGameIndexHtml = exports.validationOptions = exports.MAX_PLAYERS = void 0; const eslint_1 = require("eslint"); const eslint_plugin_rune_1 = __importDefault(require("eslint-plugin-rune")); const node_html_parser_1 = require("node-html-parser"); const path_1 = __importDefault(require("path")); const semver_1 = __importDefault(require("semver")); const getGameFiles_js_1 = require("./getGameFiles.js"); exports.MAX_PLAYERS = 6; exports.validationOptions = { sdkUrlStartOldRune: "https://cdn.jsdelivr.net/npm/rune-games-sdk", sdkUrlStartRune: "https://cdn.jsdelivr.net/npm/rune-sdk", sdkUrlStartDusk: "https://cdn.jsdelivr.net/npm/dusk-games-sdk", sdkVersionRegex: /(?:rune|rune-games|dusk-games)-sdk@(\d+(\.\d+(\.\d+)?)?)/, minSdkVersion: "4.8.1", maxFiles: 1000, maxSizeMb: 25, }; const eslint = new eslint_1.ESLint({ overrideConfigFile: true, allowInlineConfig: false, baseConfig: [ { languageOptions: { sourceType: "module", }, }, //@ts-expect-error - For some reason types not working here eslint_plugin_rune_1.default.configs.logic, ], }); function parseGameIndexHtml(indexHtmlContent) { if (!(0, node_html_parser_1.valid)(indexHtmlContent)) return null; const { sdkUrlStartRune, sdkUrlStartOldRune, sdkUrlStartDusk } = exports.validationOptions; const parsedIndexHtml = (0, node_html_parser_1.parse)(indexHtmlContent); const scripts = parsedIndexHtml.getElementsByTagName("script"); const sdkScript = scripts.find((script) => script.getAttribute("src")?.startsWith(sdkUrlStartDusk) || script.getAttribute("src")?.startsWith(sdkUrlStartRune) || script.getAttribute("src")?.startsWith(sdkUrlStartOldRune)); return { parsedIndexHtml, scripts, sdkScript }; } exports.parseGameIndexHtml = parseGameIndexHtml; async function validateGameFilesWithEval(logicRunner, files) { const logicJs = (0, getGameFiles_js_1.findShortestPathFileThatEndsWith)(files, "logic.js"); //Remove export { ... } from logic.js const logicWithoutExports = logicJs?.content?.replace(/export {[^}]*}/gm, ""); const gameConfig = logicJs ? eval(` //Prevent Math precision from being modified globalThis.Math.__SDK_PRECISION_SET__ = true ${logicRunner} ${logicWithoutExports} const bindings = globalThis.RUNE_FUNCTION_PREFIX_getLogicRunnerBindings() if (!bindings) { const invalidConfig = {} invalidConfig } else { bindings.getConfig() } `) : {}; return validateGameFiles(files, gameConfig); } exports.validateGameFilesWithEval = validateGameFilesWithEval; async function validateGameFiles(files, gameConfig) { const { sdkVersionRegex, minSdkVersion, maxFiles, maxSizeMb } = exports.validationOptions; const errors = []; let sdkName = "Rune"; if (files.length > maxFiles) { errors.push({ message: `Too many files (>${maxFiles})` }); } const totalSize = files.reduce((acc, file) => acc + file.size, 0); if (totalSize > maxSizeMb * 1e6) { errors.push({ message: `Game size must be less than ${maxSizeMb}MB` }); } const indexHtml = (0, getGameFiles_js_1.findShortestPathFileThatEndsWith)(files, "index.html"); const logicJs = (0, getGameFiles_js_1.findShortestPathFileThatEndsWith)(files, "logic.js"); if (logicJs && logicJs.size > 1e6) { errors.push({ message: `logic.js size can't be more than 1MB` }); } if (!indexHtml) { errors.push({ message: "Game must include index.html" }); } else if (!indexHtml.content) { errors.push({ message: "index.html content has not been provided for validation", }); } else { const gameIndexHtmlElements = parseGameIndexHtml(indexHtml.content); if (!gameIndexHtmlElements) { errors.push({ message: "index.html is not valid HTML" }); } else if (!gameIndexHtmlElements.sdkScript) { errors.push({ message: `Game index.html must include ${sdkName} SDK script`, }); } else { const { sdkScript, scripts } = gameIndexHtmlElements; const { sdkUrlStartDusk, sdkUrlStartRune, sdkUrlStartOldRune } = exports.validationOptions; sdkName = scripts.some((script) => script.getAttribute("src")?.startsWith(sdkUrlStartDusk)) ? "Dusk" : "Rune"; if (scripts.filter((script) => script.getAttribute("src")?.startsWith(sdkUrlStartRune) || script.getAttribute("src")?.startsWith(sdkUrlStartOldRune) || script.getAttribute("src")?.startsWith(sdkUrlStartDusk)).length > 1) { errors.push({ message: `${sdkName} SDK is imported 2+ times in index.html. If using the ${sdkName} Vite plugin, then remove your SDK import in index.html.`, }); } if (sdkScript.getAttribute("src")?.endsWith("/multiplayer.js") || sdkScript.getAttribute("src")?.endsWith("/multiplayer-dev.js")) { await validateMultiplayer({ errors, gameConfig, logicJs, indexHtml, }); if (scripts.indexOf(sdkScript) !== 0) { errors.push({ message: `${sdkName} SDK must be the first script in index.html`, }); } const sdkVersion = sdkScript .getAttribute("src") ?.match(sdkVersionRegex)?.[1]; if (!sdkVersion) { errors.push({ message: `${sdkName} SDK must specify a version` }); } const [major, minor, patch] = (sdkVersion ?? "").split("."); const maxedOutSdkVersion = `${major}.${minor ?? 999}.${patch ?? 999}`; const sdkVersionCoerced = semver_1.default.coerce(sdkVersion && maxedOutSdkVersion); const minSdkVersionCoerced = semver_1.default.coerce(minSdkVersion); if (sdkVersionCoerced && minSdkVersionCoerced && semver_1.default.lt(sdkVersionCoerced, minSdkVersionCoerced)) { errors.push({ message: `${sdkName} SDK is below minimum version (included ${sdkVersion}, min ${minSdkVersion})`, }); } } else { errors.push({ message: `${sdkName} SDK script url must end with /multiplayer.js or /multiplayer-dev.js`, }); } } } return { valid: errors.length === 0, errors, sdk: sdkName, }; } exports.validateGameFiles = validateGameFiles; async function validateMultiplayer({ errors, gameConfig, logicJs, indexHtml, }) { if (!logicJs) { errors.push({ message: "logic.js must be included in the game files", }); } else { if (path_1.default.dirname(indexHtml.path) !== path_1.default.dirname(logicJs.path)) { errors.push({ message: "logic.js must be in the same directory as index.html", }); } return validateMultiplayerLogicJsContent({ logicJs, gameConfig, errors, }); } } async function validateMultiplayerLogicJsContent({ logicJs, gameConfig, errors, }) { if (!logicJs.content) { errors.push({ message: "logic.js content has not been provided for validation", }); } else { await eslint .lintText(logicJs.content) .then((results) => { const result = results.at(0); if (result) { const lintErrors = result.messages.filter((err) => err.severity === 2); if (lintErrors.length > 0) { errors.push({ message: "logic.js contains invalid code", lintErrors, }); } } else { errors.push({ message: "failed to lint logic.js" }); } }) .catch(() => { errors.push({ message: "failed to lint logic.js" }); }); if (!gameConfig.minPlayers || typeof gameConfig.minPlayers !== "number" || isNaN(gameConfig.minPlayers)) { errors.push({ message: "logic.js: minPlayers not found or is invalid", }); } else if (gameConfig.minPlayers && (gameConfig.minPlayers < 1 || gameConfig.minPlayers > exports.MAX_PLAYERS)) { errors.push({ message: `logic.js: minPlayers must be between 1 and ${exports.MAX_PLAYERS}`, }); } if (!gameConfig.maxPlayers || typeof gameConfig.maxPlayers !== "number" || isNaN(gameConfig.maxPlayers)) { errors.push({ message: "logic.js: maxPlayers not found or is invalid", }); } else if (gameConfig.maxPlayers && (gameConfig.maxPlayers < 1 || gameConfig.maxPlayers > exports.MAX_PLAYERS)) { errors.push({ message: `logic.js: maxPlayers must be between 1 and ${exports.MAX_PLAYERS}`, }); } if (gameConfig.maxPlayers && gameConfig.minPlayers && gameConfig.maxPlayers < gameConfig.minPlayers) { errors.push({ message: "logic.js: maxPlayers must be greater than or equal to minPlayers", }); } if (gameConfig.updatesPerSecond === undefined) { errors.push({ message: "logic.js: updatesPerSecond must be a constant (updatesPerSecond: 1-30)", }); } else if (gameConfig.updatesPerSecond < 1 || gameConfig.updatesPerSecond > 30) { errors.push({ message: "logic.js: updatesPerSecond must be undefined or between 1 and 30", }); } } }