├── .eslintrc.json ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── CI.yml │ └── CacheClean.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .vscode └── settings.json ├── README.md ├── babel.config.mts ├── biome.jsonc ├── example └── CMakeLists.txt ├── package.json ├── pnpm-lock.yaml ├── prettier.config.mjs ├── src ├── args.ts ├── argumentBuilder.ts ├── build.ts ├── config-types.d.ts ├── config.ts ├── deps │ ├── aws-sdk-client-s3.ts │ ├── mkdirp.ts │ └── types.d.ts ├── generator.ts ├── lib.d.mts ├── lib.ts ├── libc.ts ├── loader.ts ├── main.d.mts ├── main.ts ├── manifest.ts ├── nodeAPIInclude │ ├── index.ts │ ├── resolve.ts │ └── search.ts ├── override.ts ├── runtimeDistribution.ts ├── tsconfig.json ├── urlRegistry.ts ├── utils │ ├── download.ts │ ├── env.ts │ ├── exec.ts │ ├── fs.ts │ ├── logger.ts │ └── retry.ts └── vcvarsall.ts ├── test ├── args.test.ts ├── config.test.ts ├── download.test.ts ├── env.test.ts ├── fs.test.ts ├── logger.test.ts ├── package-lock.json ├── package.json ├── patches │ └── zeromq+6.4.2.patch ├── retry.test.ts ├── zeromq.test.ts └── zeromq.ts ├── tsconfig.json ├── turbo.json ├── vite.config.mts └── vitest.config.mts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-atomic", 3 | "ignorePatterns": ["build/", "dist/", "node_modules/", "coverage/", "stats.html", ".cache/", ".turbo/", "html/"], 4 | "overrides": [ 5 | { 6 | "files": ["**/*.ts"], 7 | "rules": { 8 | "no-empty-function": "off", 9 | "@typescript-eslint/no-empty-function": "error", 10 | "no-useless-constructor": "off", 11 | "@typescript-eslint/no-useless-constructor": "error", 12 | "node/shebang": "off" 13 | } 14 | }, 15 | { 16 | "files": ["**/*.ts"], 17 | "rules": { 18 | "import/no-extraneous-dependencies": "off" 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # line endings 2 | * text=auto eol=lf 3 | *.{cmd,[cC][mM][dD]} text eol=crlf 4 | *.{bat,[bB][aA][tT]} text eol=crlf 5 | *.{vcxproj,vcxproj.filters} text eol=crlf 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [aminya] 2 | polar: aminya 3 | patreon: aminya 4 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | Test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | - macos-13 18 | - windows-latest 19 | cpp_arch: 20 | - x64 21 | include: 22 | - os: macos-14 # arm 23 | cpp_arch: arm64 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | submodules: true 28 | 29 | - uses: actions/setup-node@v4 30 | with: 31 | node-version-file: .nvmrc 32 | 33 | - uses: pnpm/action-setup@v4 34 | 35 | - name: Setup Home Directory 36 | id: home 37 | shell: bash 38 | run: | 39 | if [[ ${{ runner.os }} == "Windows" ]]; then 40 | echo "HOME=$USERPROFILE" >> $GITHUB_OUTPUT 41 | else 42 | echo "HOME=$HOME" >> $GITHUB_OUTPUT 43 | fi 44 | 45 | - name: Cache vcpkg/pnpm 46 | uses: actions/cache@v4 47 | with: 48 | path: | 49 | ${{ steps.home.outputs.HOME }}/.pnpm-store 50 | ${{ steps.home.outputs.HOME }}/vcpkg 51 | key: ${{ matrix.os }}-${{ matrix.cpp_arch }}-${{ hashFiles('pnpm-lock.yaml', 'package.json') }} 52 | restore-keys: | 53 | ${{ matrix.os }}-${{ matrix.cpp_arch }}- 54 | 55 | - name: Cache .cache 56 | uses: actions/cache@v4 57 | with: 58 | path: | 59 | ./.cache/ 60 | key: ${{ matrix.os }}-${{ matrix.cpp_arch }}-${{ hashFiles('./**/*', '!**/node_modules/**', '!**/build/**', '!**/coverage/**', '!.git/**') }} 61 | restore-keys: | 62 | ${{ matrix.os }}-${{ matrix.cpp_arch }}- 63 | 64 | - name: Setup Cpp 65 | uses: aminya/setup-cpp@v1 66 | with: 67 | vcvarsall: true 68 | cmake: true 69 | ninja: true 70 | python: true 71 | vcpkg: 608d1dbcd6969679f82b1ca6b89d58939c9b228e 72 | architecture: ${{ matrix.cpp_arch }} 73 | 74 | - name: Install Zeromq Mac-OS Dependencies 75 | if: ${{ contains(matrix.os, 'macos') }} 76 | run: | 77 | brew install gnutls autoconf automake libtool 78 | - name: Install Zeromq Ubuntu Dependencies 79 | if: ${{ contains(matrix.os, 'ubuntu') }} 80 | run: | 81 | sudo apt-get update -q -y 82 | sudo apt-get install -y --no-install-recommends autoconf automake libtool 83 | 84 | - name: Install dependencies 85 | run: pnpm install 86 | 87 | - name: Build 88 | run: pnpm run build 89 | 90 | - name: Lint 91 | run: pnpm run test.lint 92 | 93 | - name: Tests 94 | run: pnpm test 95 | 96 | - name: Setup Node 12 97 | if: matrix.os != 'macos-14' 98 | uses: actions/setup-node@v4 99 | with: 100 | node-version: 12 101 | 102 | - name: Smoke test Node 12 103 | if: matrix.os != 'macos-14' 104 | run: node ./build/main.js --help 105 | -------------------------------------------------------------------------------- /.github/workflows/CacheClean.yml: -------------------------------------------------------------------------------- 1 | name: Cleanup Branch Cache 2 | on: 3 | pull_request: 4 | types: 5 | - closed 6 | workflow_dispatch: 7 | 8 | jobs: 9 | cleanup: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Cleanup 13 | run: | 14 | gh extension install actions/gh-actions-cache 15 | 16 | echo "Fetching list of cache key" 17 | cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) 18 | 19 | ## Setting this to not fail the workflow while deleting cache keys. 20 | set +e 21 | echo "Deleting caches..." 22 | for cacheKey in $cacheKeysForPR 23 | do 24 | gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm 25 | done 26 | echo "Done" 27 | env: 28 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | REPO: ${{ github.repository }} 30 | BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | *.tgz 4 | stats.html 5 | .eslintcache 6 | .tmp/ 7 | coverage/ 8 | .turbo/ 9 | .cache/ 10 | .DS_Store 11 | html/ 12 | *.tsbuildinfo 13 | test/package-test.json 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | lockfile=true 3 | public-hoist-pattern[]=*@types* 4 | public-hoist-pattern[]=*eslint* 5 | public-hoist-pattern[]=rollup 6 | public-hoist-pattern[]=*@vitest* 7 | 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.14.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | node_modules 3 | package.json 4 | .cache/ 5 | .turbo/ 6 | html/ 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "biomejs.biome" 4 | }, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "biomejs.biome" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "biomejs.biome" 10 | }, 11 | "[jsonc]": { 12 | "editor.defaultFormatter": "biomejs.biome" 13 | }, 14 | "[yaml]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "[markdown]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "dependi.npm.lockFileEnabled": true, 21 | "cSpell.words": ["vcvars", "vcvarsall", "vsvars", "vsversion", "vswhere"] 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cmake-ts 2 | 3 | A CMake-based build system for native NodeJS and Electron addons. 4 | 5 | This project is loosely inspired by [cmake-js](https://github.com/cmake-js/cmake-js) but attempts to fix several design flaws. 6 | 7 | It is intended to prebuild addons for different versions of NodeJS and Electron and ship a binary version. 8 | 9 | ## Example 10 | 11 | See [zeromq.js](https://github.com/zeromq/zeromq.js) for an real-world example of how to use this module. 12 | 13 | ## Getting Started 14 | 15 | Create your `CMakeLists.txt` file based on [the example](/example/CMakeLists.txt) and run the following command to build your project. 16 | 17 | ```bash 18 | cmake-ts build 19 | ``` 20 | 21 | cmake-ts can build the projects with built-in configurations that are selected depending on the arguments and the environment. This includes cross-compilation for different architectures, including Windows arm64, Linux arm64, etc. 22 | 23 | ```bash 24 | cmake-ts build --config debug 25 | ``` 26 | 27 | You can cross-compile by specifying the built-in cross configs: 28 | 29 | ```bash 30 | cmake-ts build --config cross-win32-arm64-release 31 | ``` 32 | 33 | Or by specifying the `npm_config_target_os` and `npm_config_target_arch` environment variables: 34 | 35 | ```bash 36 | npm_config_target_os=linux npm_config_target_arch=arm64 cmake-ts build 37 | ``` 38 | 39 | ### CLI Arguments 40 | 41 | `build` command: 42 | 43 | ```sh 44 | Usage: cmake-ts build [options] 45 | 46 | Build the project 47 | 48 | Options: 49 | --config, --configs 50 | Named config(s) to build, which could be from default configs or the ones defined in the config file (package.json) 51 | 52 | If no config is provided, it will build for the current runtime on the current system with the Release build type 53 | 54 | The default configs are combinations of ``, ``, ``, and ``. 55 | 56 | - ``: the runtime to use 57 | 58 | e.g.: `node`, `electron`, `iojs` 59 | 60 | - ``: the cmake build type (optimization level) 61 | 62 | e.g.: `debug`, `release`, `relwithdebinfo`, or `minsizerel` 63 | 64 | - ``: the target platform 65 | 66 | e.g.: `win32`, `linux`, `darwin`, `aix`, `android`, `freebsd`, `haiku`, `openbsd`, `sunos`, `cygwin`, `netbsd` 67 | 68 | - ``: the target architecture 69 | 70 | e.g.: `x64`, `arm64`, `ia32`, `arm`, `loong64`, `mips`, `mipsel`, `ppc`, `ppc64`, `riscv64`, `s390`, `s390x` 71 | 72 | Any combination of ``, ``, ``, and `` is valid. Some examples: 73 | 74 | - `release` 75 | - `debug` 76 | - `relwithdebinfo` 77 | - `node-release` 78 | - `node-debug` 79 | - `electron-release` 80 | - `electron-debug` 81 | - `win32-x64` 82 | - `win32-x64-debug` 83 | - `linux-x64-debug` 84 | - `linux-x64-node-debug` 85 | - `linux-x64-electron-release` 86 | - `darwin-x64-node-release` 87 | - `darwin-arm64-node-release` 88 | - `darwin-arm64-electron-relwithdebinfo` 89 | 90 | To explicitly indicate cross-compilation, prefix the config name with `cross-`: 91 | 92 | - `cross-win32-ia32-node-release` 93 | - `cross-linux-arm64-node-release` 94 | - `cross-darwin-x64-electron-relwithdebinfo` 95 | 96 | You can also define your own configs in the config file (package.json). 97 | 98 | - ``: the name of the config 99 | 100 | e.g.: `my-config` 101 | 102 | The configs can also be in format of `named-`, which builds the configs that match the property. 103 | 104 | - `named-os`: build all the configs in the config file that have the same OS 105 | - `named-os-dev`: build all the configs in the config file that have the same OS and `dev` is true 106 | - `named-all`: build all the configs in the config file 107 | 108 | 109 | The configs can be combined with `,` or multiple `--configs` flags. They will be merged together. 110 | (default: []) 111 | -h, --help display help for command 112 | ``` 113 | 114 | ## Runtime Addon Loader 115 | 116 | The runtime addon loader allows you to load the addon for the current runtime during runtime. 117 | 118 | In ES modules: 119 | 120 | ```ts 121 | import { loadAddon } from 'cmake-ts/build/loader.mjs'; 122 | import path from 'path'; 123 | import { fileURLToPath } from 'url'; 124 | 125 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 126 | 127 | const addon = loadAddon(path.resolve(__dirname, '..', 'build')); 128 | ``` 129 | 130 | or in CommonJS: 131 | 132 | ```js 133 | const { loadAddon } = require('cmake-ts/build/loader.js'); 134 | 135 | const addon = loadAddon(path.resolve(__dirname, '..', 'build')); 136 | ``` 137 | 138 | You can pass the types of the addon to the loader to get type safety: 139 | 140 | ```ts 141 | type MyAddon = { 142 | myFunction: (name: string) => void; 143 | }; 144 | 145 | const addon = loadAddon(path.resolve(__dirname, '..', 'build')); 146 | ``` 147 | 148 | ## Configuration File 149 | 150 | Configuration is done entirely via `package.json`. You can specify multiple build configurations under the `cmake-ts` key: 151 | 152 | ```js 153 | "cmake-ts": { 154 | "nodeAPI": "node-addon-api" // Specify the node API package such as `node-addon-api`, `nan`, or the path to a directory that has the nodeAPI header. Default is `node-addon-api`, a warning is emitted if nan is used 155 | "configurations": [ 156 | { 157 | "name": "win-x64", // name for named-configs mode 158 | "os": "win32", // win32, linux and darwin are supported 159 | "arch": "x64", // x64, x86 should work 160 | "runtime": "electron", // node or electron 161 | "runtimeVersion": "4.0.1", // Version of the runtime which it is built 162 | "toolchainFile": "/windows.cmake", // CMake Toolchain file to use for crosscompiling 163 | "CMakeOptions": [ //Same syntax as for the globalCMakeOptions 164 | { 165 | "name": "MY_CMAKE_OPTION", 166 | "value": "my_value", 167 | } 168 | ], 169 | "addonSubdirectory": "avx2-generic" // if you build addons for multiple architectures in high performance scenarios, you can put the addon inside another subdirectory 170 | }, // more build configurations... 171 | { 172 | "dev": true, // whether this configuration is eligible to be used in a dev test build 173 | "os": "linux", // win32, linux and darwin are supported 174 | "arch": "x64", // x64, x86 should work 175 | "runtime": "node", // node or electron 176 | "runtimeVersion": "10.3.0", // Version of the runtime which it is built 177 | } // more build configurations ... 178 | ], 179 | "targetDirectory": "build", // where to build your project 180 | "buildType": "Release", // Debug or Release build, most likely set it to Release 181 | "projectName": "addon" // The name of your CMake project. 182 | "globalCMakeOptions": [{ // this might be omitted of no further options should be passed to CMake 183 | "name": "CMAKE_CXX_FLAGS", 184 | "value": "-Og" 185 | }, { 186 | "name": "CMAKE_CXX_FLAGS", 187 | "value": "-I$ROOT$/include", // $ROOT$ will be replaced by the package.json directory 188 | }, { 189 | "name": "CMAKE_EXPORT_COMPILE_COMMANDS", 190 | "value": "1" 191 | }] 192 | } 193 | ``` 194 | 195 | ## Workflow 196 | 197 | While it is desirable to perform a full build (all configurations) within a CI environment, long build times hinder local package development. Therefore cmake-ts knows multiple build modes: 198 | 199 | - **TODO** `nativeonly` -> Builds the native code **only** for the runtime cmake-ts is currently running on, ignoring all previously specified configurations. This is useful if you'd like to run some unit tests against the compiled code. When running `cmake-ts nativeonly`, cmake-ts will determine the runtime, ABI, and platform from the environment, and build only the configuration required to run on this platform. 200 | - _Example using the configuration above_ 201 | - You run `cmake-ts nativeonly` on **NodeJS 11.7 on MacOS**, `cmake-ts` will **ignore** all specified configurations above and build the native addon for **NodeJS 11.7 on MacOS** 202 | - **TODO** `osonly` -> Builds the native code for all configurations which match the current operating system. This is useful for those developing for example an electron addon and want to test their code in electron. In such a case, you would specify electron and NodeJS runtimes for several platforms in your configuration and you can use `cmake-ts osonly` to build a local package you can install in your application. 203 | - _Example using the configuration above_ 204 | - You run `cmake-ts osonly` on **NodeJS 11.7 on Linux**, `cmake-ts` will **ignore** all configurations above where `os != linux` and build the native addon for **all** remaining configurations, in this case it will build for **NodeJS 10.3 on Linux**. 205 | - **TODO** **HINT**: For both `osonly` and `nativeonly`, the specified CMake Toolchain files are ignored since I assume you got your toolchain set up correctly for your **own** operating system. 206 | - None / Omitted: Builds all configs 207 | - `dev-os-only` builds the first config that has `dev == true` and `os` matches the current OS 208 | - `named-configs arg1 arg2 ...` builds all configs for which `name` is one of the args 209 | 210 | ## Cross Compilation 211 | 212 | This module supports cross-compilation from Linux to macOS and Windows, given a correct toolchain setup. There is a docker container that has a cross-toolchain based on CLang 7 setup for Windows and macOS which might be used in a CI. 213 | 214 | [Docker Image](https://hub.docker.com/r/martin31821/crossdev) 215 | -------------------------------------------------------------------------------- /babel.config.mts: -------------------------------------------------------------------------------- 1 | import type { TransformOptions } from "@babel/core" 2 | // @ts-expect-error untyped plugin 3 | import RemoveNodePrefix from "@upleveled/babel-plugin-remove-node-prefix" 4 | 5 | const babelConfig: TransformOptions = { 6 | plugins: [RemoveNodePrefix], 7 | sourceMaps: true, 8 | sourceType: "module", 9 | } 10 | export default babelConfig 11 | -------------------------------------------------------------------------------- /biome.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.1/schema.json", 3 | "files": { 4 | "ignore": [ 5 | "**/node_modules/**", 6 | "**/.pnpm-store/**", 7 | "**/dist/**", 8 | "**/build/**", 9 | "**/.*cache/**", 10 | "**/.turbo/**", 11 | "coverage/**", 12 | "**/coverage/**", 13 | "**/stats.html", 14 | "**/html/**" 15 | ], 16 | "ignoreUnknown": true 17 | }, 18 | "organizeImports": { 19 | "enabled": true 20 | }, 21 | "linter": { 22 | "enabled": true, 23 | "rules": { 24 | "recommended": true, 25 | "style": { 26 | "noInferrableTypes": "off", 27 | "noUselessElse": "off", 28 | "noNonNullAssertion": "off", 29 | "useNodejsImportProtocol": "off" 30 | }, 31 | "complexity": { 32 | "useLiteralKeys": "off" 33 | }, 34 | "suspicious": { 35 | "noConfusingVoidType": "off" 36 | }, 37 | "correctness": { 38 | "useImportExtensions": { 39 | "level": "error", 40 | "options": { 41 | "suggestedExtensions": { 42 | "ts": { 43 | "component": "js", 44 | "module": "js" 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | "formatter": { 53 | "enabled": true, 54 | "indentWidth": 2, 55 | "lineWidth": 120, 56 | "indentStyle": "space" 57 | }, 58 | "json": { 59 | "formatter": { 60 | "enabled": true, 61 | "trailingCommas": "none" 62 | }, 63 | "parser": { 64 | "allowComments": true, 65 | "allowTrailingCommas": true 66 | } 67 | }, 68 | "javascript": { 69 | "formatter": { 70 | "enabled": true, 71 | "semicolons": "asNeeded", 72 | "quoteStyle": "double" 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /example/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | macro(set_option_from_env OPTION_NAME) 4 | string(TOLOWER ${OPTION_NAME} OPTION_NAME_LOWER) 5 | 6 | if(DEFINED ENV{npm_config_${OPTION_NAME_LOWER}}) 7 | if("$ENV{npm_config_${OPTION_NAME_LOWER}}" STREQUAL "true") 8 | set("${OPTION_NAME}" 9 | ON 10 | CACHE BOOL "npm_config_${OPTION_NAME_LOWER}" FORCE) 11 | elseif("$ENV{npm_config_${OPTION_NAME_LOWER}}" STREQUAL "false") 12 | set("${OPTION_NAME}" 13 | OFF 14 | CACHE BOOL "npm_config_${OPTION_NAME_LOWER}" FORCE) 15 | else() 16 | set("${OPTION_NAME}" 17 | "$ENV{npm_config_${OPTION_NAME_LOWER}}" 18 | CACHE STRING "npm_config_${OPTION_NAME_LOWER}" FORCE) 19 | endif() 20 | endif() 21 | 22 | if(${OPTION_NAME}) 23 | string(REPLACE "addon_" "" OPTION_NAME_LOWER "${OPTION_NAME_LOWER}") 24 | string(REPLACE "_" "-" OPTION_NAME_LOWER "${OPTION_NAME_LOWER}") 25 | list(APPEND VCPKG_MANIFEST_FEATURES ${OPTION_NAME_LOWER}) 26 | endif() 27 | 28 | message(STATUS "${OPTION_NAME}: ${${OPTION_NAME}}") 29 | endmacro() 30 | 31 | option(ADDON_SOMETHING "Something" ON) 32 | set_option_from_env(ADDON_SOMETHING) 33 | 34 | if(APPLE) 35 | option(MACOSX_DEPLOYMENT_TARGET "MacOS deployment target" "10.15") 36 | set_option_from_env(MACOSX_DEPLOYMENT_TARGET) 37 | set(CMAKE_OSX_DEPLOYMENT_TARGET ${MACOSX_DEPLOYMENT_TARGET}) 38 | endif() 39 | 40 | # target system on Windows (for cross-compiling x86) and static linking runtimes 41 | if(WIN32) 42 | if("$ENV{Platform}" STREQUAL "x86") 43 | set(CMAKE_SYSTEM_PROCESSOR "x86") 44 | set(VCPKG_TARGET_TRIPLET "x86-windows-static") 45 | elseif(NOT "$ENV{PROCESSOR_ARCHITEW6432}" STREQUAL "") 46 | set(CMAKE_SYSTEM_PROCESSOR "$ENV{PROCESSOR_ARCHITEW6432}") 47 | set(VCPKG_TARGET_TRIPLET "x86-windows-static") 48 | else() 49 | set(CMAKE_SYSTEM_PROCESSOR "$ENV{PROCESSOR_ARCHITECTURE}") 50 | set(VCPKG_TARGET_TRIPLET "x64-windows-static") 51 | endif() 52 | 53 | # Avoid loading of project_optinos/WindowsToolchain 54 | set(CMAKE_TOOLCHAIN_FILE ";") 55 | 56 | # use static runtime library 57 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 58 | endif() 59 | 60 | include(FetchContent) 61 | 62 | if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") 63 | cmake_policy(SET CMP0135 NEW) 64 | endif() 65 | 66 | set(CMAKE_POSITION_INDEPENDENT_CODE TRUE) 67 | 68 | # Add project_options from https://github.com/aminya/project_options Change the 69 | # version in the following URL to update the package (watch the releases of the 70 | # repository for future updates) 71 | set(PROJECT_OPTIONS_VERSION "v0.36.6") 72 | FetchContent_Declare( 73 | _project_options 74 | URL https://github.com/aminya/project_options/archive/refs/tags/${PROJECT_OPTIONS_VERSION}.zip 75 | ) 76 | FetchContent_MakeAvailable(_project_options) 77 | include(${_project_options_SOURCE_DIR}/Index.cmake) 78 | 79 | # MacOS flags that should be set prior to any project calls 80 | if(APPLE) 81 | set(CMAKE_SHARED_LINKER_FLAGS 82 | "${CMAKE_SHARED_LINKER_FLAGS} -undefined dynamic_lookup") 83 | endif() 84 | 85 | # VCPKG 86 | run_vcpkg(VCPKG_URL "https://github.com/microsoft/vcpkg.git" VCPKG_REV 87 | "608d1dbcd6969679f82b1ca6b89d58939c9b228e") 88 | 89 | # Name of the project (will be the name of the plugin) 90 | project(addon LANGUAGES C CXX) 91 | 92 | project_options( 93 | prefix addon 94 | ENABLE_CACHE 95 | ENABLE_COMPILE_COMMANDS_SYMLINK 96 | ) 97 | 98 | file(GLOB_RECURSE SOURCES "./src/*.cc") 99 | add_library(addon SHARED ${SOURCES}) 100 | 101 | target_link_libraries(addon PRIVATE addon_project_options addon_project_warnings) 102 | 103 | # Node specific 104 | target_include_system_directories(addon PRIVATE ${CMAKE_JS_INC}) 105 | target_link_system_libraries(addon PRIVATE ${CMAKE_JS_LIB}) 106 | 107 | target_compile_definitions(addon PRIVATE V8_COMPRESS_POINTERS) 108 | target_compile_definitions(addon PRIVATE V8_31BIT_SMIS_ON_64BIT_ARCH) 109 | target_compile_definitions(addon PRIVATE V8_REVERSE_JSARGS) 110 | target_compile_definitions(addon PRIVATE BUILDING_NODE_EXTENSION) 111 | target_compile_definitions(addon PRIVATE NAPI_CPP_EXCEPTIONS) 112 | 113 | if(WIN32) 114 | target_compile_definitions(addon PRIVATE "NOMINMAX") 115 | target_compile_definitions(addon PRIVATE "NOGDI") 116 | target_compile_definitions(addon PRIVATE "WIN32_LEAN_AND_MEAN") 117 | endif() 118 | 119 | # Use `.node` for the library without any "lib" prefix 120 | set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node") 121 | 122 | # Windows 123 | if(WIN32) 124 | set_property(TARGET addon PROPERTY LINK_FLAGS "-Xlinker /DELAYLOAD:NODE.EXE") 125 | target_link_libraries(addon PRIVATE "ShLwApi.lib" "delayimp.lib") 126 | endif() 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cmake-ts", 3 | "version": "1.0.2", 4 | "description": "cmake-js rewrite in typescript to support advanced build configurations", 5 | "main": "build/lib.js", 6 | "module": "build/lib.mjs", 7 | "exports": { 8 | ".": { 9 | "require": "./build/lib.js", 10 | "import": "./build/lib.mjs" 11 | }, 12 | "./*": { 13 | "require": "./build/*.js", 14 | "import": "./build/*.mjs" 15 | }, 16 | "./*.js": { 17 | "require": "./build/*.js", 18 | "import": "./build/*.mjs" 19 | }, 20 | "./*.mjs": { 21 | "require": "./build/*.js", 22 | "import": "./build/*.mjs" 23 | }, 24 | "./build/*": { 25 | "require": "./build/*.js", 26 | "import": "./build/*.mjs" 27 | }, 28 | "./build/*.js": { 29 | "require": "./build/*.js", 30 | "import": "./build/*.mjs" 31 | }, 32 | "./build/*.mjs": { 33 | "require": "./build/*.js", 34 | "import": "./build/*.mjs" 35 | } 36 | }, 37 | "bin": "build/main.js", 38 | "scripts": { 39 | "clean": "shx rm -rf build ./node_modules/zeromq/build ./node_modules/zeromq/staging", 40 | "lint.eslint": "eslint . --cache --cache-location ./.cache/.eslintcache \"./**/*.{js,ts,mjs,mts,cjs,cts,json,yaml,yml}\" --fix", 41 | "lint.biome": "biome check --write --unsafe .", 42 | "lint": "turbo run lint.eslint lint.biome format.prettier format.biome", 43 | "test.lint.eslint": "eslint . --cache --cache-location ./.cache/.eslintcache \"./**/*.{js,ts,mjs,mts,cjs,cts,json,yaml,yml}\"", 44 | "test.lint.biome": "biome check .", 45 | "test.lint": "turbo run test.lint.eslint test.lint.biome test.format.prettier test.format.biome", 46 | "format.prettier": "prettier -l --write --cache --cache-location ./.cache/.prettiercache \"./**/*.{yaml,yml,md}\"", 47 | "format.biome": "biome format --write .", 48 | "format": "turbo run format.prettier format.biome", 49 | "test.format.prettier": "prettier --check --cache --cache-location ./.cache/.prettiercache \"./**/*.{yaml,yml,md}\"", 50 | "test.format.biome": "biome format .", 51 | "test.format": "turbo run test.format.prettier test.format.biome", 52 | "dev.tsc": "tsc -w --pretty", 53 | "dev.tsc.lib": "tsc -w --pretty --project ./src/tsconfig.json", 54 | "dev.legacy-main": "vite build --watch --mode legacy-main", 55 | "dev.modern-main": "vite build --watch --mode modern-main", 56 | "dev": "cross-env NODE_ENV=development run-p dev.legacy-main dev.modern-main dev.tsc dev.tsc.lib", 57 | "build.tsc": "tsc --pretty", 58 | "build.tsc.lib": "tsc --pretty --project ./src/tsconfig.json && shx cp -r ./src/*.d.mts ./build/", 59 | "build.legacy-main": "vite build --mode legacy-main", 60 | "build.modern-main": "vite build --mode modern-main", 61 | "build.legacy-library": "vite build --mode legacy-library", 62 | "build.modern-library": "vite build --mode modern-library", 63 | "build.modern-loader": "vite build --mode modern-loader", 64 | "build.legacy-loader": "vite build --mode legacy-loader", 65 | "build": "turbo run build.tsc build.tsc.lib build.legacy-main build.modern-main build.legacy-library build.modern-library build.modern-loader build.legacy-loader", 66 | "test": "cross-env NODE_OPTIONS=--enable-source-maps pnpx vitest --watch false", 67 | "coverage": "cross-env NODE_OPTIONS=--enable-source-maps pnpx vitest --coverage --watch false" 68 | }, 69 | "files": ["build/**/*", "src/**/*", "./*.mts", "./tsconfig.json"], 70 | "repository": { 71 | "type": "git", 72 | "url": "git+https://github.com/EmbeddedEnterprises/cmake-ts.git" 73 | }, 74 | "keywords": ["cmake", "nan", "node", "native", "addon", "build", "cmake-js"], 75 | "author": "Amin Yahyaabadi ", 76 | "contributors": ["Amin Yahyaabadi ", "Martin Koppehel "], 77 | "license": "MIT", 78 | "bugs": { 79 | "url": "https://github.com/EmbeddedEnterprises/cmake-ts/issues" 80 | }, 81 | "homepage": "https://github.com/EmbeddedEnterprises/cmake-ts#readme", 82 | "devDependencies": { 83 | "@babel/core": "7.26.10", 84 | "@types/babel__core": "7.20.5", 85 | "@types/fs-extra": "11.0.4", 86 | "@types/node": "22.14.0", 87 | "@types/resolve": "1.20.6", 88 | "@types/semver": "7.7.0", 89 | "@types/tar": "6.1.13", 90 | "@types/url-join": "4.0.3", 91 | "@types/which": "3.0.4", 92 | "@types/escape-quotes": "1.0.0", 93 | "@upleveled/babel-plugin-remove-node-prefix": "1.0.5", 94 | "@types/memoizee": "0.4.7", 95 | "turbo": "2.5.0", 96 | "cross-env": "7.0.3", 97 | "eslint": "^8", 98 | "eslint-config-atomic": "1.22.1", 99 | "npm-run-all2": "7.0.2", 100 | "rollup-plugin-visualizer": "5.14.0", 101 | "shx": "0.4.0", 102 | "typescript": "5.8.3", 103 | "vite": "6.2.7", 104 | "vitest": "3.1.1", 105 | "vite-plugin-babel": "1.3.0", 106 | "@vitest/coverage-v8": "3.1.1", 107 | "@vitest/ui": "3.1.1", 108 | "@biomejs/biome": "1.9.4", 109 | "prettier": "3.5.3", 110 | "prettier-config-atomic": "4.0.0", 111 | "execa": "9.5.2", 112 | "ci-info": "4.2.0", 113 | "fast-glob": "3.3.3", 114 | "fs-extra": "^10", 115 | "resolve": "^1.22.10", 116 | "semver": "^7.7.1", 117 | "tar": "^6", 118 | "url-join": "^4.0.1", 119 | "which": "^2", 120 | "node-downloader-helper": "^2.1.9", 121 | "escape-quotes": "^1.0.2", 122 | "commander": "^13.1.0", 123 | "msvc-dev-cmd": "github:aminya/msvc-dev-cmd#c01f519bd995460228ed3dec4df51df92dc290fd", 124 | "memoizee": "0.4.17" 125 | }, 126 | "packageManager": "pnpm@10.8.0", 127 | "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/package.json", 128 | "pnpm": { 129 | "onlyBuiltDependencies": ["@biomejs/biome", "core-js", "esbuild"] 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | import prettierConfigAtomic from "prettier-config-atomic" 2 | 3 | /** @type {import('prettier').Config} */ 4 | const config = { 5 | ...prettierConfigAtomic, 6 | printWidth: 120, 7 | tabWidth: 2, 8 | semi: true, 9 | singleQuote: true, 10 | } 11 | 12 | export default config 13 | -------------------------------------------------------------------------------- /src/args.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-deprecated */ 2 | import { Command as Commander } from "commander" 3 | import type { BuildCommandOptions, DeprecatedGlobalOptions, GlobalOptions, Options } from "./config-types.d" 4 | import { getEnvVar } from "./utils/env.js" 5 | import { logger } from "./utils/logger.js" 6 | 7 | /** 8 | * Parse the command line arguments and return the options. 9 | * 10 | * @returns The options parsed from the command line arguments. 11 | */ 12 | export function parseArgs(args?: string[]): Options { 13 | const program = new Commander("cmake-ts") 14 | 15 | // Debug flag can be set via environment variable 16 | const CMAKETSDEBUG = getEnvVar("CMAKETSDEBUG") 17 | const debugDefault = CMAKETSDEBUG === "true" || CMAKETSDEBUG === "1" 18 | 19 | const commandOptions: Pick = { 20 | command: { type: "none" }, 21 | } 22 | 23 | program 24 | .exitOverride((err) => { 25 | if (err.exitCode !== 0 && err.code !== "commander.help") { 26 | logger.error(err) 27 | commandOptions.command.type = "error" 28 | } 29 | }) 30 | .description("A CMake-based build system for native NodeJS and Electron addons.") 31 | .usage("[build or help] [options]") 32 | .option( 33 | "--logger ", 34 | "Set the log level (trace, debug, info, warn, error, off)", 35 | debugDefault ? "debug" : "info", 36 | ) 37 | .showHelpAfterError(false) 38 | .showSuggestionAfterError(true) 39 | 40 | // Build command 41 | const buildCommand = program 42 | .command("build") 43 | .description("Build the project") 44 | .option( 45 | "--config, --configs ", 46 | ` 47 | Named config(s) to build, which could be from default configs or the ones defined in the config file (package.json) 48 | 49 | If no config is provided, it will build for the current runtime on the current system with the Release build type 50 | 51 | The default configs are combinations of \`\`, \`\`, \`\`, and \`\`. 52 | 53 | - \`\`: the runtime to use 54 | 55 | e.g.: \`node\`, \`electron\`, \`iojs\` 56 | 57 | - \`\`: the cmake build type (optimization level) 58 | 59 | e.g.: \`debug\`, \`release\`, \`relwithdebinfo\`, or \`minsizerel\` 60 | 61 | - \`\`: the target platform 62 | 63 | e.g.: \`win32\`, \`linux\`, \`darwin\`, \`aix\`, \`android\`, \`freebsd\`, \`haiku\`, \`openbsd\`, \`sunos\`, \`cygwin\`, \`netbsd\` 64 | 65 | - \`\`: the target architecture 66 | 67 | e.g.: \`x64\`, \`arm64\`, \`ia32\`, \`arm\`, \`loong64\`, \`mips\`, \`mipsel\`, \`ppc\`, \`ppc64\`, \`riscv64\`, \`s390\`, \`s390x\` 68 | 69 | Any combination of \`\`, \`\`, \`\`, and \`\` is valid. Some examples: 70 | 71 | - \`release\` 72 | - \`debug\` 73 | - \`relwithdebinfo\` 74 | - \`node-release\` 75 | - \`node-debug\` 76 | - \`electron-release\` 77 | - \`electron-debug\` 78 | - \`win32-x64\` 79 | - \`win32-x64-debug\` 80 | - \`linux-x64-debug\` 81 | - \`linux-x64-node-debug\` 82 | - \`linux-x64-electron-release\` 83 | - \`darwin-x64-node-release\` 84 | - \`darwin-arm64-node-release\` 85 | - \`darwin-arm64-electron-relwithdebinfo\` 86 | 87 | To explicitly indicate cross-compilation, prefix the config name with \`cross-\`: 88 | 89 | - \`cross-win32-ia32-node-release\` 90 | - \`cross-linux-arm64-node-release\` 91 | - \`cross-darwin-x64-electron-relwithdebinfo\` 92 | 93 | You can also define your own configs in the config file (package.json). 94 | 95 | - \`\`: the name of the config 96 | 97 | e.g.: \`my-config\` 98 | 99 | The configs can also be in format of \`named-\`, which builds the configs that match the property. 100 | 101 | - \`named-os\`: build all the configs in the config file that have the same OS 102 | - \`named-os-dev\`: build all the configs in the config file that have the same OS and \`dev\` is true 103 | - \`named-all\`: build all the configs in the config file 104 | 105 | 106 | The configs can be combined with \`,\` or multiple \`--config\` flags. They will be merged together. 107 | `, 108 | [], 109 | ) 110 | .option("--project-name ", "The name of the built node addon.") 111 | .option("--addon-subdirectory ", "The subdirectory of the package which is being built.") 112 | .option("--package-directory ", "The directory of the package which is being built.") 113 | .option("--target-directory ", "The directory where the binaries will end.") 114 | .option("--staging-directory ", "The directory where intermediate files will end up.") 115 | .action(() => { 116 | commandOptions.command.type = "build" 117 | }) 118 | 119 | const deprecatedOpts: DeprecatedGlobalOptions = { 120 | all: false, 121 | nativeonly: false, 122 | osonly: false, 123 | devOsOnly: false, 124 | namedConfigs: undefined, 125 | } 126 | 127 | // For backward compatibility, add the old flags as options to the root command 128 | program 129 | .command("all", { hidden: true }) 130 | .description("(deprecated) Build all configurations. Use `build --configs named-all` instead.") 131 | .action(() => { 132 | deprecatedOpts.all = true 133 | commandOptions.command.type = "build" 134 | }) 135 | program 136 | .command("nativeonly", { hidden: true }) 137 | .description("(deprecated) Building only for the current runtime. Use `build` instead.") 138 | .action(() => { 139 | deprecatedOpts.nativeonly = true 140 | commandOptions.command.type = "build" 141 | }) 142 | program 143 | .command("osonly", { hidden: true }) 144 | .description("(deprecated) Building only for the current OS. Use `build --configs named-os` instead.") 145 | .action(() => { 146 | deprecatedOpts.osonly = true 147 | commandOptions.command.type = "build" 148 | }) 149 | program 150 | .command("dev-os-only", { 151 | hidden: true, 152 | }) 153 | .description("(deprecated) Build only dev OS configurations. Use `build --configs named-os-dev` instead.") 154 | .action(() => { 155 | deprecatedOpts.devOsOnly = true 156 | commandOptions.command.type = "build" 157 | }) 158 | program 159 | .command("named-configs ", { 160 | hidden: true, 161 | }) 162 | .description("(deprecated) Build only named configurations. Use `build --configs ` instead") 163 | .action((configs: string[]) => { 164 | deprecatedOpts.namedConfigs = configs 165 | commandOptions.command.type = "build" 166 | }) 167 | 168 | program.parse(args) 169 | 170 | // get the global options 171 | const opts: Options & DeprecatedGlobalOptions = { 172 | ...commandOptions, 173 | ...deprecatedOpts, 174 | ...program.opts(), 175 | } 176 | 177 | logger.setLevel(opts.logger) 178 | 179 | const debugOpts = () => { 180 | logger.debug("args", JSON.stringify(opts, null, 2)) 181 | } 182 | 183 | // Handle build command 184 | const buildOpts = buildCommand.opts() 185 | if (opts.command.type === "build") { 186 | addLegacyOptions(buildOpts, opts) 187 | 188 | debugOpts() 189 | return { 190 | command: { 191 | type: "build", 192 | options: buildOpts, 193 | }, 194 | help: opts.help, 195 | logger: opts.logger, 196 | } 197 | } 198 | 199 | debugOpts() 200 | return opts 201 | } 202 | 203 | /** 204 | * Parse the legacy options and add them to the build options 205 | */ 206 | function addLegacyOptions(buildOptions: BuildCommandOptions, opts: DeprecatedGlobalOptions) { 207 | if (opts.namedConfigs !== undefined) { 208 | // Handle legacy named-configs option 209 | buildOptions.configs = opts.namedConfigs.flatMap((c) => c.split(",")) 210 | 211 | logger.warn("The --named-configs option is deprecated. Use --configs instead.") 212 | } 213 | 214 | // Handle legacy mode flags by converting them to appropriate configs 215 | if (opts.nativeonly) { 216 | buildOptions.configs.push("release") 217 | 218 | logger.warn("The --nativeonly option is deprecated. Use --configs release instead.") 219 | } 220 | 221 | if (opts.osonly) { 222 | buildOptions.configs.push("named-os") 223 | 224 | logger.warn("The --osonly option is deprecated. Use --configs named-os instead.") 225 | } 226 | 227 | if (opts.devOsOnly) { 228 | buildOptions.configs.push("named-os-dev") 229 | 230 | logger.warn("The --dev-os-only option is deprecated. Use --configs named-os-dev instead.") 231 | } 232 | 233 | if (opts.all) { 234 | buildOptions.configs.push("named-all") 235 | 236 | logger.warn("The --all option is deprecated. Use --configs named-all instead.") 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/argumentBuilder.ts: -------------------------------------------------------------------------------- 1 | import { join, resolve } from "path" 2 | import type { BuildConfiguration } from "./config-types.d" 3 | import { getNodeApiInclude } from "./nodeAPIInclude/index.js" 4 | import type { RuntimeDistribution } from "./runtimeDistribution.js" 5 | import { getPathsForConfig } from "./urlRegistry.js" 6 | import { logger } from "./utils/logger.js" 7 | import { setupMSVCDevCmd } from "./vcvarsall.js" 8 | 9 | export class ArgumentBuilder { 10 | constructor( 11 | private config: BuildConfiguration, 12 | private rtd: RuntimeDistribution, 13 | ) {} 14 | 15 | async configureCommand(): Promise<[string, string[]]> { 16 | const args = [this.config.packageDirectory, "--no-warn-unused-cli"] 17 | const defines = await this.buildDefines() 18 | for (const [name, value] of defines) { 19 | args.push(`-D${name}=${value}`) 20 | } 21 | if (this.config.generatorToUse !== "native") { 22 | args.push("-G", this.config.generatorToUse) 23 | if (this.config.generatorFlags !== undefined) { 24 | args.push(...this.config.generatorFlags) 25 | } 26 | } 27 | return [this.config.cmakeToUse, args] 28 | } 29 | 30 | buildCommand(stagingDir: string): [string, string[]] { 31 | return [this.config.cmakeToUse, ["--build", stagingDir, "--config", this.config.buildType, "--parallel"]] 32 | } 33 | 34 | async buildDefines(): Promise<[string, string][]> { 35 | const pathConfig = getPathsForConfig(this.config) 36 | const retVal: [string, string][] = [] 37 | retVal.push(["CMAKE_BUILD_TYPE", this.config.buildType]) 38 | 39 | if (this.config.toolchainFile !== undefined) { 40 | retVal.push(["CMAKE_TOOLCHAIN_FILE", resolve(this.config.toolchainFile)]) 41 | } 42 | 43 | // Trust me, I'm an engineer? 44 | if (this.config.os === "win32") { 45 | const libs = this.rtd.winLibs() 46 | if (libs.length) { 47 | retVal.push(["CMAKE_JS_LIB", libs.join(";")]) 48 | } 49 | } else if (this.config.os === "darwin") { 50 | // Darwin can't link against node, so skip it. 51 | retVal.push(["CMAKE_JS_CXX_FLAGS", "-undefined dynamic_lookup"]) 52 | } 53 | 54 | // Search headers, modern node versions have those in /include/node 55 | const includes: string[] = [] 56 | if (pathConfig.headerOnly) { 57 | includes.push(join(this.rtd.internalPath(), "/include/node")) 58 | } else { 59 | // ancient ones need v8 includes, too 60 | includes.push( 61 | join(this.rtd.internalPath(), "/src"), 62 | join(this.rtd.internalPath(), "/deps/v8/include"), 63 | join(this.rtd.internalPath(), "/deps/uv/include"), 64 | ) 65 | } 66 | 67 | // Search nodeAPI if installed and required 68 | if (this.config.nodeAPI?.includes("nan") === true) { 69 | logger.warn( 70 | `Specified nodeAPI ${this.config.nodeAPI} seems to be nan - The usage of nan is discouraged due to subtle and hard-to-fix ABI issues! Consider using node-addon-api / N-API instead!`, 71 | ) 72 | } 73 | if (this.config.nodeAPI === undefined) { 74 | logger.warn( 75 | 'NodeAPI was not specified. The default changed from "nan" to "node-addon-api" in v0.3.0! Please make sure this is intended.', 76 | ) 77 | } 78 | const nodeApiInclude = await getNodeApiInclude( 79 | this.config.packageDirectory, 80 | this.config.nodeAPI ?? "node-addon-api", 81 | ) 82 | if (this.config.nodeAPI !== undefined && nodeApiInclude === null) { 83 | logger.warn(`NodeAPI was specified, but module "${this.config.nodeAPI}" could not be found!`) 84 | } 85 | if (nodeApiInclude !== null) { 86 | includes.push(nodeApiInclude) 87 | } 88 | // Pass includes to cmake 89 | retVal.push(["CMAKE_JS_INC", includes.join(";")]) 90 | 91 | retVal.push( 92 | ["NODE_RUNTIME", this.config.runtime], 93 | ["NODE_ARCH", this.config.arch], 94 | ["NODE_PLATFORM", this.config.os], 95 | ["NODE_RUNTIMEVERSION", this.config.runtimeVersion], 96 | ["NODE_ABI_VERSION", `${this.rtd.abi()}`], 97 | ) 98 | 99 | // push additional overrides 100 | retVal.push(["CMAKE_JS_DEFINES", this.config.additionalDefines.join(";")]) 101 | 102 | // Pass the architecture to cmake if the host architecture is not the same as the target architecture 103 | if (this.config.cross === true) { 104 | const cmakeArch = getCMakeArchitecture(this.config.arch, this.config.os) 105 | const cmakeOs = getCMakeSystemName(this.config.os) 106 | logger.info(`Cross-compiling for ${cmakeOs}/${cmakeArch}`) 107 | 108 | retVal.push(["CMAKE_SYSTEM_PROCESSOR", cmakeArch], ["CMAKE_SYSTEM_NAME", cmakeOs]) 109 | 110 | if (cmakeOs === "Darwin") { 111 | retVal.push(["CMAKE_OSX_ARCHITECTURES", cmakeArch]) 112 | } 113 | 114 | if (this.config.os === "win32") { 115 | const isVisualStudio = this.config.generatorToUse.includes("Visual Studio") 116 | try { 117 | setupMSVCDevCmd(this.config.arch) 118 | if (isVisualStudio) { 119 | logger.debug("Removing the generator flags in favour of the vcvarsall.bat script") 120 | this.config.generatorFlags = undefined 121 | } 122 | } catch (e) { 123 | logger.warn(`Failed to setup MSVC variables for ${this.config.arch}: ${e}.`) 124 | if (isVisualStudio) { 125 | logger.debug("Setting the CMake generator platform to the target architecture") 126 | // set the CMake generator platform to the target architecture 127 | retVal.push(["CMAKE_GENERATOR_PLATFORM", cmakeArch]) 128 | } 129 | } 130 | } 131 | } 132 | 133 | if (this.config.CMakeOptions.length !== 0) { 134 | for (const j of this.config.CMakeOptions) { 135 | retVal.push([j.name, j.value.replace(/\$ROOT\$/g, resolve(this.config.packageDirectory))]) 136 | } 137 | } 138 | return retVal 139 | } 140 | } 141 | 142 | /** 143 | * Get the architecture for cmake 144 | * @param arch - The architecture of the target 145 | * @param os - The operating system of the target 146 | * @returns The architecture for cmake 147 | * 148 | * @note Based on https://stackoverflow.com/a/70498851/7910299 149 | */ 150 | export function getCMakeArchitecture(arch: NodeJS.Architecture, os: NodeJS.Platform) { 151 | return os in cmakeArchMap && arch in cmakeArchMap[os] 152 | ? cmakeArchMap[os][arch] 153 | : os === "win32" 154 | ? arch.toUpperCase() 155 | : arch 156 | } 157 | 158 | const cmakeArchMap: Record> = { 159 | win32: { 160 | arm64: "arm64", 161 | x64: "AMD64", 162 | ia32: "X86", 163 | }, 164 | darwin: { 165 | arm64: "arm64", 166 | x64: "x86_64", 167 | ppc64: "powerpc64", 168 | ppc: "powerpc", 169 | }, 170 | linux: { 171 | arm64: "aarch64", 172 | x64: "x86_64", 173 | ia32: "i386", 174 | arm: "arm", 175 | loong64: "loong64", 176 | mips: "mips", 177 | mipsel: "mipsel", 178 | ppc64: "ppc64", 179 | }, 180 | } as const 181 | 182 | /** 183 | * Get the system name for cmake 184 | * @param os - The operating system of the target 185 | * @returns The system name for cmake 186 | * 187 | * @note Based on https://cmake.org/cmake/help/latest/variable/CMAKE_SYSTEM_NAME.html 188 | */ 189 | function getCMakeSystemName(os: string) { 190 | return os in cmakeSystemNameMap ? cmakeSystemNameMap[os as NodeJS.Platform] : os.toUpperCase() 191 | } 192 | 193 | const cmakeSystemNameMap = { 194 | win32: "Windows", 195 | darwin: "Darwin", 196 | linux: "Linux", 197 | android: "Android", 198 | openbsd: "OpenBSD", 199 | freebsd: "FreeBSD", 200 | netbsd: "NetBSD", 201 | cygwin: "CYGWIN", 202 | aix: "AIX", 203 | sunos: "SunOS", 204 | haiku: "Haiku", 205 | } as const 206 | -------------------------------------------------------------------------------- /src/build.ts: -------------------------------------------------------------------------------- 1 | import { join, relative, resolve } from "path" 2 | import { copy, ensureDir, pathExists, readFile, remove, writeFile } from "fs-extra" 3 | import { ArgumentBuilder } from "./argumentBuilder.js" 4 | import type { BuildConfiguration, Options } from "./config-types.d" 5 | import { getConfigFile, parseBuildConfigs } from "./config.js" 6 | import { applyOverrides } from "./override.js" 7 | import { RuntimeDistribution } from "./runtimeDistribution.js" 8 | import { runProgram } from "./utils/exec.js" 9 | import { logger } from "./utils/logger.js" 10 | import { retry } from "./utils/retry.js" 11 | 12 | /** 13 | * Build the project via cmake-ts 14 | * 15 | * @param opts - The options to use for the build 16 | * @returns The configurations that were built or null if there was an error 17 | */ 18 | export async function build(opts: Options): Promise { 19 | if (opts.command.type === "error") { 20 | logger.error("The given options are invalid") 21 | return null 22 | } 23 | if (opts.command.type === "none") { 24 | return [] 25 | } 26 | if (opts.help) { 27 | return [] 28 | } 29 | 30 | const packageJsonPath = resolve(join(process.cwd(), "package.json")) 31 | const configFile = await getConfigFile(packageJsonPath) 32 | if (configFile instanceof Error) { 33 | logger.error(configFile) 34 | return null 35 | } 36 | 37 | // set the missing options to their default value 38 | const configsToBuild = await parseBuildConfigs(opts, configFile) 39 | if (configsToBuild === null) { 40 | logger.error("No configs to build") 41 | return null 42 | } 43 | 44 | for (const config of configsToBuild) { 45 | try { 46 | // eslint-disable-next-line no-await-in-loop 47 | await buildConfig(config, opts) 48 | } catch (err) { 49 | logger.error("Error building config", config.name, err) 50 | return null 51 | } 52 | } 53 | 54 | return configsToBuild 55 | } 56 | 57 | export async function buildConfig(config: BuildConfiguration, opts: Options) { 58 | logger.debug("config", JSON.stringify(config, null, 2)) 59 | 60 | config.targetDirectory = resolve(join(config.packageDirectory, config.targetDirectory)) 61 | logger.debug("running in", config.packageDirectory, "command", opts) 62 | 63 | logger.debug("> Setting up staging directory... ") 64 | config.stagingDirectory = resolve(join(config.packageDirectory, config.stagingDirectory)) 65 | const stagingExists = await pathExists(config.stagingDirectory) 66 | if (stagingExists) { 67 | await retry(() => remove(config.stagingDirectory)) 68 | } 69 | await ensureDir(config.stagingDirectory) 70 | 71 | const dist = new RuntimeDistribution(config) 72 | 73 | // Download files 74 | logger.debug("> Distribution File Download... ") 75 | await dist.ensureDownloaded() 76 | 77 | logger.debug("> Determining ABI... ") 78 | await dist.determineABI() 79 | 80 | logger.debug("> Building directories... ") 81 | 82 | const subDirectory = join( 83 | config.os, 84 | config.arch, 85 | config.runtime, 86 | `${config.libc}-${dist.abi()}-${config.buildType}`, 87 | config.addonSubdirectory, 88 | ) 89 | 90 | const stagingDir = resolve(join(config.stagingDirectory, subDirectory)) 91 | const targetDir = resolve(join(config.targetDirectory, subDirectory)) 92 | 93 | applyOverrides(config) 94 | 95 | const argBuilder = new ArgumentBuilder(config, dist) 96 | const [configureCmd, configureArgs] = await argBuilder.configureCommand() 97 | const [buildCmd, buildArgs] = argBuilder.buildCommand(stagingDir) 98 | 99 | logger.info(getConfigInfo(config, dist)) 100 | 101 | // Invoke CMake 102 | logger.debug(`> Configure: ${configureCmd} ${configureArgs.map((a) => `"${a}"`).join(" ")} in ${stagingDir}`) 103 | 104 | await ensureDir(stagingDir) 105 | await runProgram(configureCmd, configureArgs, stagingDir) 106 | 107 | // Actually build the software 108 | logger.debug(`> Build ${config.generatorBinary} ${buildArgs.map((a) => `"${a}"`).join(" ")} in ${stagingDir}`) 109 | 110 | await runProgram(buildCmd, buildArgs, stagingDir) 111 | 112 | // Copy back the previously built binary 113 | logger.debug(`> Copying ${config.projectName}.node to ${targetDir}`) 114 | 115 | const addonPath = join(targetDir, `${config.projectName}.node`) 116 | const sourceAddonPath = config.generatorToUse.includes("Visual Studio") 117 | ? join(stagingDir, config.buildType, `${config.projectName}.node`) 118 | : join(stagingDir, `${config.projectName}.node`) 119 | await ensureDir(targetDir) 120 | await retry(() => copy(sourceAddonPath, addonPath)) 121 | 122 | logger.debug("Adding the built config to the manifest file...") 123 | 124 | // read the manifest if it exists 125 | const manifestPath = join(config.targetDirectory, "manifest.json") 126 | let manifest: Record = {} 127 | if (await pathExists(manifestPath)) { 128 | const manifestContent = await readFile(manifestPath, "utf-8") 129 | manifest = JSON.parse(manifestContent) 130 | } 131 | // add the new entry to the manifest 132 | manifest[serializeConfig(config, config.packageDirectory)] = relative(config.targetDirectory, addonPath) 133 | const manifestSerialized = JSON.stringify(manifest, null, 2) 134 | await retry(() => writeFile(manifestPath, manifestSerialized)) 135 | } 136 | 137 | function getConfigInfo(config: BuildConfiguration, dist: RuntimeDistribution): string { 138 | return `${config.name} ${config.os} ${config.arch} ${config.libc} ${config.runtime} ${config.runtimeVersion} ABI ${dist.abi()} 139 | ${config.generatorToUse} ${config.generatorFlags?.join(" ") ?? ""} ${config.buildType} ${config.toolchainFile !== undefined ? `toolchain ${config.toolchainFile}` : ""} 140 | ${config.CMakeOptions.join(" ")}` 141 | } 142 | 143 | function serializeConfig(config: BuildConfiguration, rootDir: string) { 144 | return JSON.stringify( 145 | config, 146 | // replace absolute paths with relative paths in the values 147 | (_key, value) => { 148 | if (typeof value === "string" && value.startsWith(rootDir)) { 149 | return relative(rootDir, value) 150 | } 151 | return value 152 | }, 153 | ) 154 | } 155 | -------------------------------------------------------------------------------- /src/config-types.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The options of cmake-ts that includes the command to run and the global options 3 | */ 4 | export type Options = { 5 | /** 6 | * The command to run 7 | */ 8 | command: Command 9 | } & GlobalOptions 10 | 11 | /** 12 | * The build command is a command that builds the project 13 | */ 14 | export type BuildCommand = { 15 | type: "build" 16 | options: BuildCommandOptions 17 | } 18 | 19 | /** 20 | * The build config is a config that describes the build to run for cmake-ts 21 | */ 22 | export type BuildCommandOptions = { 23 | /** 24 | * Named config(s) to build, which could be from default configs or the ones defined in the config file (package.json) 25 | * 26 | * If no config is provided, it will build for the current runtime on the current system with the Release build type 27 | * 28 | * The default configs are combinations of ``, ``, ``, and ``. 29 | * 30 | * - ``: the runtime to use 31 | * 32 | * e.g.: `node`, `electron`, `iojs` 33 | * 34 | * - ``: the cmake build type (optimization level) 35 | * 36 | * e.g.: `debug`, `release`, `relwithdebinfo`, `minsizerel` 37 | * 38 | * - ``: the target platform 39 | * 40 | * e.g.: `win32`, `linux`, `darwin`, `aix`, `android`, `freebsd`, `haiku`, `openbsd`, `sunos`, `cygwin`, `netbsd` 41 | * 42 | * - ``: the target architecture 43 | * 44 | * e.g.: `x64`, `arm64`, `ia32`, `arm`, `loong64`, `mips`, `mipsel`, `ppc`, `ppc64`, `riscv64`, `s390`, `s390x` 45 | * 46 | * Any combination of ``, ``, ``, and `` is valid. Some examples: 47 | * 48 | * - `release` 49 | * - `debug` 50 | * - `relwithdebinfo` 51 | * - `node-release` 52 | * - `node-debug` 53 | * - `electron-release` 54 | * - `electron-debug` 55 | * - `win32-x64` 56 | * - `win32-x64-debug` 57 | * - `linux-x64-debug` 58 | * - `linux-x64-node-debug` 59 | * - `linux-x64-electron-release` 60 | * - `darwin-x64-node-release` 61 | * - `darwin-arm64-node-release` 62 | * - `darwin-arm64-electron-relwithdebinfo` 63 | * 64 | * You can also define your own configs in the config file (package.json). 65 | * 66 | * - ``: the name of the config 67 | * 68 | * e.g.: `my-config` 69 | * 70 | * The configs can also be in format of `named-`, which builds the configs that match the property. 71 | * 72 | * - `named-os`: build all the configs in the config file that have the same OS 73 | * - `named-os-dev`: build all the configs in the config file that have the same OS and `dev` is true 74 | * - `named-all`: build all the configs in the config file 75 | * 76 | * 77 | * The configs can be combined with `,` or multiple `--configs` flags. They will be merged together. 78 | */ 79 | configs: string[] 80 | 81 | // Path options that override the default values 82 | 83 | /** project name */ 84 | projectName?: string 85 | /** The subdirectory of the package which is being built. */ 86 | addonSubdirectory?: string 87 | /** directory of the package which is being built */ 88 | packageDirectory?: string 89 | /** directory where the binaries will end */ 90 | targetDirectory?: string 91 | /** directory where intermediate files will end up */ 92 | stagingDirectory?: string 93 | 94 | /** Show help */ 95 | help: boolean 96 | } 97 | 98 | /** 99 | * The help command is a command that shows the help message 100 | */ 101 | export type HelpCommand = { 102 | type: "help" 103 | } 104 | 105 | /** 106 | * A command is an object that describes the command to run for cmake-ts 107 | */ 108 | export type Command = BuildCommand | HelpCommand | { type: "error" | "none" } 109 | 110 | /** 111 | * Global options are options that are available for all commands provided by the user as --option 112 | */ 113 | export type GlobalOptions = { 114 | /** 115 | * Set the log level 116 | * Default: "info" 117 | */ 118 | logger: "trace" | "debug" | "info" | "warn" | "error" | "off" 119 | 120 | /** Show help */ 121 | help: boolean 122 | } 123 | 124 | /** 125 | * Global options are options that are available for all commands provided by the user as --option 126 | * @deprecated Use the alternative options instead 127 | */ 128 | export type DeprecatedGlobalOptions = { 129 | /** Build all configurations 130 | * @deprecated Use `build --config named-all` instead 131 | */ 132 | all: boolean 133 | 134 | /** Build only native configurations 135 | * @deprecated Use `build` instead 136 | */ 137 | nativeonly: boolean 138 | 139 | /** Build only OS configurations 140 | * @deprecated Use `build --config named-os` instead 141 | */ 142 | osonly: boolean 143 | 144 | /** Build only dev OS configurations 145 | * @deprecated Use `build --config named-os-dev` instead 146 | */ 147 | devOsOnly: boolean 148 | 149 | /** Build only named configurations 150 | * @deprecated Use `build --config ` instead 151 | */ 152 | namedConfigs?: string[] 153 | } 154 | 155 | export type ArrayOrSingle = T | T[] 156 | 157 | export type BuildConfiguration = { 158 | /** The name of the build configuration. */ 159 | name: string 160 | 161 | // Platform 162 | 163 | /** The operating system that is used by the runtime (e.g. win32, darwin, linux, etc.) */ 164 | os: typeof process.platform 165 | /** The architecture that is used by the runtime (e.g. x64, arm64, etc.) */ 166 | arch: typeof process.arch 167 | /** Whether the build is cross-compiling. */ 168 | cross?: boolean 169 | 170 | // Runtime 171 | 172 | /** The runtime that is used by the runtime (e.g. node, electron, iojs, etc.) */ 173 | runtime: "node" | "electron" | "iojs" 174 | /** node abstraction API to use (e.g. nan or node-addon-api) */ 175 | nodeAPI?: string 176 | /** The version of the runtime that is used by the runtime. */ 177 | runtimeVersion: string 178 | 179 | // ABI/libc 180 | 181 | /** The ABI number that is used by the runtime. */ 182 | abi?: number 183 | /** The libc that is used by the runtime. */ 184 | libc?: string 185 | 186 | // Optimization levels 187 | 188 | /** Release, Debug, or RelWithDebInfo build */ 189 | buildType: "Release" | "Debug" | "RelWithDebInfo" | "MinSizeRel" 190 | /** Whether the build is a development build. */ 191 | dev: boolean 192 | 193 | // Paths 194 | 195 | /** The subdirectory of the package which is being built. */ 196 | addonSubdirectory: string 197 | /** directory of the package which is being built */ 198 | packageDirectory: string 199 | /** name of the built node addon */ 200 | projectName: string 201 | /** directory where the binaries will end */ 202 | targetDirectory: string 203 | /** directory where intermediate files will end up */ 204 | stagingDirectory: string 205 | 206 | // Cmake paths 207 | 208 | /** which cmake instance to use */ 209 | cmakeToUse: string 210 | /** cmake generator binary. */ 211 | generatorBinary?: string 212 | 213 | // Cmake options 214 | 215 | /** The toolchain file to use. */ 216 | toolchainFile?: string 217 | /** (alias) cmake options */ 218 | CMakeOptions: { name: string; value: string }[] 219 | /** cmake options */ 220 | cmakeOptions?: { name: string; value: string }[] 221 | /** list of additional definitions to fixup node quirks for some specific versions */ 222 | additionalDefines: string[] 223 | /** which cmake generator to use */ 224 | generatorToUse: string 225 | /** which cmake generator flags to use */ 226 | generatorFlags?: string[] 227 | } 228 | 229 | export type BuildConfigurations = { 230 | /** A list of configurations to build */ 231 | configurations: Partial[] 232 | 233 | /** global options applied to all configurations in case they are missing */ 234 | } & Partial 235 | 236 | export type CompleteBuildConfigurations = { 237 | /** A list of configurations to build */ 238 | configurations: BuildConfiguration[] 239 | } 240 | 241 | export type OverrideConfig = { 242 | match: { 243 | os?: ArrayOrSingle 244 | arch?: ArrayOrSingle 245 | runtime?: ArrayOrSingle 246 | runtimeVersion?: ArrayOrSingle 247 | } 248 | addDefines: ArrayOrSingle 249 | } 250 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { readJson } from "fs-extra" 2 | import which from "which" 3 | import type { BuildCommandOptions, BuildConfiguration, BuildConfigurations, Options } from "./config-types.d" 4 | import { getCmakeGenerator } from "./generator.js" 5 | import { logger } from "./lib.js" 6 | 7 | export async function parseBuildConfigs( 8 | opts: Options, 9 | configFile: Partial, 10 | ): Promise { 11 | if (opts.command.type !== "build") { 12 | return null 13 | } 14 | 15 | const buildOptions = opts.command.options 16 | const givenConfigNames = new Set(buildOptions.configs) 17 | 18 | const configsToBuild: BuildConfiguration[] = [] 19 | 20 | // if no named configs are provided, build for the current runtime/system 21 | if (givenConfigNames.size === 0) { 22 | configsToBuild.push(await getBuildConfig(buildOptions, {}, configFile)) 23 | return configsToBuild 24 | } 25 | 26 | // check if the given config names are a subset of the config names in the config file 27 | for (const partialConfig of configFile.configurations ?? []) { 28 | /* eslint-disable no-await-in-loop */ 29 | const config = await getBuildConfig(buildOptions, partialConfig, configFile) 30 | 31 | if ( 32 | givenConfigNames.has(config.name) || 33 | givenConfigNames.has("named-all") || 34 | (givenConfigNames.has("named-os") && config.os === process.platform) || 35 | (givenConfigNames.has("named-os-dev") && config.os === process.platform && config.dev) 36 | ) { 37 | configsToBuild.push(config) 38 | givenConfigNames.delete(config.name) 39 | } 40 | } 41 | 42 | // parse the remaining config names to extract the runtime, build type, and system 43 | for (const configName of givenConfigNames) { 44 | const config = parseBuiltInConfigs(configName) 45 | configsToBuild.push(await getBuildConfig(buildOptions, config, configFile)) 46 | } 47 | 48 | return configsToBuild 49 | } 50 | 51 | /** 52 | * Add the missing fields to the given build configuration. 53 | */ 54 | export async function getBuildConfig( 55 | buildOptions: BuildCommandOptions, 56 | config: Partial, 57 | globalConfig: Partial, 58 | ) { 59 | /* eslint-disable require-atomic-updates */ 60 | 61 | config.name ??= globalConfig.name ?? "" 62 | 63 | // Platform 64 | 65 | config.cross = detectCrossCompilation(globalConfig, config) 66 | 67 | config.os ??= globalConfig.os ?? process.platform 68 | config.arch ??= globalConfig.arch ?? process.arch 69 | 70 | // Runtime 71 | 72 | config.runtime ??= globalConfig.runtime ?? "node" 73 | config.nodeAPI ??= globalConfig.nodeAPI ?? "node-addon-api" 74 | config.runtimeVersion ??= 75 | globalConfig.runtimeVersion ?? (config.runtime === "node" ? process.versions.node : undefined) 76 | 77 | // Optimization levels 78 | 79 | config.buildType ??= globalConfig.buildType ?? "Release" 80 | config.dev ??= globalConfig.dev ?? false 81 | 82 | // Paths 83 | config.addonSubdirectory ??= buildOptions.addonSubdirectory ?? globalConfig.addonSubdirectory ?? "" 84 | config.packageDirectory ??= buildOptions.packageDirectory ?? globalConfig.packageDirectory ?? process.cwd() 85 | config.projectName ??= buildOptions.projectName ?? globalConfig.projectName ?? "addon" 86 | config.targetDirectory ??= buildOptions.targetDirectory ?? globalConfig.targetDirectory ?? "build" 87 | config.stagingDirectory ??= buildOptions.stagingDirectory ?? globalConfig.stagingDirectory ?? "staging" 88 | 89 | // Cmake options 90 | config.CMakeOptions = [ 91 | ...(config.CMakeOptions ?? []), 92 | ...(globalConfig.CMakeOptions ?? []), 93 | // alias 94 | ...(config.cmakeOptions ?? []), 95 | ...(globalConfig.cmakeOptions ?? []), 96 | ] 97 | 98 | config.additionalDefines ??= globalConfig.additionalDefines ?? [] 99 | 100 | config.cmakeToUse ??= globalConfig.cmakeToUse ?? (await which("cmake", { nothrow: true })) ?? "cmake" 101 | 102 | const { generator, generatorFlags, binary } = await getCmakeGenerator(config.cmakeToUse, config.os, config.arch) 103 | config.generatorToUse ??= globalConfig.generatorToUse ?? generator 104 | config.generatorFlags ??= globalConfig.generatorFlags ?? generatorFlags 105 | config.generatorBinary ??= globalConfig.generatorBinary ?? binary 106 | 107 | return config as BuildConfiguration 108 | } 109 | 110 | export function detectCrossCompilation( 111 | globalConfig: Partial, 112 | config: Partial, 113 | ) { 114 | if (globalConfig.cross === true) { 115 | logger.debug("Cross compilation detected: globalConfig.cross is true") 116 | return true 117 | } 118 | if (config.os !== undefined && platforms.has(config.os as NodeJS.Platform) && config.os !== process.platform) { 119 | // if the config os is set, check if the current os is different from the config os 120 | logger.debug( 121 | `Cross compilation detected: config.os (${config.os}) differs from process.platform (${process.platform})`, 122 | ) 123 | return true 124 | } 125 | if ( 126 | config.arch !== undefined && 127 | architectures.has(config.arch as NodeJS.Architecture) && 128 | config.arch !== process.arch 129 | ) { 130 | // if the config arch is set, check if the current arch is different from the config arch 131 | logger.debug(`Cross compilation detected: config.arch (${config.arch}) differs from process.arch (${process.arch})`) 132 | return true 133 | } 134 | if ( 135 | config.os === undefined && 136 | process.env.npm_config_target_os !== undefined && 137 | platforms.has(process.env.npm_config_target_os as NodeJS.Platform) && 138 | process.env.npm_config_target_os !== process.platform 139 | ) { 140 | // if the target os is set via npm_config_target_os, check if it is different from the config os 141 | logger.debug( 142 | `Cross compilation detected: npm_config_target_os (${process.env.npm_config_target_os}) differs from process.platform (${process.platform})`, 143 | ) 144 | config.os = process.env.npm_config_target_os as NodeJS.Platform 145 | return true 146 | } 147 | if ( 148 | config.arch === undefined && 149 | process.env.npm_config_target_arch !== undefined && 150 | architectures.has(process.env.npm_config_target_arch as NodeJS.Architecture) && 151 | process.env.npm_config_target_arch !== process.arch 152 | ) { 153 | // if the target arch is set via npm_config_target_arch, check if it is different from the config arch 154 | logger.debug( 155 | `Cross compilation detected: npm_config_target_arch (${process.env.npm_config_target_arch}) differs from process.arch (${process.arch})`, 156 | ) 157 | config.arch = process.env.npm_config_target_arch as NodeJS.Architecture 158 | return true 159 | } 160 | return false 161 | } 162 | 163 | export function parseBuiltInConfigs(configName: string) { 164 | const parts = configName.split("-") 165 | 166 | let cross = false 167 | let os: BuildConfiguration["os"] | undefined 168 | let arch: BuildConfiguration["arch"] | undefined 169 | let runtime: BuildConfiguration["runtime"] | undefined 170 | let buildType: BuildConfiguration["buildType"] | undefined 171 | 172 | for (const part of parts) { 173 | if (platforms.has(part as BuildConfiguration["os"])) { 174 | os = part as BuildConfiguration["os"] 175 | } else if (architectures.has(part as BuildConfiguration["arch"])) { 176 | arch = part as BuildConfiguration["arch"] 177 | } else if (runtimes.has(part as BuildConfiguration["runtime"])) { 178 | runtime = part as BuildConfiguration["runtime"] 179 | } else if (buildTypes.has(part as BuildConfiguration["buildType"])) { 180 | buildType = buildTypes.get(part as BuildConfiguration["buildType"]) 181 | } else if (part === "cross") { 182 | cross = true 183 | } else { 184 | throw new Error(`Invalid config part in ${configName}: ${part}`) 185 | } 186 | } 187 | 188 | return { os, arch, runtime, buildType, cross } 189 | } 190 | 191 | const platforms = new Set([ 192 | "aix", 193 | "android", 194 | "darwin", 195 | "freebsd", 196 | "haiku", 197 | "linux", 198 | "openbsd", 199 | "sunos", 200 | "win32", 201 | "cygwin", 202 | "netbsd", 203 | ]) 204 | 205 | const architectures = new Set([ 206 | "arm", 207 | "arm64", 208 | "ia32", 209 | "loong64", 210 | "mips", 211 | "mipsel", 212 | "ppc", 213 | "ppc64", 214 | "riscv64", 215 | "s390", 216 | "s390x", 217 | "x64", 218 | ]) 219 | 220 | const buildTypes = new Map([ 221 | ["release", "Release"], 222 | ["Release", "Release"], 223 | ["debug", "Debug"], 224 | ["Debug", "Debug"], 225 | ["relwithdebinfo", "RelWithDebInfo"], 226 | ["RelWithDebInfo", "RelWithDebInfo"], 227 | ["minsizerel", "MinSizeRel"], 228 | ["MinSizeRel", "MinSizeRel"], 229 | ]) 230 | 231 | const runtimes = new Set(["node", "electron", "iojs"]) 232 | 233 | export async function getConfigFile(packageJsonPath: string) { 234 | let packJson: { "cmake-ts": Partial | undefined } & Record 235 | try { 236 | packJson = await readJson(packageJsonPath) 237 | } catch (err) { 238 | logger.warn(`Failed to load package.json at ${packageJsonPath}: ${err}. Using defaults.`) 239 | return {} 240 | } 241 | 242 | const configFile = packJson["cmake-ts"] 243 | if (configFile === undefined) { 244 | logger.debug("Package.json does not have cmake-ts key defined. Using defaults.") 245 | return {} 246 | } 247 | 248 | return configFile 249 | } 250 | -------------------------------------------------------------------------------- /src/deps/aws-sdk-client-s3.ts: -------------------------------------------------------------------------------- 1 | export const GetObjectCommand = undefined 2 | export const HeadObjectCommand = undefined 3 | -------------------------------------------------------------------------------- /src/deps/mkdirp.ts: -------------------------------------------------------------------------------- 1 | import { mkdirp, mkdirpSync } from "fs-extra/lib/mkdirs/index.js" 2 | 3 | export default mkdirp 4 | export { mkdirpSync as sync } 5 | -------------------------------------------------------------------------------- /src/deps/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "tar/lib/extract.js" { 2 | export { extract as default } from "tar" 3 | } 4 | 5 | declare module "fs-extra/lib/mkdirs/index.js" { 6 | export { mkdirp, mkdirpSync } from "fs-extra" 7 | } 8 | 9 | declare module "resolve/async.js" { 10 | import resolve from "resolve" 11 | export default resolve 12 | } 13 | -------------------------------------------------------------------------------- /src/generator.ts: -------------------------------------------------------------------------------- 1 | import memoizee from "memoizee" 2 | import which from "which" 3 | import { getCMakeArchitecture } from "./argumentBuilder.js" 4 | import { execCapture } from "./utils/exec.js" 5 | import { logger } from "./utils/logger.js" 6 | 7 | export const getCmakeGenerator = memoizee( 8 | async ( 9 | cmake: string, 10 | os: NodeJS.Platform, 11 | arch: NodeJS.Architecture, 12 | ): Promise<{ 13 | generator?: string 14 | generatorFlags?: string[] 15 | binary?: string 16 | }> => { 17 | // use ninja if available 18 | const ninja = await which("ninja", { nothrow: true }) 19 | if (ninja !== null) { 20 | logger.debug(`Using generator: Ninja for ${os} ${arch}`) 21 | return { 22 | generator: "Ninja", 23 | binary: ninja, 24 | } 25 | } 26 | 27 | // find the MSVC generator on Windows and see if an arch switch is needed 28 | if (os === "win32") { 29 | try { 30 | const cmakeG = await execCapture(`"${cmake}" -G`) 31 | const hasCR = cmakeG.includes("\r\n") 32 | const output = hasCR ? cmakeG.split("\r\n") : cmakeG.split("\n") 33 | 34 | // Find the first Visual Studio generator (marked with * or not) 35 | let matchedGeneratorLine: RegExpMatchArray | undefined = undefined 36 | for (const line of output) { 37 | const match = line.match(/^\s*(?:\* )?(Visual\s+Studio\s+\d+\s+\d+)(\s+\[arch])?\s*=.*$/) 38 | if (match !== null) { 39 | matchedGeneratorLine = match 40 | break 41 | } 42 | } 43 | 44 | // if found a match, use the generator 45 | if (matchedGeneratorLine !== undefined) { 46 | const [_line, parsedGenerator, archBracket] = matchedGeneratorLine 47 | const useArchSwitch = (archBracket as string | undefined) === undefined 48 | const archString = arch === "x64" ? " Win64" : arch === "ia32" ? " Win32" : "" 49 | if (archString === "") { 50 | logger.warn( 51 | `Unsupported architecture: ${arch} for generator ${parsedGenerator}. Using without arch specification.`, 52 | ) 53 | } 54 | const generator = useArchSwitch ? parsedGenerator : `${parsedGenerator}${archString}` 55 | const generatorFlags = useArchSwitch ? ["-A", getCMakeArchitecture(arch, os)] : undefined 56 | 57 | logger.debug(`Using generator: ${generator} ${generatorFlags} for ${os} ${arch}`) 58 | return { 59 | generator, 60 | generatorFlags, 61 | binary: undefined, 62 | } 63 | } 64 | } catch (e) { 65 | logger.warn("Failed to find valid VS gen, using native.") 66 | // fall back to native 67 | } 68 | } 69 | 70 | // use native generator 71 | logger.debug(`Using generator: native for ${os} ${arch}`) 72 | return { 73 | generator: "native", 74 | binary: undefined, 75 | } 76 | }, 77 | { promise: true }, 78 | ) 79 | -------------------------------------------------------------------------------- /src/lib.d.mts: -------------------------------------------------------------------------------- 1 | export * from "./build.js" 2 | export * from "./config-types.d" 3 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | export * from "./build.js" 2 | export * from "./config-types.d" 3 | export * from "./utils/logger.js" 4 | export * from "./loader.js" 5 | -------------------------------------------------------------------------------- /src/libc.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | 3 | export function detectLibc(os: typeof process.platform) { 4 | if (os === "linux") { 5 | if (fs.existsSync("/etc/alpine-release")) { 6 | return "musl" 7 | } 8 | return "glibc" 9 | } else if (os === "darwin") { 10 | return "libc" 11 | } else if (os === "win32") { 12 | return "msvc" 13 | } 14 | return "unknown" 15 | } 16 | -------------------------------------------------------------------------------- /src/loader.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "module" 2 | import { Manifest, getPlatform } from "./manifest.js" 3 | import { errorString, logger } from "./utils/logger.js" 4 | 5 | /** 6 | * Find the addon for the current runtime. 7 | * 8 | * @param buildDir - The directory containing the build artifacts. It should contain a `manifest.json` file. 9 | * @returns The addon for the current runtime. 10 | */ 11 | export function loadAddon(buildDir: string): Addon | undefined { 12 | let addon: undefined | Addon = undefined 13 | try { 14 | // detect the platform of the current runtime 15 | const platform = getPlatform() 16 | 17 | // read the manifest file 18 | const manifest = new Manifest(buildDir) 19 | 20 | // find the compatible configs for the current runtime 21 | const compatibleAddons = manifest.findCompatibleConfigs(platform) 22 | 23 | // require function polyfill for ESM or CommonJS 24 | const requireFn = typeof require === "function" ? require : createRequire(import.meta.url) 25 | 26 | // try loading each available addon in order 27 | for (const [_config, addonPath] of compatibleAddons) { 28 | try { 29 | logger.debug(`Loading addon at ${addonPath}`) 30 | addon = requireFn(addonPath) 31 | break 32 | } catch (err) { 33 | logger.warn(`Failed to load addon at ${addonPath}: ${errorString(err)}\nTrying others...`) 34 | } 35 | } 36 | } catch (err) { 37 | throw new Error(`Failed to load zeromq.js addon.node: ${errorString(err)}`) 38 | } 39 | 40 | if (addon === undefined) { 41 | throw new Error("No compatible zeromq.js addon found") 42 | } 43 | 44 | return addon 45 | } 46 | -------------------------------------------------------------------------------- /src/main.d.mts: -------------------------------------------------------------------------------- 1 | export * from "./main.js" 2 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { parseArgs } from "./args.js" 4 | import { build, logger } from "./lib.js" 5 | 6 | async function main(): Promise { 7 | const opts = parseArgs() 8 | const configs = await build(opts) 9 | if (configs === null) { 10 | return 1 11 | } 12 | return 0 13 | } 14 | 15 | main() 16 | .then((exitCode) => { 17 | process.exit(exitCode) 18 | }) 19 | .catch((err: Error) => { 20 | logger.error(err) 21 | return 1 22 | }) 23 | -------------------------------------------------------------------------------- /src/manifest.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import path from "path" 3 | import type { BuildConfiguration } from "./config-types.d" 4 | import { detectLibc } from "./libc.js" 5 | import { errorString, logger } from "./utils/logger.js" 6 | 7 | /** 8 | * A class that represents the manifest file. 9 | */ 10 | 11 | export class Manifest { 12 | private buildDir: string 13 | private manifest: Record 14 | 15 | /** 16 | * Create a new manifest from the build directory. 17 | * 18 | * @param buildDir - The directory containing the build artifacts. It should contain a `manifest.json` file. 19 | */ 20 | constructor(buildDir: string) { 21 | this.buildDir = buildDir 22 | const manifestPath = path.resolve(buildDir, "manifest.json") 23 | if (!fs.existsSync(manifestPath)) { 24 | throw new Error(`Manifest file not found at ${manifestPath}`) 25 | } 26 | try { 27 | logger.debug(`Reading and parsing manifest file at ${manifestPath}`) 28 | const manifestContent = fs.readFileSync(manifestPath, "utf-8") 29 | this.manifest = JSON.parse(manifestContent) as Record 30 | } catch (err) { 31 | throw new Error(`Failed to read and parse the manifest file at ${manifestPath}: ${errorString(err)}`) 32 | } 33 | } 34 | 35 | /** 36 | * Find the compatible configs for the current runtime. 37 | * 38 | * @param platform - The platform of the current runtime. 39 | * @returns The compatible configs. 40 | */ 41 | findCompatibleConfigs(platform: Platform) { 42 | // get the configs from the manifest 43 | const configKeys = this.getConfigKeys() 44 | 45 | // find the compatible addons (config -> addon path) 46 | const compatibleAddons: [BuildConfiguration, string][] = [] 47 | for (const configKey of configKeys) { 48 | try { 49 | // parse the config key 50 | const config = this.getConfig(configKey) 51 | 52 | // check if the config is compatible with the current runtime 53 | if (config.os !== platform.os || config.arch !== platform.arch || config.libc !== platform.libc) { 54 | logger.debug(`Config ${configKey} is not compatible with the current runtime. Skipping...`) 55 | continue 56 | } 57 | 58 | // get the relative path to the addon 59 | const addonRelativePath = this.getAddonPath(configKey) 60 | 61 | // add the addon to the list of compatible addons 62 | compatibleAddons.push([config, path.resolve(this.buildDir, addonRelativePath)]) 63 | } catch (err) { 64 | logger.warn(`Failed to parse config ${configKey}: ${errorString(err)}`) 65 | } 66 | } 67 | 68 | if (compatibleAddons.length === 0) { 69 | throw new Error( 70 | `No compatible zeromq.js addon found for ${platform.os} ${platform.arch} ${platform.libc}. The candidates were:\n${configKeys.join( 71 | "\n", 72 | )}`, 73 | ) 74 | } 75 | 76 | // sort the compatible addons by the ABI in descending order 77 | compatibleAddons.sort(([c1, _p1], [c2, _p2]) => { 78 | return (c2.abi ?? 0) - (c1.abi ?? 0) 79 | }) 80 | 81 | return compatibleAddons 82 | } 83 | 84 | /** 85 | * Get the config keys from the manifest in the string format. 86 | * 87 | * @returns The config keys in the string format. 88 | */ 89 | getConfigKeys() { 90 | return Object.keys(this.manifest) 91 | } 92 | 93 | /** 94 | * Get the config from the manifest. 95 | * 96 | * @param configKey - The key of the config. 97 | * @returns The config. 98 | */ 99 | // eslint-disable-next-line class-methods-use-this 100 | getConfig(configKey: string) { 101 | return JSON.parse(configKey) as BuildConfiguration 102 | } 103 | 104 | /** 105 | * Get the addon path from the manifest. 106 | * 107 | * @param configKey - The key of the config. 108 | * @returns The addon path. 109 | */ 110 | getAddonPath(configKey: string) { 111 | return this.manifest[configKey] 112 | } 113 | } 114 | /** 115 | * Get the platform of the current runtime. 116 | * 117 | * @returns The platform of the current runtime. 118 | */ 119 | export function getPlatform(): Platform { 120 | return { 121 | os: process.platform, 122 | arch: process.arch, 123 | libc: detectLibc(process.platform), 124 | } 125 | } 126 | /** 127 | * The platform of the current runtime. 128 | */ 129 | 130 | export type Platform = { 131 | os: string 132 | arch: string 133 | libc: string 134 | } 135 | -------------------------------------------------------------------------------- /src/nodeAPIInclude/index.ts: -------------------------------------------------------------------------------- 1 | import { pathExists } from "fs-extra" 2 | import { requireInclude, resolvePackage } from "./resolve.js" 3 | import { searchPackage } from "./search.js" 4 | 5 | export async function getNodeApiInclude(projectRoot: string, nodeAPI: string): Promise { 6 | // first check if the given nodeAPI is a include path 7 | if (await pathExists(nodeAPI)) { 8 | return nodeAPI 9 | } 10 | // then resolve 11 | const resolvedPath = await resolvePackage(projectRoot, nodeAPI) 12 | if (typeof resolvedPath === "string") { 13 | return requireInclude(resolvedPath) 14 | } 15 | // if not found then search 16 | return searchPackage(projectRoot, nodeAPI) 17 | } 18 | -------------------------------------------------------------------------------- /src/nodeAPIInclude/resolve.ts: -------------------------------------------------------------------------------- 1 | import resolve from "resolve/async.js" 2 | 3 | type NodeAddonApiImport = { 4 | include: string 5 | include_dir: string 6 | /** @deprecated */ 7 | gyp: string 8 | targets: string 9 | version: string 10 | isNodeApiBuiltin: boolean 11 | needsFlag: false 12 | } 13 | 14 | async function getRequire() { 15 | if (typeof require === "function") { 16 | // eslint-disable-next-line @typescript-eslint/no-var-requires 17 | return require 18 | } 19 | const { createRequire } = await import("module") 20 | return createRequire(import.meta.url) 21 | } 22 | 23 | export async function requireInclude(resolvedPath: string) { 24 | try { 25 | const require = await getRequire() 26 | 27 | let consoleOutput: string | null = null 28 | const origConsole = console.log 29 | console.log = (msg: string) => { 30 | consoleOutput = msg 31 | } 32 | 33 | const requireResult = require(resolvedPath) as NodeAddonApiImport 34 | 35 | console.log = origConsole 36 | 37 | if (typeof requireResult === "string") { 38 | // for NAN 39 | return requireResult 40 | } else if (typeof requireResult === "object") { 41 | if (typeof requireResult.include_dir === "string") { 42 | // for NAPI 43 | return requireResult.include_dir 44 | } else if (typeof requireResult.include === "string") { 45 | // for old NAPI 46 | return requireResult.include 47 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 48 | } else if (consoleOutput !== null) { 49 | // for recent NAN packages. Will open a PR with NAN. 50 | return consoleOutput 51 | } 52 | } 53 | } catch { 54 | // continue 55 | } 56 | return resolvedPath 57 | } 58 | 59 | export async function resolvePackage(projectRoot: string, packageName: string) { 60 | try { 61 | const resolvedPath = await resolveAsync(packageName, projectRoot) 62 | if (resolvedPath !== undefined) { 63 | return resolvedPath 64 | } 65 | } catch { 66 | // continue 67 | } 68 | return null 69 | } 70 | 71 | function resolveAsync(name: string, basedir: string) { 72 | return new Promise((promiseResolve) => { 73 | resolve(name, { basedir }, (err, res) => { 74 | if (err) { 75 | throw err 76 | } 77 | return promiseResolve(res) 78 | }) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /src/nodeAPIInclude/search.ts: -------------------------------------------------------------------------------- 1 | import { join as joinPath, normalize as normalizePath, sep as pathSeparator } from "path" 2 | import { stat } from "../utils/fs.js" 3 | import { logger } from "../utils/logger.js" 4 | 5 | export async function searchPackage(projectRoot: string, packageName: string): Promise { 6 | const isNode = await isNodeProject(projectRoot) 7 | if (!isNode) { 8 | return null 9 | } 10 | const packagePath = joinPath(projectRoot, "node_modules", packageName) 11 | const hasHeader = await dirHasFile(packagePath, packageName === "node-addon-api" ? "napi.h" : `${packageName}.h`) 12 | if (hasHeader) { 13 | logger.debug(`Found package "${packageName}" at path ${packagePath}!`) 14 | return packagePath 15 | } 16 | return searchPackage(goUp(projectRoot), packageName) 17 | } 18 | 19 | async function isNodeProject(dir: string) { 20 | const pjson = joinPath(dir, "package.json") 21 | const node_modules = joinPath(dir, "node_modules") 22 | return (await stat(pjson)).isFile() || (await stat(node_modules)).isDirectory() 23 | } 24 | 25 | async function dirHasFile(dir: string, fileName: string) { 26 | const filePath = joinPath(dir, fileName) 27 | return (await stat(filePath)).isFile() 28 | } 29 | 30 | function goUp(dir: string) { 31 | let myDir = dir 32 | const items = myDir.split(pathSeparator) 33 | const scope = items[items.length - 2] 34 | if (scope && scope.charAt(0) === "@") { 35 | myDir = joinPath(myDir, "..") 36 | } 37 | myDir = joinPath(myDir, "..", "..") 38 | return normalizePath(myDir) 39 | } 40 | -------------------------------------------------------------------------------- /src/override.ts: -------------------------------------------------------------------------------- 1 | import satisfies from "semver/functions/satisfies.js" 2 | import type { ArrayOrSingle, BuildConfiguration, OverrideConfig } from "./config-types.d" 3 | import { logger } from "./utils/logger.js" 4 | 5 | const knownOverrides: OverrideConfig[] = [ 6 | { 7 | match: { 8 | arch: ["x64", "arm64"], 9 | runtime: "electron", 10 | runtimeVersion: ">=9", 11 | }, 12 | addDefines: "V8_COMPRESS_POINTERS", 13 | }, 14 | { 15 | match: { 16 | runtime: "electron", 17 | runtimeVersion: ">=9", 18 | }, 19 | addDefines: "V8_31BIT_SMIS_ON_64BIT_ARCH", 20 | }, 21 | { 22 | match: { 23 | runtime: "electron", 24 | runtimeVersion: ">=11", 25 | }, 26 | addDefines: "V8_REVERSE_JSARGS", 27 | }, 28 | { 29 | match: { 30 | runtime: "electron", 31 | runtimeVersion: ">=16", 32 | arch: ["x64", "arm64"], 33 | }, 34 | addDefines: "V8_COMPRESS_POINTERS_IN_ISOLATE_CAGE", 35 | }, 36 | ] 37 | 38 | function matchAgainstArray(value: T, target?: ArrayOrSingle) { 39 | if (target === undefined) { 40 | // no target value means all are accepted 41 | return true 42 | } 43 | if (!Array.isArray(target)) { 44 | return target === value 45 | } 46 | return target.includes(value) 47 | } 48 | 49 | function matchOverride(config: BuildConfiguration, ov: OverrideConfig) { 50 | const archMatch = matchAgainstArray(config.arch, ov.match.arch) 51 | const osMath = matchAgainstArray(config.os, ov.match.os) 52 | const runtimeMatch = matchAgainstArray(config.runtime, ov.match.runtime) 53 | 54 | if (!archMatch || !osMath || !runtimeMatch) { 55 | return false 56 | } 57 | 58 | if (ov.match.runtimeVersion !== undefined) { 59 | const compares = Array.isArray(ov.match.runtimeVersion) ? ov.match.runtimeVersion : [ov.match.runtimeVersion] 60 | 61 | if ( 62 | !compares.some((v) => 63 | satisfies(config.runtimeVersion, v, { 64 | includePrerelease: true, 65 | }), 66 | ) 67 | ) { 68 | return false 69 | } 70 | } 71 | 72 | if (Array.isArray(ov.addDefines)) { 73 | config.additionalDefines.push(...ov.addDefines) 74 | } else { 75 | config.additionalDefines.push(ov.addDefines) 76 | } 77 | logger.debug(`Adding define: ${ov.addDefines}`) 78 | return true 79 | } 80 | 81 | export function applyOverrides(config: BuildConfiguration) { 82 | for (const override of knownOverrides) { 83 | matchOverride(config, override) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/runtimeDistribution.ts: -------------------------------------------------------------------------------- 1 | import { extname, join as joinPath } from "path" 2 | import glob from "fast-glob" 3 | import { ensureDir, readFile } from "fs-extra" 4 | import urlJoin from "url-join" 5 | import type { BuildConfiguration } from "./config-types.d" 6 | import { detectLibc } from "./libc.js" 7 | import { HOME_DIRECTORY, getPathsForConfig } from "./urlRegistry.js" 8 | import { downloadFile, downloadTgz, downloadToString } from "./utils/download.js" 9 | import { stat } from "./utils/fs.js" 10 | 11 | export type HashSum = { getPath: string; sum: string } 12 | function testHashSum(sums: HashSum[], sum: string | undefined, fPath: string) { 13 | const serverSum = sums.find((s) => s.getPath === fPath) 14 | if (serverSum && serverSum.sum === sum) { 15 | return true 16 | } 17 | return false 18 | } 19 | 20 | export class RuntimeDistribution { 21 | private _abi: number | null = null 22 | 23 | // TODO the code uses a side effect of TypeScript constructors in defining the class props 24 | /* eslint-disable-next-line no-useless-constructor */ /* eslint-disable-next-line no-empty-function */ 25 | constructor(private config: BuildConfiguration) {} 26 | 27 | public internalPath() { 28 | return joinPath( 29 | HOME_DIRECTORY, 30 | ".cmake-ts", 31 | this.config.runtime, 32 | this.config.os, 33 | this.config.arch, 34 | `v${this.config.runtimeVersion}`, 35 | ) 36 | } 37 | 38 | private externalPath() { 39 | return getPathsForConfig(this.config).externalPath 40 | } 41 | 42 | public winLibs() { 43 | return getPathsForConfig(this.config).winLibs.map((lib) => joinPath(this.internalPath(), lib.dir, lib.name)) 44 | } 45 | 46 | private headerOnly() { 47 | return getPathsForConfig(this.config).headerOnly 48 | } 49 | 50 | public abi() { 51 | return this._abi 52 | } 53 | 54 | private async checkDownloaded(): Promise { 55 | let headers = false 56 | let libs = true 57 | let stats = await stat(this.internalPath()) 58 | if (!stats.isDirectory()) { 59 | headers = false 60 | } 61 | if (this.headerOnly()) { 62 | stats = await stat(joinPath(this.internalPath(), "include/node/node.h")) 63 | headers = stats.isFile() 64 | } else { 65 | stats = await stat(joinPath(this.internalPath(), "src/node.h")) 66 | if (stats.isFile()) { 67 | stats = await stat(joinPath(this.internalPath(), "deps/v8/include/v8.h")) 68 | headers = stats.isFile() 69 | } 70 | } 71 | if (this.config.os === "win32") { 72 | const libStats = await Promise.all(this.winLibs().map((lib) => stat(lib))) 73 | const libsAreFile = libStats.every((libStat) => libStat.isFile()) 74 | libs = libsAreFile 75 | } 76 | return headers && libs 77 | } 78 | 79 | public async determineABI(): Promise { 80 | const files = await glob("*/node_version.h", { 81 | cwd: joinPath(this.internalPath(), "include"), 82 | absolute: true, 83 | onlyFiles: true, 84 | braceExpansion: false, 85 | }) 86 | const filesNum = files.length 87 | if (filesNum === 0) { 88 | return Promise.reject( 89 | new Error( 90 | `couldn't find include/*/node_version.h in ${this.internalPath()}. Make sure you install the dependencies for building the package.`, 91 | ), 92 | ) 93 | } 94 | if (filesNum !== 1) { 95 | return Promise.reject(new Error(`more than one node_version.h was found in ${this.internalPath()}.`)) 96 | } 97 | const fName = files[0] 98 | let contents: string 99 | try { 100 | contents = await readFile(fName, "utf8") 101 | } catch (err) { 102 | if (err instanceof Error) { 103 | return Promise.reject(err) 104 | } 105 | throw err 106 | } 107 | const match = contents.match(/#define\s+NODE_MODULE_VERSION\s+(\d+)/) 108 | if (!match) { 109 | return Promise.reject(new Error("Failed to find NODE_MODULE_VERSION macro")) 110 | } 111 | const version = Number.parseInt(match[1], 10) 112 | if (Number.isNaN(version)) { 113 | return Promise.reject(new Error("Invalid version specified by NODE_MODULE_VERSION macro")) 114 | } 115 | this._abi = version 116 | 117 | this.config.abi = version 118 | 119 | this.config.libc = detectLibc(this.config.os) 120 | 121 | return Promise.resolve() 122 | } 123 | 124 | public async ensureDownloaded(): Promise { 125 | if (!(await this.checkDownloaded())) { 126 | await this.download() 127 | } 128 | } 129 | 130 | private async download(): Promise { 131 | await ensureDir(this.internalPath()) 132 | const sums = await this.downloadHashSums() 133 | await this.downloadTar(sums) 134 | await this.downloadLibs(sums) 135 | } 136 | 137 | private async downloadHashSums(): Promise { 138 | if (this.config.runtime === "node" || this.config.runtime === "iojs") { 139 | const sumurl = urlJoin(this.externalPath(), "SHASUMS256.txt") 140 | const str = await downloadToString(sumurl) 141 | return str 142 | .split("\n") 143 | .map((line) => { 144 | const parts = line.split(/\s+/) 145 | return { 146 | getPath: parts[1], 147 | sum: parts[0], 148 | } 149 | }) 150 | .filter((i) => i.getPath && i.sum) 151 | } 152 | return null 153 | } 154 | 155 | private async downloadTar(sums: HashSum[] | null): Promise { 156 | const tarLocalPath = getPathsForConfig(this.config).tarPath 157 | const tarUrl = urlJoin(this.externalPath(), tarLocalPath) 158 | const sum = await downloadTgz(tarUrl, { 159 | hashType: sums ? "sha256" : undefined, 160 | extractOptions: { 161 | cwd: this.internalPath(), 162 | strip: 1, 163 | filter: (p: string) => { 164 | if (p === this.internalPath()) { 165 | return true 166 | } 167 | const ext = extname(p) 168 | return ext !== "" && ext.toLowerCase() === ".h" 169 | }, 170 | }, 171 | }) 172 | if (sums && !testHashSum(sums, sum, tarLocalPath)) { 173 | throw new Error("Checksum mismatch") 174 | } 175 | } 176 | 177 | private async downloadLibs(sums: HashSum[] | null): Promise { 178 | if (this.config.os !== "win32") { 179 | return 180 | } 181 | const paths = getPathsForConfig(this.config) 182 | // download libs in parallel 183 | await Promise.all(paths.winLibs.map((path) => this.downloadLib(path, sums))) 184 | } 185 | 186 | private async downloadLib(path: { dir: string; name: string }, sums: HashSum[] | null) { 187 | const fPath = path.dir ? urlJoin(path.dir, path.name) : path.name 188 | const libUrl = urlJoin(this.externalPath(), fPath) 189 | await ensureDir(joinPath(this.internalPath(), path.dir)) 190 | const sum = await downloadFile(libUrl, { 191 | path: joinPath(this.internalPath(), fPath), 192 | hashType: sums ? "sha256" : undefined, 193 | }) 194 | if (sums && !testHashSum(sums, sum, fPath)) { 195 | throw new Error("Checksum mismatch") 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": true, 5 | "outDir": "../build", 6 | "noEmit": false 7 | }, 8 | "include": ["./**/*.ts", "./**/*.mts", "./**/*.d.ts", "./**/*.d.mts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/urlRegistry.ts: -------------------------------------------------------------------------------- 1 | import os from "os" 2 | import gte from "semver/functions/gte" 3 | import lt from "semver/functions/lt" 4 | import type { BuildConfiguration } from "./config-types.d" 5 | import { getEnvVar } from "./utils/env.js" 6 | 7 | const NODE_MIRROR = getEnvVar("NVM_NODEJS_ORG_MIRROR") ?? "https://nodejs.org/dist" 8 | const IOJS_MIRROR = getEnvVar("NVM_IOJS_ORG_MIRROR") ?? "https://iojs.org/dist" 9 | const ELECTRON_MIRROR = getEnvVar("ELECTRON_MIRROR") ?? "https://artifacts.electronjs.org/headers/dist" 10 | 11 | export const HOME_DIRECTORY = process.env[os.platform() === "win32" ? "USERPROFILE" : "HOME"] as string 12 | 13 | export function getPathsForConfig(config: BuildConfiguration) { 14 | switch (config.runtime) { 15 | case "node": { 16 | return (lt(config.runtimeVersion, "4.0.0") ? nodePrehistoric : nodeModern)(config) 17 | } 18 | case "iojs": { 19 | return { 20 | externalPath: `${IOJS_MIRROR}/v${config.runtimeVersion}/`, 21 | winLibs: [ 22 | // https://iojs.org/dist/v3.3.1/win-x64/iojs.lib 23 | // https://iojs.org/dist/v3.3.1/win-x86/iojs.lib 24 | { 25 | dir: config.arch === "x64" || config.arch === "arm64" ? "win-x64" : "win-x86", 26 | name: `${config.runtime}.lib`, 27 | }, 28 | ], 29 | tarPath: `${config.runtime}-v${config.runtimeVersion}.tar.gz`, 30 | headerOnly: false, 31 | } 32 | } 33 | case "electron": { 34 | return { 35 | externalPath: `${ELECTRON_MIRROR}/v${config.runtimeVersion}/`, 36 | winLibs: [ 37 | { 38 | // https://artifacts.electronjs.org/headers/dist/v17.0.0/x64/node.lib 39 | // https://artifacts.electronjs.org/headers/dist/v17.0.0/arm64/node.lib 40 | // https://artifacts.electronjs.org/headers/dist/v17.0.0/node.lib 41 | dir: config.arch === "x64" ? "x64" : config.arch === "arm64" ? "arm64" : "", 42 | name: "node.lib", 43 | }, 44 | ], 45 | tarPath: `node-v${config.runtimeVersion}.tar.gz`, 46 | headerOnly: gte(config.runtimeVersion, "4.0.0-alpha"), 47 | } 48 | } 49 | default: { 50 | throw new Error(`Unsupported runtime ${config.runtime}`) 51 | } 52 | } 53 | } 54 | 55 | function nodePrehistoric(config: BuildConfiguration) { 56 | return { 57 | externalPath: `${NODE_MIRROR}/v${config.runtimeVersion}/`, 58 | winLibs: [ 59 | { 60 | // https://nodejs.org/dist/v0.9.9/x64/node.lib 61 | dir: config.arch === "x64" || config.arch === "arm64" ? "x64" : "", 62 | name: `${config.runtime}.lib`, 63 | }, 64 | ], 65 | tarPath: `${config.runtime}-v${config.runtimeVersion}.tar.gz`, 66 | headerOnly: false, 67 | } 68 | } 69 | 70 | function nodeModern(config: BuildConfiguration) { 71 | return { 72 | externalPath: `${NODE_MIRROR}/v${config.runtimeVersion}/`, 73 | winLibs: [ 74 | // https://nodejs.org/dist/v22.14.0/win-x64/node.lib 75 | // https://nodejs.org/dist/v22.14.0/win-x86/node.lib 76 | // https://nodejs.org/dist/v22.14.0/win-arm64/node.lib 77 | { 78 | dir: 79 | config.arch === "x64" 80 | ? "win-x64" 81 | : config.arch === "arm64" 82 | ? "win-arm64" 83 | : config.arch === "ia32" 84 | ? "win-x86" 85 | : "", 86 | name: `${config.runtime}.lib`, 87 | }, 88 | ], 89 | tarPath: `${config.runtime}-v${config.runtimeVersion}-headers.tar.gz`, 90 | headerOnly: true, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/utils/download.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto" 2 | import { tmpdir } from "os" 3 | import { basename, dirname, join } from "path" 4 | import { ensureDir, readFile, remove } from "fs-extra" 5 | import { DownloaderHelper } from "node-downloader-helper" 6 | import type { ExtractOptions as TarExtractOptions } from "tar" 7 | import extractTar from "tar/lib/extract.js" 8 | import { logger } from "./logger.js" 9 | import { retry } from "./retry.js" 10 | 11 | export type HashType = "sha256" | "sha512" | "sha1" | "md5" | "sha384" | "sha224" 12 | 13 | export type DownloadCoreOptions = { 14 | hashType?: HashType 15 | hashSum?: string 16 | timeout?: number 17 | } 18 | 19 | export type DownloadOptions = DownloadCoreOptions & { 20 | path?: string 21 | } 22 | 23 | export type DownloadFileOptions = DownloadCoreOptions & { 24 | path: string 25 | } 26 | 27 | export type DownloadTgzOptions = DownloadOptions & { 28 | removeAfterExtract?: boolean 29 | extractOptions?: TarExtractOptions 30 | } 31 | 32 | type DownloadResult = { 33 | filePath: string 34 | hash: string | undefined 35 | } 36 | 37 | /** Downloads a file to a temporary location and returns the file path and hash */ 38 | async function download(url: string, opts: DownloadOptions) { 39 | try { 40 | const filePath = opts.path ?? join(tmpdir(), "cmake-ts", `${Math.random()}`, basename(url)) 41 | const fileName = basename(filePath) 42 | const fileDir = dirname(filePath) 43 | 44 | logger.debug(`Downloading ${url} to ${filePath}`) 45 | 46 | await ensureDir(fileDir) 47 | const downloader = new DownloaderHelper(url, fileDir, { 48 | fileName, 49 | timeout: opts.timeout ?? -1, 50 | override: true, 51 | retry: { 52 | maxRetries: 3, 53 | delay: 1000, 54 | }, 55 | }) 56 | 57 | // Create a promise that will reject if an error occurs 58 | const downloadPromise = new Promise((resolve, reject) => { 59 | downloader.on("error", (err) => { 60 | reject(err) 61 | }) 62 | 63 | downloader.on("end", () => { 64 | resolve() 65 | }) 66 | }) 67 | 68 | const result: DownloadResult = { 69 | filePath, 70 | hash: undefined, 71 | } 72 | 73 | // Start the download and wait for it to complete or error 74 | await Promise.all([downloader.start(), downloadPromise]) 75 | 76 | // calculate hash after download is complete 77 | result.hash = opts.hashType !== undefined ? await calculateHash(filePath, opts.hashType) : undefined 78 | 79 | return result 80 | } catch (err) { 81 | throw new Error(`Failed to download ${url}: ${err}`) 82 | } 83 | } 84 | 85 | /** Calculates the hash of a file */ 86 | export async function calculateHash(filePath: string, hashType: HashType) { 87 | const fileBuffer = await readFile(filePath) 88 | const shasum = crypto.createHash(hashType) 89 | shasum.update(fileBuffer) 90 | return shasum.digest("hex") 91 | } 92 | 93 | /** Downloads content from a URL and returns it as a string */ 94 | export async function downloadToString(url: string, options: DownloadCoreOptions = {}): Promise { 95 | const { filePath } = await download(url, options) 96 | 97 | try { 98 | return await readFile(filePath, "utf8") 99 | } finally { 100 | await remove(filePath).catch((err) => { 101 | // Ignore errors 102 | logger.debug("Ignoring error removing temporary file", filePath, err) 103 | }) 104 | } 105 | } 106 | 107 | /** Downloads a file from a URL to a specified path */ 108 | export async function downloadFile(url: string, options: DownloadFileOptions): Promise { 109 | const { hash } = await download(url, options) 110 | 111 | // Verify hash if needed 112 | if (!isHashSumValid(hash, options)) { 113 | throw new Error(`Checksum mismatch for download ${url}. Expected ${options.hashSum}, got ${hash}`) 114 | } 115 | 116 | return hash 117 | } 118 | 119 | /** Downloads and extracts a .tgz file */ 120 | export async function downloadTgz(url: string, options: DownloadTgzOptions): Promise { 121 | const { filePath, hash } = await download(url, options) 122 | 123 | try { 124 | // Verify hash if needed 125 | if (!isHashSumValid(hash, options)) { 126 | throw new Error(`Checksum mismatch for download ${url}. Expected ${options.hashSum}, got ${hash}`) 127 | } 128 | 129 | // Extract the tgz file 130 | await retry(() => 131 | extractTar({ 132 | file: filePath, 133 | ...options.extractOptions, 134 | }), 135 | ) 136 | 137 | return hash 138 | } finally { 139 | if (options.removeAfterExtract ?? true) { 140 | await remove(filePath).catch((err) => { 141 | // Ignore errors 142 | logger.debug("Ignoring error removing temporary file", filePath, err) 143 | }) 144 | } 145 | } 146 | } 147 | 148 | /** Checks if the calculated hash matches the expected hash */ 149 | function isHashSumValid(sum: string | undefined, options: DownloadOptions): boolean { 150 | // No hash type or hash sum is valid 151 | return ( 152 | options.hashType === undefined || 153 | options.hashSum === undefined || 154 | // Check if the hash sum is valid 155 | options.hashSum === sum 156 | ) 157 | } 158 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get an environment variable. 3 | * 4 | * @param name - The name of the environment variable. 5 | * @returns The value of the environment variable or undefined if it is not set. 6 | */ 7 | export function getEnvVar(name: string) { 8 | const value = process.env[name] 9 | if (typeof value === "string" && value.length > 0 && value !== "undefined" && value !== "null") { 10 | return value 11 | } 12 | return undefined 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/exec.ts: -------------------------------------------------------------------------------- 1 | import * as cp from "child_process" 2 | 3 | /** 4 | * Capture the output of a command 5 | * @note this ignores the running errors 6 | */ 7 | 8 | export function execCapture(command: string): Promise { 9 | return new Promise((resolve) => { 10 | cp.exec(command, (_, stdout, stderr) => { 11 | resolve(stdout || stderr) 12 | }) 13 | }) 14 | } 15 | 16 | export function runProgram( 17 | program: string, 18 | args: string[], 19 | cwd: string = process.cwd(), 20 | silent: boolean = false, 21 | ): Promise { 22 | return new Promise((resolve, reject) => { 23 | const child = cp.spawn(program, args, { 24 | stdio: silent ? "ignore" : "inherit", 25 | cwd, 26 | env: process.env, 27 | }) 28 | let ended = false 29 | child.on("error", (e) => { 30 | if (!ended) { 31 | reject(e) 32 | ended = true 33 | } 34 | }) 35 | child.on("exit", (code, signal) => { 36 | if (ended) { 37 | return 38 | } 39 | if (code === 0) { 40 | resolve() 41 | } else { 42 | reject(new Error(`Process terminated: ${code ?? signal}`)) 43 | } 44 | ended = true 45 | }) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import type { PathLike, StatOptions, Stats, StatsBase } from "fs" 2 | import { stat as rawStat } from "fs-extra" 3 | 4 | /** Exception safe version of stat */ 5 | export async function stat( 6 | path: PathLike, 7 | options?: StatOptions & { bigint: false }, 8 | ): Promise { 9 | try { 10 | return await rawStat(path, options) 11 | } catch { 12 | return new NoStats() 13 | } 14 | } 15 | 16 | export type FunctionalStats = Pick< 17 | StatsBase, 18 | "isFile" | "isDirectory" | "isBlockDevice" | "isCharacterDevice" | "isSymbolicLink" | "isFIFO" | "isSocket" 19 | > 20 | 21 | /* eslint-disable class-methods-use-this */ 22 | export class NoStats implements FunctionalStats { 23 | isFile = () => false 24 | isDirectory = () => false 25 | isBlockDevice = () => false 26 | isCharacterDevice = () => false 27 | isSymbolicLink = () => false 28 | isFIFO = () => false 29 | isSocket = () => false 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | 3 | class Logger { 4 | private level: number = 2 5 | 6 | /** 7 | * Set the log level 8 | * @param level - The log level 9 | * 10 | * trace - 4 11 | * debug - 3 12 | * info - 2 13 | * warn - 1 14 | * error - 0 15 | * off - -1 16 | * 17 | * @default "info" 18 | */ 19 | setLevel(level: "trace" | "debug" | "info" | "warn" | "error" | "off" = "info") { 20 | this.level = 21 | level === "trace" 22 | ? 4 23 | : level === "debug" 24 | ? 3 25 | : level === "info" 26 | ? 2 27 | : level === "warn" 28 | ? 1 29 | : level === "error" 30 | ? 0 31 | : -1 32 | } 33 | 34 | error(...args: unknown[]) { 35 | if (this.level >= 0) { 36 | console.error("\x1b[31m[ERROR cmake-ts]\x1b[0m", ...args) 37 | } 38 | } 39 | 40 | warn(...args: unknown[]) { 41 | if (this.level >= 1) { 42 | console.warn("\x1b[33m[WARN cmake-ts]\x1b[0m", ...args) 43 | } 44 | } 45 | 46 | info(...args: unknown[]) { 47 | if (this.level >= 2) { 48 | console.info("\x1b[32m[INFO cmake-ts]\x1b[0m", ...args) 49 | } 50 | } 51 | 52 | log(...args: unknown[]) { 53 | return this.info(...args) 54 | } 55 | 56 | debug(...args: unknown[]) { 57 | if (this.level >= 3) { 58 | console.debug("\x1b[34m[DEBUG cmake-ts]\x1b[0m", ...args) 59 | } 60 | } 61 | 62 | trace(...args: unknown[]) { 63 | if (this.level >= 4) { 64 | console.trace("\x1b[34m[TRACE cmake-ts]\x1b[0m", ...args) 65 | } 66 | } 67 | } 68 | 69 | export const logger = new Logger() 70 | 71 | /** 72 | * Get the error string. 73 | * 74 | * @param error - The error. 75 | * @returns The error string. 76 | */ 77 | export function errorString(error: unknown) { 78 | return error instanceof Error && error.stack !== undefined ? error.stack : String(error) 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/retry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Retries an async function a specified number of times with a delay between attempts. 3 | * @param fn - The function to retry. 4 | * @param retries - The number of times to retry the function. 5 | * @param delay - The delay in milliseconds between attempts. 6 | * @returns The result of the function. 7 | */ 8 | export async function retry(fn: () => Promise, retries: number = 3, delay: number = 1000): Promise { 9 | for (let i_try = 0; i_try !== retries; i_try++) { 10 | try { 11 | // eslint-disable-next-line no-await-in-loop 12 | return await fn() 13 | } catch (error) { 14 | if (i_try === retries - 1) { 15 | throw error 16 | } 17 | // eslint-disable-next-line no-await-in-loop 18 | await sleep(delay) 19 | } 20 | } 21 | throw new Error("Retry failed") 22 | } 23 | 24 | /** 25 | * Sleeps for a specified number of milliseconds. 26 | * @param ms - The number of milliseconds to sleep. 27 | * @returns A promise that resolves after the specified number of milliseconds. 28 | */ 29 | export function sleep(ms: number) { 30 | return new Promise((resolve) => { 31 | setTimeout(resolve, ms) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/vcvarsall.ts: -------------------------------------------------------------------------------- 1 | // Modified from msvc-dev-cmd MIT License ilammy 2 | 3 | import { execSync } from "child_process" 4 | import { existsSync } from "fs" 5 | import { delimiter } from "path" 6 | import { logger } from "./utils/logger.js" 7 | 8 | const PROGRAM_FILES_X86 = process.env["ProgramFiles(x86)"] 9 | const PROGRAM_FILES = [process.env["ProgramFiles(x86)"], process.env.ProgramFiles] 10 | 11 | const EDITIONS = ["Enterprise", "Professional", "Community", "BuildTools"] 12 | const YEARS = ["2022", "2019", "2017"] 13 | 14 | const VsYearVersion: Record = { 15 | "2022": "17.0", 16 | "2019": "16.0", 17 | "2017": "15.0", 18 | "2015": "14.0", 19 | "2013": "12.0", 20 | } 21 | 22 | /** 23 | * Convert the vs version (e.g. 2022) or year (e.g. 17.0) to the version number (e.g. 17.0) 24 | * @param {string} vsversion the year (e.g. 2022) or version number (e.g. 17.0) 25 | * @returns {string | undefined} the version number (e.g. 17.0) 26 | */ 27 | function vsversion_to_versionnumber(vsversion: string): string | undefined { 28 | if (Object.values(VsYearVersion).includes(vsversion)) { 29 | return vsversion 30 | } 31 | if (vsversion in VsYearVersion) { 32 | return VsYearVersion[vsversion] 33 | } 34 | return vsversion 35 | } 36 | 37 | /** 38 | * Convert the vs version (e.g. 17.0) or year (e.g. 2022) to the year (e.g. 2022) 39 | * @param {string} vsversion the version number (e.g. 17.0) or year (e.g. 2022) 40 | * @returns {string} the year (e.g. 2022) 41 | */ 42 | function vsversion_to_year(vsversion: string): string { 43 | if (Object.keys(VsYearVersion).includes(vsversion)) { 44 | return vsversion 45 | } else { 46 | for (const [year, ver] of Object.entries(VsYearVersion)) { 47 | if (ver === vsversion) { 48 | return year 49 | } 50 | } 51 | } 52 | return vsversion 53 | } 54 | 55 | const VSWHERE_PATH = `${PROGRAM_FILES_X86}\\Microsoft Visual Studio\\Installer` 56 | 57 | /** 58 | * Find MSVC tools with vswhere 59 | * @param {string} pattern the pattern to search for 60 | * @param {string} version_pattern the version pattern to search for 61 | * @returns {string | null} the path to the found MSVC tools 62 | */ 63 | function findWithVswhere(pattern: string, version_pattern: string): string | null { 64 | try { 65 | const installationPath = execSync(`vswhere -products * ${version_pattern} -prerelease -property installationPath`) 66 | .toString() 67 | .trim() 68 | return `${installationPath}\\${pattern}` 69 | } catch (e) { 70 | logger.debug(`vswhere failed: ${e}`) 71 | } 72 | return null 73 | } 74 | 75 | /** 76 | * Find the vcvarsall.bat file for the given Visual Studio version 77 | * @param {string | undefined} vsversion the version of Visual Studio to find (year or version number) 78 | * @returns {string} the path to the vcvarsall.bat file 79 | */ 80 | function findVcvarsall(vsversion?: string): string { 81 | const vsversion_number = vsversion === undefined ? undefined : vsversion_to_versionnumber(vsversion) 82 | const version_pattern = 83 | vsversion_number === undefined ? "-latest" : `-version "${vsversion_number},${vsversion_number.split(".")[0]}.9"` 84 | 85 | // If vswhere is available, ask it about the location of the latest Visual Studio. 86 | let vcvarsallPath = findWithVswhere("VC\\Auxiliary\\Build\\vcvarsall.bat", version_pattern) 87 | if (vcvarsallPath !== null && existsSync(vcvarsallPath)) { 88 | logger.debug(`Found with vswhere: ${vcvarsallPath}`) 89 | return vcvarsallPath 90 | } 91 | logger.debug("Not found with vswhere") 92 | 93 | // If that does not work, try the standard installation locations, 94 | // starting with the latest and moving to the oldest. 95 | const years = vsversion !== undefined ? [vsversion_to_year(vsversion)] : YEARS 96 | for (const prog_files of PROGRAM_FILES) { 97 | for (const ver of years) { 98 | for (const ed of EDITIONS) { 99 | vcvarsallPath = `${prog_files}\\Microsoft Visual Studio\\${ver}\\${ed}\\VC\\Auxiliary\\Build\\vcvarsall.bat` 100 | logger.debug(`Trying standard location: ${vcvarsallPath}`) 101 | if (existsSync(vcvarsallPath)) { 102 | logger.debug(`Found standard location: ${vcvarsallPath}`) 103 | return vcvarsallPath 104 | } 105 | } 106 | } 107 | } 108 | logger.debug("Not found in standard locations") 109 | 110 | // Special case for Visual Studio 2015 (and maybe earlier), try it out too. 111 | vcvarsallPath = `${PROGRAM_FILES_X86}\\Microsoft Visual C++ Build Tools\\vcbuildtools.bat` 112 | if (existsSync(vcvarsallPath)) { 113 | logger.debug(`Found VS 2015: ${vcvarsallPath}`) 114 | return vcvarsallPath 115 | } 116 | logger.debug(`Not found in VS 2015 location: ${vcvarsallPath}`) 117 | 118 | throw new Error("Microsoft Visual Studio not found") 119 | } 120 | 121 | function isPathVariable(name: string) { 122 | const pathLikeVariables = ["PATH", "INCLUDE", "LIB", "LIBPATH"] 123 | return pathLikeVariables.includes(name.toUpperCase()) 124 | } 125 | 126 | function filterPathValue(pathValue: string) { 127 | return pathValue 128 | .split(";") 129 | .filter((value: string, index: number, self: string[]) => self.indexOf(value) === index) 130 | .join(";") 131 | } 132 | 133 | export function getMsvcArch(arch: string): string { 134 | switch (arch) { 135 | case "ia32": { 136 | return "x86" 137 | } 138 | case "x64": { 139 | return "amd64" 140 | } 141 | default: { 142 | return arch 143 | } 144 | } 145 | } 146 | 147 | /** 148 | * Setup MSVC Developer Command Prompt 149 | * @param {string} arch - Target architecture 150 | * @param {string | undefined} vsversion - The Visual Studio version to use. This can be the version number (e.g. 16.0 for 2019) or the year (e.g. "2019"). 151 | */ 152 | export function setupMSVCDevCmd(arch: string, vsversion?: string) { 153 | if (process.platform !== "win32") { 154 | return 155 | } 156 | const hostArch = getMsvcArch(process.arch) 157 | const targetArch = getMsvcArch(arch) 158 | const msvcArch = hostArch === targetArch ? targetArch : `${hostArch}_${targetArch}` 159 | logger.debug(`Setting up MSVC for ${msvcArch}`) 160 | console.group() 161 | 162 | // Add standard location of "vswhere" to PATH, in case it's not there. 163 | process.env.PATH += delimiter + VSWHERE_PATH 164 | 165 | // Due to the way Microsoft Visual C++ is configured, we have to resort to the following hack: 166 | // Call the configuration batch file and then output *all* the environment variables. 167 | 168 | const args = [msvcArch] 169 | 170 | const vcvars = `"${findVcvarsall(vsversion)}" ${args.join(" ")}` 171 | logger.debug(`vcvars command-line: ${vcvars}`) 172 | 173 | const cmd_output_string = execSync(`set && cls && ${vcvars} && cls && set`, { shell: "cmd" }).toString() 174 | const cmd_output_parts = cmd_output_string.split("\f") 175 | 176 | const old_environment = cmd_output_parts[0].split("\r\n") 177 | const vcvars_output = cmd_output_parts[1].split("\r\n") 178 | const new_environment = cmd_output_parts[2].split("\r\n") 179 | 180 | // If vsvars.bat is given an incorrect command line, it will print out 181 | // an error and *still* exit successfully. Parse out errors from output 182 | // which don't look like environment variables, and fail if appropriate. 183 | const error_messages = vcvars_output.filter((line) => { 184 | if (line.match(/^\[ERROR.*]/)) { 185 | // Don't print this particular line which will be confusing in output. 186 | if (!line.match(/Error in script usage. The correct usage is:$/)) { 187 | return true 188 | } 189 | } 190 | return false 191 | }) 192 | if (error_messages.length > 0) { 193 | throw new Error(`invalid parameters\r\n${error_messages.join("\r\n")}`) 194 | } 195 | 196 | // Convert old environment lines into a dictionary for easier lookup. 197 | const old_env_vars: Record = {} 198 | for (const string of old_environment) { 199 | const [name, value] = string.split("=") 200 | old_env_vars[name] = value 201 | } 202 | 203 | // Now look at the new environment and export everything that changed. 204 | // These are the variables set by vsvars.bat. Also export everything 205 | // that was not there during the first sweep: those are new variables. 206 | for (const string of new_environment) { 207 | // vsvars.bat likes to print some fluff at the beginning. 208 | // Skip lines that don't look like environment variables. 209 | if (!string.includes("=")) { 210 | continue 211 | } 212 | // eslint-disable-next-line prefer-const 213 | let [name, new_value] = string.split("=") 214 | const old_value = old_env_vars[name] 215 | // For new variables "old_value === undefined". 216 | if (new_value !== old_value) { 217 | logger.debug(`Setting env var ${name}=${new_value}`) 218 | // Special case for a bunch of PATH-like variables: vcvarsall.bat 219 | // just prepends its stuff without checking if its already there. 220 | // This makes repeated invocations of this action fail after some 221 | // point, when the environment variable overflows. Avoid that. 222 | if (isPathVariable(name)) { 223 | new_value = filterPathValue(new_value) 224 | } 225 | process.env[name] = new_value 226 | } 227 | } 228 | console.groupEnd() 229 | } 230 | -------------------------------------------------------------------------------- /test/args.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, expect, suite, test, vi } from "vitest" 2 | import { parseArgs } from "../src/args.js" 3 | import type { BuildCommand } from "../src/config-types.d" 4 | import { logger } from "../src/utils/logger.js" 5 | 6 | suite("parseArgs", () => { 7 | logger.setLevel("debug") 8 | 9 | const originalArgv = process.argv 10 | 11 | const commonArgs = ["node", "./node_modules/cmake-ts/build/main.js"] 12 | 13 | beforeEach(() => { 14 | // Reset process.argv before each test 15 | process.argv = commonArgs 16 | }) 17 | 18 | afterEach(() => { 19 | // Restore original process.argv after each test 20 | process.argv = originalArgv 21 | vi.restoreAllMocks() 22 | }) 23 | 24 | test("exits with error when no command is provided", () => { 25 | let exitCode = 0 26 | process.exit = vi.fn((code?: number | null | undefined): never => { 27 | exitCode = code ?? 0 28 | throw new Error("process.exit was called") 29 | }) 30 | expect(() => parseArgs()).toThrow("process.exit was called") 31 | expect(exitCode).toEqual(1) 32 | vi.unstubAllGlobals() 33 | }) 34 | 35 | suite("build command", () => { 36 | test("should parse build command correctly", () => { 37 | const result = parseArgs([...commonArgs, "build"])! 38 | expect(result.command.type).toEqual("build") 39 | }) 40 | 41 | test("should parse build command with a single config correctly", () => { 42 | const result = parseArgs([...commonArgs, "build", "--config", "debug"])! 43 | expect(result.command.type).toEqual("build") 44 | expect((result.command as BuildCommand).options.configs).toEqual(["debug"]) 45 | }) 46 | 47 | test("should parse build command with multiple configs correctly", () => { 48 | const result = parseArgs([...commonArgs, "build", "--configs", "debug", "release"])! 49 | expect(result.command.type).toEqual("build") 50 | expect((result.command as BuildCommand).options.configs).toEqual(["debug", "release"]) 51 | }) 52 | 53 | test("should parse build command with platform-specific configs correctly", () => { 54 | const result = parseArgs([...commonArgs, "build", "--configs", "win32-x64-debug"])! 55 | expect(result.command.type).toEqual("build") 56 | expect((result.command as BuildCommand).options.configs).toEqual(["win32-x64-debug"]) 57 | }) 58 | 59 | test("should parse build command with runtime-specific configs correctly", () => { 60 | const result = parseArgs([...commonArgs, "build", "--configs", "electron-release"])! 61 | expect(result.command.type).toEqual("build") 62 | expect((result.command as BuildCommand).options.configs).toEqual(["electron-release"]) 63 | }) 64 | 65 | test("should parse build command with complex config combinations correctly", () => { 66 | const result = parseArgs([ 67 | ...commonArgs, 68 | "build", 69 | "--configs", 70 | "darwin-arm64-node-release", 71 | "linux-x64-electron-debug", 72 | "win32-arm64", 73 | ])! 74 | expect(result.command.type).toEqual("build") 75 | expect((result.command as BuildCommand).options.configs).toEqual([ 76 | "darwin-arm64-node-release", 77 | "linux-x64-electron-debug", 78 | "win32-arm64", 79 | ]) 80 | }) 81 | 82 | test("should parse build command with named configs correctly", () => { 83 | const result = parseArgs([...commonArgs, "build", "--configs", "named-all"])! 84 | expect(result.command.type).toEqual("build") 85 | expect((result.command as BuildCommand).options.configs).toEqual(["named-all"]) 86 | }) 87 | 88 | test("should parse build command with debug flag correctly", () => { 89 | const spy = vi.spyOn(console, "debug") 90 | const result = parseArgs([...commonArgs, "build", "--logger", "debug", "--configs", "release"])! 91 | 92 | expect(result.command.type).toEqual("build") 93 | expect(result.logger).toEqual("debug") 94 | expect((result.command as BuildCommand).options.configs).toEqual(["release"]) 95 | expect(spy).toHaveBeenCalled() 96 | }) 97 | }) 98 | 99 | suite("debug mode", () => { 100 | test("should parse debug flag correctly", () => { 101 | const spy = vi.spyOn(console, "debug") 102 | 103 | const result = parseArgs([...commonArgs, "build", "--logger", "debug"])! 104 | expect(result.logger).toEqual("debug") 105 | expect(spy).toHaveBeenCalledWith( 106 | "\x1b[34m[DEBUG cmake-ts]\x1b[0m", 107 | "args", 108 | JSON.stringify( 109 | { 110 | command: { 111 | type: "build", 112 | }, 113 | all: false, 114 | nativeonly: false, 115 | osonly: false, 116 | devOsOnly: false, 117 | logger: "debug", 118 | }, 119 | null, 120 | 2, 121 | ), 122 | ) 123 | }) 124 | }) 125 | 126 | suite("help", () => { 127 | test("should parse help flag correctly", () => { 128 | let exitCode = 0 129 | process.exit = vi.fn((code?: number | null | undefined): never => { 130 | exitCode = code ?? 0 131 | throw new Error("process.exit was called") 132 | }) 133 | const spy = vi.spyOn(process.stdout, "write") 134 | 135 | expect(() => parseArgs([...commonArgs, "--help"])).toThrow("process.exit was called") 136 | expect(exitCode).toEqual(0) 137 | expect(spy).toHaveBeenCalledWith(expect.stringContaining("cmake-ts")) 138 | 139 | vi.unstubAllGlobals() 140 | }) 141 | }) 142 | 143 | suite("deprecated build mode flags", () => { 144 | test('should parse "all" flag correctly', () => { 145 | const result = parseArgs([...commonArgs, "all"])! 146 | expect(result.command.type).toEqual("build") 147 | expect((result.command as BuildCommand).options.configs).toEqual(["named-all"]) 148 | }) 149 | 150 | test('should parse "nativeonly" flag correctly', () => { 151 | const result = parseArgs([...commonArgs, "nativeonly"])! 152 | expect(result.command.type).toEqual("build") 153 | expect((result.command as BuildCommand).options.configs).toEqual(["release"]) 154 | }) 155 | 156 | test('should parse "osonly" flag correctly', () => { 157 | const result = parseArgs([...commonArgs, "osonly"])! 158 | expect(result.command.type).toEqual("build") 159 | expect((result.command as BuildCommand).options.configs).toEqual(["named-os"]) 160 | }) 161 | 162 | test('should parse "dev-os-only" flag correctly', () => { 163 | const result = parseArgs([...commonArgs, "dev-os-only"])! 164 | expect(result.command.type).toEqual("build") 165 | expect((result.command as BuildCommand).options.configs).toEqual(["named-os-dev"]) 166 | }) 167 | 168 | test('should parse "named-configs" with a single config correctly', () => { 169 | const result = parseArgs([...commonArgs, "named-configs", "config1"])! 170 | expect(result.command.type).toEqual("build") 171 | expect((result.command as BuildCommand).options.configs).toEqual(["config1"]) 172 | }) 173 | 174 | test('should parse "named-configs" with multiple configs as comma-separated string', () => { 175 | const result = parseArgs([...commonArgs, "named-configs", "config1,config2,config3"])! 176 | expect(result.command.type).toEqual("build") 177 | expect((result.command as BuildCommand).options.configs).toEqual(["config1", "config2", "config3"]) 178 | }) 179 | 180 | test('should parse "named-configs" with multiple configs as array', () => { 181 | // This test simulates how mri would handle multiple occurrences of the same flag 182 | const result = parseArgs([...commonArgs, "named-configs", "config1", "config2"])! 183 | expect(result.command.type).toEqual("build") 184 | expect((result.command as BuildCommand).options.configs).toEqual(["config1", "config2"]) 185 | }) 186 | }) 187 | }) 188 | -------------------------------------------------------------------------------- /test/config.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path" 2 | import { writeJson } from "fs-extra" 3 | import { afterEach, beforeEach, expect, suite, test, vi } from "vitest" 4 | import type { BuildCommandOptions, BuildConfiguration, BuildConfigurations, Options } from "../src/config-types.d.js" 5 | import { 6 | detectCrossCompilation, 7 | getBuildConfig, 8 | getConfigFile, 9 | parseBuildConfigs, 10 | parseBuiltInConfigs, 11 | } from "../src/config.js" 12 | import { logger } from "../src/lib.js" 13 | 14 | suite("Config Functions", () => { 15 | logger.setLevel("debug") 16 | 17 | const mockBuildOptions: BuildCommandOptions = { 18 | configs: [], 19 | addonSubdirectory: "", 20 | packageDirectory: process.cwd(), 21 | projectName: "test-project", 22 | targetDirectory: "build", 23 | stagingDirectory: "staging", 24 | help: false, 25 | } 26 | 27 | const mockConfigFile: Partial = { 28 | name: "test-config", 29 | configurations: [ 30 | { 31 | name: "linux-x64", 32 | os: "linux", 33 | arch: "x64", 34 | }, 35 | { 36 | name: "windows-x64", 37 | os: "win32", 38 | arch: "x64", 39 | }, 40 | ], 41 | } 42 | 43 | const testPackageJsonPath = join(process.cwd(), "test", "package-test.json") 44 | 45 | beforeEach(async () => { 46 | // Reset environment variables 47 | // biome-ignore lint/performance/noDelete: https://github.com/biomejs/biome/issues/5643 48 | delete process.env.npm_config_target_arch 49 | // biome-ignore lint/performance/noDelete: https://github.com/biomejs/biome/issues/5643 50 | delete process.env.npm_config_target_os 51 | 52 | // Create test package.json 53 | await writeJson(testPackageJsonPath, { 54 | name: "test-package", 55 | version: "1.0.0", 56 | "cmake-ts": mockConfigFile, 57 | }) 58 | }) 59 | 60 | afterEach(async () => { 61 | // Clean up environment variables 62 | // biome-ignore lint/performance/noDelete: https://github.com/biomejs/biome/issues/5643 63 | delete process.env.npm_config_target_arch 64 | // biome-ignore lint/performance/noDelete: https://github.com/biomejs/biome/issues/5643 65 | delete process.env.npm_config_target_os 66 | 67 | // Remove test package.json 68 | try { 69 | await writeJson(testPackageJsonPath, {}) 70 | } catch (err) { 71 | // Ignore errors if file doesn't exist 72 | } 73 | }) 74 | 75 | suite("parseBuildConfigs", () => { 76 | test("should return null for non-build commands", async () => { 77 | const result = await parseBuildConfigs( 78 | { command: { type: "none" }, logger: "info", help: false } as Options, 79 | mockConfigFile, 80 | ) 81 | expect(result).toBeNull() 82 | }) 83 | 84 | test("should build for current runtime/system when no configs specified", async () => { 85 | const result = await parseBuildConfigs( 86 | { command: { type: "build", options: mockBuildOptions }, logger: "info", help: false } as Options, 87 | mockConfigFile, 88 | ) 89 | expect(result).toHaveLength(1) 90 | expect(result![0].os).toBe(process.platform) 91 | expect(result![0].arch).toBe(process.arch) 92 | }) 93 | 94 | test("should build specified named configs", async () => { 95 | const options = { ...mockBuildOptions, configs: ["linux-x64"] } 96 | const result = await parseBuildConfigs( 97 | { command: { type: "build", options }, logger: "info", help: false } as Options, 98 | mockConfigFile, 99 | ) 100 | expect(result).toHaveLength(1) 101 | expect(result![0].name).toBe("linux-x64") 102 | expect(result![0].os).toBe("linux") 103 | expect(result![0].arch).toBe("x64") 104 | }) 105 | 106 | test("should use default values when no config file is provided", async () => { 107 | const result = await parseBuildConfigs( 108 | { command: { type: "build", options: mockBuildOptions }, logger: "info", help: false } as Options, 109 | {}, 110 | ) 111 | expect(result).toHaveLength(1) 112 | expect(result![0].os).toBe(process.platform) 113 | expect(result![0].arch).toBe(process.arch) 114 | expect(result![0].runtime).toBe("node") 115 | expect(result![0].buildType).toBe("Release") 116 | expect(result![0].dev).toBe(false) 117 | }) 118 | }) 119 | 120 | suite("getBuildConfig", () => { 121 | test("should merge configs correctly", async () => { 122 | const partialConfig: Partial = { 123 | name: "test-config", 124 | os: "linux", 125 | } 126 | 127 | const result = await getBuildConfig(mockBuildOptions, partialConfig, mockConfigFile) 128 | 129 | expect(result.name).toBe("test-config") 130 | expect(result.os).toBe("linux") 131 | expect(result.arch).toBe(process.arch) 132 | expect(result.runtime).toBe("node") 133 | expect(result.buildType).toBe("Release") 134 | }) 135 | 136 | test("should detect os cross compilation", async () => { 137 | const partialConfig: Partial = { 138 | os: process.platform === "win32" ? "linux" : "win32", 139 | arch: "x64", 140 | } 141 | 142 | const result = await getBuildConfig(mockBuildOptions, partialConfig, mockConfigFile) 143 | 144 | expect(result.cross).toBe(true) 145 | expect(result.os).toBe(process.platform === "win32" ? "linux" : "win32") 146 | }) 147 | 148 | test("should respect npm_config_target_arch when it matches config.arch", async () => { 149 | // Set npm_config_target_arch to a different architecture than the current one 150 | process.env.npm_config_target_arch = process.arch === "x64" ? "arm64" : "x64" 151 | 152 | const partialConfig: Partial = { 153 | os: process.platform, 154 | arch: process.env.npm_config_target_arch as NodeJS.Architecture, 155 | } 156 | 157 | const result = await getBuildConfig(mockBuildOptions, partialConfig, mockConfigFile) 158 | 159 | expect(result.cross).toBe(true) 160 | expect(result.arch).toBe(process.env.npm_config_target_arch) 161 | }) 162 | 163 | test("should respect config.arch when it matches process.arch", async () => { 164 | // Mock process.arch 165 | vi.spyOn(process, "arch", "get").mockReturnValue("arm64") 166 | 167 | const partialConfig: Partial = { 168 | os: process.platform, 169 | arch: process.arch, 170 | } 171 | 172 | console.log(process.arch, detectCrossCompilation(mockConfigFile, partialConfig)) 173 | 174 | const result = await getBuildConfig(mockBuildOptions, partialConfig, mockConfigFile) 175 | 176 | expect(result.cross).toBe(false) 177 | expect(result.arch).toBe(process.arch) 178 | 179 | vi.restoreAllMocks() 180 | }) 181 | 182 | test("should respect config.arch when it differs from process.arch", async () => { 183 | // Mock process.arch 184 | vi.spyOn(process, "arch", "get").mockReturnValue("arm64") 185 | 186 | const partialConfig: Partial = { 187 | os: process.platform, 188 | arch: "x64", // Different from process.arch 189 | } 190 | 191 | const result = await getBuildConfig(mockBuildOptions, partialConfig, mockConfigFile) 192 | 193 | expect(result.cross).toBe(true) 194 | expect(result.arch).toBe("x64") 195 | 196 | vi.restoreAllMocks() 197 | }) 198 | 199 | test("should respect config.arch when it differs from process.arch even if npm_config_target_arch is set", async () => { 200 | // Set npm_config_target_arch to a different architecture than the current one 201 | process.env.npm_config_target_arch = process.arch === "x64" ? "arm64" : "x64" 202 | 203 | const partialConfig: Partial = { 204 | os: process.platform, 205 | arch: process.arch, 206 | } 207 | 208 | const result = await getBuildConfig(mockBuildOptions, partialConfig, mockConfigFile) 209 | 210 | expect(result.cross).toBe(false) 211 | expect(result.arch).toBe(process.arch) 212 | }) 213 | 214 | test("should respect npm_config_target_arch when it differs from process.arch with default config", async () => { 215 | // Set npm_config_target_arch to a different architecture than the current one 216 | process.env.npm_config_target_arch = process.arch === "x64" ? "arm64" : "x64" 217 | 218 | const result = await getBuildConfig(mockBuildOptions, {}, mockConfigFile) 219 | 220 | expect(result.cross).toBe(true) 221 | expect(result.arch).toBe(process.env.npm_config_target_arch) 222 | expect(result.os).toBe(process.platform) 223 | }) 224 | 225 | test("should respect npm_config_target_os when it differs from process.platform with default config", async () => { 226 | // Set npm_config_target_os to a different platform than the current one 227 | process.env.npm_config_target_os = process.platform === "win32" ? "linux" : "win32" 228 | 229 | const result = await getBuildConfig(mockBuildOptions, {}, mockConfigFile) 230 | 231 | expect(result.cross).toBe(true) 232 | expect(result.os).toBe(process.env.npm_config_target_os) 233 | expect(result.arch).toBe(process.arch) 234 | }) 235 | 236 | test("should use default values when no config file is provided", async () => { 237 | const partialConfig: Partial = { 238 | name: "test-config", 239 | } 240 | 241 | const result = await getBuildConfig(mockBuildOptions, partialConfig, {}) 242 | 243 | expect(result.name).toBe("test-config") 244 | expect(result.os).toBe(process.platform) 245 | expect(result.arch).toBe(process.arch) 246 | expect(result.runtime).toBe("node") 247 | expect(result.buildType).toBe("Release") 248 | expect(result.dev).toBe(false) 249 | }) 250 | }) 251 | 252 | suite("parseBuiltInConfigs", () => { 253 | test("should parse valid config names", () => { 254 | const result = parseBuiltInConfigs("linux-x64-node-release") 255 | expect(result.os).toBe("linux") 256 | expect(result.arch).toBe("x64") 257 | expect(result.runtime).toBe("node") 258 | expect(result.buildType).toBe("Release") 259 | }) 260 | 261 | test("should handle cross compilation flag", () => { 262 | const result = parseBuiltInConfigs("linux-x64-cross") 263 | expect(result.os).toBe("linux") 264 | expect(result.arch).toBe("x64") 265 | expect(result.cross).toBe(true) 266 | }) 267 | 268 | test("should throw error for invalid config parts", () => { 269 | expect(() => parseBuiltInConfigs("invalid-os-x64")).toThrow() 270 | }) 271 | }) 272 | 273 | suite("getConfigFile", () => { 274 | test("should return config from package.json", async () => { 275 | const result = await getConfigFile(testPackageJsonPath) 276 | expect(result).toEqual(mockConfigFile) 277 | }) 278 | 279 | test("should return empty object when package.json is not found", async () => { 280 | const result = await getConfigFile(join(process.cwd(), "non-existent-package.json")) 281 | expect(result).toEqual({}) 282 | }) 283 | 284 | test("should return empty object when cmake-ts key is missing", async () => { 285 | await writeJson(testPackageJsonPath, { 286 | name: "test-package", 287 | version: "1.0.0", 288 | }) 289 | const result = await getConfigFile(testPackageJsonPath) 290 | expect(result).toEqual({}) 291 | }) 292 | }) 293 | }) 294 | -------------------------------------------------------------------------------- /test/download.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from "path" 2 | import { fileURLToPath } from "url" 3 | import { isCI } from "ci-info" 4 | import { ensureDir, pathExists, readFile, readdir, remove } from "fs-extra" 5 | import { beforeAll, expect, suite, test } from "vitest" 6 | import { calculateHash, downloadFile, downloadTgz, downloadToString } from "../src/utils/download.js" 7 | import { logger } from "../src/utils/logger.js" 8 | 9 | const _dirname = typeof __dirname === "string" ? __dirname : dirname(fileURLToPath(import.meta.url)) 10 | const root = dirname(_dirname) 11 | const testTmpDir = join(root, "test", ".tmp") 12 | 13 | const suiteFn = isCI ? suite : suite.concurrent 14 | 15 | suiteFn("Download Module", { timeout: 20_000, retry: 3 }, () => { 16 | logger.setLevel("debug") 17 | 18 | // Real Node.js distribution URLs for testing 19 | const nodeBaseUrl = "https://nodejs.org/dist/v16.0.0" 20 | const nodeHeadersUrl = `${nodeBaseUrl}/node-v16.0.0-headers.tar.gz` 21 | const nodeDocsUrl = `${nodeBaseUrl}/docs/apilinks.json` 22 | const nodeShasumUrl = `${nodeBaseUrl}/SHASUMS256.txt` 23 | 24 | beforeAll(async () => { 25 | await remove(testTmpDir) 26 | await ensureDir(testTmpDir) 27 | }) 28 | 29 | suite("downloadToString", () => { 30 | test("should download Node.js SHASUMS as a string", async () => { 31 | const content = await downloadToString(nodeShasumUrl) 32 | expect(content).toBeTruthy() 33 | expect(content).toContain("node-v16.0.0") 34 | }) 35 | 36 | test("should download Node.js API docs as a string", async () => { 37 | const content = await downloadToString(nodeDocsUrl) 38 | expect(content).toBeTruthy() 39 | expect(JSON.parse(content)).toHaveProperty("fs.readFileSync") 40 | }) 41 | }) 42 | 43 | suite("downloadFile", () => { 44 | test("should download Node.js SHASUMS to a file", async () => { 45 | const targetPath = join(testTmpDir, "SHASUMS256.txt") 46 | await downloadFile(nodeShasumUrl, { path: targetPath }) 47 | 48 | const exists = await pathExists(targetPath) 49 | expect(exists).toBe(true) 50 | 51 | const content = await readFile(targetPath, "utf8") 52 | expect(content).toContain("node-v16.0.0") 53 | }) 54 | 55 | test("should download Node.js API docs to a file", async () => { 56 | const targetPath = join(testTmpDir, "apilinks.json") 57 | await downloadFile(nodeDocsUrl, { path: targetPath }) 58 | 59 | const exists = await pathExists(targetPath) 60 | expect(exists).toBe(true) 61 | 62 | const content = await readFile(targetPath, "utf8") 63 | expect(JSON.parse(content)).toHaveProperty("fs.readFileSync") 64 | }) 65 | 66 | test("should download a file with hash verification", async () => { 67 | // First download the file to calculate its hash 68 | const tempPath = join(testTmpDir, "temp-shasums.txt") 69 | await downloadFile(nodeShasumUrl, { path: tempPath }) 70 | const hash = await calculateHash(tempPath, "sha256") 71 | 72 | // Now download with hash verification 73 | const targetPath = join(testTmpDir, "verified-shasums.txt") 74 | const result = await downloadFile(nodeShasumUrl, { 75 | path: targetPath, 76 | hashType: "sha256", 77 | hashSum: hash, 78 | }) 79 | 80 | expect(result).toBe(hash) 81 | 82 | const exists = await pathExists(targetPath) 83 | expect(exists).toBe(true) 84 | }) 85 | 86 | test("should throw an error if hash verification fails", async () => { 87 | const targetPath = join(testTmpDir, "hash-fail.txt") 88 | 89 | await expect( 90 | downloadFile(nodeShasumUrl, { 91 | path: targetPath, 92 | hashType: "sha256", 93 | hashSum: "invalid-hash", 94 | }), 95 | ).rejects.toThrow("Checksum mismatch") 96 | }) 97 | }) 98 | 99 | suite("downloadTgz", () => { 100 | test("should download and extract Node.js headers tar.gz file", async () => { 101 | const extractPath = join(testTmpDir, "node-headers") 102 | await ensureDir(extractPath) 103 | 104 | await downloadTgz(nodeHeadersUrl, { 105 | extractOptions: { cwd: extractPath }, 106 | }) 107 | 108 | // Check if files were extracted 109 | const files = await readdir(extractPath) 110 | expect(files.length).toBeGreaterThan(0) 111 | 112 | // Verify specific files that should be in the Node.js headers package 113 | const nodeDir = join(extractPath, "node-v16.0.0") 114 | expect(await pathExists(nodeDir)).toBe(true) 115 | 116 | // Check for include directory 117 | const includeDir = join(nodeDir, "include") 118 | expect(await pathExists(includeDir)).toBe(true) 119 | 120 | // Check for node.h file 121 | const nodeHeaderFile = join(includeDir, "node", "node.h") 122 | expect(await pathExists(nodeHeaderFile)).toBe(true) 123 | }) 124 | 125 | test("should support strip option for Node.js headers tar.gz file", async () => { 126 | const extractPath = join(testTmpDir, "node-headers-strip") 127 | await ensureDir(extractPath) 128 | 129 | await downloadTgz(nodeHeadersUrl, { 130 | extractOptions: { 131 | strip: 1, 132 | cwd: extractPath, 133 | }, 134 | }) 135 | 136 | // With strip=1, the node-v16.0.0 directory should be stripped 137 | // and the contents should be directly in the extract path 138 | const includeDir = join(extractPath, "include") 139 | expect(await pathExists(includeDir)).toBe(true) 140 | 141 | // Check for node.h file 142 | const nodeHeaderFile = join(includeDir, "node", "node.h") 143 | expect(await pathExists(nodeHeaderFile)).toBe(true) 144 | }) 145 | 146 | test("should verify the hash of the downloaded file", async () => { 147 | const extractPath = join(testTmpDir, "node-headers") 148 | await ensureDir(extractPath) 149 | 150 | const shasum = await downloadToString(nodeShasumUrl) 151 | const hashSums = parseSHASUM(shasum) 152 | const nodeHeadersHash = hashSums.find((h) => h.file === "node-v16.0.0-headers.tar.gz")?.hash 153 | expect(nodeHeadersHash).toBeDefined() 154 | 155 | const downloadPath = join(testTmpDir, "node-headers.tar.gz") 156 | await downloadTgz(nodeHeadersUrl, { 157 | path: downloadPath, 158 | hashSum: nodeHeadersHash, 159 | removeAfterExtract: false, 160 | extractOptions: { cwd: extractPath }, 161 | }) 162 | 163 | expect(await pathExists(downloadPath)).toBe(true) 164 | expect(await pathExists(join(extractPath, "node-v16.0.0"))).toBe(true) 165 | 166 | const hash = await calculateHash(downloadPath, "sha256") 167 | expect(hash).toBe(nodeHeadersHash) 168 | }) 169 | }) 170 | }) 171 | 172 | type HashSum = { hash: string; file: string } 173 | 174 | function parseSHASUM(content: string): HashSum[] { 175 | const lines = content.split("\n") 176 | return lines.map((line) => { 177 | const [hash, file] = line.split(" ") 178 | return { hash, file } 179 | }) 180 | } 181 | -------------------------------------------------------------------------------- /test/env.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, expect, suite, test } from "vitest" 2 | import { getEnvVar } from "../src/utils/env.ts" 3 | 4 | suite("getEnvVar", () => { 5 | const originalEnv = process.env 6 | 7 | beforeEach(() => { 8 | process.env = { ...originalEnv } 9 | }) 10 | 11 | afterEach(() => { 12 | process.env = originalEnv 13 | }) 14 | 15 | test("should return undefined for unset environment variables", () => { 16 | expect(getEnvVar("NON_EXISTENT_VAR")).toBeUndefined() 17 | }) 18 | 19 | test("should return undefined for empty environment variables", () => { 20 | process.env.EMPTY_VAR = "" 21 | expect(getEnvVar("EMPTY_VAR")).toBeUndefined() 22 | }) 23 | 24 | test("should return the value for set environment variables", () => { 25 | process.env.TEST_VAR = "test value" 26 | expect(getEnvVar("TEST_VAR")).toBe("test value") 27 | }) 28 | 29 | test("should handle environment variables with spaces", () => { 30 | process.env.SPACE_VAR = "test value with spaces" 31 | expect(getEnvVar("SPACE_VAR")).toBe("test value with spaces") 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/fs.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from "path" 2 | import { fileURLToPath } from "url" 3 | import { Stats, writeFile } from "fs-extra" 4 | import { expect, suite, test } from "vitest" 5 | import { NoStats, stat } from "../src/utils/fs.js" 6 | 7 | const _dirname = typeof __dirname === "string" ? __dirname : dirname(fileURLToPath(import.meta.url)) 8 | const root = dirname(_dirname) 9 | const testTmpDir = join(root, "test", ".tmp") 10 | 11 | suite("stat", () => { 12 | test("should return NoStats when stat fails", async () => { 13 | const result = await stat("nonexistent/path") 14 | expect(result).toBeInstanceOf(NoStats) 15 | }) 16 | 17 | test("should return stats when stat succeeds", async () => { 18 | // create a file 19 | const filePath = join(testTmpDir, "test.txt") 20 | await writeFile(filePath, "test") 21 | const result = await stat(filePath) 22 | expect(result).toBeInstanceOf(Stats) 23 | expect(result.isFile()).toBe(true) 24 | expect(result.isDirectory()).toBe(false) 25 | expect(result.isBlockDevice()).toBe(false) 26 | expect(result.isCharacterDevice()).toBe(false) 27 | expect(result.isSymbolicLink()).toBe(false) 28 | expect(result.isFIFO()).toBe(false) 29 | expect(result.isSocket()).toBe(false) 30 | }) 31 | }) 32 | 33 | suite("NoStats", () => { 34 | test("should return false for all file type checks", () => { 35 | const noStats = new NoStats() 36 | expect(noStats.isFile()).toBe(false) 37 | expect(noStats.isDirectory()).toBe(false) 38 | expect(noStats.isBlockDevice()).toBe(false) 39 | expect(noStats.isCharacterDevice()).toBe(false) 40 | expect(noStats.isSymbolicLink()).toBe(false) 41 | expect(noStats.isFIFO()).toBe(false) 42 | expect(noStats.isSocket()).toBe(false) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /test/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, expect, suite, test, vi } from "vitest" 2 | import { errorString, logger } from "../src/utils/logger.ts" 3 | 4 | suite("Logger", () => { 5 | const originalConsole = { ...console } 6 | const mockConsole = { 7 | error: vi.fn(), 8 | warn: vi.fn(), 9 | info: vi.fn(), 10 | debug: vi.fn(), 11 | trace: vi.fn(), 12 | } 13 | 14 | beforeEach(() => { 15 | Object.assign(console, mockConsole) 16 | vi.clearAllMocks() 17 | }) 18 | 19 | afterEach(() => { 20 | Object.assign(console, originalConsole) 21 | }) 22 | 23 | suite("setLevel", () => { 24 | test("should set the correct level for each log level", () => { 25 | logger.setLevel("off") 26 | logger.trace("test") 27 | logger.debug("test") 28 | logger.info("test") 29 | logger.warn("test") 30 | logger.error("test") 31 | expect(console.trace).not.toHaveBeenCalled() 32 | expect(console.debug).not.toHaveBeenCalled() 33 | expect(console.info).not.toHaveBeenCalled() 34 | expect(console.warn).not.toHaveBeenCalled() 35 | expect(console.error).not.toHaveBeenCalled() 36 | 37 | logger.setLevel("trace") 38 | logger.trace("test") 39 | expect(console.trace).toHaveBeenCalled() 40 | 41 | logger.setLevel("debug") 42 | logger.debug("test") 43 | expect(console.debug).toHaveBeenCalled() 44 | 45 | logger.setLevel("info") 46 | logger.info("test") 47 | expect(console.info).toHaveBeenCalled() 48 | 49 | logger.setLevel("warn") 50 | logger.warn("test") 51 | expect(console.warn).toHaveBeenCalled() 52 | 53 | logger.setLevel("error") 54 | logger.error("test") 55 | expect(console.error).toHaveBeenCalled() 56 | }) 57 | }) 58 | 59 | suite("log methods", () => { 60 | test("should log with the correct prefix and color", () => { 61 | logger.setLevel("info") 62 | logger.info("test message") 63 | expect(console.info).toHaveBeenCalledWith("\x1b[32m[INFO cmake-ts]\x1b[0m", "test message") 64 | }) 65 | 66 | test("should not log when level is below current level", () => { 67 | logger.setLevel("warn") 68 | logger.info("test message") 69 | expect(console.info).not.toHaveBeenCalled() 70 | }) 71 | }) 72 | }) 73 | 74 | suite("errorString", () => { 75 | test("should return stack trace for Error objects", () => { 76 | const error = new Error("test error") 77 | error.stack = "test stack trace" 78 | expect(errorString(error)).toBe("test stack trace") 79 | }) 80 | 81 | test("should return string representation for non-Error objects", () => { 82 | expect(errorString("test")).toBe("test") 83 | expect(errorString(123)).toBe("123") 84 | expect(errorString({ key: "value" })).toBe("[object Object]") 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /test/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "hasInstallScript": true, 8 | "dependencies": { 9 | "zeromq": "6.4.2" 10 | }, 11 | "devDependencies": { 12 | "patch-package": "8.0.0" 13 | } 14 | }, 15 | "node_modules/@yarnpkg/lockfile": { 16 | "version": "1.1.0", 17 | "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", 18 | "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", 19 | "dev": true, 20 | "license": "BSD-2-Clause" 21 | }, 22 | "node_modules/ansi-styles": { 23 | "version": "4.3.0", 24 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 25 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 26 | "dev": true, 27 | "license": "MIT", 28 | "dependencies": { 29 | "color-convert": "^2.0.1" 30 | }, 31 | "engines": { 32 | "node": ">=8" 33 | }, 34 | "funding": { 35 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 36 | } 37 | }, 38 | "node_modules/at-least-node": { 39 | "version": "1.0.0", 40 | "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", 41 | "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", 42 | "dev": true, 43 | "license": "ISC", 44 | "engines": { 45 | "node": ">= 4.0.0" 46 | } 47 | }, 48 | "node_modules/balanced-match": { 49 | "version": "1.0.2", 50 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 51 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 52 | "dev": true, 53 | "license": "MIT" 54 | }, 55 | "node_modules/brace-expansion": { 56 | "version": "1.1.11", 57 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 58 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 59 | "dev": true, 60 | "license": "MIT", 61 | "dependencies": { 62 | "balanced-match": "^1.0.0", 63 | "concat-map": "0.0.1" 64 | } 65 | }, 66 | "node_modules/braces": { 67 | "version": "3.0.3", 68 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 69 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 70 | "dev": true, 71 | "license": "MIT", 72 | "dependencies": { 73 | "fill-range": "^7.1.1" 74 | }, 75 | "engines": { 76 | "node": ">=8" 77 | } 78 | }, 79 | "node_modules/call-bind": { 80 | "version": "1.0.8", 81 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", 82 | "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", 83 | "dev": true, 84 | "license": "MIT", 85 | "dependencies": { 86 | "call-bind-apply-helpers": "^1.0.0", 87 | "es-define-property": "^1.0.0", 88 | "get-intrinsic": "^1.2.4", 89 | "set-function-length": "^1.2.2" 90 | }, 91 | "engines": { 92 | "node": ">= 0.4" 93 | }, 94 | "funding": { 95 | "url": "https://github.com/sponsors/ljharb" 96 | } 97 | }, 98 | "node_modules/call-bind-apply-helpers": { 99 | "version": "1.0.2", 100 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 101 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 102 | "dev": true, 103 | "license": "MIT", 104 | "dependencies": { 105 | "es-errors": "^1.3.0", 106 | "function-bind": "^1.1.2" 107 | }, 108 | "engines": { 109 | "node": ">= 0.4" 110 | } 111 | }, 112 | "node_modules/call-bound": { 113 | "version": "1.0.4", 114 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 115 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 116 | "dev": true, 117 | "license": "MIT", 118 | "dependencies": { 119 | "call-bind-apply-helpers": "^1.0.2", 120 | "get-intrinsic": "^1.3.0" 121 | }, 122 | "engines": { 123 | "node": ">= 0.4" 124 | }, 125 | "funding": { 126 | "url": "https://github.com/sponsors/ljharb" 127 | } 128 | }, 129 | "node_modules/chalk": { 130 | "version": "4.1.2", 131 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 132 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 133 | "dev": true, 134 | "license": "MIT", 135 | "dependencies": { 136 | "ansi-styles": "^4.1.0", 137 | "supports-color": "^7.1.0" 138 | }, 139 | "engines": { 140 | "node": ">=10" 141 | }, 142 | "funding": { 143 | "url": "https://github.com/chalk/chalk?sponsor=1" 144 | } 145 | }, 146 | "node_modules/ci-info": { 147 | "version": "3.9.0", 148 | "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", 149 | "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", 150 | "dev": true, 151 | "funding": [ 152 | { 153 | "type": "github", 154 | "url": "https://github.com/sponsors/sibiraj-s" 155 | } 156 | ], 157 | "license": "MIT", 158 | "engines": { 159 | "node": ">=8" 160 | } 161 | }, 162 | "node_modules/cmake-ts": { 163 | "version": "0.6.1", 164 | "resolved": "https://registry.npmjs.org/cmake-ts/-/cmake-ts-0.6.1.tgz", 165 | "integrity": "sha512-uUn2qGhf20j8W/sQ7+UnvvqO1zNccqgbLgwRJi7S23FsjMWJqxvKK80Vc+tvLNKfpJzwH0rgoQD1l24SMnX0yg==", 166 | "license": "MIT", 167 | "bin": { 168 | "cmake-ts": "build/main.js" 169 | } 170 | }, 171 | "node_modules/color-convert": { 172 | "version": "2.0.1", 173 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 174 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 175 | "dev": true, 176 | "license": "MIT", 177 | "dependencies": { 178 | "color-name": "~1.1.4" 179 | }, 180 | "engines": { 181 | "node": ">=7.0.0" 182 | } 183 | }, 184 | "node_modules/color-name": { 185 | "version": "1.1.4", 186 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 187 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 188 | "dev": true, 189 | "license": "MIT" 190 | }, 191 | "node_modules/concat-map": { 192 | "version": "0.0.1", 193 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 194 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 195 | "dev": true, 196 | "license": "MIT" 197 | }, 198 | "node_modules/cross-spawn": { 199 | "version": "7.0.6", 200 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 201 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 202 | "dev": true, 203 | "license": "MIT", 204 | "dependencies": { 205 | "path-key": "^3.1.0", 206 | "shebang-command": "^2.0.0", 207 | "which": "^2.0.1" 208 | }, 209 | "engines": { 210 | "node": ">= 8" 211 | } 212 | }, 213 | "node_modules/define-data-property": { 214 | "version": "1.1.4", 215 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 216 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 217 | "dev": true, 218 | "license": "MIT", 219 | "dependencies": { 220 | "es-define-property": "^1.0.0", 221 | "es-errors": "^1.3.0", 222 | "gopd": "^1.0.1" 223 | }, 224 | "engines": { 225 | "node": ">= 0.4" 226 | }, 227 | "funding": { 228 | "url": "https://github.com/sponsors/ljharb" 229 | } 230 | }, 231 | "node_modules/dunder-proto": { 232 | "version": "1.0.1", 233 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 234 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 235 | "dev": true, 236 | "license": "MIT", 237 | "dependencies": { 238 | "call-bind-apply-helpers": "^1.0.1", 239 | "es-errors": "^1.3.0", 240 | "gopd": "^1.2.0" 241 | }, 242 | "engines": { 243 | "node": ">= 0.4" 244 | } 245 | }, 246 | "node_modules/es-define-property": { 247 | "version": "1.0.1", 248 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 249 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 250 | "dev": true, 251 | "license": "MIT", 252 | "engines": { 253 | "node": ">= 0.4" 254 | } 255 | }, 256 | "node_modules/es-errors": { 257 | "version": "1.3.0", 258 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 259 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 260 | "dev": true, 261 | "license": "MIT", 262 | "engines": { 263 | "node": ">= 0.4" 264 | } 265 | }, 266 | "node_modules/es-object-atoms": { 267 | "version": "1.1.1", 268 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 269 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 270 | "dev": true, 271 | "license": "MIT", 272 | "dependencies": { 273 | "es-errors": "^1.3.0" 274 | }, 275 | "engines": { 276 | "node": ">= 0.4" 277 | } 278 | }, 279 | "node_modules/fill-range": { 280 | "version": "7.1.1", 281 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 282 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 283 | "dev": true, 284 | "license": "MIT", 285 | "dependencies": { 286 | "to-regex-range": "^5.0.1" 287 | }, 288 | "engines": { 289 | "node": ">=8" 290 | } 291 | }, 292 | "node_modules/find-yarn-workspace-root": { 293 | "version": "2.0.0", 294 | "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", 295 | "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", 296 | "dev": true, 297 | "license": "Apache-2.0", 298 | "dependencies": { 299 | "micromatch": "^4.0.2" 300 | } 301 | }, 302 | "node_modules/fs-extra": { 303 | "version": "9.1.0", 304 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", 305 | "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", 306 | "dev": true, 307 | "license": "MIT", 308 | "dependencies": { 309 | "at-least-node": "^1.0.0", 310 | "graceful-fs": "^4.2.0", 311 | "jsonfile": "^6.0.1", 312 | "universalify": "^2.0.0" 313 | }, 314 | "engines": { 315 | "node": ">=10" 316 | } 317 | }, 318 | "node_modules/fs.realpath": { 319 | "version": "1.0.0", 320 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 321 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 322 | "dev": true, 323 | "license": "ISC" 324 | }, 325 | "node_modules/function-bind": { 326 | "version": "1.1.2", 327 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 328 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 329 | "dev": true, 330 | "license": "MIT", 331 | "funding": { 332 | "url": "https://github.com/sponsors/ljharb" 333 | } 334 | }, 335 | "node_modules/get-intrinsic": { 336 | "version": "1.3.0", 337 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 338 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 339 | "dev": true, 340 | "license": "MIT", 341 | "dependencies": { 342 | "call-bind-apply-helpers": "^1.0.2", 343 | "es-define-property": "^1.0.1", 344 | "es-errors": "^1.3.0", 345 | "es-object-atoms": "^1.1.1", 346 | "function-bind": "^1.1.2", 347 | "get-proto": "^1.0.1", 348 | "gopd": "^1.2.0", 349 | "has-symbols": "^1.1.0", 350 | "hasown": "^2.0.2", 351 | "math-intrinsics": "^1.1.0" 352 | }, 353 | "engines": { 354 | "node": ">= 0.4" 355 | }, 356 | "funding": { 357 | "url": "https://github.com/sponsors/ljharb" 358 | } 359 | }, 360 | "node_modules/get-proto": { 361 | "version": "1.0.1", 362 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 363 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 364 | "dev": true, 365 | "license": "MIT", 366 | "dependencies": { 367 | "dunder-proto": "^1.0.1", 368 | "es-object-atoms": "^1.0.0" 369 | }, 370 | "engines": { 371 | "node": ">= 0.4" 372 | } 373 | }, 374 | "node_modules/glob": { 375 | "version": "7.2.3", 376 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 377 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 378 | "deprecated": "Glob versions prior to v9 are no longer supported", 379 | "dev": true, 380 | "license": "ISC", 381 | "dependencies": { 382 | "fs.realpath": "^1.0.0", 383 | "inflight": "^1.0.4", 384 | "inherits": "2", 385 | "minimatch": "^3.1.1", 386 | "once": "^1.3.0", 387 | "path-is-absolute": "^1.0.0" 388 | }, 389 | "engines": { 390 | "node": "*" 391 | }, 392 | "funding": { 393 | "url": "https://github.com/sponsors/isaacs" 394 | } 395 | }, 396 | "node_modules/gopd": { 397 | "version": "1.2.0", 398 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 399 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 400 | "dev": true, 401 | "license": "MIT", 402 | "engines": { 403 | "node": ">= 0.4" 404 | }, 405 | "funding": { 406 | "url": "https://github.com/sponsors/ljharb" 407 | } 408 | }, 409 | "node_modules/graceful-fs": { 410 | "version": "4.2.11", 411 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 412 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 413 | "dev": true, 414 | "license": "ISC" 415 | }, 416 | "node_modules/has-flag": { 417 | "version": "4.0.0", 418 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 419 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 420 | "dev": true, 421 | "license": "MIT", 422 | "engines": { 423 | "node": ">=8" 424 | } 425 | }, 426 | "node_modules/has-property-descriptors": { 427 | "version": "1.0.2", 428 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 429 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 430 | "dev": true, 431 | "license": "MIT", 432 | "dependencies": { 433 | "es-define-property": "^1.0.0" 434 | }, 435 | "funding": { 436 | "url": "https://github.com/sponsors/ljharb" 437 | } 438 | }, 439 | "node_modules/has-symbols": { 440 | "version": "1.1.0", 441 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 442 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 443 | "dev": true, 444 | "license": "MIT", 445 | "engines": { 446 | "node": ">= 0.4" 447 | }, 448 | "funding": { 449 | "url": "https://github.com/sponsors/ljharb" 450 | } 451 | }, 452 | "node_modules/hasown": { 453 | "version": "2.0.2", 454 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 455 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 456 | "dev": true, 457 | "license": "MIT", 458 | "dependencies": { 459 | "function-bind": "^1.1.2" 460 | }, 461 | "engines": { 462 | "node": ">= 0.4" 463 | } 464 | }, 465 | "node_modules/inflight": { 466 | "version": "1.0.6", 467 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 468 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 469 | "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", 470 | "dev": true, 471 | "license": "ISC", 472 | "dependencies": { 473 | "once": "^1.3.0", 474 | "wrappy": "1" 475 | } 476 | }, 477 | "node_modules/inherits": { 478 | "version": "2.0.4", 479 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 480 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 481 | "dev": true, 482 | "license": "ISC" 483 | }, 484 | "node_modules/is-docker": { 485 | "version": "2.2.1", 486 | "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", 487 | "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", 488 | "dev": true, 489 | "license": "MIT", 490 | "bin": { 491 | "is-docker": "cli.js" 492 | }, 493 | "engines": { 494 | "node": ">=8" 495 | }, 496 | "funding": { 497 | "url": "https://github.com/sponsors/sindresorhus" 498 | } 499 | }, 500 | "node_modules/is-number": { 501 | "version": "7.0.0", 502 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 503 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 504 | "dev": true, 505 | "license": "MIT", 506 | "engines": { 507 | "node": ">=0.12.0" 508 | } 509 | }, 510 | "node_modules/is-wsl": { 511 | "version": "2.2.0", 512 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", 513 | "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", 514 | "dev": true, 515 | "license": "MIT", 516 | "dependencies": { 517 | "is-docker": "^2.0.0" 518 | }, 519 | "engines": { 520 | "node": ">=8" 521 | } 522 | }, 523 | "node_modules/isarray": { 524 | "version": "2.0.5", 525 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", 526 | "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", 527 | "dev": true, 528 | "license": "MIT" 529 | }, 530 | "node_modules/isexe": { 531 | "version": "2.0.0", 532 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 533 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 534 | "dev": true, 535 | "license": "ISC" 536 | }, 537 | "node_modules/json-stable-stringify": { 538 | "version": "1.2.1", 539 | "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz", 540 | "integrity": "sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA==", 541 | "dev": true, 542 | "license": "MIT", 543 | "dependencies": { 544 | "call-bind": "^1.0.8", 545 | "call-bound": "^1.0.3", 546 | "isarray": "^2.0.5", 547 | "jsonify": "^0.0.1", 548 | "object-keys": "^1.1.1" 549 | }, 550 | "engines": { 551 | "node": ">= 0.4" 552 | }, 553 | "funding": { 554 | "url": "https://github.com/sponsors/ljharb" 555 | } 556 | }, 557 | "node_modules/jsonfile": { 558 | "version": "6.1.0", 559 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", 560 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 561 | "dev": true, 562 | "license": "MIT", 563 | "dependencies": { 564 | "universalify": "^2.0.0" 565 | }, 566 | "optionalDependencies": { 567 | "graceful-fs": "^4.1.6" 568 | } 569 | }, 570 | "node_modules/jsonify": { 571 | "version": "0.0.1", 572 | "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", 573 | "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", 574 | "dev": true, 575 | "license": "Public Domain", 576 | "funding": { 577 | "url": "https://github.com/sponsors/ljharb" 578 | } 579 | }, 580 | "node_modules/klaw-sync": { 581 | "version": "6.0.0", 582 | "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", 583 | "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", 584 | "dev": true, 585 | "license": "MIT", 586 | "dependencies": { 587 | "graceful-fs": "^4.1.11" 588 | } 589 | }, 590 | "node_modules/math-intrinsics": { 591 | "version": "1.1.0", 592 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 593 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 594 | "dev": true, 595 | "license": "MIT", 596 | "engines": { 597 | "node": ">= 0.4" 598 | } 599 | }, 600 | "node_modules/micromatch": { 601 | "version": "4.0.8", 602 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", 603 | "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", 604 | "dev": true, 605 | "license": "MIT", 606 | "dependencies": { 607 | "braces": "^3.0.3", 608 | "picomatch": "^2.3.1" 609 | }, 610 | "engines": { 611 | "node": ">=8.6" 612 | } 613 | }, 614 | "node_modules/minimatch": { 615 | "version": "3.1.2", 616 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 617 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 618 | "dev": true, 619 | "license": "ISC", 620 | "dependencies": { 621 | "brace-expansion": "^1.1.7" 622 | }, 623 | "engines": { 624 | "node": "*" 625 | } 626 | }, 627 | "node_modules/minimist": { 628 | "version": "1.2.8", 629 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 630 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 631 | "dev": true, 632 | "license": "MIT", 633 | "funding": { 634 | "url": "https://github.com/sponsors/ljharb" 635 | } 636 | }, 637 | "node_modules/node-addon-api": { 638 | "version": "8.3.1", 639 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", 640 | "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", 641 | "license": "MIT", 642 | "engines": { 643 | "node": "^18 || ^20 || >= 21" 644 | } 645 | }, 646 | "node_modules/object-keys": { 647 | "version": "1.1.1", 648 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 649 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 650 | "dev": true, 651 | "license": "MIT", 652 | "engines": { 653 | "node": ">= 0.4" 654 | } 655 | }, 656 | "node_modules/once": { 657 | "version": "1.4.0", 658 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 659 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 660 | "dev": true, 661 | "license": "ISC", 662 | "dependencies": { 663 | "wrappy": "1" 664 | } 665 | }, 666 | "node_modules/open": { 667 | "version": "7.4.2", 668 | "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", 669 | "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", 670 | "dev": true, 671 | "license": "MIT", 672 | "dependencies": { 673 | "is-docker": "^2.0.0", 674 | "is-wsl": "^2.1.1" 675 | }, 676 | "engines": { 677 | "node": ">=8" 678 | }, 679 | "funding": { 680 | "url": "https://github.com/sponsors/sindresorhus" 681 | } 682 | }, 683 | "node_modules/os-tmpdir": { 684 | "version": "1.0.2", 685 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 686 | "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", 687 | "dev": true, 688 | "license": "MIT", 689 | "engines": { 690 | "node": ">=0.10.0" 691 | } 692 | }, 693 | "node_modules/patch-package": { 694 | "version": "8.0.0", 695 | "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", 696 | "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", 697 | "dev": true, 698 | "license": "MIT", 699 | "dependencies": { 700 | "@yarnpkg/lockfile": "^1.1.0", 701 | "chalk": "^4.1.2", 702 | "ci-info": "^3.7.0", 703 | "cross-spawn": "^7.0.3", 704 | "find-yarn-workspace-root": "^2.0.0", 705 | "fs-extra": "^9.0.0", 706 | "json-stable-stringify": "^1.0.2", 707 | "klaw-sync": "^6.0.0", 708 | "minimist": "^1.2.6", 709 | "open": "^7.4.2", 710 | "rimraf": "^2.6.3", 711 | "semver": "^7.5.3", 712 | "slash": "^2.0.0", 713 | "tmp": "^0.0.33", 714 | "yaml": "^2.2.2" 715 | }, 716 | "bin": { 717 | "patch-package": "index.js" 718 | }, 719 | "engines": { 720 | "node": ">=14", 721 | "npm": ">5" 722 | } 723 | }, 724 | "node_modules/path-is-absolute": { 725 | "version": "1.0.1", 726 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 727 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 728 | "dev": true, 729 | "license": "MIT", 730 | "engines": { 731 | "node": ">=0.10.0" 732 | } 733 | }, 734 | "node_modules/path-key": { 735 | "version": "3.1.1", 736 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 737 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 738 | "dev": true, 739 | "license": "MIT", 740 | "engines": { 741 | "node": ">=8" 742 | } 743 | }, 744 | "node_modules/picomatch": { 745 | "version": "2.3.1", 746 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 747 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 748 | "dev": true, 749 | "license": "MIT", 750 | "engines": { 751 | "node": ">=8.6" 752 | }, 753 | "funding": { 754 | "url": "https://github.com/sponsors/jonschlinkert" 755 | } 756 | }, 757 | "node_modules/rimraf": { 758 | "version": "2.7.1", 759 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", 760 | "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", 761 | "deprecated": "Rimraf versions prior to v4 are no longer supported", 762 | "dev": true, 763 | "license": "ISC", 764 | "dependencies": { 765 | "glob": "^7.1.3" 766 | }, 767 | "bin": { 768 | "rimraf": "bin.js" 769 | } 770 | }, 771 | "node_modules/semver": { 772 | "version": "7.7.1", 773 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", 774 | "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", 775 | "dev": true, 776 | "license": "ISC", 777 | "bin": { 778 | "semver": "bin/semver.js" 779 | }, 780 | "engines": { 781 | "node": ">=10" 782 | } 783 | }, 784 | "node_modules/set-function-length": { 785 | "version": "1.2.2", 786 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 787 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 788 | "dev": true, 789 | "license": "MIT", 790 | "dependencies": { 791 | "define-data-property": "^1.1.4", 792 | "es-errors": "^1.3.0", 793 | "function-bind": "^1.1.2", 794 | "get-intrinsic": "^1.2.4", 795 | "gopd": "^1.0.1", 796 | "has-property-descriptors": "^1.0.2" 797 | }, 798 | "engines": { 799 | "node": ">= 0.4" 800 | } 801 | }, 802 | "node_modules/shebang-command": { 803 | "version": "2.0.0", 804 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 805 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 806 | "dev": true, 807 | "license": "MIT", 808 | "dependencies": { 809 | "shebang-regex": "^3.0.0" 810 | }, 811 | "engines": { 812 | "node": ">=8" 813 | } 814 | }, 815 | "node_modules/shebang-regex": { 816 | "version": "3.0.0", 817 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 818 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 819 | "dev": true, 820 | "license": "MIT", 821 | "engines": { 822 | "node": ">=8" 823 | } 824 | }, 825 | "node_modules/slash": { 826 | "version": "2.0.0", 827 | "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", 828 | "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", 829 | "dev": true, 830 | "license": "MIT", 831 | "engines": { 832 | "node": ">=6" 833 | } 834 | }, 835 | "node_modules/supports-color": { 836 | "version": "7.2.0", 837 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 838 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 839 | "dev": true, 840 | "license": "MIT", 841 | "dependencies": { 842 | "has-flag": "^4.0.0" 843 | }, 844 | "engines": { 845 | "node": ">=8" 846 | } 847 | }, 848 | "node_modules/tmp": { 849 | "version": "0.0.33", 850 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 851 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 852 | "dev": true, 853 | "license": "MIT", 854 | "dependencies": { 855 | "os-tmpdir": "~1.0.2" 856 | }, 857 | "engines": { 858 | "node": ">=0.6.0" 859 | } 860 | }, 861 | "node_modules/to-regex-range": { 862 | "version": "5.0.1", 863 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 864 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 865 | "dev": true, 866 | "license": "MIT", 867 | "dependencies": { 868 | "is-number": "^7.0.0" 869 | }, 870 | "engines": { 871 | "node": ">=8.0" 872 | } 873 | }, 874 | "node_modules/universalify": { 875 | "version": "2.0.1", 876 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", 877 | "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", 878 | "dev": true, 879 | "license": "MIT", 880 | "engines": { 881 | "node": ">= 10.0.0" 882 | } 883 | }, 884 | "node_modules/which": { 885 | "version": "2.0.2", 886 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 887 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 888 | "dev": true, 889 | "license": "ISC", 890 | "dependencies": { 891 | "isexe": "^2.0.0" 892 | }, 893 | "bin": { 894 | "node-which": "bin/node-which" 895 | }, 896 | "engines": { 897 | "node": ">= 8" 898 | } 899 | }, 900 | "node_modules/wrappy": { 901 | "version": "1.0.2", 902 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 903 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 904 | "dev": true, 905 | "license": "ISC" 906 | }, 907 | "node_modules/yaml": { 908 | "version": "2.7.1", 909 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", 910 | "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", 911 | "dev": true, 912 | "license": "ISC", 913 | "bin": { 914 | "yaml": "bin.mjs" 915 | }, 916 | "engines": { 917 | "node": ">= 14" 918 | } 919 | }, 920 | "node_modules/zeromq": { 921 | "version": "6.4.2", 922 | "resolved": "https://registry.npmjs.org/zeromq/-/zeromq-6.4.2.tgz", 923 | "integrity": "sha512-FnQlI4lEAewE4JexJ6kqQuBVzRf0Mg1n/qE3uXilfosf+X5lqJPiaYfdL/w4SzgAEVBTyqbMt9NbjwI5H89Yaw==", 924 | "hasInstallScript": true, 925 | "license": "MIT AND MPL-2.0", 926 | "dependencies": { 927 | "cmake-ts": "^0.6.1", 928 | "node-addon-api": "^8.3.0" 929 | }, 930 | "engines": { 931 | "node": ">= 12" 932 | } 933 | } 934 | } 935 | } 936 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "zeromq": "6.4.2" 4 | }, 5 | "devDependencies": { 6 | "patch-package": "8.0.0" 7 | }, 8 | "scripts": { 9 | "postinstall": "patch-package" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/patches/zeromq+6.4.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/zeromq/CMakeLists.txt b/node_modules/zeromq/CMakeLists.txt 2 | index 3ace20f..46f5da8 100644 3 | --- a/node_modules/zeromq/CMakeLists.txt 4 | +++ b/node_modules/zeromq/CMakeLists.txt 5 | @@ -65,15 +65,25 @@ endif() 6 | 7 | # target system on Windows (for cross-compiling x86) and static linking runtimes 8 | if(WIN32) 9 | - if("$ENV{Platform}" STREQUAL "x86") 10 | - set(CMAKE_SYSTEM_PROCESSOR "x86") 11 | - set(VCPKG_TARGET_TRIPLET "x86-windows-static") 12 | - elseif(NOT "$ENV{PROCESSOR_ARCHITEW6432}" STREQUAL "") 13 | - set(CMAKE_SYSTEM_PROCESSOR "$ENV{PROCESSOR_ARCHITEW6432}") 14 | + if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "") 15 | + if("$ENV{Platform}" STREQUAL "x86") 16 | + set(CMAKE_SYSTEM_PROCESSOR "x86") 17 | + elseif(NOT "$ENV{PROCESSOR_ARCHITEW6432}" STREQUAL "") 18 | + set(CMAKE_SYSTEM_PROCESSOR "$ENV{PROCESSOR_ARCHITEW6432}") 19 | + else() 20 | + set(CMAKE_SYSTEM_PROCESSOR "$ENV{PROCESSOR_ARCHITECTURE}") 21 | + endif() 22 | + endif() 23 | + 24 | + string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" CMAKE_SYSTEM_PROCESSOR_LOWER) 25 | + if("${CMAKE_SYSTEM_PROCESSOR_LOWER}" STREQUAL "amd64" OR "${CMAKE_SYSTEM_PROCESSOR_LOWER}" STREQUAL "x64") 26 | + set(VCPKG_TARGET_TRIPLET "x64-windows-static") 27 | + elseif("${CMAKE_SYSTEM_PROCESSOR_LOWER}" STREQUAL "arm64" OR "${CMAKE_SYSTEM_PROCESSOR_LOWER}" STREQUAL "aarch64") 28 | + set(VCPKG_TARGET_TRIPLET "arm64-windows-static") 29 | + elseif("${CMAKE_SYSTEM_PROCESSOR_LOWER}" STREQUAL "x86") 30 | set(VCPKG_TARGET_TRIPLET "x86-windows-static") 31 | else() 32 | - set(CMAKE_SYSTEM_PROCESSOR "$ENV{PROCESSOR_ARCHITECTURE}") 33 | - set(VCPKG_TARGET_TRIPLET "x64-windows-static") 34 | + message(STATUS "Not setting VCPKG_TARGET_TRIPLET for ${CMAKE_SYSTEM_PROCESSOR}") 35 | endif() 36 | 37 | # Avoid loading of project_optinos/WindowsToolchain 38 | -------------------------------------------------------------------------------- /test/retry.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, suite, test, vi } from "vitest" 2 | import { retry, sleep } from "../src/utils/retry.ts" 3 | 4 | suite("retry", () => { 5 | test("should succeed on first try", async () => { 6 | const fn = vi.fn().mockResolvedValueOnce("success") 7 | const result = await retry(fn) 8 | expect(result).toBe("success") 9 | expect(fn).toHaveBeenCalledTimes(1) 10 | }) 11 | 12 | test("should retry and succeed after failures", async () => { 13 | const fn = vi 14 | .fn() 15 | .mockRejectedValueOnce(new Error("first failure")) 16 | .mockRejectedValueOnce(new Error("second failure")) 17 | .mockResolvedValueOnce("success") 18 | 19 | const result = await retry(fn, 3, 0) 20 | expect(result).toBe("success") 21 | expect(fn).toHaveBeenCalledTimes(3) 22 | }) 23 | 24 | test("should throw after all retries fail", async () => { 25 | const error = new Error("all retries failed") 26 | const fn = vi.fn().mockRejectedValue(error) 27 | 28 | await expect(retry(fn, 3, 0)).rejects.toThrow("all retries failed") 29 | expect(fn).toHaveBeenCalledTimes(3) 30 | }) 31 | 32 | test("should use default retry count and delay", async () => { 33 | const fn = vi.fn().mockResolvedValue("success") 34 | await retry(fn) 35 | expect(fn).toHaveBeenCalledTimes(1) 36 | }) 37 | }) 38 | 39 | suite("sleep", () => { 40 | test("should resolve after specified time", async () => { 41 | const start = Date.now() 42 | await sleep(100) 43 | const end = Date.now() 44 | expect(end - start).toBeGreaterThanOrEqual(100) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/zeromq.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from "path" 2 | import { fileURLToPath } from "url" 3 | import { isCI } from "ci-info" 4 | import { execa } from "execa" 5 | import { remove } from "fs-extra" 6 | import { beforeAll, suite, test } from "vitest" 7 | import { HOME_DIRECTORY } from "../src/urlRegistry.js" 8 | import { testZeromqBuild } from "./zeromq.js" 9 | 10 | const _dirname = typeof __dirname === "string" ? __dirname : dirname(fileURLToPath(import.meta.url)) 11 | const root = dirname(_dirname) 12 | const zeromqPath = join(root, "test", "node_modules", "zeromq") 13 | 14 | const suiteFn = isCI ? suite : suite.concurrent 15 | 16 | suiteFn("zeromq", { timeout: 20 * 60 * 1000 }, () => { 17 | beforeAll(async () => { 18 | await Promise.all([ 19 | // build cmake-ts bundles 20 | execa("pnpm", ["build"], { 21 | stdio: "inherit", 22 | env: { 23 | ...process.env, 24 | NODE_ENV: "development", 25 | }, 26 | shell: true, 27 | cwd: root, 28 | }), 29 | // install zeromq 30 | execa("npm", ["install"], { 31 | stdio: "inherit", 32 | cwd: join(root, "test"), 33 | env: { 34 | ...process.env, 35 | NODE_ENV: "development", 36 | }, 37 | }), 38 | ]) 39 | console.log("Build completed") 40 | 41 | await Promise.all([ 42 | remove(join(HOME_DIRECTORY, ".cmake-ts")), 43 | remove(join(zeromqPath, "build")), 44 | remove(join(zeromqPath, "staging")), 45 | remove(join(zeromqPath, "cross-staging")), 46 | ]) 47 | }) 48 | 49 | // build 50 | suite("cmake-ts compile", () => { 51 | test("cmake-ts modern build --configs Debug --logger debug", async () => { 52 | await testZeromqBuild({ 53 | root, 54 | zeromqPath, 55 | bundle: "modern-main", 56 | args: ["build", "--configs", "Debug", "--logger", "debug"], 57 | }) 58 | }) 59 | 60 | // test legacy build command with deprecated options 61 | test("cmake-ts legacy nativeonly --logger debug", async () => { 62 | await testZeromqBuild({ root, zeromqPath, bundle: "legacy-main", args: ["nativeonly", "--logger", "debug"] }) 63 | }) 64 | }) 65 | 66 | // cross-compile 67 | 68 | test("cmake-ts cross-compile cross-darwin-x64", async (t) => { 69 | if (process.platform !== "darwin" || process.arch === "x64") { 70 | t.skip() 71 | } 72 | await testZeromqBuild({ 73 | root, 74 | zeromqPath, 75 | bundle: "modern-main", 76 | args: ["build", "--configs", "cross-darwin-x64", "--staging-directory", "cross-staging", "--logger", "debug"], 77 | }) 78 | }) 79 | 80 | test("cmake-ts cross-compile cross-linux-arm64", async (t) => { 81 | if (process.platform !== "linux" || process.arch === "arm64") { 82 | t.skip() 83 | } 84 | await testZeromqBuild({ 85 | root, 86 | zeromqPath, 87 | bundle: "modern-main", 88 | args: ["build", "--configs", "cross-linux-arm64", "--staging-directory", "cross-staging", "--logger", "debug"], 89 | }) 90 | }) 91 | 92 | test("cmake-ts cross-compile cross-win32-ia32", async (t) => { 93 | if (process.platform !== "win32" || process.arch === "ia32") { 94 | t.skip() 95 | } 96 | await testZeromqBuild({ 97 | root, 98 | zeromqPath, 99 | bundle: "modern-main", 100 | args: ["build", "--configs", "cross-win32-ia32", "--staging-directory", "cross-staging", "--logger", "debug"], 101 | }) 102 | }) 103 | 104 | test("cmake-ts cross-compile cross-win32-arm64", async (t) => { 105 | if (process.platform !== "win32" || process.arch === "arm64") { 106 | t.skip() 107 | } 108 | await testZeromqBuild({ 109 | root, 110 | zeromqPath, 111 | bundle: "modern-main", 112 | args: ["build", "--configs", "cross-win32-arm64", "--staging-directory", "cross-staging", "--logger", "debug"], 113 | }) 114 | }) 115 | 116 | test("cmake-ts cross-compile cross-darwin-arm64", async (t) => { 117 | if (process.platform !== "darwin" || process.arch === "arm64") { 118 | t.skip() 119 | } 120 | await testZeromqBuild({ 121 | root, 122 | zeromqPath, 123 | bundle: "modern-main", 124 | args: ["build", "--configs", "cross-darwin-arm64", "--staging-directory", "cross-staging", "--logger", "debug"], 125 | }) 126 | }) 127 | }) 128 | -------------------------------------------------------------------------------- /test/zeromq.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path" 2 | import { execa } from "execa" 3 | import { existsSync, readJson } from "fs-extra" 4 | import { assert, expect } from "vitest" 5 | import which from "which" 6 | import { parseArgs } from "../src/args.js" 7 | import { build } from "../src/build.js" 8 | import type { BuildConfiguration } from "../src/config-types.d" 9 | import { loadAddon } from "../src/loader.js" 10 | 11 | /** 12 | * The context of the test 13 | */ 14 | export type Ctx = { 15 | root: string 16 | zeromqPath: string 17 | bundle?: "modern-main" | "legacy-main" | "modern-library" | "legacy-library" 18 | args: string[] 19 | } 20 | 21 | /** 22 | * Test the zeromq build 23 | * @param ctx - The context of the test 24 | */ 25 | export async function testZeromqBuild(ctx: Ctx) { 26 | const bundle = ctx.bundle ?? "modern-library" 27 | 28 | // test via library 29 | if (bundle.endsWith("library")) { 30 | const opts = parseArgs() 31 | const configs = await build(opts) 32 | 33 | expect(configs).not.toBeNull() 34 | await Promise.all(configs!.map((config) => testZeromqBuildResults(config, ctx))) 35 | return 36 | } 37 | 38 | // test via main 39 | if (bundle.endsWith("main")) { 40 | const cmakeTsPath = join(ctx.root, `build/main.${bundle === "legacy-main" ? "js" : "mjs"}`) 41 | await execa(process.execPath, ["--enable-source-maps", cmakeTsPath, ...ctx.args], { 42 | stdio: "inherit", 43 | cwd: ctx.zeromqPath, 44 | }) 45 | const config = await findMainConfig(ctx, ctx.args) 46 | await testZeromqBuildResults(config, ctx) 47 | return 48 | } 49 | 50 | throw new Error(`Invalid bundle: ${bundle}`) 51 | } 52 | 53 | /** 54 | * Find the config that matches the expected props from the args 55 | * @param ctx - The context of the test 56 | * @param args - The args to parse 57 | * @returns The config that matches the expected props 58 | */ 59 | async function findMainConfig(ctx: Ctx, args: string[]) { 60 | const { os, arch, buildType, cross } = parseExpectedProps(args) 61 | 62 | const manifestPath = join(ctx.zeromqPath, "build", "manifest.json") 63 | const manifest = (await readJson(manifestPath)) as Record 64 | 65 | // find the config that matches the expected props 66 | const configKey = Object.keys(manifest).find((key) => { 67 | const parsedKey = JSON.parse(key) as BuildConfiguration 68 | return ( 69 | parsedKey.os === os && parsedKey.arch === arch && parsedKey.buildType === buildType && parsedKey.cross === cross 70 | ) 71 | }) 72 | if (configKey === undefined) { 73 | throw new Error("No config found for the expected props") 74 | } 75 | return JSON.parse(configKey) as BuildConfiguration 76 | } 77 | 78 | /** 79 | * Test the build results of the zeromq build 80 | * @param config - The config to test 81 | * @param ctx - The context of the test 82 | */ 83 | async function testZeromqBuildResults(config: BuildConfiguration, ctx: Ctx) { 84 | // check if the abi and libc are defined 85 | expect(config.abi).toBeDefined() 86 | expect(config.libc).toBeDefined() 87 | 88 | // check if the manifest file exists 89 | const manifestPath = join(ctx.zeromqPath, config.targetDirectory, "manifest.json") 90 | expect(existsSync(manifestPath), `Manifest file ${manifestPath} does not exist`).toBe(true) 91 | 92 | // read the manifest file 93 | const manifest = (await readJson(manifestPath)) as Record 94 | 95 | // check if the manifest contains the expected config 96 | const manifestKey = JSON.stringify(config) 97 | assert.hasAnyKeys(manifest, [manifestKey], "Manifest does not contain the expected config") 98 | 99 | // parse the expected props from the args 100 | const { os, arch, buildType, cross } = parseExpectedProps(ctx.args) 101 | 102 | const addonPath = manifest[manifestKey] 103 | 104 | // check if the addon.node file exists 105 | const expectedAddonPath = join(os, arch, "node", `${config.libc}-${config.abi}-${buildType}`, "addon.node") 106 | expect(addonPath).toEqual(expectedAddonPath) 107 | const addonNodePath = join(ctx.zeromqPath, config.targetDirectory, addonPath) 108 | expect(existsSync(addonNodePath), `Addon node file ${addonNodePath} does not exist`).toBe(true) 109 | 110 | // check if the config is correct 111 | const expectedConfig: BuildConfiguration = { 112 | name: "", 113 | dev: false, 114 | os, 115 | arch, 116 | runtime: "node", 117 | runtimeVersion: process.versions.node, 118 | buildType, 119 | packageDirectory: "", 120 | cross, 121 | projectName: "addon", 122 | nodeAPI: "node-addon-api", 123 | targetDirectory: "build", 124 | stagingDirectory: cross ? "cross-staging" : "staging", 125 | cmakeToUse: await which("cmake"), 126 | generatorToUse: "Ninja", 127 | generatorBinary: await which("ninja"), 128 | CMakeOptions: [], 129 | addonSubdirectory: "", 130 | additionalDefines: [], 131 | abi: config.abi, 132 | libc: config.libc, 133 | } 134 | expect(config).toEqual(expectedConfig) 135 | 136 | if (cross) { 137 | // skip loading the addon for cross-compiled builds 138 | return 139 | } 140 | const addon = loadAddon(join(ctx.zeromqPath, config.targetDirectory)) 141 | expect(addon).not.toBeNull() 142 | expect(addon).toBeTypeOf("object") 143 | expect(addon).toHaveProperty("version") 144 | expect(addon).toHaveProperty("capability") 145 | expect(addon).toHaveProperty("curveKeyPair") 146 | expect(addon).toHaveProperty("context") 147 | expect(addon).toHaveProperty("Context") 148 | expect(addon).toHaveProperty("Socket") 149 | expect(addon).toHaveProperty("Observer") 150 | expect(addon).toHaveProperty("Proxy") 151 | } 152 | 153 | /** 154 | * Parse the expected props from the args 155 | * @param args - The args to parse 156 | * @returns The expected props 157 | */ 158 | function parseExpectedProps(args: string[]) { 159 | const crossConfig = args.find((arg) => arg.includes("cross")) 160 | 161 | const os = (crossConfig?.split("-")[1] as NodeJS.Platform | undefined) ?? process.platform 162 | const arch = (crossConfig?.split("-")[2] as NodeJS.Architecture | undefined) ?? process.arch 163 | 164 | const buildType = args.includes("Debug") || args.some((arg) => arg.includes("-debug")) ? "Debug" : "Release" 165 | 166 | const cross = crossConfig !== undefined 167 | 168 | return { os, arch, buildType: buildType as BuildConfiguration["buildType"], cross } 169 | } 170 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "strictNullChecks": true, 5 | "noUnusedLocals": false, 6 | "noUnusedParameters": true, 7 | "noImplicitReturns": true, 8 | "noImplicitAny": true, 9 | "noImplicitThis": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "declaration": true, 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "incremental": true, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "preserveSymlinks": true, 19 | "removeComments": false, 20 | "skipLibCheck": true, 21 | "allowImportingTsExtensions": true, 22 | "noEmit": true, 23 | "lib": [ 24 | // target Node.js 12 https://node.green/#ES2019 25 | "ES2019", 26 | // https://node.green/#ES2020-features-String-prototype-matchAll 27 | "ES2020.String" 28 | ], 29 | "target": "ESNext", 30 | "allowJs": true, 31 | "esModuleInterop": true, 32 | "resolveJsonModule": true, 33 | "module": "ESNext", 34 | "moduleResolution": "node", 35 | "importHelpers": false 36 | }, 37 | "compileOnSave": false, 38 | "include": ["./src", "./*.mts", "test/**/*.ts", "test/**/*.mts"], 39 | "exclude": ["./node_modules", "./build", "test/fixtures"] 40 | } 41 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalEnv": ["OS", "RUNNER_OS", "BUILD_ANALYSIS"], 4 | "cacheDir": ".cache/turbo", 5 | "tasks": { 6 | "build.tsc": { 7 | "outputs": [] 8 | }, 9 | "build.tsc.lib": { 10 | "outputs": ["build/**/*.d.ts"] 11 | }, 12 | "build.legacy-main": { 13 | "outputs": ["build/main.js", "build/main.js.map"] 14 | }, 15 | "build.modern-main": { 16 | "outputs": ["build/main.mjs", "build/main.mjs.map"] 17 | }, 18 | "build.legacy-library": { 19 | "outputs": ["build/lib.js", "build/lib.js.map"] 20 | }, 21 | "build.modern-library": { 22 | "outputs": ["build/lib.mjs", "build/lib.mjs.map"] 23 | }, 24 | "lint.eslint": {}, 25 | "lint.biome": { 26 | "cache": false 27 | }, 28 | "lint.turbo": { 29 | "dependsOn": ["lint.eslint", "lint.biome"] 30 | }, 31 | "test.lint.eslint": {}, 32 | "test.lint.biome": { 33 | "cache": false 34 | }, 35 | "format.prettier": { 36 | "inputs": ["**/*.{yaml,yml,md}", "package.json", "pnpm-lock.yaml", "prettier.config.mjs", ".prettierignore"] 37 | }, 38 | "format.biome": { 39 | "cache": false 40 | }, 41 | "test.format.prettier": { 42 | "inputs": ["**/*.{yaml,yml,md}", "package.json", "pnpm-lock.yaml", "prettier.config.mjs", ".prettierignore"] 43 | }, 44 | "test.format.biome": { 45 | "cache": false 46 | } 47 | }, 48 | "ui": "stream" 49 | } 50 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import module from "module" 2 | import { type UserConfig, defineConfig } from "vite" 3 | import babel from "vite-plugin-babel" 4 | import babelConfig from "./babel.config.mts" 5 | 6 | // Instead of using TARGET env variable, we'll use Vite's mode 7 | export default defineConfig(async (configEnv) => { 8 | const isLegacy = configEnv.mode.includes("legacy") 9 | const isLibrary = configEnv.mode.includes("library") 10 | const isLoader = configEnv.mode.includes("loader") 11 | 12 | const plugins = [] 13 | 14 | if (isLegacy) { 15 | plugins.push( 16 | babel({ 17 | babelConfig, 18 | }), 19 | ) 20 | } 21 | if (process.env.BUILD_ANALYSIS === "true") { 22 | const visualizer = (await import("rollup-plugin-visualizer")).visualizer 23 | plugins.push( 24 | visualizer({ 25 | sourcemap: true, 26 | }), 27 | ) 28 | } 29 | 30 | return { 31 | build: { 32 | ssr: isLoader ? "./src/loader.ts" : isLibrary ? "./src/lib.ts" : "./src/main.ts", 33 | outDir: "./build", 34 | target: isLegacy ? "node12" : "node20", 35 | minify: process.env.NODE_ENV === "development" ? false : "esbuild", 36 | sourcemap: true, 37 | rollupOptions: { 38 | output: { 39 | format: isLegacy ? "cjs" : "es", 40 | }, 41 | }, 42 | emptyOutDir: false, 43 | }, 44 | resolve: { 45 | alias: { 46 | // unused dependency 47 | "@aws-sdk/client-s3": "./src/deps/aws-sdk-client-s3.ts", 48 | // deduplicate mkdirp via fs-extra 49 | mkdirp: "./src/deps/mkdirp.ts", 50 | }, 51 | }, 52 | ssr: { 53 | target: "node", 54 | noExternal: true, 55 | external: module.builtinModules as string[], 56 | }, 57 | plugins, 58 | } as UserConfig 59 | }) 60 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import ciInfo from "ci-info" 2 | const { GITHUB_ACTIONS } = ciInfo 3 | import { type ViteUserConfig, defineConfig, mergeConfig } from "vitest/config" 4 | import viteConfig from "./vite.config.mjs" 5 | 6 | export default defineConfig(async (configEnv) => { 7 | return mergeConfig(await viteConfig(configEnv), { 8 | test: { 9 | reporters: GITHUB_ACTIONS ? ["github-actions"] : ["default"], 10 | coverage: { 11 | provider: "v8", 12 | include: ["src/**/*.ts", "src/**/*.js", "test/**/*.ts"], 13 | reportOnFailure: true, 14 | reporters: GITHUB_ACTIONS ? ["text"] : ["test", "html"], 15 | }, 16 | include: ["test/**/*.test.ts", "test/**/*.test.mts"], 17 | typecheck: { 18 | enabled: true, 19 | tsconfig: "./tsconfig.json", 20 | }, 21 | }, 22 | } as ViteUserConfig) 23 | }) 24 | --------------------------------------------------------------------------------