diff --git a/.gitignore b/.gitignore index 499312d1..f8068666 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Generated *.gen shared/JSBindings.h +shared/JSEnums.h node_modules/ diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 42f782d1..88ccb322 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -3,6 +3,7 @@ include(FetchContent) include(cmake/DepsDownload.cmake) set(BINDINGS_SCOPE "CLIENT") include(../shared/cmake/GenerateBindings.cmake) +include(../shared/cmake/GenerateEnums.cmake) project(altv-client-js) @@ -112,6 +113,7 @@ if(DYNAMIC_BUILD EQUAL 1) ) add_dependencies(${PROJECT_NAME} ${SDK_PROJECT_NAME} js-bindings) + add_dependencies(${PROJECT_NAME} ${SDK_PROJECT_NAME} js-enums) else() ## STATIC add_library( @@ -130,6 +132,7 @@ else() ) add_dependencies(${PROJECT_NAME}-static js-bindings) + add_dependencies(${PROJECT_NAME}-static js-enums) endif() if(ALTV_JS_DEINIT_CPPSDK) diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 5e66f5c0..8865076b 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -4,6 +4,7 @@ include(../shared/deps/cpp-sdk/CMakeLists.txt) include(cmake/DepsDownload.cmake) set(BINDINGS_SCOPE "SERVER") include(../shared/cmake/GenerateBindings.cmake) +include(../shared/cmake/GenerateEnums.cmake) project(js-module) @@ -145,6 +146,7 @@ add_library( ) add_dependencies(${PROJECT_NAME} alt-sdk js-bindings) +add_dependencies(${PROJECT_NAME} alt-sdk js-enums) if(MSVC AND WIN32) set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /DEBUG") diff --git a/shared/bindings/BindingsMain.cpp b/shared/bindings/BindingsMain.cpp index f76109b3..ac64f040 100644 --- a/shared/bindings/BindingsMain.cpp +++ b/shared/bindings/BindingsMain.cpp @@ -3,6 +3,7 @@ #include "../V8Helpers.h" #include "../V8ResourceImpl.h" #include "../V8Module.h" +#include "JSEnums.h" static void HashCb(const v8::FunctionCallbackInfo& info) { @@ -465,6 +466,55 @@ static void GetNetTime(const v8::FunctionCallbackInfo& info) V8_RETURN_UINT(netTime); } +// Enums module doesn't need to import any modules +static v8::MaybeLocal HandleEnumsModuleImport(v8::Local context, v8::Local specifier, v8::Local, v8::Local referrer) { + v8::MaybeLocal maybeModule; + Log::Error << "Enums module must not import anything" << Log::Endl; + return maybeModule; +} + +static void AddEnumsToSharedModuleExports(v8::Isolate* isolate, v8::Local ctx, v8::Local exports) { + v8::Local sourceCode = V8Helpers::JSValue(JSEnums::GetBindingsCode()); + + v8::ScriptOrigin scriptOrigin(isolate, V8Helpers::JSValue("js-enums"), 0, 0, false, -1, v8::Local(), false, false, true, v8::Local()); + + v8::ScriptCompiler::Source source{ sourceCode, scriptOrigin }; + auto maybeModule = v8::ScriptCompiler::CompileModule(isolate, &source); + if (maybeModule.IsEmpty()) { + Log::Error << "Failed to compile js-enums module" << Log::Endl; + return; + } + + auto mod = maybeModule.ToLocalChecked(); + v8::Maybe result = mod->InstantiateModule(ctx, HandleEnumsModuleImport); + if(result.IsNothing() || result.ToChecked() == false) + { + Log::Error << "Failed to instantiate js-enums module" << Log::Endl; + return; + } + + auto returnValue = mod->Evaluate(ctx); + if(returnValue.IsEmpty()) + { + Log::Error << "Failed to evaluate js-enums module" << Log::Endl; + return; + } + + auto enumsNamespace = mod->GetModuleNamespace(); + auto enums = enumsNamespace.As(); + v8::Local keys; + enums->GetOwnPropertyNames(ctx).ToLocal(&keys); + + for(uint32_t i = 0; i < keys->Length(); ++i) + { + v8::Local key; + keys->Get(ctx, i).ToLocal(&key); + v8::Local value; + enums->Get(ctx, key).ToLocal(&value); + exports->Set(ctx, key, value); + } +} + extern V8Class v8BaseObject, v8WorldObject, v8Entity, v8File, v8RGBA, v8Vector2, v8Vector3, v8Quaternion, v8Blip, v8AreaBlip, v8RadiusBlip, v8PointBlip, v8Resource, v8Utils; extern V8Module @@ -531,6 +581,8 @@ extern V8Module V8_OBJECT_SET_INT(exports, "defaultDimension", alt::DEFAULT_DIMENSION); V8_OBJECT_SET_INT(exports, "globalDimension", alt::GLOBAL_DIMENSION); + AddEnumsToSharedModuleExports(isolate, ctx, exports); + #ifdef ALT_CLIENT_API V8_OBJECT_SET_BOOLEAN(exports, "isClient", true); V8_OBJECT_SET_BOOLEAN(exports, "isServer", false); diff --git a/shared/cmake/GenerateEnums.cmake b/shared/cmake/GenerateEnums.cmake new file mode 100644 index 00000000..8bdbf88b --- /dev/null +++ b/shared/cmake/GenerateEnums.cmake @@ -0,0 +1,16 @@ +# Generates the header files for the JavaScript enums transpiled from altv-types +if(NOT BINDINGS_SCOPE) + set(BINDINGS_SCOPE "SHARED") +endif() + +if (CMAKE_HOST_WIN32) + add_custom_target(js-enums + call generate-enums.bat ${BINDINGS_SCOPE} + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + ) +else() + add_custom_target(js-enums + bash generate-enums.sh ${BINDINGS_SCOPE} + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + ) +endif() diff --git a/shared/cmake/generate-enums.bat b/shared/cmake/generate-enums.bat new file mode 100644 index 00000000..7ad23597 --- /dev/null +++ b/shared/cmake/generate-enums.bat @@ -0,0 +1 @@ +node ../../tools/enums-transpiler.js .. diff --git a/shared/cmake/generate-enums.sh b/shared/cmake/generate-enums.sh new file mode 100644 index 00000000..7ad23597 --- /dev/null +++ b/shared/cmake/generate-enums.sh @@ -0,0 +1 @@ +node ../../tools/enums-transpiler.js .. diff --git a/tools/enums-transpiler.js b/tools/enums-transpiler.js new file mode 100644 index 00000000..83ce60c5 --- /dev/null +++ b/tools/enums-transpiler.js @@ -0,0 +1,212 @@ +// clang-format off +const fs = require("fs").promises; +const assert = require("assert"); +const pathUtil = require("path"); + +const ENUM_START = "enum "; +const transpiledEnumNames = new Set(); + +function transpileTSEnums(tsCode) { + let jsCode = ""; + let pos = 0; + + while (true) { + const startIndex = tsCode.indexOf(ENUM_START, pos); + if (startIndex === -1) { + // console.log("startIndex -1"); + break; + } + + const enumNameStartIndex = startIndex + ENUM_START.length; + + let bodyStartIndex = tsCode.indexOf("{", enumNameStartIndex); + assert.notEqual(bodyStartIndex, -1); + + const bodyEndIndex = tsCode.indexOf("}", bodyStartIndex); + assert.notEqual(bodyEndIndex, -1); + pos = bodyEndIndex; + + const body = tsCode + .slice(bodyStartIndex + 1, bodyEndIndex) + .replaceAll(/\/\*(.|\n)*\*\//gm, "") + .replaceAll(/\/\/(.|\n)*$/gm, ""); + + const enumName = tsCode + .slice(enumNameStartIndex, bodyStartIndex) + .trim(); + if (transpiledEnumNames.has(enumName)) { + throw new Error(`Detected enum duplicate: ${enumName}`); + } + transpiledEnumNames.add(enumName); + + const members = body.split(/(?!"),(?!")/).map((each) => each.trim()); + + // console.log({ members } /* "body:\n\n", body, "\n\n" */); + + if (members.at(-1)?.length === 0) { + // console.log("removing trailing comma space"); + members.pop(); + } + + const jsMembers = []; + + members.reduce((acc, memberContent) => { + // console.log({ + // acc, + // memberContent, + // }); + + let memberName; + let memberValue; + + const explicitMember = memberContent.match(/^(.+).*=(.*)$/); + if (explicitMember) { + // console.log({ explicitMember }); + + [, memberName, memberValue] = explicitMember; + memberName = memberName.trim(); + memberValue = memberValue.trim(); + if (!(memberValue.includes('"') || memberValue.includes("'"))) { + assert.ok( + !isNaN(+memberValue), + `raw member value: ${memberValue}` + ); + memberValue = +memberValue; + } + } else { + memberName = memberContent; + assert.ok( + typeof acc === "number" && !isNaN(acc), + `acc: ${acc} memberName: ${memberName}` + ); + memberValue = acc + 1; + } + + // console.log({ + // memberContent, + // memberName, + // memberValue, + // }); + + jsMembers.push([memberName.trim(), memberValue]); + + return memberValue; + }, -1); + + // console.log({ + // enumName, + // members, + // }); + + const jsBody = jsMembers + .map(([name, value]) => { + name = name + .replaceAll('"', "") + .replaceAll("'", "") + .replaceAll("\\", "\\\\"); + if (typeof value === "number") { + return ( + ` '${name}': ${value},\n` + ` '${value}': '${name}'` + ); + } else { + return ` '${name}': ${value},\n` + ` ${value}: '${name}'`; + } + }) + .join(",\n"); + + jsCode += `export const ${enumName} = {\n${jsBody}\n}\n`; + } + + return jsCode; +} + +const modules = [ + "https://raw.githubusercontent.com/altmp/altv-types/master/shared/index.d.ts", + "https://raw.githubusercontent.com/altmp/altv-types/master/server/index.d.ts", + "https://raw.githubusercontent.com/altmp/altv-types/master/client/index.d.ts", +]; + +(async () => { + // Base path should point to the main directory of the repo + if (process.argv.length < 3) { + showError("Missing 'basePath' argument"); + showUsage(); + process.exit(1); + } + const basePath = process.argv[2]; + + // for debug purposes + const outputJsCode = process.argv[3] ?? null; + + let jsCode = ""; + for (const m of modules) { + const content = await (await fetch(m)).text(); + jsCode += transpileTSEnums(content); + } + + if (outputJsCode) { + await fs.writeFile( + pathUtil.resolve(__dirname, basePath, outputJsCode), + jsCode + ); + showLog( + `Wrote enums transpilation result as JS code to file: ${outputJsCode}` + ); + } + + // Result bindings output path + const outputPath = "shared/JSEnums.h"; + + // Full output file + const resultTemplate = `// !!! THIS FILE WAS AUTOMATICALLY GENERATED (ON {Date}), DO NOT EDIT MANUALLY !!! +#pragma once +#include + +namespace JSEnums { + static std::string GetBindingsCode() + { + static constexpr char code[] = { {BindingsCode},'\\0' }; + return code; + } +} +`; + + // Convert the whole file content to a char code array + const content = jsCode.split("").map((char) => char.charCodeAt(0)); + const outputStr = resultTemplate + .replace("{BindingsCode}", content.toString()) + .replace("{Date}", `${getDate()} ${getTime()}`); + await fs.writeFile( + pathUtil.resolve(__dirname, basePath, outputPath), + outputStr + ); + showLog(`Wrote enums transpilation result to file: ${outputPath}`); +})(); + +function getDate() { + const date = new Date(); + const day = date.getDate(), + month = date.getMonth() + 1, + year = date.getFullYear(); + return `${day < 10 ? `0${day}` : day}/${ + month < 10 ? `0${month}` : month + }/${year}`; +} + +function getTime() { + const date = new Date(); + const hours = date.getHours(), + minutes = date.getMinutes(), + seconds = date.getSeconds(); + return `${hours < 10 ? `0${hours}` : hours}:${ + minutes < 10 ? `0${minutes}` : minutes + }:${seconds < 10 ? `0${seconds}` : seconds}`; +} + +function showLog(...args) { + console.log(`[${getTime()}]`, ...args); +} + +function showError(...args) { + console.error(`[${getTime()}]`, ...args); +}