diff --git a/.changeset/clear-peas-fly.md b/.changeset/clear-peas-fly.md new file mode 100644 index 00000000..32da5927 --- /dev/null +++ b/.changeset/clear-peas-fly.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": patch +--- + +Fix auto-linking failures due to lack of padding when renaming install name of libraries, by passing headerpad_max_install_names argument to linker. diff --git a/.changeset/tall-tips-fail.md b/.changeset/tall-tips-fail.md new file mode 100644 index 00000000..bedcf4c7 --- /dev/null +++ b/.changeset/tall-tips-fail.md @@ -0,0 +1,5 @@ +--- +"gyp-to-cmake": patch +--- + +Fixed escaping bundle ids to no longer contain "\_" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..bcd409ce --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,77 @@ +# Copilot Instructions for React Native Node-API + +This is a **monorepo** that brings Node-API support to React Native, enabling native addons written in C/C++/Rust to run on React Native across iOS and Android. + +## Package-Specific Instructions + +**IMPORTANT**: Before working on any package, always check for and read package-specific `copilot-instructions.md` files in the package directory. These contain critical preferences and patterns for that specific package. + +## Architecture Overview + +**Core Flow**: JS `require("./addon.node")` โ†’ Babel transform โ†’ `requireNodeAddon()` TurboModule call โ†’ native library loading โ†’ Node-API module initialization + +### Package Architecture + +See the [README.md](../README.md#packages) for detailed descriptions of each package and their roles in the system. Key packages include: + +- `packages/host` - Core Node-API runtime and Babel plugin +- `packages/cmake-rn` - CMake wrapper for native builds +- `packages/cmake-file-api` - TypeScript wrapper for CMake File API with Zod validation +- `packages/ferric` - Rust/Cargo wrapper with napi-rs integration +- `packages/gyp-to-cmake` - Legacy binding.gyp compatibility +- `apps/test-app` - Integration testing harness + +## Critical Build Dependencies + +- **Custom Hermes**: Currently depends on a patched Hermes with Node-API support (see [facebook/hermes#1377](https://github.com/facebook/hermes/pull/1377)) +- **Prebuilt Binary Spec**: All tools must output to the exact naming scheme: + - Android: `*.android.node/` with jniLibs structure + `react-native-node-api-module` marker file + - iOS: `*.apple.node` (XCFramework renamed) + marker file + +## Essential Workflows + +### Development Setup + +```bash +npm ci && npm run build # Install deps and build all packages +npm run bootstrap # Build native components (weak-node-api, examples) +``` + +### Package Development + +- **TypeScript project references**: Use `tsc --build` for incremental compilation +- **Workspace scripts**: Most build/test commands use npm workspaces (`--workspace` flag) +- **Focus on Node.js packages**: AI development primarily targets the Node.js tooling packages rather than native mobile code +- **No TypeScript type asserts**: You have to ask explicitly and justify if you want to add `as` type assertions. + +## Key Patterns + +### Babel Transformation + +The core magic happens in `packages/host/src/node/babel-plugin/plugin.ts`: + +```js +// Input: require("./addon.node") +// Output: require("react-native-node-api").requireNodeAddon("pkg-name--addon") +``` + +### CMake Integration + +For linking against Node-API in CMakeLists.txt: + +```cmake +include(${WEAK_NODE_API_CONFIG}) +target_link_libraries(addon PRIVATE weak-node-api) +``` + +### Cross-Platform Naming + +Library names use double-dash separation: `package-name--path-component--addon-name` + +### Testing + +- **Individual packages**: Some packages have VS Code test tasks and others have their own `npm test` scripts for focused iteration (e.g., `npm test --workspace cmake-rn`). Use the latter only if the former is missing. +- **Cross-package**: Use root-level `npm test` for cross-package testing once individual package tests pass +- **Mobile integration**: Available but not the primary AI development focus - ask the developer to run those tests as needed + +**Documentation**: Integration details, platform setup, and toolchain configuration are covered in existing repo documentation files. diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 53c7f734..99fa9365 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -3,6 +3,8 @@ name: Check env: # Version here should match the one in React Native template and packages/cmake-rn/src/cli.ts NDK_VERSION: 27.1.12297006 + # Building Hermes from source doesn't support CMake v4 + CMAKE_VERSION: 3.31.6 # Enabling the Gradle test on CI (disabled by default because it downloads a lot) ENABLE_GRADLE_TESTS: true @@ -23,9 +25,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: lts/jod + node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true # Set up JDK and Android SDK only because we need weak-node-api, to build ferric-example and to run the linting # TODO: Remove this once we have a way to run linting without building the native code - name: Set up JDK 17 @@ -40,14 +46,16 @@ jobs: - run: rustup target add x86_64-linux-android - run: npm ci - run: npm run build - # Bootstrap host package to get weak-node-api and ferric-example to get types + # Bootstrap weak-node-api and ferric-example to get types # TODO: Solve this by adding an option to ferric to build only types or by committing the types into the repo as a fixture for an "init" command - - run: npm run bootstrap --workspace react-native-node-api + - run: npm run bootstrap --workspace weak-node-api - run: npm run bootstrap --workspace @react-native-node-api/ferric-example - run: npm run lint env: DEBUG: eslint:eslint - run: npm run prettier:check + - run: npm run depcheck + - run: npm run publint unit-tests: strategy: fail-fast: false @@ -60,9 +68,13 @@ jobs: name: Unit tests (${{ matrix.runner }}) steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 + with: + node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 with: - node-version: lts/jod + clang-format: true - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -76,15 +88,49 @@ jobs: - run: npm ci - run: npm run bootstrap - run: npm test + weak-node-api-tests: + if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'weak-node-api') + strategy: + fail-fast: false + matrix: + runner: + - ubuntu-latest + - windows-latest + - macos-latest + runs-on: ${{ matrix.runner }} + name: Weak Node-API tests (${{ matrix.runner }}) + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v6 + with: + node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true + - run: npm ci + - run: npm run build + - name: Prepare weak-node-api + run: npm run prebuild:prepare --workspace weak-node-api + - name: Build and run weak-node-api C++ tests + run: | + cmake -S . -B build -DBUILD_TESTS=ON + cmake --build build + ctest --test-dir build --output-on-failure + working-directory: packages/weak-node-api test-ios: if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Apple ๐ŸŽ') name: Test app (iOS) runs-on: macos-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 + with: + node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 with: - node-version: lts/jod + clang-format: true - name: Set up JDK 17 uses: actions/setup-java@v3 with: @@ -98,7 +144,7 @@ jobs: - run: npm ci - run: npm run bootstrap env: - CMAKE_RN_TARGETS: arm64-apple-ios-sim + CMAKE_RN_TRIPLETS: arm64;x86_64-apple-ios-sim FERRIC_TARGETS: aarch64-apple-ios-sim - run: npm run pod-install working-directory: apps/test-app @@ -107,15 +153,56 @@ jobs: # TODO: Enable release mode when it works # run: npm run test:ios -- --mode Release working-directory: apps/test-app + test-macos: + # Disabling this on main for now, as initializing the template takes a long time and + # we don't have macOS-specific code yet + if: contains(github.event.pull_request.labels.*.name, 'MacOS ๐Ÿ’ป') + name: Test app (macOS) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v6 + with: + node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: "17" + distribution: "temurin" + # Install CMake 3 since 4.x may have compatibility issues with Hermes build system + - name: Install compatible CMake version + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: ${{ env.CMAKE_VERSION }} + - run: rustup target add x86_64-apple-darwin + - run: npm ci + - run: npm run bootstrap + env: + CMAKE_RN_TRIPLETS: arm64;x86_64-apple-darwin + FERRIC_TARGETS: aarch64-apple-darwin,x86_64-apple-darwin + - run: npm run init-macos-test-app + - run: pod install --project-directory=macos + working-directory: apps/macos-test-app + - name: Run MacOS test app + run: npm run test:allTests -- --mode Release + working-directory: apps/macos-test-app test-android: if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Android ๐Ÿค–') name: Test app (Android) - runs-on: ubuntu-latest + runs-on: ubuntu-self-hosted steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 + with: + node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 with: - node-version: lts/jod + clang-format: true - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -124,13 +211,13 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v3 with: - packages: tools platform-tools ndk;${{ env.NDK_VERSION }} + packages: tools platform-tools ndk;${{ env.NDK_VERSION }} cmake;${{ env.CMAKE_VERSION }} - run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim - run: npm ci - run: npm run bootstrap env: - CMAKE_RN_TARGETS: i686-linux-android - FERRIC_TARGETS: i686-linux-android + CMAKE_RN_TRIPLETS: x86_64-linux-android + FERRIC_TARGETS: x86_64-linux-android - name: Clone patched Hermes version shell: bash run: | @@ -151,9 +238,8 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Build weak-node-api for all architectures - run: npm run build-weak-node-api -- --android - working-directory: packages/host + - name: Build weak-node-api for all Android architectures + run: npm run prebuild:build:android --workspace weak-node-api - name: Build ferric-example for all architectures run: npm run build -- --android working-directory: packages/ferric-example @@ -163,9 +249,8 @@ jobs: with: api-level: 29 force-avd-creation: false - emulator-options: -no-snapshot-save -no-metrics -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true - arch: x86 + arch: x86_64 ndk: ${{ env.NDK_VERSION }} cmake: 3.22.1 working-directory: apps/test-app @@ -189,3 +274,55 @@ jobs: with: name: emulator-logcat path: apps/test-app/emulator-logcat.txt + test-ferric-apple-triplets: + if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Ferric ๐Ÿฆ€') + name: Test ferric Apple triplets + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v6 + with: + node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: "17" + distribution: "temurin" + - run: rustup target add x86_64-apple-darwin x86_64-apple-ios aarch64-apple-ios aarch64-apple-ios-sim + - run: rustup toolchain install nightly --component rust-src + - run: npm ci + - run: npm run build + - name: Build weak-node-api for all Apple architectures + run: | + npm run prebuild:prepare --workspace weak-node-api + npm run prebuild:build:apple --workspace weak-node-api + # Build Ferric example for all Apple architectures + - run: npx ferric --apple + working-directory: packages/ferric-example + - name: Inspect the structure of the prebuilt binary + run: lipo -info ferric_example.apple.node/*/libferric_example.framework/libferric_example > lipo-info.txt + working-directory: packages/ferric-example + - name: Upload lipo info + uses: actions/upload-artifact@v4 + with: + name: lipo-info + path: packages/ferric-example/lipo-info.txt + - name: Verify Apple triplet builds + run: | + # Create expected fixture content + cat > expected-lipo-info.txt << 'EOF' + Architectures in the fat file: ferric_example.apple.node/ios-arm64_x86_64-simulator/libferric_example.framework/libferric_example are: x86_64 arm64 + Architectures in the fat file: ferric_example.apple.node/macos-arm64_x86_64/libferric_example.framework/libferric_example are: x86_64 arm64 + Architectures in the fat file: ferric_example.apple.node/tvos-arm64_x86_64-simulator/libferric_example.framework/libferric_example are: x86_64 arm64 + Non-fat file: ferric_example.apple.node/ios-arm64/libferric_example.framework/libferric_example is architecture: arm64 + Non-fat file: ferric_example.apple.node/tvos-arm64/libferric_example.framework/libferric_example is architecture: arm64 + Non-fat file: ferric_example.apple.node/xros-arm64-simulator/libferric_example.framework/libferric_example is architecture: arm64 + Non-fat file: ferric_example.apple.node/xros-arm64/libferric_example.framework/libferric_example is architecture: arm64 + EOF + # Compare with expected fixture (will fail if files differ) + diff expected-lipo-info.txt lipo-info.txt + working-directory: packages/ferric-example diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f682950b..476f316b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,11 +15,16 @@ jobs: release: name: Release runs-on: macos-latest + environment: main steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: lts/jod + node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true - name: Set up JDK 17 uses: actions/setup-java@v3 with: @@ -30,13 +35,12 @@ jobs: with: packages: tools platform-tools ndk;${{ env.NDK_VERSION }} - run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim - - run: npm ci + - run: npm install - - name: Create Release Pull Request or Publish to npm + - name: Create Release Pull Request or Publish to NPM id: changesets uses: changesets/action@v1 with: publish: npm run release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 42f6c1a8..6a69674c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ node_modules/ dist/ *.tsbuildinfo + +# Treading the MacOS app as ephemeral +apps/macos-test-app + +# Cache used by the rust analyzer +target/rust-analyzer/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..f0c624e1 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Test cmake-file-api", + "command": "node", + "args": ["--run", "test"], + "options": { + "cwd": "${workspaceFolder}/packages/cmake-file-api" + }, + "group": "test" + } + ] +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..bf17f03d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025-present, Callstack and React Native Node API contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/test-app/CHANGELOG.md b/apps/test-app/CHANGELOG.md index d4c88c46..8e13a111 100644 --- a/apps/test-app/CHANGELOG.md +++ b/apps/test-app/CHANGELOG.md @@ -1,5 +1,27 @@ # react-native-node-api-test-app +## 0.2.2 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts ๐Ÿ™ˆ +- Updated dependencies [1dee80f] + - @react-native-node-api/ferric-example@0.1.2 + - react-native-node-api@1.0.1 + - @react-native-node-api/node-addon-examples@0.1.1 + - @react-native-node-api/node-tests@0.1.1 + - weak-node-api@0.1.1 + +## 0.2.1 + +### Patch Changes + +- 7ff2c2b: Fix minor package issues. +- Updated dependencies [7ff2c2b] +- Updated dependencies [7ff2c2b] + - weak-node-api@0.0.3 + - react-native-node-api@0.7.1 + ## 0.2.0 ### Minor Changes diff --git a/apps/test-app/babel.config.js b/apps/test-app/babel.config.js index 8ea846e2..1eecb8fa 100644 --- a/apps/test-app/babel.config.js +++ b/apps/test-app/babel.config.js @@ -1,5 +1,5 @@ module.exports = { presets: ["module:@react-native/babel-preset"], - // plugins: [['module:react-native-node-api/babel-plugin', { stripPathSuffix: true }]], + // plugins: [['module:react-native-node-api/babel-plugin', { packageName: "strip", pathSuffix: "strip" }]], plugins: ["module:react-native-node-api/babel-plugin"], }; diff --git a/apps/test-app/metro.config.js b/apps/test-app/metro.config.js index 2c321b49..95e22157 100644 --- a/apps/test-app/metro.config.js +++ b/apps/test-app/metro.config.js @@ -1,5 +1,6 @@ const { makeMetroConfig } = require("@rnx-kit/metro-config"); -module.exports = makeMetroConfig({ + +const config = makeMetroConfig({ transformer: { getTransformOptions: async () => ({ transform: { @@ -9,3 +10,13 @@ module.exports = makeMetroConfig({ }), }, }); + +if (config.watchFolders.length === 0) { + // This patch is needed to locate packages in the monorepo from the MacOS app + // which is intentionally kept outside of the workspaces configuration to prevent + // duplicate react-native version and pollution of the package lock. + const path = require("node:path"); + config.watchFolders.push(path.resolve(__dirname, "../..")); +} + +module.exports = config; diff --git a/apps/test-app/package.json b/apps/test-app/package.json index 10c33840..e2641812 100644 --- a/apps/test-app/package.json +++ b/apps/test-app/package.json @@ -1,12 +1,14 @@ { "name": "@react-native-node-api/test-app", "private": true, - "version": "0.2.0", + "type": "commonjs", + "version": "0.2.2", "scripts": { "metro": "react-native start --no-interactive", "android": "react-native run-android --no-packager --active-arch-only", "ios": "react-native run-ios --no-packager", "pod-install": "cd ios && pod install", + "mocha-and-metro": "mocha-remote --watch -- react-native start", "test:android": "mocha-remote --exit-on-error -- concurrently --kill-others-on-fail --passthrough-arguments npm:metro 'npm:android -- {@}' --", "test:android:allTests": "MOCHA_REMOTE_CONTEXT=allTests node --run test:android -- ", "test:android:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples node --run test:android -- ", @@ -41,6 +43,7 @@ "react": "19.1.0", "react-native": "0.81.4", "react-native-node-api": "*", - "react-native-test-app": "^4.4.7" + "react-native-test-app": "^4.4.7", + "weak-node-api": "*" } } diff --git a/configs/tsconfig.node-tests.json b/configs/tsconfig.node-tests.json new file mode 100644 index 00000000..2b914535 --- /dev/null +++ b/configs/tsconfig.node-tests.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "declarationMap": false + }, + "include": ["${configDir}/src/**/*.test.ts"], + "exclude": [] +} diff --git a/configs/tsconfig.node.json b/configs/tsconfig.node.json new file mode 100644 index 00000000..ff37e16c --- /dev/null +++ b/configs/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "composite": true, + "declarationMap": true, + "outDir": "${configDir}/dist", + "rootDir": "${configDir}/src", + "types": ["node"] + }, + "include": ["${configDir}/src/"], + "exclude": ["${configDir}/**/*.test.ts"] +} diff --git a/docs/WEAK-NODE-API.md b/docs/WEAK-NODE-API.md new file mode 100644 index 00000000..0664a3b6 --- /dev/null +++ b/docs/WEAK-NODE-API.md @@ -0,0 +1,7 @@ +# The `weak-node-api` library + +Android's dynamic linker imposes restrictions on the access to global symbols (such as the Node-API free functions): A dynamic library must explicitly declare any dependency bringing symbols it needs as `DT_NEEDED`. + +The implementation of Node-API is split between Hermes and our host package and to avoid addons having to explicitly link against either, we've introduced a `weak-node-api` library (published in `react-native-node-api` package). This library exposes only Node-API and will have its implementation injected by the host. + +While technically not a requirement on non-Android platforms, we choose to make this the general approach across React Native platforms. This keeps things aligned across platforms, while exposing just the Node-API without forcing libraries to build with suppression of errors for undefined symbols. diff --git a/eslint.config.js b/eslint.config.js index c0af837e..e1bacb32 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -47,6 +47,7 @@ export default tseslint.config( { files: [ "apps/test-app/*.js", + "apps/macos-test-app/*.js", "packages/node-addon-examples/**/*.js", "packages/host/babel-plugin.js", "packages/host/react-native.config.js", @@ -68,6 +69,7 @@ export default tseslint.config( }, { files: [ + "**/metro.config.js", "packages/gyp-to-cmake/bin/*.js", "packages/host/bin/*.mjs", "packages/host/scripts/*.mjs", diff --git a/package-lock.json b/package-lock.json index 98108da6..2552398b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,14 +7,17 @@ "name": "@react-native-node-api/root", "license": "MIT", "workspaces": [ - "apps/test-app", - "packages/gyp-to-cmake", + "packages/cli-utils", + "packages/cmake-file-api", + "packages/weak-node-api", "packages/cmake-rn", "packages/ferric", + "packages/gyp-to-cmake", "packages/host", "packages/node-addon-examples", "packages/node-tests", - "packages/ferric-example" + "packages/ferric-example", + "apps/test-app" ], "devDependencies": { "@changesets/cli": "^2.29.5", @@ -24,19 +27,22 @@ "@tsconfig/node22": "^22.0.0", "@tsconfig/react-native": "3.0.6", "@types/node": "^22", + "depcheck": "^1.4.7", "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.8", "globals": "^16.0.0", "prettier": "^3.6.2", + "publint": "^0.3.15", "react-native": "0.81.4", - "tsx": "^4.20.5", + "read-pkg": "^9.0.1", + "tsx": "^4.20.6", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" } }, "apps/test-app": { "name": "@react-native-node-api/test-app", - "version": "0.2.0", + "version": "0.2.2", "dependencies": { "@babel/core": "^7.26.10", "@babel/preset-env": "^7.26.9", @@ -60,7 +66,8 @@ "react": "19.1.0", "react-native": "0.81.4", "react-native-node-api": "*", - "react-native-test-app": "^4.4.7" + "react-native-test-app": "^4.4.7", + "weak-node-api": "*" } }, "node_modules/@actions/core": { @@ -130,6 +137,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -383,9 +391,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -428,12 +436,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -1926,13 +1934,13 @@ } }, "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2276,16 +2284,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", @@ -2912,6 +2910,17 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@expo/plist": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.7.tgz", + "integrity": "sha512-dGxqHPvCZKeRKDU1sJZMmuyVtcASuSYh1LPFVaM1DuffqPL36n6FMEL0iUqq2Tx3xhWk8wCnWl34IKplUjJDdA==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.2.3", + "xmlbuilder": "^15.1.1" + } + }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -4773,6 +4782,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.4.tgz", "integrity": "sha512-jOT8V1Ba5BdC79sKrRWDdMT5l1R+XNHTPR6CPWzUP2EcfAcvIHZWF0eAbmRcpOOP5gVIwnqNg0C4nvh6Abc3OA==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", @@ -5302,6 +5312,19 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/@publint/pack": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.2.tgz", + "integrity": "sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, "node_modules/@react-native-community/cli": { "version": "20.0.2", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.0.2.tgz", @@ -5404,63 +5427,6 @@ "yaml": "^2.2.1" } }, - "node_modules/@react-native-community/cli-doctor/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@react-native-community/cli-doctor/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -5473,12 +5439,6 @@ "node": ">=10" } }, - "node_modules/@react-native-community/cli-doctor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/@react-native-community/cli-platform-android": { "version": "20.0.2", "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-20.0.2.tgz", @@ -5550,18 +5510,6 @@ "semver": "^7.5.2" } }, - "node_modules/@react-native-community/cli-tools/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@react-native-community/cli-tools/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5578,15 +5526,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@react-native-community/cli-tools/node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@react-native-community/cli-tools/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5602,29 +5541,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@react-native-community/cli-tools/node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@react-native-community/cli-tools/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5655,19 +5571,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@react-native-community/cli-tools/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@react-native-community/cli-tools/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -5680,12 +5583,6 @@ "node": ">=10" } }, - "node_modules/@react-native-community/cli-tools/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/@react-native-community/cli-tools/node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -5806,6 +5703,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@react-native-node-api/cli-utils": { + "resolved": "packages/cli-utils", + "link": true + }, "node_modules/@react-native-node-api/ferric-example": { "resolved": "packages/ferric-example", "link": true @@ -6066,6 +5967,7 @@ "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.81.4.tgz", "integrity": "sha512-aEXhRMsz6yN5X63Zk+cdKByQ0j3dsKv+ETRP9lLARdZ82fBOCMuK6IfmZMwK3A/3bI7gSvt2MFPn3QHy3WnByw==", "license": "MIT", + "peer": true, "dependencies": { "@react-native/js-polyfills": "0.81.4", "@react-native/metro-babel-transformer": "0.81.4", @@ -6671,6 +6573,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mocha": { "version": "10.0.10", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", @@ -6678,10 +6587,11 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.18.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.5.tgz", - "integrity": "sha512-g9BpPfJvxYBXUWI9bV37j6d6LTMNQ88hPwdWWUeYZnMhlo66FIg9gCc1/DZb15QylJSKwOZjwrckvOTWpOiChg==", + "version": "22.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", + "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -6692,6 +6602,13 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "license": "MIT" }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", @@ -6768,6 +6685,7 @@ "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", @@ -6999,6 +6917,76 @@ "integrity": "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==", "license": "MIT" }, + "node_modules/@vue/compiler-core": { + "version": "3.5.23", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.23.tgz", + "integrity": "sha512-nW7THWj5HOp085ROk65LwaoxuzDsjIxr485F4iu63BoxsXoSqKqmsUUoP4A7Gl67DgIgi0zJ8JFgHfvny/74MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.23", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.23", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.23.tgz", + "integrity": "sha512-AT8RMw0vEzzzO0JU5gY0F6iCzaWUIh/aaRVordzMBKXRpoTllTT4kocHDssByPsvodNCfump/Lkdow2mT/O5KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.23", + "@vue/shared": "3.5.23" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.23", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.23.tgz", + "integrity": "sha512-3QTEUo4qg7FtQwaDJa8ou1CUikx5WTtZlY61rRRDu3lK2ZKrGoAGG8mvDgOpDsQ4A1bez9s+WtBB6DS2KuFCPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.23", + "@vue/compiler-dom": "3.5.23", + "@vue/compiler-ssr": "3.5.23", + "@vue/shared": "3.5.23", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.23", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.23.tgz", + "integrity": "sha512-Hld2xphbMjXs9Q9WKxPf2EqmE+Rq/FEDnK/wUBtmYq74HCV4XDdSCheAaB823OQXIIFGq9ig/RbAZkF9s4U0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.23", + "@vue/shared": "3.5.23" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.23", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.23.tgz", + "integrity": "sha512-0YZ1DYuC5o/YJPf6pFdt2KYxVGDxkDbH/1NYJnVJWUkzr8ituBEmFVQRNX2gCaAsFEjEDnLkWpgqlZA7htgS/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -7038,6 +7026,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7219,6 +7208,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -7229,6 +7228,16 @@ "node": ">=8" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -7257,13 +7266,6 @@ "node": ">=4" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -7618,6 +7620,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -7766,6 +7769,15 @@ "node": ">=4" } }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -7925,36 +7937,16 @@ "node": ">=8" } }, - "node_modules/clang-format": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.8.0.tgz", - "integrity": "sha512-pK8gzfu55/lHzIpQ1givIbWfn3eXnU7SfxqIwVgnn5jEM6j4ZJYjpFqFs4iSBPNedzRMmfjYjuQhu657WAXHXw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.3", - "glob": "^7.0.0", - "resolve": "^1.1.6" - }, - "bin": { - "check-clang-format": "bin/check-clang-format.js", - "clang-format": "index.js", - "git-clang-format": "bin/git-clang-format" - } - }, "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "license": "MIT", "dependencies": { - "restore-cursor": "^5.0.0" + "restore-cursor": "^3.1.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/cli-spinners": { @@ -8007,50 +7999,21 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/clone": { @@ -8062,6 +8025,10 @@ "node": ">=0.8" } }, + "node_modules/cmake-file-api": { + "resolved": "packages/cmake-file-api", + "link": true + }, "node_modules/cmake-js": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-7.3.1.tgz", @@ -8549,6 +8516,182 @@ "dev": true, "license": "MIT" }, + "node_modules/depcheck": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.7.tgz", + "integrity": "sha512-1lklS/bV5chOxwNKA/2XUUk/hPORp8zihZsXflr8x0kLwmcZ9Y9BsS6Hs3ssvA+2wUVbG0U2Ciqvm1SokNjPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.2", + "@vue/compiler-sfc": "^3.3.4", + "callsite": "^1.0.0", + "camelcase": "^6.3.0", + "cosmiconfig": "^7.1.0", + "debug": "^4.3.4", + "deps-regex": "^0.2.0", + "findup-sync": "^5.0.0", + "ignore": "^5.2.4", + "is-core-module": "^2.12.0", + "js-yaml": "^3.14.1", + "json5": "^2.2.3", + "lodash": "^4.17.21", + "minimatch": "^7.4.6", + "multimatch": "^5.0.0", + "please-upgrade-node": "^3.2.0", + "readdirp": "^3.6.0", + "require-package-name": "^2.0.1", + "resolve": "^1.22.3", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "yargs": "^16.2.0" + }, + "bin": { + "depcheck": "bin/depcheck.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/depcheck/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/depcheck/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/depcheck/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/depcheck/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/depcheck/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/depcheck/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8558,6 +8701,13 @@ "node": ">= 0.8" } }, + "node_modules/deps-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/deps-regex/-/deps-regex-0.2.0.tgz", + "integrity": "sha512-PwuBojGMQAYbWkMXOY9Pd/NWCDNHVH12pnS7WHqZkTSeMESe4hwnKKRp0yR87g37113x4JPbo/oIvXY+s/f56Q==", + "dev": true, + "license": "MIT" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -8568,6 +8718,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -8647,9 +8807,9 @@ } }, "node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/encodeurl": { @@ -8661,16 +8821,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -8685,6 +8835,19 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -8858,6 +9021,7 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9104,6 +9268,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -9160,6 +9331,19 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", @@ -9396,6 +9580,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -9509,13 +9709,6 @@ "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT" - }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -9624,23 +9817,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/gauge/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/gauge/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/gauge/node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -9648,21 +9824,6 @@ "dev": true, "license": "ISC" }, - "node_modules/gauge/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -9798,6 +9959,51 @@ "node": ">=10.13.0" } }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", @@ -9958,7 +10164,20 @@ "hermes-estree": "0.29.1" } }, - "node_modules/hosted-git-info": { + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", @@ -10295,15 +10514,12 @@ } }, "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/is-nan": { @@ -10832,6 +11048,13 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", @@ -10923,35 +11146,6 @@ "node": ">=0.10.0" } }, - "node_modules/logkitty/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/logkitty/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/logkitty/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/logkitty/node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -11014,6 +11208,16 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -11568,13 +11772,6 @@ "node": ">=10" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT" - }, "node_modules/mocha": { "version": "11.7.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.2.tgz", @@ -11893,6 +12090,26 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -11902,6 +12119,25 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -11927,32 +12163,6 @@ "node": ">=12.0.0" } }, - "node_modules/node-abi": { - "version": "3.77.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", - "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", @@ -11965,12 +12175,11 @@ }, "node_modules/node-addon-examples": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/nodejs/node-addon-examples.git#4213d4c9d07996ae68629c67926251e117f8e52a", - "integrity": "sha512-4HfGZCsD1IwOx213KjizaXTgEFb7+sXi3dtlay1kERtHRnXMeTsMzruRrSQmm5f1QkVqK3sZPawc1+EW135s4Q==", + "resolved": "git+ssh://git@github.com/nodejs/node-addon-examples.git#4b7dd86a85644610e6de80154df9acac9329b509", + "integrity": "sha512-9bQgZbEIjN7umKgCT4z8781K8h+2EtAaMgXhByll8arccTjSTGyO8ipUngV14ieo58cf89ghM3Jh7quXL1vUwA==", "dev": true, "dependencies": { "chalk": "^5.4.1", - "clang-format": "^1.4.0", "cmake-js": "^7.1.1", "semver": "^7.1.3" } @@ -12005,7 +12214,8 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.5.0.tgz", "integrity": "sha512-Yi/FgnN8IU/Cd6KeLxyHkylBUvDTsSScT0Tna2zTrz8klmc8qF2ppj6Q1LHsmOueJWhigQwR4cO2p0XBGW5IaQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/node-int64": { "version": "0.4.0", @@ -12247,107 +12457,28 @@ } }, "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/outdent": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", @@ -12493,6 +12624,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12619,6 +12760,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -12628,35 +12779,33 @@ "node": ">= 0.4" } }, - "node_modules/prebuildify": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/prebuildify/-/prebuildify-6.0.1.tgz", - "integrity": "sha512-8Y2oOOateom/s8dNBsGIcnm6AxPmLH4/nanQzL5lQMU+sC0CMhzARZHizwr36pUPLdvBnOkCNQzxg4djuFSgIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "mkdirp-classic": "^0.5.3", - "node-abi": "^3.3.0", - "npm-run-path": "^3.1.0", - "pump": "^3.0.0", - "tar-fs": "^2.1.0" - }, - "bin": { - "prebuildify": "bin.js" - } - }, - "node_modules/prebuildify/node_modules/npm-run-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", - "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "path-key": "^3.0.0" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14" } }, "node_modules/prelude-ls": { @@ -12740,19 +12889,37 @@ "dev": true, "license": "MIT" }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "node_modules/publint": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.15.tgz", + "integrity": "sha512-xPbRAPW+vqdiaKy5sVVY0uFAu3LaviaPO3pZ9FaRx59l9+U/RKR1OEbLhkug87cwiVKxPXyB4txsv5cad67u+A==", "dev": true, "license": "MIT", "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "@publint/pack": "^0.1.2", + "package-manager-detector": "^1.3.0", + "picocolors": "^1.1.1", + "sade": "^1.8.1" + }, + "bin": { + "publint": "src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" } }, - "node_modules/punycode": { - "version": "2.3.1", + "node_modules/publint/node_modules/package-manager-detector": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.5.0.tgz", + "integrity": "sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, @@ -12898,6 +13065,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12944,6 +13112,7 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.4.tgz", "integrity": "sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ==", "license": "MIT", + "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.4", @@ -13277,6 +13446,13 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, + "node_modules/require-package-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", + "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -13297,6 +13473,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -13317,35 +13507,23 @@ } }, "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "license": "MIT", "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" }, "node_modules/reusify": { "version": "1.1.0", @@ -13447,6 +13625,19 @@ "tslib": "^2.1.0" } }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -13505,6 +13696,13 @@ "semver": "bin/semver.js" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT" + }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -13818,6 +14016,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -13965,20 +14173,17 @@ } }, "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/string-width-cjs": { @@ -13996,12 +14201,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -14011,31 +14210,13 @@ "node": ">=8" } }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/strip-ansi": { @@ -14148,43 +14329,6 @@ "node": ">=10" } }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -14315,9 +14459,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.20.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", - "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", "dependencies": { @@ -14406,6 +14550,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14664,6 +14809,10 @@ "defaults": "^1.0.3" } }, + "node_modules/weak-node-api": { + "resolved": "packages/weak-node-api", + "link": true + }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", @@ -14722,38 +14871,6 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "node_modules/wide-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wide-align/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wide-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -14802,64 +14919,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -14894,6 +14953,15 @@ "async-limiter": "~1.0.0" } }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -14963,35 +15031,6 @@ "node": ">=10" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", @@ -15017,43 +15056,48 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, - "packages/cmake-rn": { - "version": "0.3.1", + "packages/cli-utils": { + "name": "@react-native-node-api/cli-utils", + "version": "0.1.4", "dependencies": { - "@commander-js/extra-typings": "^13.1.0", + "@commander-js/extra-typings": "^14.0.0", "bufout": "^0.3.2", "chalk": "^5.4.1", - "commander": "^13.1.0", + "commander": "^14.0.1", "ora": "^8.2.0", - "react-native-node-api": "0.4.0" - }, - "bin": { - "cmake-rn": "bin/cmake-rn.js" - }, - "peerDependencies": { - "node-addon-api": "^8.3.1", - "node-api-headers": "^1.5.0" + "p-limit": "^7.2.0" } }, - "packages/cmake-rn/node_modules/@commander-js/extra-typings": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-13.1.0.tgz", - "integrity": "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==", + "packages/cli-utils/node_modules/@commander-js/extra-typings": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-14.0.0.tgz", + "integrity": "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==", "license": "MIT", "peerDependencies": { - "commander": "~13.1.0" + "commander": "~14.0.0" + } + }, + "packages/cli-utils/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "packages/cmake-rn/node_modules/chalk": { + "packages/cli-utils/node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", @@ -15065,109 +15109,255 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "packages/cmake-rn/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "packages/cli-utils/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/ferric": { - "name": "ferric-cli", - "version": "0.3.1", - "dependencies": { - "@commander-js/extra-typings": "^13.1.0", - "@napi-rs/cli": "~3.0.3", - "bufout": "^0.3.2", - "chalk": "^5.4.1", - "commander": "^13.1.0", - "ora": "^8.2.0", - "react-native-node-api": "0.4.0" + "packages/cli-utils/node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20" + } + }, + "packages/cli-utils/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "license": "MIT" + }, + "packages/cli-utils/node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" }, - "bin": { - "ferric": "bin/ferric.js" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/ferric-example": { - "name": "@react-native-node-api/ferric-example", - "version": "0.1.1", - "devDependencies": { - "ferric-cli": "*" + "packages/cli-utils/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/ferric/node_modules/@commander-js/extra-typings": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-13.1.0.tgz", - "integrity": "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==", + "packages/cli-utils/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", "license": "MIT", - "peerDependencies": { - "commander": "~13.1.0" + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/ferric/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "packages/cli-utils/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "license": "MIT", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/ferric/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "packages/cli-utils/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/gyp-to-cmake": { - "version": "0.2.0", + "packages/cli-utils/node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", "dependencies": { - "@commander-js/extra-typings": "^13.1.0", - "commander": "^13.1.0", - "gyp-parser": "^1.0.4" + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" }, - "bin": { - "gyp-to-cmake": "bin/gyp-to-cmake.js" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/gyp-to-cmake/node_modules/@commander-js/extra-typings": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-13.1.0.tgz", - "integrity": "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==", + "packages/cli-utils/node_modules/p-limit": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.2.0.tgz", + "integrity": "sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ==", "license": "MIT", - "peerDependencies": { - "commander": "~13.1.0" + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli-utils/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/gyp-to-cmake/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "packages/cli-utils/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli-utils/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "packages/cmake-file-api": { + "version": "0.1.2", + "dependencies": { + "zod": "^4.1.11" + } + }, + "packages/cmake-rn": { + "version": "0.6.3", + "dependencies": { + "@react-native-node-api/cli-utils": "0.1.4", + "cmake-file-api": "0.1.2", + "react-native-node-api": "1.0.1", + "weak-node-api": "0.1.1", + "zod": "^4.1.11" + }, + "bin": { + "cmake-rn": "bin/cmake-rn.js" + }, + "peerDependencies": { + "node-addon-api": "^8.3.1", + "node-api-headers": "^1.5.0" + } + }, + "packages/ferric": { + "name": "ferric-cli", + "version": "0.3.11", + "dependencies": { + "@napi-rs/cli": "~3.0.3", + "@react-native-node-api/cli-utils": "0.1.4", + "react-native-node-api": "1.0.1", + "weak-node-api": "0.1.1" + }, + "bin": { + "ferric": "bin/ferric.js" + } + }, + "packages/ferric-example": { + "name": "@react-native-node-api/ferric-example", + "version": "0.1.2", + "devDependencies": { + "ferric-cli": "*" + } + }, + "packages/gyp-to-cmake": { + "version": "0.5.3", + "dependencies": { + "@react-native-node-api/cli-utils": "0.1.4", + "gyp-parser": "^1.0.4", + "pkg-dir": "^8.0.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "gyp-to-cmake": "bin/gyp-to-cmake.js" } }, "packages/host": { "name": "react-native-node-api", - "version": "0.4.0", + "version": "1.0.1", "license": "MIT", "dependencies": { - "@commander-js/extra-typings": "^13.1.0", - "bufout": "^0.3.2", - "chalk": "^5.4.1", - "commander": "^13.1.0", - "ora": "^8.2.0", + "@expo/plist": "^0.4.7", + "@react-native-node-api/cli-utils": "0.1.4", "pkg-dir": "^8.0.0", - "read-pkg": "^9.0.1" + "read-pkg": "^9.0.1", + "zod": "^4.1.11" }, "bin": { "react-native-node-api": "bin/react-native-node-api.mjs" @@ -15175,67 +15365,47 @@ "devDependencies": { "@babel/core": "^7.26.10", "@babel/types": "^7.27.0", - "fswin": "^3.24.829", - "node-api-headers": "^1.5.0", - "zod": "^3.24.3" + "fswin": "^3.24.829" }, "peerDependencies": { "@babel/core": "^7.26.10", - "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4" - } - }, - "packages/host/node_modules/@commander-js/extra-typings": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-13.1.0.tgz", - "integrity": "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==", - "license": "MIT", - "peerDependencies": { - "commander": "~13.1.0" - } - }, - "packages/host/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "packages/host/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "license": "MIT", - "engines": { - "node": ">=18" + "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.79.7 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4 || 0.81.5", + "weak-node-api": "0.1.1" } }, "packages/node-addon-examples": { "name": "@react-native-node-api/node-addon-examples", + "version": "0.1.1", "dependencies": { "assert": "^2.1.0" }, "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", - "node-addon-examples": "github:nodejs/node-addon-examples#4213d4c9d07996ae68629c67926251e117f8e52a", + "node-addon-examples": "github:nodejs/node-addon-examples#4b7dd86a85644610e6de80154df9acac9329b509", "read-pkg": "^9.0.1" } }, "packages/node-tests": { "name": "@react-native-node-api/node-tests", + "version": "0.1.1", "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", - "prebuildify": "^6.0.1", - "react-native-node-api": "^0.4.0", + "react-native-node-api": "^1.0.1", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } + }, + "packages/weak-node-api": { + "version": "0.1.1", + "license": "MIT", + "dependencies": { + "node-api-headers": "^1.5.0" + }, + "devDependencies": { + "zod": "^4.1.11" + } } } } diff --git a/package.json b/package.json index 083d3d02..774e27eb 100644 --- a/package.json +++ b/package.json @@ -4,27 +4,34 @@ "type": "module", "private": true, "workspaces": [ - "apps/test-app", - "packages/gyp-to-cmake", + "packages/cli-utils", + "packages/cmake-file-api", + "packages/weak-node-api", "packages/cmake-rn", "packages/ferric", + "packages/gyp-to-cmake", "packages/host", "packages/node-addon-examples", "packages/node-tests", - "packages/ferric-example" + "packages/ferric-example", + "apps/test-app" ], "homepage": "https://github.com/callstackincubator/react-native-node-api#readme", "scripts": { "build": "tsc --build", - "clean": "tsc --build --clean", + "clean": "tsc --build --clean && git clean -fdx -e node_modules", "dev": "tsc --build --watch", "lint": "eslint .", + "depcheck": "node scripts/depcheck.ts", + "publint": "node scripts/run-in-published.ts npx publint --strict", "prettier:check": "prettier --experimental-cli --check .", "prettier:write": "prettier --experimental-cli --write .", "test": "npm test --workspace react-native-node-api --workspace cmake-rn --workspace gyp-to-cmake --workspace node-addon-examples", "bootstrap": "node --run build && npm run bootstrap --workspaces --if-present", - "prerelease": "node --run build && npm run prerelease --workspaces --if-present", - "release": "changeset publish" + "changeset": "changeset", + "release": "changeset publish", + "prerelease": "node --run build && npm run prerelease --workspaces --if-present && node --run publint", + "init-macos-test-app": "node scripts/init-macos-test-app.ts" }, "author": { "name": "Callstack", @@ -38,6 +45,14 @@ { "name": "Jamie Birch", "url": "https://github.com/shirakaba" + }, + { + "name": "Mariusz Pasiล„ski", + "url": "https://github.com/mani3xis" + }, + { + "name": "Kamil Paradowski", + "url": "https://github.com/paradowstack" } ], "license": "MIT", @@ -49,13 +64,26 @@ "@tsconfig/node22": "^22.0.0", "@tsconfig/react-native": "3.0.6", "@types/node": "^22", + "depcheck": "^1.4.7", "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.8", "globals": "^16.0.0", "prettier": "^3.6.2", + "publint": "^0.3.15", "react-native": "0.81.4", - "tsx": "^4.20.5", + "read-pkg": "^9.0.1", + "tsx": "^4.20.6", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" + }, + "devEngines": { + "runtime": { + "name": "node", + "version": "^24.0.0" + }, + "packageManager": { + "name": "npm", + "version": "^11.0.0" + } } } diff --git a/packages/cli-utils/CHANGELOG.md b/packages/cli-utils/CHANGELOG.md new file mode 100644 index 00000000..078eefd9 --- /dev/null +++ b/packages/cli-utils/CHANGELOG.md @@ -0,0 +1,25 @@ +# @react-native-node-api/cli-utils + +## 0.1.4 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts ๐Ÿ™ˆ + +## 0.1.3 + +### Patch Changes + +- 441dcc4: Add re-export of "p-limit" + +## 0.1.2 + +### Patch Changes + +- 7ff2c2b: Fix minor package issues. + +## 0.1.1 + +### Patch Changes + +- 5156d35: Refactored moving prettyPath util to CLI utils package diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json new file mode 100644 index 00000000..25cbaeef --- /dev/null +++ b/packages/cli-utils/package.json @@ -0,0 +1,20 @@ +{ + "name": "@react-native-node-api/cli-utils", + "version": "0.1.4", + "description": "Useful utilities for the CLIs in the React Native Node API mono-repo", + "type": "module", + "files": [ + "dist" + ], + "exports": { + ".": "./dist/index.js" + }, + "dependencies": { + "@commander-js/extra-typings": "^14.0.0", + "bufout": "^0.3.2", + "chalk": "^5.4.1", + "commander": "^14.0.1", + "ora": "^8.2.0", + "p-limit": "^7.2.0" + } +} diff --git a/packages/cli-utils/src/actions.ts b/packages/cli-utils/src/actions.ts new file mode 100644 index 00000000..0df0b7d2 --- /dev/null +++ b/packages/cli-utils/src/actions.ts @@ -0,0 +1,47 @@ +import { SpawnFailure } from "bufout"; +import chalk from "chalk"; +import * as commander from "@commander-js/extra-typings"; + +import { UsageError } from "./errors.js"; + +export function wrapAction< + Args extends unknown[], + Opts extends commander.OptionValues, + GlobalOpts extends commander.OptionValues, + Command extends commander.Command, + ActionArgs extends unknown[], +>(fn: (this: Command, ...args: ActionArgs) => void | Promise) { + return async function (this: Command, ...args: ActionArgs) { + try { + await fn.call(this, ...args); + } catch (error) { + process.exitCode = 1; + if (error instanceof SpawnFailure) { + error.flushOutput("both"); + } else if ( + error instanceof Error && + error.cause instanceof SpawnFailure + ) { + error.cause.flushOutput("both"); + } + // Ensure some visual distance to the previous output + console.error(); + if (error instanceof UsageError || error instanceof SpawnFailure) { + console.error(chalk.red("ERROR"), error.message); + if (error.cause instanceof Error) { + console.error(chalk.blue("CAUSE"), error.cause.message); + } + if (error instanceof UsageError && error.fix) { + console.error( + chalk.green("FIX"), + error.fix.command + ? chalk.dim("Run: ") + error.fix.command + : error.fix.instructions, + ); + } + } else { + throw error; + } + } + }; +} diff --git a/packages/ferric/src/errors.ts b/packages/cli-utils/src/errors.ts similarity index 100% rename from packages/ferric/src/errors.ts rename to packages/cli-utils/src/errors.ts diff --git a/packages/cli-utils/src/index.ts b/packages/cli-utils/src/index.ts new file mode 100644 index 00000000..712adfe8 --- /dev/null +++ b/packages/cli-utils/src/index.ts @@ -0,0 +1,9 @@ +export * from "@commander-js/extra-typings"; +export { default as chalk } from "chalk"; +export * from "ora"; +export * from "bufout"; +export { default as pLimit } from "p-limit"; + +export * from "./actions.js"; +export * from "./errors.js"; +export * from "./paths.js"; diff --git a/packages/cli-utils/src/paths.ts b/packages/cli-utils/src/paths.ts new file mode 100644 index 00000000..6ff8bd2e --- /dev/null +++ b/packages/cli-utils/src/paths.ts @@ -0,0 +1,8 @@ +import chalk from "chalk"; +import path from "node:path"; + +export function prettyPath(p: string) { + return chalk.dim( + path.relative(process.cwd(), p) || chalk.italic("current directory"), + ); +} diff --git a/packages/cli-utils/tsconfig.json b/packages/cli-utils/tsconfig.json new file mode 100644 index 00000000..f183b9a9 --- /dev/null +++ b/packages/cli-utils/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../configs/tsconfig.node.json" +} diff --git a/packages/cmake-file-api/CHANGELOG.md b/packages/cmake-file-api/CHANGELOG.md new file mode 100644 index 00000000..ae76827b --- /dev/null +++ b/packages/cmake-file-api/CHANGELOG.md @@ -0,0 +1,13 @@ +# cmake-file-api + +## 0.1.2 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts ๐Ÿ™ˆ + +## 0.1.1 + +### Patch Changes + +- 7ff2c2b: Fix minor package issues. diff --git a/packages/cmake-file-api/README.md b/packages/cmake-file-api/README.md new file mode 100644 index 00000000..50dce7ea --- /dev/null +++ b/packages/cmake-file-api/README.md @@ -0,0 +1,7 @@ +# CMake File API (unofficial) + +The CMake File API provides an interface for querying CMake's configuration and project information. + +The API is based on files, where queries are written by client tools and read by CMake and replies are then written by CMake and read by client tools. The API is versioned, and the current version is v1 and these files are located in a directory named `.cmake/api/v1` in the build directory. + +This package provides a TypeScript interface to create query files and read replies and is intended to serve the same purpose to the TypeScript community that the [`cmake-file-api` crate](https://crates.io/crates/cmake-file-api), serves to the Rust community. diff --git a/packages/cmake-file-api/copilot-instructions.md b/packages/cmake-file-api/copilot-instructions.md new file mode 100644 index 00000000..c6def984 --- /dev/null +++ b/packages/cmake-file-api/copilot-instructions.md @@ -0,0 +1,259 @@ +# Copilot Instructions for cmake-file-api Package + +This package provides a TypeScript wrapper around the CMake File API using Zod schemas for validation. + +## Code Style Preferences + +### Node.js Built-ins + +- **Always** use Node.js built-ins with the `node:` prefix (e.g., `node:fs`, `node:path`, `node:assert/strict`) +- Prefer async APIs where possible (e.g., `fs.promises.readFile`, `fs.promises.writeFile`) + +### Schema Validation + +- Use **Zod** for all schema validation with strict typing +- Follow the CMake File API v1 specification precisely - read the documentation in `docs/cmake-file-api.7.rst.txt` +- Use `z.enum()` instead of generic strings for known enumeration values +- Make record values optional when they might not exist (e.g., `z.record(key, value.optional())`) +- **Keep schema files clean** - avoid inline comments except when strictly necessary for clarity +- **Use `index = z.number().int().min(0)`** for all *Index and *Indexes fields (they are documented as "unsigned integer 0-based index" in the CMake File API) + +### TypeScript Patterns + +- **No TypeScript type assertions (`as`)** unless explicitly justified +- Use destructuring to extract values from objects instead of accessing object properties repeatedly +- Prefer explicit assertions with meaningful messages over implicit type assumptions + +### Testing + +- Use Node.js built-in test runner and run test using the "Test cmake-file-api" task in VS Code +- **Prefer `assert.deepStrictEqual(result, mockData)`** over individual field assertions for schema validation +- Create comprehensive test cases that validate the complete schema structure +- Use proper type guards with assertions when dealing with optional values +- Test both positive cases (valid data) and ensure schemas properly validate structure + +### Error Handling + +- Use `assert` from `node:assert/strict` for runtime validation +- Provide descriptive error messages that help with debugging +- Handle CMake File API error objects properly (they have an `error` field instead of the expected structure) + +## Architecture Patterns + +### Schema Organization + +- Export schemas with versioned names (e.g., `ReplyFileReferenceV1`, `IndexReplyV1`) +- Organize related schemas in dedicated files under `src/schemas/` +- Keep the main API functions in `src/reply.ts` and `src/query.ts` + +### Minor Version Schema Pattern + +When implementing schemas that support multiple minor versions (as documented in CMake File API), use this hierarchical extension pattern: + +1. **Base Version Schema**: Create the earliest version as the base (e.g., `DirectoryV2_0`) +2. **Extended Version Schemas**: Use `.extend()` to add new fields for later versions (e.g., `DirectoryV2_3 = DirectoryV2_0.extend({...})`) +3. **Hierarchical Composition**: Parent schemas should also follow this pattern (e.g., `ConfigurationV2_3 = ConfigurationV2_0.extend({...})`) +4. **Version Constraints**: Use `minor: z.number().max(X)` for earlier versions and `minor: z.number().min(X)` for later versions +5. **Union Export**: Combine all versions using `z.union([SchemaV2_0, SchemaV2_3])` and export as the main schema name + +This pattern ensures: + +- Type safety across different minor versions +- Proper validation based on version numbers +- Clear inheritance hierarchy +- Backward compatibility support + +### Context-Dependent Object Versioning + +For objects that don't contain version information themselves (like Target objects), use the versioned extension pattern and export a union of all versions. Keep it DRY by versioning nested schemas separately: + +```typescript +// Version nested schemas separately to avoid duplication +const SourceV2_0 = z.object({ + path: z.string(), + // ... base fields +}); + +const SourceV2_5 = SourceV2_0.extend({ + fileSetIndex: index.optional(), // Added in v2.5 +}); + +const CompileGroupV2_0 = z.object({ + sourceIndexes: z.array(index), + language: z.string(), + // ... base fields +}); + +const CompileGroupV2_1 = CompileGroupV2_0.extend({ + precompileHeaders: z.array(PrecompileHeader).optional(), // Added in v2.1 +}); + +// Build main object versions using versioned nested schemas +const TargetV2_0 = z.object({ + // ... base fields + sources: z.array(SourceV2_0).optional(), + compileGroups: z.array(CompileGroupV2_0).optional(), +}); + +const TargetV2_1 = TargetV2_0.extend({ + compileGroups: z.array(CompileGroupV2_1).optional(), // Use versioned nested schema +}); + +const TargetV2_5 = TargetV2_2.extend({ + fileSets: z.array(FileSet).optional(), + sources: z.array(SourceV2_5).optional(), // Use versioned nested schema +}); + +// Export union of all versions for flexible validation +export const TargetV2 = z.union([ + TargetV2_0, + TargetV2_1, + TargetV2_2, + TargetV2_5, + TargetV2_6, + TargetV2_7, + TargetV2_8, +]); + +// Also export individual versions for specific use cases +export { + TargetV2_0, + TargetV2_1, + TargetV2_2, + TargetV2_5, + TargetV2_6, + TargetV2_7, + TargetV2_8, +}; +``` + +Then, reader functions should accept an optional schema parameter defaulting to the latest version: + +```typescript +export async function readTarget( + filePath: string, + schema: z.ZodSchema = TargetV2_8, // Default to latest version +) { + // ... implementation +} +``` + +This approach provides flexibility while maintaining type safety, avoiding code duplication, and allowing callers to specify the exact version schema when needed. + +### Function Design + +- Functions should be async where file I/O is involved +- Use clear, descriptive function names that indicate their purpose +- Validate file paths and extensions before processing +- Parse and validate JSON using Zod schemas rather than manual type checking + +### Documentation References + +- Always refer to the official CMake File API documentation +- The specification is available in `docs/cmake-file-api.7.rst.txt` +- When implementing Object Kinds, check the docs for exact field requirements and optional properties. Pay attention to indention in the document as it indicates nested structures. + +## Example Patterns + +### Good Schema Pattern + +```typescript +const index = z.number().int().min(0); + +export const MySchemaV1 = z.object({ + kind: z.enum(["validValue1", "validValue2"]), + optionalField: z.string().optional(), + parentIndex: index.optional(), // For *Index fields + childIndexes: z.array(index).optional(), // For *Indexes fields + requiredNested: z.object({ + major: z.number(), + minor: z.number(), + }), +}); +``` + +### Good Minor Version Schema Pattern + +```typescript +// Base version schema (earliest version) +const ItemV2_0 = z.object({ + name: z.string(), + type: z.enum(["TYPE1", "TYPE2"]), + paths: z.object({ + source: z.string(), + build: z.string(), + }), +}); + +// Extended version schema (adds fields introduced in v2.3) +const ItemV2_3 = ItemV2_0.extend({ + jsonFile: z.string(), + metadata: z + .object({ + version: z.string(), + }) + .optional(), +}); + +// Parent schema versions +const ContainerV2_0 = z.object({ + kind: z.literal("container"), + version: z.object({ + major: z.literal(2), + minor: z.number().max(2), // Versions 2.0-2.2 + }), + items: z.array(ItemV2_0), +}); + +const ContainerV2_3 = ContainerV2_0.extend({ + version: z.object({ + major: z.literal(2), + minor: z.number().min(3), // Versions 2.3+ + }), + items: z.array(ItemV2_3), +}); + +// Union export for all versions +export const ContainerV2 = z.union([ContainerV2_0, ContainerV2_3]); +``` + +### Good Function Pattern + +```typescript +export async function readSomething(filePath: string) { + assert( + path.basename(filePath).startsWith("expected-") && + path.extname(filePath) === ".json", + "Expected a path to an expected-*.json file", + ); + const content = await fs.promises.readFile(filePath, "utf-8"); + const { field1, field2 } = MySchemaV1.parse(JSON.parse(content)); + // Use destructured values directly + return { field1, field2 }; +} +``` + +### Good Test Pattern + +```typescript +it("validates complete structure", async function (context) { + const mockData = { + // Complete, realistic test data based on CMake File API docs + field1: "expectedValue", + field2: { nested: "structure" }, + optionalField: "presentValue", + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["example-file.json", mockData], + ]); + const result = await readSomething(path.join(tmpPath, "example-file.json")); + + // Prefer deepStrictEqual for complete schema validation + assert.deepStrictEqual(result, mockData); + + // Only use individual assertions when testing specific edge cases + // const optionalValue = result.optionalField; + // assert(optionalValue, "Expected optional field to exist in this test case"); +}); +``` diff --git a/packages/cmake-file-api/docs/cmake-file-api.7.rst.txt b/packages/cmake-file-api/docs/cmake-file-api.7.rst.txt new file mode 100644 index 00000000..4211c572 --- /dev/null +++ b/packages/cmake-file-api/docs/cmake-file-api.7.rst.txt @@ -0,0 +1,1864 @@ +The following is a snapshot of https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html + +.. cmake-manual-description: CMake File-Based API + +cmake-file-api(7) +***************** + +.. only:: html + + .. contents:: + +Introduction +============ + +CMake provides a file-based API that clients may use to get semantic +information about the buildsystems CMake generates. Clients may use +the API by writing query files to a specific location in a build tree +to request zero or more `Object Kinds`_. When CMake generates the +buildsystem in that build tree it will read the query files and write +reply files for the client to read. + +The file-based API uses a ``/.cmake/api/`` directory at the top +of a build tree. The API is versioned to support changes to the layout +of files within the API directory. API file layout versioning is +orthogonal to the versioning of `Object Kinds`_ used in replies. +This version of CMake supports only one API version, `API v1`_. + +.. versionadded:: 3.27 + Projects may also submit queries for the current run using the + :command:`cmake_file_api` command. + +.. _`file-api v1`: + +API v1 +====== + +API v1 is housed in the ``/.cmake/api/v1/`` directory. +It has the following subdirectories: + +``query/`` + Holds query files written by clients. + These may be `v1 Shared Stateless Query Files`_, + `v1 Client Stateless Query Files`_, or `v1 Client Stateful Query Files`_. + +``reply/`` + Holds reply files written by CMake when it runs to generate a build system. + Clients may read reply files only when referenced by a reply index: + + ``index-*.json`` + A `v1 Reply Index File`_ written when CMake generates a build system. + + ``error-*.json`` + .. versionadded:: 4.1 + + A `v1 Reply Error Index`_ written when CMake fails to generate a build + system due to an error. + + Clients may look for and read a reply index at any time. + Clients may optionally create the ``reply/`` directory at any time + and monitor it for the appearance of a new reply index. + CMake owns all reply files. Clients must never remove them. + +.. versionadded:: 3.31 + Users can add query files to ``api/v1/query`` inside the + :envvar:`CMAKE_CONFIG_DIR` to create user-wide queries for all CMake projects. + +v1 Shared Stateless Query Files +------------------------------- + +Shared stateless query files allow clients to share requests for +major versions of the `Object Kinds`_ and get all requested versions +recognized by the CMake that runs. + +Clients may create shared requests by creating empty files in the +``v1/query/`` directory. The form is:: + + /.cmake/api/v1/query/-v + +where ```` is one of the `Object Kinds`_, ``-v`` is literal, +and ```` is the major version number. + +Files of this form are stateless shared queries not owned by any specific +client. Once created they should not be removed without external client +coordination or human intervention. + +v1 Client Stateless Query Files +------------------------------- + +Client stateless query files allow clients to create owned requests for +major versions of the `Object Kinds`_ and get all requested versions +recognized by the CMake that runs. + +Clients may create owned requests by creating empty files in +client-specific query subdirectories. The form is:: + + /.cmake/api/v1/query/client-/-v + +where ``client-`` is literal, ```` is a string uniquely +identifying the client, ```` is one of the `Object Kinds`_, +``-v`` is literal, and ```` is the major version number. +Each client must choose a unique ```` identifier via its +own means. + +Files of this form are stateless queries owned by the client ````. +The owning client may remove them at any time. + +v1 Client Stateful Query Files +------------------------------ + +Stateful query files allow clients to request a list of versions of +each of the `Object Kinds`_ and get only the most recent version +recognized by the CMake that runs. + +Clients may create owned stateful queries by creating ``query.json`` +files in client-specific query subdirectories. The form is:: + + /.cmake/api/v1/query/client-/query.json + +where ``client-`` is literal, ```` is a string uniquely +identifying the client, and ``query.json`` is literal. Each client +must choose a unique ```` identifier via its own means. + +``query.json`` files are stateful queries owned by the client ````. +The owning client may update or remove them at any time. When a +given client installation is updated it may then update the stateful +query it writes to build trees to request newer object versions. +This can be used to avoid asking CMake to generate multiple object +versions unnecessarily. + +A ``query.json`` file must contain a JSON object: + +.. code-block:: json + + { + "requests": [ + { "kind": "" , "version": 1 }, + { "kind": "" , "version": { "major": 1, "minor": 2 } }, + { "kind": "" , "version": [2, 1] }, + { "kind": "" , "version": [2, { "major": 1, "minor": 2 }] }, + { "kind": "" , "version": 1, "client": {} }, + { "kind": "..." } + ], + "client": {} + } + +The members are: + +``requests`` + A JSON array containing zero or more requests. Each request is + a JSON object with members: + + ``kind`` + Specifies one of the `Object Kinds`_ to be included in the reply. + + ``version`` + Indicates the version(s) of the object kind that the client + understands. Versions have major and minor components following + semantic version conventions. The value must be + + * a JSON integer specifying a (non-negative) major version number, or + * a JSON object containing ``major`` and (optionally) ``minor`` + members specifying non-negative integer version components, or + * a JSON array whose elements are each one of the above. + + ``client`` + Optional member reserved for use by the client. This value is + preserved in the reply written for the client in the + `v1 Reply Index File`_ but is otherwise ignored. Clients may use + this to pass custom information with a request through to its reply. + + For each requested object kind CMake will choose the *first* version + that it recognizes for that kind among those listed in the request. + The response will use the selected *major* version with the highest + *minor* version known to the running CMake for that major version. + Therefore clients should list all supported major versions in + preferred order along with the minimal minor version required + for each major version. + +``client`` + Optional member reserved for use by the client. This value is + preserved in the reply written for the client in the + `v1 Reply Index File`_ but is otherwise ignored. Clients may use + this to pass custom information with a query through to its reply. + +Other ``query.json`` top-level members are reserved for future use. +If present they are ignored for forward compatibility. + +v1 Reply Index File +------------------- + +CMake writes an ``index-*.json`` file to the ``v1/reply/`` directory +when it successfully generates a build system. Clients must read the +reply index file first and may read other `v1 Reply Files`_ only by +following references. The form of the reply index file name is:: + + /.cmake/api/v1/reply/index-.json + +where ``index-`` is literal and ```` is an unspecified +name selected by CMake. Whenever a new index file is generated it +is given a new name and any old one is deleted. During the short +time between these steps there may be multiple index files present; +the one with the largest name in lexicographic order is the current +index file. + +The reply index file contains a JSON object: + +.. code-block:: json + + { + "cmake": { + "version": { + "major": 3, "minor": 14, "patch": 0, "suffix": "", + "string": "3.14.0", "isDirty": false + }, + "paths": { + "cmake": "/prefix/bin/cmake", + "ctest": "/prefix/bin/ctest", + "cpack": "/prefix/bin/cpack", + "root": "/prefix/share/cmake-3.14" + }, + "generator": { + "multiConfig": false, + "name": "Unix Makefiles" + } + }, + "objects": [ + { "kind": "", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "" }, + { "...": "..." } + ], + "reply": { + "-v": { "kind": "", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "" }, + "": { "error": "unknown query file" }, + "...": {}, + "client-": { + "-v": { "kind": "", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "" }, + "": { "error": "unknown query file" }, + "...": {}, + "query.json": { + "requests": [ {}, {}, {} ], + "responses": [ + { "kind": "", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "" }, + { "error": "unknown query file" }, + { "...": {} } + ], + "client": {} + } + } + } + } + +The members are: + +``cmake`` + A JSON object containing information about the instance of CMake that + generated the reply. It contains members: + + ``version`` + A JSON object specifying the version of CMake with members: + + ``major``, ``minor``, ``patch`` + Integer values specifying the major, minor, and patch version components. + ``suffix`` + A string specifying the version suffix, if any, e.g. ``g0abc3``. + ``string`` + A string specifying the full version in the format + ``..[-]``. + ``isDirty`` + A boolean indicating whether the version was built from a version + controlled source tree with local modifications. + + ``paths`` + A JSON object specifying paths to things that come with CMake. + It has members for :program:`cmake`, :program:`ctest`, and :program:`cpack` + whose values are JSON strings specifying the absolute path to each tool, + represented with forward slashes. It also has a ``root`` member for + the absolute path to the directory containing CMake resources like the + ``Modules/`` directory (see :variable:`CMAKE_ROOT`). + + ``generator`` + A JSON object describing the CMake generator used for the build. + It has members: + + ``multiConfig`` + A boolean specifying whether the generator supports multiple output + configurations. + ``name`` + A string specifying the name of the generator. + ``platform`` + If the generator supports :variable:`CMAKE_GENERATOR_PLATFORM`, + this is a string specifying the generator platform name. + +``objects`` + A JSON array listing all versions of all `Object Kinds`_ generated + as part of the reply. Each array entry is a + `v1 Reply File Reference`_. + +``reply`` + A JSON object mirroring the content of the ``query/`` directory + that CMake loaded to produce the reply. The members are of the form + + ``-v`` + A member of this form appears for each of the + `v1 Shared Stateless Query Files`_ that CMake recognized as a + request for object kind ```` with major version ````. + The value is + + * a `v1 Reply File Reference`_ to the corresponding reply file for + that object kind and version, or + * in a `v1 Reply Error Index`_, a JSON object with a single ``error`` + member containing a string with an error message. + + ```` + A member of this form appears for each of the + `v1 Shared Stateless Query Files`_ that CMake did not recognize. + The value is a JSON object with a single ``error`` member + containing a string with an error message indicating that the + query file is unknown. + + ``client-`` + A member of this form appears for each client-owned directory + holding `v1 Client Stateless Query Files`_. + The value is a JSON object mirroring the content of the + ``query/client-/`` directory. The members are of the form: + + ``-v`` + A member of this form appears for each of the + `v1 Client Stateless Query Files`_ that CMake recognized as a + request for object kind ```` with major version ````. + The value is + + * a `v1 Reply File Reference`_ to the corresponding reply file for + that object kind and version, or + * in a `v1 Reply Error Index`_, a JSON object with a single ``error`` + member containing a string with an error message. + + ```` + A member of this form appears for each of the + `v1 Client Stateless Query Files`_ that CMake did not recognize. + The value is a JSON object with a single ``error`` member + containing a string with an error message indicating that the + query file is unknown. + + ``query.json`` + This member appears for clients using + `v1 Client Stateful Query Files`_. + If the ``query.json`` file failed to read or parse as a JSON object, + this member is a JSON object with a single ``error`` member + containing a string with an error message. Otherwise, this member + is a JSON object mirroring the content of the ``query.json`` file. + The members are: + + ``client`` + A copy of the ``query.json`` file ``client`` member, if it exists. + + ``requests`` + A copy of the ``query.json`` file ``requests`` member, if it exists. + + ``responses`` + If the ``query.json`` file ``requests`` member is missing or invalid, + this member is a JSON object with a single ``error`` member + containing a string with an error message. Otherwise, this member + contains a JSON array with a response for each entry of the + ``requests`` array, in the same order. Each response is + + * a `v1 Reply File Reference`_ to the corresponding reply file for + the requested object kind and selected version, or + * a JSON object with a single ``error`` member containing a string + with an error message. + +After reading the reply index file, clients may read the other +`v1 Reply Files`_ it references. + +v1 Reply File Reference +^^^^^^^^^^^^^^^^^^^^^^^ + +The reply index file represents each reference to another reply file +using a JSON object with members: + +``kind`` + A string specifying one of the `Object Kinds`_. +``version`` + A JSON object with members ``major`` and ``minor`` specifying + integer version components of the object kind. +``jsonFile`` + A JSON string specifying a path relative to the reply index file + to another JSON file containing the object. + +.. _`file-api reply error index`: + +v1 Reply Error Index +^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 4.1 + +CMake writes an ``error-*.json`` file to the ``v1/reply/`` directory +when it fails to generate a build system. This reply error index +follows the same naming pattern, syntax, and semantics of a +`v1 Reply Index File`_, with the following exceptions: + +* The ``index-`` prefix is replaced by an ``error-`` prefix. + +* When a new error index is generated, old index files are *not* + deleted. If a `v1 Reply Index File`_ exists, it indexes replies + from the most recent successful run. If multiple ``index-*.json`` + and/or ``error-*.json`` files are present, the one with the largest + name in lexicographic order, excluding the ``index-`` or ``error-`` + prefix, is the current index. + +* Only a subset of `Object Kinds`_ are provided: + + `configureLog `_ + .. versionadded:: 4.1 + + Index entries for other object kinds contain an ``error`` message + instead of a `v1 Reply File Reference`_. + +v1 Reply Files +-------------- + +Reply files containing specific `Object Kinds`_ are written by CMake. +The names of these files are unspecified and must not be interpreted +by clients. Clients must first read the `v1 Reply Index File`_ and +follow references to the names of the desired response objects. + +Reply files (including the index file) will never be replaced by +files of the same name but different content. This allows a client +to read the files concurrently with a running CMake that may generate +a new reply. However, after generating a new reply CMake will attempt +to remove reply files from previous runs that it did not just write. +If a client attempts to read a reply file referenced by the index but +finds the file missing, that means a concurrent CMake has generated +a new reply. The client may simply start again by reading the new +reply index file. + +.. _`file-api object kinds`: + +Object Kinds +============ + +The CMake file-based API reports semantic information about the build +system using the following kinds of JSON objects. Each kind of object +is versioned independently using semantic versioning with major and +minor components. Every kind of object has the form: + +.. code-block:: json + + { + "kind": "", + "version": { "major": 1, "minor": 0 }, + "...": {} + } + +The ``kind`` member is a string specifying the object kind name. +The ``version`` member is a JSON object with ``major`` and ``minor`` +members specifying integer components of the object kind's version. +Additional top-level members are specific to each object kind. + +Object Kind "codemodel" +----------------------- + +The ``codemodel`` object kind describes the build system structure as +modeled by CMake. + +There is only one ``codemodel`` object major version, version 2. +Version 1 does not exist to avoid confusion with that from +:manual:`cmake-server(7)` mode. + +"codemodel" version 2 +^^^^^^^^^^^^^^^^^^^^^ + +``codemodel`` object version 2 is a JSON object: + +.. code-block:: json + + { + "kind": "codemodel", + "version": { "major": 2, "minor": 8 }, + "paths": { + "source": "/path/to/top-level-source-dir", + "build": "/path/to/top-level-build-dir" + }, + "configurations": [ + { + "name": "Debug", + "directories": [ + { + "source": ".", + "build": ".", + "childIndexes": [ 1 ], + "projectIndex": 0, + "targetIndexes": [ 0 ], + "hasInstallRule": true, + "minimumCMakeVersion": { + "string": "3.14" + }, + "jsonFile": "" + }, + { + "source": "sub", + "build": "sub", + "parentIndex": 0, + "projectIndex": 0, + "targetIndexes": [ 1 ], + "minimumCMakeVersion": { + "string": "3.14" + }, + "jsonFile": "" + } + ], + "projects": [ + { + "name": "MyProject", + "directoryIndexes": [ 0, 1 ], + "targetIndexes": [ 0, 1 ] + } + ], + "targets": [ + { + "name": "MyExecutable", + "directoryIndex": 0, + "projectIndex": 0, + "jsonFile": "" + }, + { + "name": "MyLibrary", + "directoryIndex": 1, + "projectIndex": 0, + "jsonFile": "" + } + ] + } + ] + } + +The members specific to ``codemodel`` objects are: + +``paths`` + A JSON object containing members: + + ``source`` + A string specifying the absolute path to the top-level source directory, + represented with forward slashes. + + ``build`` + A string specifying the absolute path to the top-level build directory, + represented with forward slashes. + +``configurations`` + A JSON array of entries corresponding to available build configurations. + On single-configuration generators there is one entry for the value + of the :variable:`CMAKE_BUILD_TYPE` variable. For multi-configuration + generators there is an entry for each configuration listed in the + :variable:`CMAKE_CONFIGURATION_TYPES` variable. + Each entry is a JSON object containing members: + + ``name`` + A string specifying the name of the configuration, e.g. ``Debug``. + + ``directories`` + A JSON array of entries each corresponding to a build system directory + whose source directory contains a ``CMakeLists.txt`` file. The first + entry corresponds to the top-level directory. Each entry is a + JSON object containing members: + + ``source`` + A string specifying the path to the source directory, represented + with forward slashes. If the directory is inside the top-level + source directory then the path is specified relative to that + directory (with ``.`` for the top-level source directory itself). + Otherwise the path is absolute. + + ``build`` + A string specifying the path to the build directory, represented + with forward slashes. If the directory is inside the top-level + build directory then the path is specified relative to that + directory (with ``.`` for the top-level build directory itself). + Otherwise the path is absolute. + + ``parentIndex`` + Optional member that is present when the directory is not top-level. + The value is an unsigned integer 0-based index of another entry in + the main ``directories`` array that corresponds to the parent + directory that added this directory as a subdirectory. + + ``childIndexes`` + Optional member that is present when the directory has subdirectories. + The value is a JSON array of entries corresponding to child directories + created by the :command:`add_subdirectory` or :command:`subdirs` + command. Each entry is an unsigned integer 0-based index of another + entry in the main ``directories`` array. + + ``projectIndex`` + An unsigned integer 0-based index into the main ``projects`` array + indicating the build system project to which the this directory belongs. + + ``targetIndexes`` + Optional member that is present when the directory itself has targets, + excluding those belonging to subdirectories. The value is a JSON + array of entries corresponding to the targets. Each entry is an + unsigned integer 0-based index into the main ``targets`` array. + + ``minimumCMakeVersion`` + Optional member present when a minimum required version of CMake is + known for the directory. This is the ```` version given to the + most local call to the :command:`cmake_minimum_required(VERSION)` + command in the directory itself or one of its ancestors. + The value is a JSON object with one member: + + ``string`` + A string specifying the minimum required version in the format:: + + .[.[.]][] + + Each component is an unsigned integer and the suffix may be an + arbitrary string. + + ``hasInstallRule`` + Optional member that is present with boolean value ``true`` when + the directory or one of its subdirectories contains any + :command:`install` rules, i.e. whether a ``make install`` + or equivalent rule is available. + + ``jsonFile`` + A JSON string specifying a path relative to the codemodel file + to another JSON file containing a + `"codemodel" version 2 "directory" object`_. + + This field was added in codemodel version 2.3. + + ``projects`` + A JSON array of entries corresponding to the top-level project + and sub-projects defined in the build system. Each (sub-)project + corresponds to a source directory whose ``CMakeLists.txt`` file + calls the :command:`project` command with a project name different + from its parent directory. The first entry corresponds to the + top-level project. + + Each entry is a JSON object containing members: + + ``name`` + A string specifying the name given to the :command:`project` command. + + ``parentIndex`` + Optional member that is present when the project is not top-level. + The value is an unsigned integer 0-based index of another entry in + the main ``projects`` array that corresponds to the parent project + that added this project as a sub-project. + + ``childIndexes`` + Optional member that is present when the project has sub-projects. + The value is a JSON array of entries corresponding to the sub-projects. + Each entry is an unsigned integer 0-based index of another + entry in the main ``projects`` array. + + ``directoryIndexes`` + A JSON array of entries corresponding to build system directories + that are part of the project. The first entry corresponds to the + top-level directory of the project. Each entry is an unsigned + integer 0-based index into the main ``directories`` array. + + ``targetIndexes`` + Optional member that is present when the project itself has targets, + excluding those belonging to sub-projects. The value is a JSON + array of entries corresponding to the targets. Each entry is an + unsigned integer 0-based index into the main ``targets`` array. + + ``targets`` + A JSON array of entries corresponding to the build system targets. + Such targets are created by calls to :command:`add_executable`, + :command:`add_library`, and :command:`add_custom_target`, excluding + imported targets and interface libraries (which do not generate any + build rules). Each entry is a JSON object containing members: + + ``name`` + A string specifying the target name. + + ``id`` + A string uniquely identifying the target. This matches the ``id`` + field in the file referenced by ``jsonFile``. + + ``directoryIndex`` + An unsigned integer 0-based index into the main ``directories`` array + indicating the build system directory in which the target is defined. + + ``projectIndex`` + An unsigned integer 0-based index into the main ``projects`` array + indicating the build system project in which the target is defined. + + ``jsonFile`` + A JSON string specifying a path relative to the codemodel file + to another JSON file containing a + `"codemodel" version 2 "target" object`_. + +"codemodel" version 2 "directory" object +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A codemodel "directory" object is referenced by a `"codemodel" version 2`_ +object's ``directories`` array. Each "directory" object is a JSON object +with members: + +``paths`` + A JSON object containing members: + + ``source`` + A string specifying the path to the source directory, represented + with forward slashes. If the directory is inside the top-level + source directory then the path is specified relative to that + directory (with ``.`` for the top-level source directory itself). + Otherwise the path is absolute. + + ``build`` + A string specifying the path to the build directory, represented + with forward slashes. If the directory is inside the top-level + build directory then the path is specified relative to that + directory (with ``.`` for the top-level build directory itself). + Otherwise the path is absolute. + +``installers`` + A JSON array of entries corresponding to :command:`install` rules. + Each entry is a JSON object containing members: + + ``component`` + A string specifying the component selected by the corresponding + :command:`install` command invocation. + + ``destination`` + Optional member that is present for specific ``type`` values below. + The value is a string specifying the install destination path. + The path may be absolute or relative to the install prefix. + + ``paths`` + Optional member that is present for specific ``type`` values below. + The value is a JSON array of entries corresponding to the paths + (files or directories) to be installed. Each entry is one of: + + * A string specifying the path from which a file or directory + is to be installed. The portion of the path not preceded by + a ``/`` also specifies the path (name) to which the file + or directory is to be installed under the destination. + + * A JSON object with members: + + ``from`` + A string specifying the path from which a file or directory + is to be installed. + + ``to`` + A string specifying the path to which the file or directory + is to be installed under the destination. + + In both cases the paths are represented with forward slashes. If + the "from" path is inside the top-level directory documented by the + corresponding ``type`` value, then the path is specified relative + to that directory. Otherwise the path is absolute. + + ``type`` + A string specifying the type of installation rule. The value is one + of the following, with some variants providing additional members: + + ``file`` + An :command:`install(FILES)` or :command:`install(PROGRAMS)` call. + The ``destination`` and ``paths`` members are populated, with paths + under the top-level *source* directory expressed relative to it. + The ``isOptional`` member may exist. + This type has no additional members. + + ``directory`` + An :command:`install(DIRECTORY)` call. + The ``destination`` and ``paths`` members are populated, with paths + under the top-level *source* directory expressed relative to it. + The ``isOptional`` member may exist. + This type has no additional members. + + ``target`` + An :command:`install(TARGETS)` call. + The ``destination`` and ``paths`` members are populated, with paths + under the top-level *build* directory expressed relative to it. + The ``isOptional`` member may exist. + This type has additional members ``targetId``, ``targetIndex``, + ``targetIsImportLibrary``, and ``targetInstallNamelink``. + + ``export`` + An :command:`install(EXPORT)` call. + The ``destination`` and ``paths`` members are populated, with paths + under the top-level *build* directory expressed relative to it. + The ``paths`` entries refer to files generated automatically by + CMake for installation, and their actual values are considered + private implementation details. + This type has additional members ``exportName`` and ``exportTargets``. + + ``script`` + An :command:`install(SCRIPT)` call. + This type has additional member ``scriptFile``. + + ``code`` + An :command:`install(CODE)` call. + This type has no additional members. + + ``importedRuntimeArtifacts`` + An :command:`install(IMPORTED_RUNTIME_ARTIFACTS)` call. + The ``destination`` member is populated. The ``isOptional`` member may + exist. This type has no additional members. + + ``runtimeDependencySet`` + An :command:`install(RUNTIME_DEPENDENCY_SET)` call or an + :command:`install(TARGETS)` call with ``RUNTIME_DEPENDENCIES``. The + ``destination`` member is populated. This type has additional members + ``runtimeDependencySetName`` and ``runtimeDependencySetType``. + + ``fileSet`` + An :command:`install(TARGETS)` call with ``FILE_SET``. + The ``destination`` and ``paths`` members are populated. + The ``isOptional`` member may exist. + This type has additional members ``fileSetName``, ``fileSetType``, + ``fileSetDirectories``, and ``fileSetTarget``. + + This type was added in codemodel version 2.4. + + ``cxxModuleBmi`` + An :command:`install(TARGETS)` call with ``CXX_MODULES_BMI``. + The ``destination`` member is populated and the ``isOptional`` member + may exist. This type has an additional ``cxxModuleBmiTarget`` member. + + This type was added in codemodel version 2.5. + + ``isExcludeFromAll`` + Optional member that is present with boolean value ``true`` when + :command:`install` is called with the ``EXCLUDE_FROM_ALL`` option. + + ``isForAllComponents`` + Optional member that is present with boolean value ``true`` when + :command:`install(SCRIPT|CODE)` is called with the + ``ALL_COMPONENTS`` option. + + ``isOptional`` + Optional member that is present with boolean value ``true`` when + :command:`install` is called with the ``OPTIONAL`` option. + This is allowed when ``type`` is ``file``, ``directory``, or ``target``. + + ``targetId`` + Optional member that is present when ``type`` is ``target``. + The value is a string uniquely identifying the target to be installed. + This matches the ``id`` member of the target in the main + "codemodel" object's ``targets`` array. + + ``targetIndex`` + Optional member that is present when ``type`` is ``target``. + The value is an unsigned integer 0-based index into the main "codemodel" + object's ``targets`` array for the target to be installed. + + ``targetIsImportLibrary`` + Optional member that is present when ``type`` is ``target`` and + the installer is for a Windows DLL import library file or for an + AIX linker import file. If present, it has boolean value ``true``. + + ``targetInstallNamelink`` + Optional member that is present when ``type`` is ``target`` and + the installer corresponds to a target that may use symbolic links + to implement the :prop_tgt:`VERSION` and :prop_tgt:`SOVERSION` + target properties. + The value is a string indicating how the installer is supposed to + handle the symlinks: ``skip`` means the installer should skip the + symlinks and install only the real file, and ``only`` means the + installer should install only the symlinks and not the real file. + In all cases the ``paths`` member lists what it actually installs. + + ``exportName`` + Optional member that is present when ``type`` is ``export``. + The value is a string specifying the name of the export. + + ``exportTargets`` + Optional member that is present when ``type`` is ``export``. + The value is a JSON array of entries corresponding to the targets + included in the export. Each entry is a JSON object with members: + + ``id`` + A string uniquely identifying the target. This matches + the ``id`` member of the target in the main "codemodel" + object's ``targets`` array. + + ``index`` + An unsigned integer 0-based index into the main "codemodel" + object's ``targets`` array for the target. + + ``runtimeDependencySetName`` + Optional member that is present when ``type`` is ``runtimeDependencySet`` + and the installer was created by an + :command:`install(RUNTIME_DEPENDENCY_SET)` call. The value is a string + specifying the name of the runtime dependency set that was installed. + + ``runtimeDependencySetType`` + Optional member that is present when ``type`` is ``runtimeDependencySet``. + The value is a string with one of the following values: + + ``library`` + Indicates that this installer installs dependencies that are not macOS + frameworks. + + ``framework`` + Indicates that this installer installs dependencies that are macOS + frameworks. + + ``fileSetName`` + Optional member that is present when ``type`` is ``fileSet``. The value is + a string with the name of the file set. + + This field was added in codemodel version 2.4. + + ``fileSetType`` + Optional member that is present when ``type`` is ``fileSet``. The value is + a string with the type of the file set. + + This field was added in codemodel version 2.4. + + ``fileSetDirectories`` + Optional member that is present when ``type`` is ``fileSet``. The value + is a list of strings with the file set's base directories (determined by + genex-evaluation of :prop_tgt:`HEADER_DIRS` or + :prop_tgt:`HEADER_DIRS_`). + + This field was added in codemodel version 2.4. + + ``fileSetTarget`` + Optional member that is present when ``type`` is ``fileSet``. The value + is a JSON object with members: + + ``id`` + A string uniquely identifying the target. This matches + the ``id`` member of the target in the main "codemodel" + object's ``targets`` array. + + ``index`` + An unsigned integer 0-based index into the main "codemodel" + object's ``targets`` array for the target. + + This field was added in codemodel version 2.4. + + ``cxxModuleBmiTarget`` + Optional member that is present when ``type`` is ``cxxModuleBmi``. + The value is a JSON object with members: + + ``id`` + A string uniquely identifying the target. This matches + the ``id`` member of the target in the main "codemodel" + object's ``targets`` array. + + ``index`` + An unsigned integer 0-based index into the main "codemodel" + object's ``targets`` array for the target. + + This field was added in codemodel version 2.5. + + ``scriptFile`` + Optional member that is present when ``type`` is ``script``. + The value is a string specifying the path to the script file on disk, + represented with forward slashes. If the file is inside the top-level + source directory then the path is specified relative to that directory. + Otherwise the path is absolute. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`install` or other command invocation that added this + installer is available. The value is an unsigned integer 0-based + index into the ``backtraceGraph`` member's ``nodes`` array. + +``backtraceGraph`` + A `"codemodel" version 2 "backtrace graph"`_ whose nodes are referenced + from ``backtrace`` members elsewhere in this "directory" object. + +"codemodel" version 2 "target" object +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A codemodel "target" object is referenced by a `"codemodel" version 2`_ +object's ``targets`` array. Each "target" object is a JSON object +with members: + +``name`` + A string specifying the logical name of the target. + +``id`` + A string uniquely identifying the target. The format is unspecified + and should not be interpreted by clients. + +``type`` + A string specifying the type of the target. The value is one of + ``EXECUTABLE``, ``STATIC_LIBRARY``, ``SHARED_LIBRARY``, + ``MODULE_LIBRARY``, ``OBJECT_LIBRARY``, ``INTERFACE_LIBRARY``, + or ``UTILITY``. + +``backtrace`` + Optional member that is present when a CMake language backtrace to + the command in the source code that created the target is available. + The value is an unsigned integer 0-based index into the + ``backtraceGraph`` member's ``nodes`` array. + +``folder`` + Optional member that is present when the :prop_tgt:`FOLDER` target + property is set. The value is a JSON object with one member: + + ``name`` + A string specifying the name of the target folder. + +``paths`` + A JSON object containing members: + + ``source`` + A string specifying the path to the target's source directory, + represented with forward slashes. If the directory is inside the + top-level source directory then the path is specified relative to + that directory (with ``.`` for the top-level source directory itself). + Otherwise the path is absolute. + + ``build`` + A string specifying the path to the target's build directory, + represented with forward slashes. If the directory is inside the + top-level build directory then the path is specified relative to + that directory (with ``.`` for the top-level build directory itself). + Otherwise the path is absolute. + +``nameOnDisk`` + Optional member that is present for executable and library targets + that are linked or archived into a single primary artifact. + The value is a string specifying the file name of that artifact on disk. + +``artifacts`` + Optional member that is present for executable and library targets + that produce artifacts on disk meant for consumption by dependents. + The value is a JSON array of entries corresponding to the artifacts. + Each entry is a JSON object containing one member: + + ``path`` + A string specifying the path to the file on disk, represented with + forward slashes. If the file is inside the top-level build directory + then the path is specified relative to that directory. + Otherwise the path is absolute. + +``isGeneratorProvided`` + Optional member that is present with boolean value ``true`` if the + target is provided by CMake's build system generator rather than by + a command in the source code. + +``install`` + Optional member that is present when the target has an :command:`install` + rule. The value is a JSON object with members: + + ``prefix`` + A JSON object specifying the installation prefix. It has one member: + + ``path`` + A string specifying the value of :variable:`CMAKE_INSTALL_PREFIX`. + + ``destinations`` + A JSON array of entries specifying an install destination path. + Each entry is a JSON object with members: + + ``path`` + A string specifying the install destination path. The path may + be absolute or relative to the install prefix. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`install` command invocation that specified this + destination is available. The value is an unsigned integer 0-based + index into the ``backtraceGraph`` member's ``nodes`` array. + +``launchers`` + Optional member that is present on executable targets that have + at least one launcher specified by the project. The value is a + JSON array of entries corresponding to the specified launchers. + Each entry is a JSON object with members: + + ``command`` + A string specifying the path to the launcher on disk, represented + with forward slashes. If the file is inside the top-level source + directory then the path is specified relative to that directory. + + ``arguments`` + Optional member that is present when the launcher command has + arguments preceding the executable to be launched. The value + is a JSON array of strings representing the arguments. + + ``type`` + A string specifying the type of launcher. The value is one of + the following: + + ``emulator`` + An emulator for the target platform when cross-compiling. + See the :prop_tgt:`CROSSCOMPILING_EMULATOR` target property. + + ``test`` + A start program for the execution of tests. + See the :prop_tgt:`TEST_LAUNCHER` target property. + + This field was added in codemodel version 2.7. + +``link`` + Optional member that is present for executables and shared library + targets that link into a runtime binary. The value is a JSON object + with members describing the link step: + + ``language`` + A string specifying the language (e.g. ``C``, ``CXX``, ``Fortran``) + of the toolchain is used to invoke the linker. + + ``commandFragments`` + Optional member that is present when fragments of the link command + line invocation are available. The value is a JSON array of entries + specifying ordered fragments. Each entry is a JSON object with members: + + ``fragment`` + A string specifying a fragment of the link command line invocation. + The value is encoded in the build system's native shell format. + + ``role`` + A string specifying the role of the fragment's content: + + * ``flags``: link flags. + * ``libraries``: link library file paths or flags. + * ``libraryPath``: library search path flags. + * ``frameworkPath``: macOS framework search path flags. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`target_link_libraries`, :command:`target_link_options`, + or other command invocation that added this link fragment is available. + The value is an unsigned integer 0-based index into the ``backtraceGraph`` + member's ``nodes`` array. + + ``lto`` + Optional member that is present with boolean value ``true`` + when link-time optimization (a.k.a. interprocedural optimization + or link-time code generation) is enabled. + + ``sysroot`` + Optional member that is present when the :variable:`CMAKE_SYSROOT_LINK` + or :variable:`CMAKE_SYSROOT` variable is defined. The value is a + JSON object with one member: + + ``path`` + A string specifying the absolute path to the sysroot, represented + with forward slashes. + +``archive`` + Optional member that is present for static library targets. The value + is a JSON object with members describing the archive step: + + ``commandFragments`` + Optional member that is present when fragments of the archiver command + line invocation are available. The value is a JSON array of entries + specifying the fragments. Each entry is a JSON object with members: + + ``fragment`` + A string specifying a fragment of the archiver command line invocation. + The value is encoded in the build system's native shell format. + + ``role`` + A string specifying the role of the fragment's content: + + * ``flags``: archiver flags. + + ``lto`` + Optional member that is present with boolean value ``true`` + when link-time optimization (a.k.a. interprocedural optimization + or link-time code generation) is enabled. + +``debugger`` + Optional member that is present when the target has one of the + following fields set. + The value is a JSON object of entries corresponding to + debugger specific values set. + + This field was added in codemodel version 2.8. + + ``workingDirectory`` + Optional member that is present when the + :prop_tgt:`DEBUGGER_WORKING_DIRECTORY` target property is set. + The member will also be present in :ref:`Visual Studio Generators` + when :prop_tgt:`VS_DEBUGGER_WORKING_DIRECTORY` is set. + + This field was added in codemodel version 2.8. + +``dependencies`` + Optional member that is present when the target depends on other targets. + The value is a JSON array of entries corresponding to the dependencies. + Each entry is a JSON object with members: + + ``id`` + A string uniquely identifying the target on which this target depends. + This matches the main ``id`` member of the other target. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`add_dependencies`, :command:`target_link_libraries`, + or other command invocation that created this dependency is + available. The value is an unsigned integer 0-based index into + the ``backtraceGraph`` member's ``nodes`` array. + +``fileSets`` + An optional member that is present when a target defines one or more + file sets. The value is a JSON array of entries corresponding to the + target's file sets. Each entry is a JSON object with members: + + ``name`` + A string specifying the name of the file set. + + ``type`` + A string specifying the type of the file set. See + :command:`target_sources` supported file set types. + + ``visibility`` + A string specifying the visibility of the file set; one of ``PUBLIC``, + ``PRIVATE``, or ``INTERFACE``. + + ``baseDirectories`` + A JSON array of strings, each specifying a base directory containing + sources in the file set. If the directory is inside the top-level source + directory then the path is specified relative to that directory. + Otherwise the path is absolute. + + This field was added in codemodel version 2.5. + +``sources`` + A JSON array of entries corresponding to the target's source files. + Each entry is a JSON object with members: + + ``path`` + A string specifying the path to the source file on disk, represented + with forward slashes. If the file is inside the top-level source + directory then the path is specified relative to that directory. + Otherwise the path is absolute. + + ``compileGroupIndex`` + Optional member that is present when the source is compiled. + The value is an unsigned integer 0-based index into the + ``compileGroups`` array. + + ``sourceGroupIndex`` + Optional member that is present when the source is part of a source + group either via the :command:`source_group` command or by default. + The value is an unsigned integer 0-based index into the + ``sourceGroups`` array. + + ``isGenerated`` + Optional member that is present with boolean value ``true`` if + the source is :prop_sf:`GENERATED`. + + ``fileSetIndex`` + Optional member that is present when the source is part of a file set. + The value is an unsigned integer 0-based index into the ``fileSets`` + array. + + This field was added in codemodel version 2.5. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`target_sources`, :command:`add_executable`, + :command:`add_library`, :command:`add_custom_target`, or other + command invocation that added this source to the target is + available. The value is an unsigned integer 0-based index into + the ``backtraceGraph`` member's ``nodes`` array. + +``sourceGroups`` + Optional member that is present when sources are grouped together by + the :command:`source_group` command or by default. The value is a + JSON array of entries corresponding to the groups. Each entry is + a JSON object with members: + + ``name`` + A string specifying the name of the source group. + + ``sourceIndexes`` + A JSON array listing the sources belonging to the group. + Each entry is an unsigned integer 0-based index into the + main ``sources`` array for the target. + +``compileGroups`` + Optional member that is present when the target has sources that compile. + The value is a JSON array of entries corresponding to groups of sources + that all compile with the same settings. Each entry is a JSON object + with members: + + ``sourceIndexes`` + A JSON array listing the sources belonging to the group. + Each entry is an unsigned integer 0-based index into the + main ``sources`` array for the target. + + ``language`` + A string specifying the language (e.g. ``C``, ``CXX``, ``Fortran``) + of the toolchain is used to compile the source file. + + ``languageStandard`` + Optional member that is present when the language standard is set + explicitly (e.g. via :prop_tgt:`CXX_STANDARD`) or implicitly by + compile features. Each entry is a JSON object with two members: + + ``backtraces`` + Optional member that is present when a CMake language backtrace to + the ``_STANDARD`` setting is available. If the language + standard was set implicitly by compile features those are used as + the backtrace(s). It's possible for multiple compile features to + require the same language standard so there could be multiple + backtraces. The value is a JSON array with each entry being an + unsigned integer 0-based index into the ``backtraceGraph`` + member's ``nodes`` array. + + ``standard`` + String representing the language standard. + + This field was added in codemodel version 2.2. + + ``compileCommandFragments`` + Optional member that is present when fragments of the compiler command + line invocation are available. The value is a JSON array of entries + specifying ordered fragments. Each entry is a JSON object with + one member: + + ``fragment`` + A string specifying a fragment of the compile command line invocation. + The value is encoded in the build system's native shell format. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the command invocation that added this fragment is available. + The value is an unsigned integer 0-based index into the + ``backtraceGraph`` member's ``nodes`` array. + + ``includes`` + Optional member that is present when there are include directories. + The value is a JSON array with an entry for each directory. Each + entry is a JSON object with members: + + ``path`` + A string specifying the path to the include directory, + represented with forward slashes. + + ``isSystem`` + Optional member that is present with boolean value ``true`` if + the include directory is marked as a system include directory. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`target_include_directories` or other command invocation + that added this include directory is available. The value is + an unsigned integer 0-based index into the ``backtraceGraph`` + member's ``nodes`` array. + + ``frameworks`` + Optional member that is present when, on Apple platforms, there are + frameworks. The value is a JSON array with an entry for each directory. + Each entry is a JSON object with members: + + ``path`` + A string specifying the path to the framework directory, + represented with forward slashes. + + ``isSystem`` + Optional member that is present with boolean value ``true`` if + the framework is marked as a system one. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`target_link_libraries` or other command invocation + that added this framework is available. The value is + an unsigned integer 0-based index into the ``backtraceGraph`` + member's ``nodes`` array. + + This field was added in codemodel version 2.6. + + ``precompileHeaders`` + Optional member that is present when :command:`target_precompile_headers` + or other command invocations set :prop_tgt:`PRECOMPILE_HEADERS` on the + target. The value is a JSON array with an entry for each header. Each + entry is a JSON object with members: + + ``header`` + Full path to the precompile header file. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`target_precompile_headers` or other command invocation + that added this precompiled header is available. The value is an + unsigned integer 0-based index into the ``backtraceGraph`` member's + ``nodes`` array. + + This field was added in codemodel version 2.1. + + ``defines`` + Optional member that is present when there are preprocessor definitions. + The value is a JSON array with an entry for each definition. Each + entry is a JSON object with members: + + ``define`` + A string specifying the preprocessor definition in the format + ``[=]``, e.g. ``DEF`` or ``DEF=1``. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`target_compile_definitions` or other command invocation + that added this preprocessor definition is available. The value is + an unsigned integer 0-based index into the ``backtraceGraph`` + member's ``nodes`` array. + + ``sysroot`` + Optional member that is present when the + :variable:`CMAKE_SYSROOT_COMPILE` or :variable:`CMAKE_SYSROOT` + variable is defined. The value is a JSON object with one member: + + ``path`` + A string specifying the absolute path to the sysroot, represented + with forward slashes. + +``backtraceGraph`` + A `"codemodel" version 2 "backtrace graph"`_ whose nodes are referenced + from ``backtrace`` members elsewhere in this "target" object. + +"codemodel" version 2 "backtrace graph" +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``backtraceGraph`` member of a `"codemodel" version 2 "directory" object`_, +or `"codemodel" version 2 "target" object`_ is a JSON object describing a +graph of backtraces. Its nodes are referenced from ``backtrace`` members +elsewhere in the containing object. The backtrace graph object members are: + +``nodes`` + A JSON array listing nodes in the backtrace graph. Each entry + is a JSON object with members: + + ``file`` + An unsigned integer 0-based index into the backtrace ``files`` array. + + ``line`` + An optional member present when the node represents a line within + the file. The value is an unsigned integer 1-based line number. + + ``command`` + An optional member present when the node represents a command + invocation within the file. The value is an unsigned integer + 0-based index into the backtrace ``commands`` array. + + ``parent`` + An optional member present when the node is not the bottom of + the call stack. The value is an unsigned integer 0-based index + of another entry in the backtrace ``nodes`` array. + +``commands`` + A JSON array listing command names referenced by backtrace nodes. + Each entry is a string specifying a command name. + +``files`` + A JSON array listing CMake language files referenced by backtrace nodes. + Each entry is a string specifying the path to a file, represented + with forward slashes. If the file is inside the top-level source + directory then the path is specified relative to that directory. + Otherwise the path is absolute. + +.. _`file-api configureLog`: + +Object Kind "configureLog" +-------------------------- + +.. versionadded:: 3.26 + +The ``configureLog`` object kind describes the location and contents of +a :manual:`cmake-configure-log(7)` file. + +There is only one ``configureLog`` object major version, version 1. + +"configureLog" version 1 +^^^^^^^^^^^^^^^^^^^^^^^^ + +``configureLog`` object version 1 is a JSON object: + +.. code-block:: json + + { + "kind": "configureLog", + "version": { "major": 1, "minor": 0 }, + "path": "/path/to/top-level-build-dir/CMakeFiles/CMakeConfigureLog.yaml", + "eventKindNames": [ "try_compile-v1", "try_run-v1" ] + } + +The members specific to ``configureLog`` objects are: + +``path`` + A string specifying the path to the configure log file. + Clients must read the log file from this path, which may be + different than the path documented by :manual:`cmake-configure-log(7)`. + The log file may not exist if no events are logged. + +``eventKindNames`` + A JSON array whose entries are each a JSON string naming one + of the :manual:`cmake-configure-log(7)` versioned event kinds. + At most one version of each configure log event kind will be listed. + Although the configure log may contain other (versioned) event kinds, + clients must ignore those that are not listed in this field. + +Object Kind "cache" +------------------- + +The ``cache`` object kind lists cache entries. These are the +:ref:`CMake Language Variables` stored in the persistent cache +(``CMakeCache.txt``) for the build tree. + +There is only one ``cache`` object major version, version 2. +Version 1 does not exist to avoid confusion with that from +:manual:`cmake-server(7)` mode. + +"cache" version 2 +^^^^^^^^^^^^^^^^^ + +``cache`` object version 2 is a JSON object: + +.. code-block:: json + + { + "kind": "cache", + "version": { "major": 2, "minor": 0 }, + "entries": [ + { + "name": "BUILD_SHARED_LIBS", + "value": "ON", + "type": "BOOL", + "properties": [ + { + "name": "HELPSTRING", + "value": "Build shared libraries" + } + ] + }, + { + "name": "CMAKE_GENERATOR", + "value": "Unix Makefiles", + "type": "INTERNAL", + "properties": [ + { + "name": "HELPSTRING", + "value": "Name of generator." + } + ] + } + ] + } + +The members specific to ``cache`` objects are: + +``entries`` + A JSON array whose entries are each a JSON object specifying a + cache entry. The members of each entry are: + + ``name`` + A string specifying the name of the entry. + + ``value`` + A string specifying the value of the entry. + + ``type`` + A string specifying the type of the entry used by + :manual:`cmake-gui(1)` to choose a widget for editing. + + ``properties`` + A JSON array of entries specifying associated + :ref:`cache entry properties `. + Each entry is a JSON object containing members: + + ``name`` + A string specifying the name of the cache entry property. + + ``value`` + A string specifying the value of the cache entry property. + +Object Kind "cmakeFiles" +------------------------ + +The ``cmakeFiles`` object kind lists files used by CMake while +configuring and generating the build system. These include the +``CMakeLists.txt`` files as well as included ``.cmake`` files. + +There is only one ``cmakeFiles`` object major version, version 1. + +"cmakeFiles" version 1 +^^^^^^^^^^^^^^^^^^^^^^ + +``cmakeFiles`` object version 1 is a JSON object: + +.. code-block:: json + + { + "kind": "cmakeFiles", + "version": { "major": 1, "minor": 1 }, + "paths": { + "build": "/path/to/top-level-build-dir", + "source": "/path/to/top-level-source-dir" + }, + "inputs": [ + { + "path": "CMakeLists.txt" + }, + { + "isGenerated": true, + "path": "/path/to/top-level-build-dir/.../CMakeSystem.cmake" + }, + { + "isExternal": true, + "path": "/path/to/external/third-party/module.cmake" + }, + { + "isCMake": true, + "isExternal": true, + "path": "/path/to/cmake/Modules/CMakeGenericSystem.cmake" + } + ], + "globsDependent": [ + { + "expression": "src/*.cxx", + "recurse": true, + "files": [ + "src/foo.cxx", + "src/bar.cxx" + ] + } + ] + } + +The members specific to ``cmakeFiles`` objects are: + +``paths`` + A JSON object containing members: + + ``source`` + A string specifying the absolute path to the top-level source directory, + represented with forward slashes. + + ``build`` + A string specifying the absolute path to the top-level build directory, + represented with forward slashes. + +``inputs`` + A JSON array whose entries are each a JSON object specifying an input + file used by CMake when configuring and generating the build system. + The members of each entry are: + + ``path`` + A string specifying the path to an input file to CMake, represented + with forward slashes. If the file is inside the top-level source + directory then the path is specified relative to that directory. + Otherwise the path is absolute. + + ``isGenerated`` + Optional member that is present with boolean value ``true`` + if the path specifies a file that is under the top-level + build directory and the build is out-of-source. + This member is not available on in-source builds. + + ``isExternal`` + Optional member that is present with boolean value ``true`` + if the path specifies a file that is not under the top-level + source or build directories. + + ``isCMake`` + Optional member that is present with boolean value ``true`` + if the path specifies a file in the CMake installation. + +``globsDependent`` + Optional member that is present when the project calls :command:`file(GLOB)` + or :command:`file(GLOB_RECURSE)` with the ``CONFIGURE_DEPENDS`` option. + The value is a JSON array of JSON objects, each specifying a globbing + expression and the list of paths it matched. If the globbing expression + no longer matches the same list of paths, CMake considers the build system + to be out of date. + + This field was added in ``cmakeFiles`` version 1.1. + + The members of each entry are: + + ``expression`` + A string specifying the globbing expression. + + ``recurse`` + Optional member that is present with boolean value ``true`` + if the entry corresponds to a :command:`file(GLOB_RECURSE)` call. + Otherwise the entry corresponds to a :command:`file(GLOB)` call. + + ``listDirectories`` + Optional member that is present with boolean value ``true`` if + :command:`file(GLOB)` was called without ``LIST_DIRECTORIES false`` or + :command:`file(GLOB_RECURSE)` was called with ``LIST_DIRECTORIES true``. + + ``followSymlinks`` + Optional member that is present with boolean value ``true`` if + :command:`file(GLOB)` was called with the ``FOLLOW_SYMLINKS`` option. + + ``relative`` + Optional member that is present if :command:`file(GLOB)` was called + with the ``RELATIVE `` option. The value is a string containing + the ```` given. + + ``paths`` + A JSON array of strings specifying the paths matched by the call + to :command:`file(GLOB)` or :command:`file(GLOB_RECURSE)`. + +Object Kind "toolchains" +------------------------ + +The ``toolchains`` object kind lists properties of the toolchains used during +the build. These include the language, compiler path, ID, and version. + +There is only one ``toolchains`` object major version, version 1. + +"toolchains" version 1 +^^^^^^^^^^^^^^^^^^^^^^ + +``toolchains`` object version 1 is a JSON object: + +.. code-block:: json + + { + "kind": "toolchains", + "version": { "major": 1, "minor": 0 }, + "toolchains": [ + { + "language": "C", + "compiler": { + "path": "/usr/bin/cc", + "id": "GNU", + "version": "9.3.0", + "implicit": { + "includeDirectories": [ + "/usr/lib/gcc/x86_64-linux-gnu/9/include", + "/usr/local/include", + "/usr/include/x86_64-linux-gnu", + "/usr/include" + ], + "linkDirectories": [ + "/usr/lib/gcc/x86_64-linux-gnu/9", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib", + "/lib/x86_64-linux-gnu", + "/lib" + ], + "linkFrameworkDirectories": [], + "linkLibraries": [ "gcc", "gcc_s", "c", "gcc", "gcc_s" ] + } + }, + "sourceFileExtensions": [ "c", "m" ] + }, + { + "language": "CXX", + "compiler": { + "path": "/usr/bin/c++", + "id": "GNU", + "version": "9.3.0", + "implicit": { + "includeDirectories": [ + "/usr/include/c++/9", + "/usr/include/x86_64-linux-gnu/c++/9", + "/usr/include/c++/9/backward", + "/usr/lib/gcc/x86_64-linux-gnu/9/include", + "/usr/local/include", + "/usr/include/x86_64-linux-gnu", + "/usr/include" + ], + "linkDirectories": [ + "/usr/lib/gcc/x86_64-linux-gnu/9", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib", + "/lib/x86_64-linux-gnu", + "/lib" + ], + "linkFrameworkDirectories": [], + "linkLibraries": [ + "stdc++", "m", "gcc_s", "gcc", "c", "gcc_s", "gcc" + ] + } + }, + "sourceFileExtensions": [ + "C", "M", "c++", "cc", "cpp", "cxx", "mm", "CPP" + ] + } + ] + } + +The members specific to ``toolchains`` objects are: + +``toolchains`` + A JSON array whose entries are each a JSON object specifying a toolchain + associated with a particular language. The members of each entry are: + + ``language`` + A JSON string specifying the toolchain language, like C or CXX. Language + names are the same as language names that can be passed to the + :command:`project` command. Because CMake only supports a single toolchain + per language, this field can be used as a key. + + ``compiler`` + A JSON object containing members: + + ``path`` + Optional member that is present when the + :variable:`CMAKE__COMPILER` variable is defined for the current + language. Its value is a JSON string holding the path to the compiler. + + ``id`` + Optional member that is present when the + :variable:`CMAKE__COMPILER_ID` variable is defined for the current + language. Its value is a JSON string holding the ID (GNU, MSVC, etc.) of + the compiler. + + ``version`` + Optional member that is present when the + :variable:`CMAKE__COMPILER_VERSION` variable is defined for the + current language. Its value is a JSON string holding the version of the + compiler. + + ``target`` + Optional member that is present when the + :variable:`CMAKE__COMPILER_TARGET` variable is defined for the + current language. Its value is a JSON string holding the cross-compiling + target of the compiler. + + ``implicit`` + A JSON object containing members: + + ``includeDirectories`` + Optional member that is present when the + :variable:`CMAKE__IMPLICIT_INCLUDE_DIRECTORIES` variable is + defined for the current language. Its value is a JSON array of JSON + strings where each string holds a path to an implicit include + directory for the compiler. + + ``linkDirectories`` + Optional member that is present when the + :variable:`CMAKE__IMPLICIT_LINK_DIRECTORIES` variable is + defined for the current language. Its value is a JSON array of JSON + strings where each string holds a path to an implicit link directory + for the compiler. + + ``linkFrameworkDirectories`` + Optional member that is present when the + :variable:`CMAKE__IMPLICIT_LINK_FRAMEWORK_DIRECTORIES` variable + is defined for the current language. Its value is a JSON array of JSON + strings where each string holds a path to an implicit link framework + directory for the compiler. + + ``linkLibraries`` + Optional member that is present when the + :variable:`CMAKE__IMPLICIT_LINK_LIBRARIES` variable is defined + for the current language. Its value is a JSON array of JSON strings + where each string holds a path to an implicit link library for the + compiler. + + ``sourceFileExtensions`` + Optional member that is present when the + :variable:`CMAKE__SOURCE_FILE_EXTENSIONS` variable is defined for + the current language. Its value is a JSON array of JSON strings where + each string holds a file extension (without the leading dot) for the + language. diff --git a/packages/cmake-file-api/package.json b/packages/cmake-file-api/package.json new file mode 100644 index 00000000..7c4245c5 --- /dev/null +++ b/packages/cmake-file-api/package.json @@ -0,0 +1,32 @@ +{ + "name": "cmake-file-api", + "version": "0.1.2", + "type": "module", + "description": "TypeScript wrapper around the CMake File API", + "homepage": "https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html", + "scripts": { + "build": "tsc --build", + "lint": "eslint 'src/**/*.ts'", + "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" + }, + "files": [ + "dist/", + "!*.test.d.ts", + "!*.test.d.ts.map" + ], + "exports": { + ".": "./dist/index.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/callstackincubator/react-native-node-api.git", + "directory": "packages/cmake-file-api" + }, + "author": { + "name": "Krรฆn Hansen", + "url": "https://github.com/kraenhansen" + }, + "dependencies": { + "zod": "^4.1.11" + } +} diff --git a/packages/cmake-file-api/src/index.ts b/packages/cmake-file-api/src/index.ts new file mode 100644 index 00000000..a0f3fa3a --- /dev/null +++ b/packages/cmake-file-api/src/index.ts @@ -0,0 +1,26 @@ +export { + createSharedStatelessQuery, + createClientStatelessQuery, + createClientStatefulQuery, + type VersionSpec, + type QueryRequest, + type StatefulQuery, +} from "./query.js"; + +export { + readReplyIndex, + isReplyErrorIndexPath, + readReplyErrorIndex, + readCodemodel, + readTarget, + readCache, + readCmakeFiles, + readToolchains, + readConfigureLog, + findCurrentReplyIndexPath, + readCurrentSharedCodemodel, + readCurrentTargets, + readCurrentTargetsDeep, +} from "./reply.js"; + +export * from "./schemas.js"; diff --git a/packages/cmake-file-api/src/query.test.ts b/packages/cmake-file-api/src/query.test.ts new file mode 100644 index 00000000..5995cd23 --- /dev/null +++ b/packages/cmake-file-api/src/query.test.ts @@ -0,0 +1,228 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, it, type TestContext } from "node:test"; + +import { + createSharedStatelessQuery, + createClientStatelessQuery, + createClientStatefulQuery, + type StatefulQuery, +} from "./query.js"; + +function createTempBuildDir(context: TestContext) { + const tmpPath = fs.mkdtempSync(path.join(os.tmpdir(), "cmake-api-test-")); + + context.after(() => { + fs.rmSync(tmpPath, { recursive: true, force: true }); + }); + + return tmpPath; +} + +describe("createSharedStatelessQuery", () => { + it("creates a shared stateless query file", async function (context) { + const buildPath = createTempBuildDir(context); + + await createSharedStatelessQuery(buildPath, "codemodel", "2"); + + const queryPath = path.join(buildPath, ".cmake/api/v1/query/codemodel-v2"); + assert(fs.existsSync(queryPath), "Query file should exist"); + + const content = fs.readFileSync(queryPath, "utf-8"); + assert.strictEqual(content, "", "Query file should be empty"); + }); + + it("creates directory structure recursively", async function (context) { + const buildPath = createTempBuildDir(context); + + await createSharedStatelessQuery(buildPath, "cache", "2"); + + const queryDir = path.join(buildPath, ".cmake/api/v1/query"); + assert(fs.existsSync(queryDir), "Query directory should exist"); + + const queryPath = path.join(queryDir, "cache-v2"); + assert(fs.existsSync(queryPath), "Query file should exist"); + }); + + it("supports all object kinds", async function (context) { + const buildPath = createTempBuildDir(context); + const kinds = [ + "codemodel", + "configureLog", + "cache", + "cmakeFiles", + "toolchains", + ] as const; + + for (const kind of kinds) { + await createSharedStatelessQuery(buildPath, kind, "1"); + + const queryPath = path.join(buildPath, `.cmake/api/v1/query/${kind}-v1`); + assert(fs.existsSync(queryPath), `Query file for ${kind} should exist`); + } + }); +}); + +describe("createClientStatelessQuery", () => { + it("creates a client stateless query file", async function (context) { + const buildPath = createTempBuildDir(context); + + await createClientStatelessQuery(buildPath, "my-client", "codemodel", "2"); + + const queryPath = path.join( + buildPath, + ".cmake/api/v1/query/client-my-client/codemodel-v2", + ); + assert(fs.existsSync(queryPath), "Client query file should exist"); + + const content = fs.readFileSync(queryPath, "utf-8"); + assert.strictEqual(content, "", "Client query file should be empty"); + }); + + it("creates client directory structure", async function (context) { + const buildPath = createTempBuildDir(context); + + await createClientStatelessQuery(buildPath, "test-client", "cache", "2"); + + const clientDir = path.join( + buildPath, + ".cmake/api/v1/query/client-test-client", + ); + assert(fs.existsSync(clientDir), "Client directory should exist"); + + const queryPath = path.join(clientDir, "cache-v2"); + assert(fs.existsSync(queryPath), "Client query file should exist"); + }); + + it("supports multiple clients", async function (context) { + const buildPath = createTempBuildDir(context); + + await createClientStatelessQuery(buildPath, "client-a", "codemodel", "2"); + await createClientStatelessQuery(buildPath, "client-b", "cache", "2"); + + const clientAPath = path.join( + buildPath, + ".cmake/api/v1/query/client-client-a/codemodel-v2", + ); + const clientBPath = path.join( + buildPath, + ".cmake/api/v1/query/client-client-b/cache-v2", + ); + + assert(fs.existsSync(clientAPath), "Client A query should exist"); + assert(fs.existsSync(clientBPath), "Client B query should exist"); + }); +}); + +describe("createClientStatefulQuery", () => { + it("creates a client stateful query file with simple request", async function (context) { + const buildPath = createTempBuildDir(context); + + const query: StatefulQuery = { + requests: [{ kind: "codemodel", version: 2 }], + }; + + await createClientStatefulQuery(buildPath, "my-client", query); + + const queryPath = path.join( + buildPath, + ".cmake/api/v1/query/client-my-client/query.json", + ); + assert(fs.existsSync(queryPath), "Stateful query file should exist"); + + const content = fs.readFileSync(queryPath, "utf-8"); + const parsed = JSON.parse(content) as StatefulQuery; + + assert.deepStrictEqual(parsed, query, "Parsed query should match input"); + }); + + it("creates stateful query with complex version specifications", async function (context) { + const buildPath = createTempBuildDir(context); + + const query: StatefulQuery = { + requests: [ + { + kind: "codemodel", + version: [2, { major: 1, minor: 5 }], + }, + { + kind: "cache", + version: { major: 2, minor: 0 }, + }, + { + kind: "toolchains", + }, + ], + client: { name: "test-tool", version: "1.0.0" }, + }; + + await createClientStatefulQuery(buildPath, "advanced-client", query); + + const queryPath = path.join( + buildPath, + ".cmake/api/v1/query/client-advanced-client/query.json", + ); + const content = fs.readFileSync(queryPath, "utf-8"); + const parsed = JSON.parse(content) as StatefulQuery; + + assert.deepStrictEqual(parsed, query, "Complex query should be preserved"); + }); + + it("creates well-formatted JSON", async function (context) { + const buildPath = createTempBuildDir(context); + + const query: StatefulQuery = { + requests: [ + { kind: "codemodel", version: 2 }, + { kind: "cache", version: 2 }, + ], + }; + + await createClientStatefulQuery(buildPath, "format-test", query); + + const queryPath = path.join( + buildPath, + ".cmake/api/v1/query/client-format-test/query.json", + ); + const content = fs.readFileSync(queryPath, "utf-8"); + + // Should be pretty-printed with 2-space indentation + assert(content.includes(" "), "JSON should be indented"); + assert(content.includes("\n"), "JSON should have newlines"); + + // Should be valid JSON + assert.doesNotThrow(() => JSON.parse(content), "Should be valid JSON"); + }); + + it("supports client-specific data in requests", async function (context) { + const buildPath = createTempBuildDir(context); + + const query: StatefulQuery = { + requests: [ + { + kind: "codemodel", + version: 2, + client: { requestId: "req-001", priority: "high" }, + }, + ], + client: { sessionId: "session-123" }, + }; + + await createClientStatefulQuery(buildPath, "custom-client", query); + + const queryPath = path.join( + buildPath, + ".cmake/api/v1/query/client-custom-client/query.json", + ); + const content = fs.readFileSync(queryPath, "utf-8"); + const parsed = JSON.parse(content) as StatefulQuery; + + assert.deepStrictEqual(parsed.requests[0]?.client, { + requestId: "req-001", + priority: "high", + }); + assert.deepStrictEqual(parsed.client, { sessionId: "session-123" }); + }); +}); diff --git a/packages/cmake-file-api/src/query.ts b/packages/cmake-file-api/src/query.ts new file mode 100644 index 00000000..ccb3a2d8 --- /dev/null +++ b/packages/cmake-file-api/src/query.ts @@ -0,0 +1,93 @@ +import fs from "node:fs"; +import path from "node:path"; + +/** + * Creates a shared stateless query file for the specified object kind and major version. + * These are stateless shared queries not owned by any specific client. + * + * @param buildPath Path to the build directory + * @param kind Object kind to query for + * @param majorVersion Major version number as string + */ +export async function createSharedStatelessQuery( + buildPath: string, + kind: "codemodel" | "configureLog" | "cache" | "cmakeFiles" | "toolchains", + majorVersion: string, +) { + const queryPath = path.join( + buildPath, + `.cmake/api/v1/query/${kind}-v${majorVersion}`, + ); + await fs.promises.mkdir(path.dirname(queryPath), { recursive: true }); + await fs.promises.writeFile(queryPath, ""); +} + +/** + * Creates a client stateless query file for the specified client, object kind and major version. + * These are stateless queries owned by the specified client. + * + * @param buildPath Path to the build directory + * @param clientName Unique identifier for the client + * @param kind Object kind to query for + * @param majorVersion Major version number as string + */ +export async function createClientStatelessQuery( + buildPath: string, + clientName: string, + kind: "codemodel" | "configureLog" | "cache" | "cmakeFiles" | "toolchains", + majorVersion: string, +) { + const queryPath = path.join( + buildPath, + `.cmake/api/v1/query/client-${clientName}/${kind}-v${majorVersion}`, + ); + await fs.promises.mkdir(path.dirname(queryPath), { recursive: true }); + await fs.promises.writeFile(queryPath, ""); +} + +/** + * Version specification for stateful queries + */ +export type VersionSpec = + | number // major version only + | { major: number; minor?: number } // major with optional minor + | (number | { major: number; minor?: number })[]; // array of version specs + +/** + * Request specification for stateful queries + */ +export interface QueryRequest { + kind: "codemodel" | "configureLog" | "cache" | "cmakeFiles" | "toolchains"; + version?: VersionSpec; + client?: unknown; // Reserved for client use +} + +/** + * Stateful query specification + */ +export interface StatefulQuery { + requests: QueryRequest[]; + client?: unknown; // Reserved for client use +} + +/** + * Creates a client stateful query file (query.json) for the specified client. + * These are stateful queries owned by the specified client that can request + * specific versions and get only the most recent version recognized by CMake. + * + * @param buildPath Path to the build directory + * @param clientName Unique identifier for the client + * @param query Stateful query specification + */ +export async function createClientStatefulQuery( + buildPath: string, + clientName: string, + query: StatefulQuery, +) { + const queryPath = path.join( + buildPath, + `.cmake/api/v1/query/client-${clientName}/query.json`, + ); + await fs.promises.mkdir(path.dirname(queryPath), { recursive: true }); + await fs.promises.writeFile(queryPath, JSON.stringify(query, null, 2)); +} diff --git a/packages/cmake-file-api/src/reply.test.ts b/packages/cmake-file-api/src/reply.test.ts new file mode 100644 index 00000000..87c1a7bd --- /dev/null +++ b/packages/cmake-file-api/src/reply.test.ts @@ -0,0 +1,1075 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, it, type TestContext } from "node:test"; + +import { + findCurrentReplyIndexPath, + readReplyIndex, + readCodemodel, + readCurrentSharedCodemodel, + readTarget, + readCache, + readCmakeFiles, + readToolchains, + readConfigureLog, + isReplyErrorIndexPath, + readReplyErrorIndex, +} from "./reply.js"; + +function createTempDir(context: TestContext, prefix = "test-") { + const tmpPath = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + + context.after(() => { + fs.rmSync(tmpPath, { recursive: true, force: true }); + }); + + return tmpPath; +} + +function createMockReplyDirectory( + context: TestContext, + replyFiles: [string, Record][], +) { + const tmpPath = createTempDir(context); + + for (const [fileName, content] of replyFiles) { + const filePath = path.join(tmpPath, fileName); + fs.writeFileSync(filePath, JSON.stringify(content), { + encoding: "utf-8", + }); + } + + return tmpPath; +} + +describe("findCurrentReplyIndexPath", () => { + it("returns the correct path when only index files are present", async function (context) { + const tmpPath = createMockReplyDirectory(context, [ + ["index-a.json", {}], + ["index-b.json", {}], + ]); + const result = await findCurrentReplyIndexPath(tmpPath); + assert.strictEqual(result, path.join(tmpPath, "index-b.json")); + }); + + it("returns the correct path when only error files are present", async function (context) { + const tmpPath = createMockReplyDirectory(context, [ + ["error-a.json", {}], + ["error-b.json", {}], + ]); + const result = await findCurrentReplyIndexPath(tmpPath); + assert.strictEqual(result, path.join(tmpPath, "error-b.json")); + }); + + it("returns the correct path when both index and error files are present", async function (context) { + const tmpPath = createMockReplyDirectory(context, [ + ["index-a.json", {}], + ["error-b.json", {}], + ]); + const result = await findCurrentReplyIndexPath(tmpPath); + assert.strictEqual(result, path.join(tmpPath, "error-b.json")); + }); + + it("returns the correct path when both index and error files are present (reversed)", async function (context) { + const tmpPath = createMockReplyDirectory(context, [ + ["error-a.json", {}], + ["index-b.json", {}], + ]); + const result = await findCurrentReplyIndexPath(tmpPath); + assert.strictEqual(result, path.join(tmpPath, "index-b.json")); + }); +}); + +describe("readIndex", () => { + it("reads a well-formed index file with complete structure", async function (context) { + const mockIndex = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { + multiConfig: false, + name: "Unix Makefiles", + // Note: platform is optional according to docs - omitted here like in the example + }, + }, + objects: [ + { + kind: "codemodel", + version: { major: 2, minor: 0 }, + jsonFile: "codemodel-v2-12345.json", + }, + { + kind: "cache", + version: { major: 2, minor: 0 }, + jsonFile: "cache-v2-67890.json", + }, + ], + reply: { + "codemodel-v2": { + kind: "codemodel", + version: { major: 2, minor: 0 }, + jsonFile: "codemodel-v2-12345.json", + }, + "cache-v2": { + kind: "cache", + version: { major: 2, minor: 0 }, + jsonFile: "cache-v2-67890.json", + }, + "unknown-kind-v1": { + error: "unknown query file", + }, + "client-test-client": { + "codemodel-v2": { + kind: "codemodel", + version: { major: 2, minor: 0 }, + jsonFile: "codemodel-v2-12345.json", + }, + "unknown-v1": { + error: "unknown query file", + }, + }, + }, + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["index-a.json", mockIndex], + ]); + const result = await readReplyIndex(path.join(tmpPath, "index-a.json")); + + // Verify the entire structure matches our mock data + assert.deepStrictEqual(result, mockIndex); + }); + + it("reads index file with generator platform", async function (context) { + const mockIndexWithPlatform = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { + multiConfig: true, + name: "Visual Studio 16 2019", + platform: "x64", // Present when generator supports CMAKE_GENERATOR_PLATFORM + }, + }, + objects: [], + reply: {}, + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["index-b.json", mockIndexWithPlatform], + ]); + const result = await readReplyIndex(path.join(tmpPath, "index-b.json")); + + // Verify the entire structure matches our mock data + assert.deepStrictEqual(result, mockIndexWithPlatform); + }); +}); + +describe("readCodeModel", () => { + it("reads a well-formed codemodel file", async function (context) { + const mockCodemodel = { + kind: "codemodel", + version: { major: 2, minor: 3 }, + paths: { + source: "/path/to/source", + build: "/path/to/build", + }, + configurations: [ + { + name: "Debug", + directories: [ + { + source: ".", + build: ".", + childIndexes: [], + projectIndex: 0, + targetIndexes: [0], + hasInstallRule: true, + minimumCMakeVersion: { + string: "3.14", + }, + jsonFile: "directory-debug.json", + }, + ], + projects: [ + { + name: "MyProject", + directoryIndexes: [0], + targetIndexes: [0], + }, + ], + targets: [ + { + name: "MyExecutable", + id: "MyExecutable::@6890a9b7b1a1a2e4d6b9", + directoryIndex: 0, + projectIndex: 0, + jsonFile: "target-MyExecutable.json", + }, + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["codemodel-v2-12345.json", mockCodemodel], + ]); + const result = await readCodemodel( + path.join(tmpPath, "codemodel-v2-12345.json"), + ); + + // Verify the entire structure matches our mock data + assert.deepStrictEqual(result, mockCodemodel); + }); +}); + +describe("readTarget", () => { + // Base objects for reusable test data + const baseTarget = { + name: "MyTarget", + id: "MyTarget::@6890a9b7b1a1a2e4d6b9", + type: "EXECUTABLE" as const, + paths: { + source: ".", + build: ".", + }, + }; + + const baseCompileGroup = { + sourceIndexes: [0], + language: "CXX", + includes: [ + { + path: "/usr/include", + isSystem: true, + backtrace: 1, + }, + ], + defines: [ + { + define: "NDEBUG", + backtrace: 2, + }, + ], + }; + + const baseSource = { + path: "main.cpp", + compileGroupIndex: 0, + isGenerated: false, + backtrace: 1, + }; + + it("validates TargetV2_0 schema (base version)", async function (context) { + const targetV2_0 = { + ...baseTarget, + sources: [baseSource], + compileGroups: [baseCompileGroup], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_0.json", targetV2_0], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_0.json"), + "2.0", + ); + + assert.deepStrictEqual(result, targetV2_0); + }); + + it("validates TargetV2_1 schema (added precompileHeaders)", async function (context) { + const targetV2_1 = { + ...baseTarget, + sources: [baseSource], + compileGroups: [ + { + ...baseCompileGroup, + precompileHeaders: [ + { + header: "pch.h", + backtrace: 3, + }, + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_1.json", targetV2_1], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_1.json"), + "2.1", + ); + + assert.deepStrictEqual(result, targetV2_1); + }); + + it("validates TargetV2_2 schema (added languageStandard)", async function (context) { + const targetV2_2 = { + ...baseTarget, + sources: [baseSource], + compileGroups: [ + { + ...baseCompileGroup, + precompileHeaders: [ + { + header: "pch.h", + backtrace: 3, + }, + ], + languageStandard: { + backtraces: [4], + standard: "17", + }, + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_2.json", targetV2_2], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_2.json"), + "2.2", + ); + + assert.deepStrictEqual(result, targetV2_2); + }); + + it("validates TargetV2_5 schema (added fileSets and fileSetIndex)", async function (context) { + const targetV2_5 = { + ...baseTarget, + fileSets: [ + { + name: "HEADERS", + type: "HEADERS", + visibility: "PUBLIC" as const, + baseDirectories: ["."], + }, + ], + sources: [ + { + ...baseSource, + fileSetIndex: 0, + }, + ], + compileGroups: [ + { + ...baseCompileGroup, + precompileHeaders: [ + { + header: "pch.h", + backtrace: 3, + }, + ], + languageStandard: { + backtraces: [4], + standard: "17", + }, + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_5.json", targetV2_5], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_5.json"), + "2.5", + ); + + assert.deepStrictEqual(result, targetV2_5); + }); + + it("validates TargetV2_6 schema (added frameworks)", async function (context) { + const targetV2_6 = { + ...baseTarget, + fileSets: [ + { + name: "HEADERS", + type: "HEADERS", + visibility: "PUBLIC" as const, + baseDirectories: ["."], + }, + ], + sources: [ + { + ...baseSource, + fileSetIndex: 0, + }, + ], + compileGroups: [ + { + ...baseCompileGroup, + precompileHeaders: [ + { + header: "pch.h", + backtrace: 3, + }, + ], + languageStandard: { + backtraces: [4], + standard: "17", + }, + frameworks: [ + { + path: "/System/Library/Frameworks/Foundation.framework", + isSystem: true, + backtrace: 5, + }, + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_6.json", targetV2_6], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_6.json"), + "2.6", + ); + + assert.deepStrictEqual(result, targetV2_6); + }); + + it("validates TargetV2_7 schema (added launchers)", async function (context) { + const targetV2_7 = { + ...baseTarget, + launchers: [ + { + command: "/usr/bin/gdb", + arguments: ["--args"], + type: "test" as const, + }, + ], + fileSets: [ + { + name: "HEADERS", + type: "HEADERS", + visibility: "PUBLIC" as const, + baseDirectories: ["."], + }, + ], + sources: [ + { + ...baseSource, + fileSetIndex: 0, + }, + ], + compileGroups: [ + { + ...baseCompileGroup, + precompileHeaders: [ + { + header: "pch.h", + backtrace: 3, + }, + ], + languageStandard: { + backtraces: [4], + standard: "17", + }, + frameworks: [ + { + path: "/System/Library/Frameworks/Foundation.framework", + isSystem: true, + backtrace: 5, + }, + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_7.json", targetV2_7], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_7.json"), + "2.7", + ); + + assert.deepStrictEqual(result, targetV2_7); + }); + + it("validates TargetV2_8 schema (added debugger)", async function (context) { + const targetV2_8 = { + ...baseTarget, + debugger: { + workingDirectory: "/path/to/debug", + }, + launchers: [ + { + command: "/usr/bin/gdb", + arguments: ["--args"], + type: "test" as const, + }, + ], + fileSets: [ + { + name: "HEADERS", + type: "HEADERS", + visibility: "PUBLIC" as const, + baseDirectories: ["."], + }, + ], + sources: [ + { + ...baseSource, + fileSetIndex: 0, + }, + ], + compileGroups: [ + { + ...baseCompileGroup, + precompileHeaders: [ + { + header: "pch.h", + backtrace: 3, + }, + ], + languageStandard: { + backtraces: [4], + standard: "17", + }, + frameworks: [ + { + path: "/System/Library/Frameworks/Foundation.framework", + isSystem: true, + backtrace: 5, + }, + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_8.json", targetV2_8], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_8.json"), + "2.8", + ); + + assert.deepStrictEqual(result, targetV2_8); + }); +}); + +describe("readCache", () => { + it("reads a well-formed cache file", async function (context) { + const mockCache = { + kind: "cache", + version: { major: 2, minor: 0 }, + entries: [ + { + name: "BUILD_SHARED_LIBS", + value: "ON", + type: "BOOL", + properties: [ + { + name: "HELPSTRING", + value: "Build shared libraries", + }, + ], + }, + { + name: "CMAKE_GENERATOR", + value: "Unix Makefiles", + type: "INTERNAL", + properties: [ + { + name: "HELPSTRING", + value: "Name of generator.", + }, + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["cache-v2.json", mockCache], + ]); + const result = await readCache(path.join(tmpPath, "cache-v2.json"), "2.0"); + + // Verify the entire structure matches our mock data + assert.deepStrictEqual(result, mockCache); + }); +}); + +describe("readCmakeFiles", () => { + // Base objects for reusable test data + const baseCmakeFiles = { + kind: "cmakeFiles" as const, + paths: { + build: "/path/to/top-level-build-dir", + source: "/path/to/top-level-source-dir", + }, + inputs: [ + { + path: "CMakeLists.txt", + }, + { + isExternal: true, + path: "/path/to/external/third-party/module.cmake", + }, + ], + }; + + it("validates CmakeFilesV1_0 schema (base version)", async function (context) { + const cmakeFilesV1_0 = { + ...baseCmakeFiles, + version: { major: 1, minor: 0 }, + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["cmakeFiles-v1_0.json", cmakeFilesV1_0], + ]); + const result = await readCmakeFiles( + path.join(tmpPath, "cmakeFiles-v1_0.json"), + "1.0", + ); + + assert.deepStrictEqual(result, cmakeFilesV1_0); + }); + + it("validates CmakeFilesV1_1 schema (added globsDependent)", async function (context) { + const cmakeFilesV1_1 = { + ...baseCmakeFiles, + version: { major: 1, minor: 1 }, + globsDependent: [ + { + expression: "src/*.cxx", + recurse: true, + paths: ["src/foo.cxx", "src/bar.cxx"], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["cmakeFiles-v1_1.json", cmakeFilesV1_1], + ]); + const result = await readCmakeFiles( + path.join(tmpPath, "cmakeFiles-v1_1.json"), + "1.1", + ); + + assert.deepStrictEqual(result, cmakeFilesV1_1); + }); +}); + +describe("readToolchains", () => { + it("reads a well-formed toolchains file", async function (context) { + const mockToolchains = { + kind: "toolchains", + version: { major: 1, minor: 0 }, + toolchains: [ + { + language: "C", + compiler: { + path: "/usr/bin/cc", + id: "GNU", + version: "9.3.0", + implicit: { + includeDirectories: [ + "/usr/lib/gcc/x86_64-linux-gnu/9/include", + "/usr/local/include", + "/usr/include/x86_64-linux-gnu", + "/usr/include", + ], + linkDirectories: [ + "/usr/lib/gcc/x86_64-linux-gnu/9", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib", + "/lib/x86_64-linux-gnu", + "/lib", + ], + linkFrameworkDirectories: [], + linkLibraries: ["gcc", "gcc_s", "c", "gcc", "gcc_s"], + }, + }, + sourceFileExtensions: ["c", "m"], + }, + { + language: "CXX", + compiler: { + path: "/usr/bin/c++", + id: "GNU", + version: "9.3.0", + implicit: { + includeDirectories: [ + "/usr/include/c++/9", + "/usr/include/x86_64-linux-gnu/c++/9", + "/usr/include/c++/9/backward", + "/usr/lib/gcc/x86_64-linux-gnu/9/include", + "/usr/local/include", + "/usr/include/x86_64-linux-gnu", + "/usr/include", + ], + linkDirectories: [ + "/usr/lib/gcc/x86_64-linux-gnu/9", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib", + "/lib/x86_64-linux-gnu", + "/lib", + ], + linkFrameworkDirectories: [], + linkLibraries: [ + "stdc++", + "m", + "gcc_s", + "gcc", + "c", + "gcc_s", + "gcc", + ], + }, + }, + sourceFileExtensions: [ + "C", + "M", + "c++", + "cc", + "cpp", + "cxx", + "mm", + "CPP", + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["toolchains-v1.json", mockToolchains], + ]); + const result = await readToolchains( + path.join(tmpPath, "toolchains-v1.json"), + "1.0", + ); + + // Verify the entire structure matches our mock data + assert.deepStrictEqual(result, mockToolchains); + }); +}); + +describe("readConfigureLog", () => { + it("reads a well-formed configureLog file", async function (context) { + const mockConfigureLog = { + kind: "configureLog", + version: { major: 1, minor: 0 }, + path: "/path/to/build/dir/CMakeFiles/CMakeConfigureLog.yaml", + eventKindNames: [ + "message", + "try_compile-v1", + "try_run-v1", + "detect-c_compiler-v1", + "detect-cxx_compiler-v1", + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["configureLog-v1.json", mockConfigureLog], + ]); + const result = await readConfigureLog( + path.join(tmpPath, "configureLog-v1.json"), + "1.0", + ); + assert.deepStrictEqual(result, mockConfigureLog); + }); +}); + +describe("Reply Error Index Support", () => { + describe("isReplyErrorIndexPath", () => { + it("identifies reply error index files correctly", function () { + assert.strictEqual( + isReplyErrorIndexPath("/path/to/error-12345.json"), + true, + ); + assert.strictEqual( + isReplyErrorIndexPath("/path/to/index-12345.json"), + false, + ); + assert.strictEqual(isReplyErrorIndexPath("error-abc.json"), true); + assert.strictEqual(isReplyErrorIndexPath("index-abc.json"), false); + }); + }); + + describe("readReplyErrorIndex", () => { + it("reads a well-formed reply error index file", async function (context) { + const mockReplyErrorIndex = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { + multiConfig: false, + name: "Unix Makefiles", + }, + }, + objects: [ + { + kind: "configureLog", + version: { major: 1, minor: 0 }, + jsonFile: "configureLog-v1-12345.json", + }, + ], + reply: { + "configureLog-v1": { + kind: "configureLog", + version: { major: 1, minor: 0 }, + jsonFile: "configureLog-v1-12345.json", + }, + "codemodel-v2": { + error: "CMake failed to generate build system", + }, + "cache-v2": { + error: "CMake failed to generate build system", + }, + }, + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["error-12345.json", mockReplyErrorIndex], + ]); + const result = await readReplyErrorIndex( + path.join(tmpPath, "error-12345.json"), + ); + + assert.deepStrictEqual(result, mockReplyErrorIndex); + }); + + it("rejects non-reply error index files", async function (context) { + const tmpPath = createMockReplyDirectory(context, [ + ["index-12345.json", {}], + ]); + + await assert.rejects( + () => readReplyErrorIndex(path.join(tmpPath, "index-12345.json")), + /Expected a path to an error-\*\.json file/, + ); + }); + + it("rejects reply error index with unsupported object kind in objects array", async function (context) { + const invalidReplyErrorIndex = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { + multiConfig: false, + name: "Unix Makefiles", + }, + }, + objects: [ + { + kind: "codemodel", // Invalid: only configureLog is supported + version: { major: 2, minor: 0 }, + jsonFile: "codemodel-v2-12345.json", + }, + ], + reply: {}, + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["error-12345.json", invalidReplyErrorIndex], + ]); + + await assert.rejects( + () => readReplyErrorIndex(path.join(tmpPath, "error-12345.json")), + (error: Error) => { + return error.message.includes( + 'Invalid input: expected \\"configureLog\\"', + ); + }, + ); + }); + + it("rejects reply error index with unsupported object kind in client stateful query responses", async function (context) { + const invalidReplyErrorIndex = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { + multiConfig: false, + name: "Unix Makefiles", + }, + }, + objects: [], + reply: { + "client-test": { + "query.json": { + client: {}, + requests: [ + { + kind: "codemodel", + version: { major: 2, minor: 0 }, + }, + ], + responses: [ + { + kind: "codemodel", // Invalid: only configureLog is supported in error index + version: { major: 2, minor: 0 }, + jsonFile: "codemodel-v2-12345.json", + }, + ], + }, + }, + }, + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["error-12345.json", invalidReplyErrorIndex], + ]); + + await assert.rejects( + () => readReplyErrorIndex(path.join(tmpPath, "error-12345.json")), + (error: Error) => { + return error.message.includes( + 'Invalid input: expected \\"configureLog\\"', + ); + }, + ); + }); + }); + + describe("readCurrentCodemodel with error index handling", () => { + it("throws descriptive error when current index is a reply error index", async function (context) { + const mockReplyErrorIndex = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { multiConfig: false, name: "Unix Makefiles" }, + }, + objects: [], + reply: { + "codemodel-v2": { error: "Build system generation failed" }, + }, + }; + + const buildPath = createTempDir(context, "build-test-"); + const replyPath = path.join(buildPath, ".cmake/api/v1/reply"); + await fs.promises.mkdir(replyPath, { recursive: true }); + + fs.writeFileSync( + path.join(replyPath, "error-12345.json"), + JSON.stringify(mockReplyErrorIndex), + ); + + await assert.rejects( + () => readCurrentSharedCodemodel(buildPath), + /CMake failed to generate build system\. Error in codemodel: Build system generation failed/, + ); + }); + + it("throws generic error when reply error index has no codemodel entry", async function (context) { + const mockReplyErrorIndex = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { multiConfig: false, name: "Unix Makefiles" }, + }, + objects: [], + reply: {}, + }; + + const buildPath = createTempDir(context, "build-test-"); + const replyPath = path.join(buildPath, ".cmake/api/v1/reply"); + await fs.promises.mkdir(replyPath, { recursive: true }); + + fs.writeFileSync( + path.join(replyPath, "error-12345.json"), + JSON.stringify(mockReplyErrorIndex), + ); + + await assert.rejects( + () => readCurrentSharedCodemodel(buildPath), + /CMake failed to generate build system\. No codemodel available in error index\./, + ); + }); + }); +}); diff --git a/packages/cmake-file-api/src/reply.ts b/packages/cmake-file-api/src/reply.ts new file mode 100644 index 00000000..0a8a1a02 --- /dev/null +++ b/packages/cmake-file-api/src/reply.ts @@ -0,0 +1,240 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +import * as z from "zod"; + +import * as schemas from "./schemas.js"; + +/** + * As per https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html#v1-reply-error-index + */ +export async function findCurrentReplyIndexPath(replyPath: string) { + // If multiple index-*.json and/or error-*.json files are present, + // the one with the largest name in lexicographic order, + // excluding the index- or error- prefix, is the current index. + + const fileNames = ( + await Promise.all([ + Array.fromAsync( + fs.promises.glob("error-*.json", { + withFileTypes: false, + cwd: replyPath, + }), + ), + Array.fromAsync( + fs.promises.glob("index-*.json", { + withFileTypes: false, + cwd: replyPath, + }), + ), + ]) + ).flat(); + + const [currentIndexFileName] = fileNames + .sort((a, b) => { + const strippedA = a.replace(/^(error|index)-/, ""); + const strippedB = b.replace(/^(error|index)-/, ""); + return strippedA.localeCompare(strippedB); + }) + .reverse(); + + assert( + currentIndexFileName, + `No index-*.json or error-*.json files found in ${replyPath}`, + ); + + return path.join(replyPath, currentIndexFileName); +} + +export async function readReplyIndex(filePath: string) { + assert( + path.basename(filePath).startsWith("index-") && + path.extname(filePath) === ".json", + "Expected a path to a index-*.json file", + ); + const content = await fs.promises.readFile(filePath, "utf-8"); + return schemas.IndexReplyV1.parse(JSON.parse(content)); +} + +export function isReplyErrorIndexPath(filePath: string): boolean { + return ( + path.basename(filePath).startsWith("error-") && + path.extname(filePath) === ".json" + ); +} + +export async function readReplyErrorIndex(filePath: string) { + assert( + isReplyErrorIndexPath(filePath), + "Expected a path to an error-*.json file", + ); + const content = await fs.promises.readFile(filePath, "utf-8"); + return schemas.ReplyErrorIndex.parse(JSON.parse(content)); +} + +export async function readCodemodel(filePath: string) { + assert( + path.basename(filePath).startsWith("codemodel-") && + path.extname(filePath) === ".json", + "Expected a path to a codemodel-*.json file", + ); + const content = await fs.promises.readFile(filePath, "utf-8"); + return schemas.CodemodelV2.parse(JSON.parse(content)); +} + +/** + * Call {@link createSharedStatelessQuery} to create a shared codemodel query before reading the current shared codemodel. + */ +export async function readCurrentSharedCodemodel(buildPath: string) { + const replyPath = path.join(buildPath, `.cmake/api/v1/reply`); + const replyIndexPath = await findCurrentReplyIndexPath(replyPath); + + // Check if this is an error index - they don't contain codemodel data + if (isReplyErrorIndexPath(replyIndexPath)) { + const errorIndex = await readReplyErrorIndex(replyIndexPath); + const { reply } = errorIndex; + const codemodelFile = reply["codemodel-v2"]; + + if ( + codemodelFile && + "error" in codemodelFile && + typeof codemodelFile.error === "string" + ) { + throw new Error( + `CMake failed to generate build system. Error in codemodel: ${codemodelFile.error}`, + ); + } + + throw new Error( + "CMake failed to generate build system. No codemodel available in error index.", + ); + } + + const index = await readReplyIndex(replyIndexPath); + const { reply } = index; + const { "codemodel-v2": codemodelFile } = reply; + assert( + codemodelFile, + "Expected a codemodel-v2 reply file - was a query created?", + ); + if ("error" in codemodelFile && typeof codemodelFile.error === "string") { + throw new Error( + `Error reading codemodel-v2 reply file: ${codemodelFile.error}`, + ); + } + + // Use ReplyFileReference schema to validate and parse the codemodel file + const { kind, jsonFile } = schemas.ReplyFileReferenceV1.parse(codemodelFile); + assert(kind === "codemodel", "Expected a codemodel file reference"); + + const codemodelPath = path.join(buildPath, `.cmake/api/v1/reply`, jsonFile); + return readCodemodel(codemodelPath); +} + +export async function readCurrentTargets( + buildPath: string, + configuration: string, +) { + const { configurations } = await readCurrentSharedCodemodel(buildPath); + const relevantConfig = + configurations.length === 1 + ? configurations[0] + : configurations.find((config) => config.name === configuration); + assert( + relevantConfig, + `Unable to locate "${configuration}" configuration found`, + ); + return relevantConfig.targets; +} + +export async function readTarget( + targetPath: string, + version: keyof typeof schemas.targetSchemaPerVersion, +): Promise> { + assert( + path.basename(targetPath).startsWith("target-") && + path.extname(targetPath) === ".json", + "Expected a path to a target-*.json file", + ); + const content = await fs.promises.readFile(targetPath, "utf-8"); + return schemas.targetSchemaPerVersion[version].parse(JSON.parse(content)); +} + +export async function readCurrentTargetsDeep( + buildPath: string, + configuration: string, + version: keyof typeof schemas.targetSchemaPerVersion, +): Promise[]> { + const targets = await readCurrentTargets(buildPath, configuration); + return Promise.all( + targets.map((target) => { + const targetPath = path.join( + buildPath, + `.cmake/api/v1/reply`, + target.jsonFile, + ); + return readTarget(targetPath, version); + }), + ); +} + +export async function readCache( + cachePath: string, + version: keyof typeof schemas.cacheSchemaPerVersion, +): Promise> { + assert( + path.basename(cachePath).startsWith("cache-") && + path.extname(cachePath) === ".json", + "Expected a path to a cache-*.json file", + ); + const content = await fs.promises.readFile(cachePath, "utf-8"); + return schemas.cacheSchemaPerVersion[version].parse(JSON.parse(content)); +} + +export async function readCmakeFiles( + cmakeFilesPath: string, + version: keyof typeof schemas.cmakeFilesSchemaPerVersion, +): Promise< + z.infer<(typeof schemas.cmakeFilesSchemaPerVersion)[typeof version]> +> { + assert( + path.basename(cmakeFilesPath).startsWith("cmakeFiles-") && + path.extname(cmakeFilesPath) === ".json", + "Expected a path to a cmakeFiles-*.json file", + ); + const content = await fs.promises.readFile(cmakeFilesPath, "utf-8"); + return schemas.cmakeFilesSchemaPerVersion[version].parse(JSON.parse(content)); +} + +export async function readToolchains( + toolchainsPath: string, + version: keyof typeof schemas.toolchainsSchemaPerVersion, +): Promise< + z.infer<(typeof schemas.toolchainsSchemaPerVersion)[typeof version]> +> { + assert( + path.basename(toolchainsPath).startsWith("toolchains-") && + path.extname(toolchainsPath) === ".json", + "Expected a path to a toolchains-*.json file", + ); + const content = await fs.promises.readFile(toolchainsPath, "utf-8"); + return schemas.toolchainsSchemaPerVersion[version].parse(JSON.parse(content)); +} + +export async function readConfigureLog( + configureLogPath: string, + version: keyof typeof schemas.configureLogSchemaPerVersion, +): Promise< + z.infer<(typeof schemas.configureLogSchemaPerVersion)[typeof version]> +> { + assert( + path.basename(configureLogPath).startsWith("configureLog-") && + path.extname(configureLogPath) === ".json", + "Expected a path to a configureLog-*.json file", + ); + const content = await fs.promises.readFile(configureLogPath, "utf-8"); + return schemas.configureLogSchemaPerVersion[version].parse( + JSON.parse(content), + ); +} diff --git a/packages/cmake-file-api/src/schemas.ts b/packages/cmake-file-api/src/schemas.ts new file mode 100644 index 00000000..3984bee5 --- /dev/null +++ b/packages/cmake-file-api/src/schemas.ts @@ -0,0 +1,7 @@ +export * from "./schemas/ReplyIndexV1.js"; +export * from "./schemas/objects/CodemodelV2.js"; +export * from "./schemas/objects/TargetV2.js"; +export * from "./schemas/objects/CacheV2.js"; +export * from "./schemas/objects/CmakeFilesV1.js"; +export * from "./schemas/objects/ToolchainsV1.js"; +export * from "./schemas/objects/ConfigureLogV1.js"; diff --git a/packages/cmake-file-api/src/schemas/ReplyIndexV1.ts b/packages/cmake-file-api/src/schemas/ReplyIndexV1.ts new file mode 100644 index 00000000..272ba4ce --- /dev/null +++ b/packages/cmake-file-api/src/schemas/ReplyIndexV1.ts @@ -0,0 +1,116 @@ +import * as z from "zod"; + +export const ReplyFileReferenceV1 = z.object({ + kind: z.enum([ + "codemodel", + "configureLog", + "cache", + "cmakeFiles", + "toolchains", + ]), + version: z.object({ + major: z.number(), + minor: z.number(), + }), + jsonFile: z.string(), +}); + +const ReplyErrorObject = z.object({ + error: z.string(), +}); + +const VersionNumber = z.number(); + +const VersionObject = z.object({ + major: z.number(), + minor: z.number().optional(), +}); + +const VersionSpec = z.union([ + VersionNumber, + VersionObject, + z.array(z.union([VersionNumber, VersionObject])), +]); + +const QueryRequest = z.object({ + kind: z.string(), + version: VersionSpec.optional(), + client: z.unknown().optional(), +}); + +const ClientStatefulQueryReply = z.object({ + client: z.unknown().optional(), + requests: z.array(QueryRequest).optional(), + responses: z.array(ReplyFileReferenceV1).optional(), +}); + +export const IndexReplyV1 = z.object({ + cmake: z.object({ + version: z.object({ + major: z.number(), + minor: z.number(), + patch: z.number(), + suffix: z.string(), + string: z.string(), + isDirty: z.boolean(), + }), + paths: z.object({ + cmake: z.string(), + ctest: z.string(), + cpack: z.string(), + root: z.string(), + }), + generator: z.object({ + multiConfig: z.boolean(), + name: z.string(), + platform: z.string().optional(), + }), + }), + objects: z.array(ReplyFileReferenceV1), + reply: z.record( + z.string(), + z + .union([ + ReplyFileReferenceV1, + ReplyErrorObject, + z.record( + z.string(), + z.union([ + ReplyFileReferenceV1, + ReplyErrorObject, + ClientStatefulQueryReply, + ]), + ), + ]) + .optional(), + ), +}); + +const ReplyErrorIndexFileReference = ReplyFileReferenceV1.extend({ + kind: z.enum(["configureLog"]), +}); + +const ClientStatefulQueryReplyForErrorIndex = ClientStatefulQueryReply.extend({ + responses: z.array(ReplyErrorIndexFileReference).optional(), +}); + +export const ReplyErrorIndex = IndexReplyV1.extend({ + objects: z.array(ReplyErrorIndexFileReference), + reply: z.record( + z.string(), + z + .union([ + ReplyErrorIndexFileReference, + ReplyErrorObject, + z.record( + z.string(), + z.union([ + ReplyErrorIndexFileReference, + ReplyErrorObject, + ClientStatefulQueryReplyForErrorIndex, + ]), + ), + ]) + .optional(), + ), +}); diff --git a/packages/cmake-file-api/src/schemas/objects/CacheV2.ts b/packages/cmake-file-api/src/schemas/objects/CacheV2.ts new file mode 100644 index 00000000..a85d6caf --- /dev/null +++ b/packages/cmake-file-api/src/schemas/objects/CacheV2.ts @@ -0,0 +1,28 @@ +import * as z from "zod"; + +const CacheEntryProperty = z.object({ + name: z.string(), + value: z.string(), +}); + +const CacheEntry = z.object({ + name: z.string(), + value: z.string(), + type: z.string(), + properties: z.array(CacheEntryProperty), +}); + +export const CacheV2_0 = z.object({ + kind: z.literal("cache"), + version: z.object({ + major: z.literal(2), + minor: z.number().int().nonnegative(), + }), + entries: z.array(CacheEntry), +}); + +export const CacheV2 = z.union([CacheV2_0]); + +export const cacheSchemaPerVersion = { + "2.0": CacheV2_0, +} as const satisfies Record; diff --git a/packages/cmake-file-api/src/schemas/objects/CmakeFilesV1.ts b/packages/cmake-file-api/src/schemas/objects/CmakeFilesV1.ts new file mode 100644 index 00000000..f23bd999 --- /dev/null +++ b/packages/cmake-file-api/src/schemas/objects/CmakeFilesV1.ts @@ -0,0 +1,45 @@ +import * as z from "zod"; + +const CmakeFilesInput = z.object({ + path: z.string(), + isGenerated: z.boolean().optional(), + isExternal: z.boolean().optional(), + isCMake: z.boolean().optional(), +}); + +const CmakeFilesGlobDependent = z.object({ + expression: z.string(), + recurse: z.boolean().optional(), + listDirectories: z.boolean().optional(), + followSymlinks: z.boolean().optional(), + relative: z.string().optional(), + paths: z.array(z.string()), +}); + +export const CmakeFilesV1_0 = z.object({ + kind: z.literal("cmakeFiles"), + version: z.object({ + major: z.literal(1), + minor: z.number().max(0), + }), + paths: z.object({ + source: z.string(), + build: z.string(), + }), + inputs: z.array(CmakeFilesInput), +}); + +export const CmakeFilesV1_1 = CmakeFilesV1_0.extend({ + version: z.object({ + major: z.literal(1), + minor: z.number().min(1), + }), + globsDependent: z.array(CmakeFilesGlobDependent).optional(), +}); + +export const CmakeFilesV1 = z.union([CmakeFilesV1_0, CmakeFilesV1_1]); + +export const cmakeFilesSchemaPerVersion = { + "1.0": CmakeFilesV1_0, + "1.1": CmakeFilesV1_1, +} as const satisfies Record; diff --git a/packages/cmake-file-api/src/schemas/objects/CodemodelV2.ts b/packages/cmake-file-api/src/schemas/objects/CodemodelV2.ts new file mode 100644 index 00000000..17fc202f --- /dev/null +++ b/packages/cmake-file-api/src/schemas/objects/CodemodelV2.ts @@ -0,0 +1,77 @@ +import * as z from "zod"; + +const index = z.number().int().nonnegative(); + +const MinimumCMakeVersion = z.object({ + string: z.string(), +}); + +const DirectoryV2_0 = z.object({ + source: z.string(), + build: z.string(), + parentIndex: index.optional(), + childIndexes: z.array(index).optional(), + projectIndex: index, + targetIndexes: z.array(index).optional(), + minimumCMakeVersion: MinimumCMakeVersion.optional(), + hasInstallRule: z.boolean().optional(), +}); + +const DirectoryV2_3 = DirectoryV2_0.extend({ + jsonFile: z.string(), +}); + +const Project = z.object({ + name: z.string(), + parentIndex: index.optional(), + childIndexes: z.array(index).optional(), + directoryIndexes: z.array(index), + targetIndexes: z.array(index).optional(), +}); + +const Target = z.object({ + name: z.string(), + id: z.string(), + directoryIndex: index, + projectIndex: index, + jsonFile: z.string(), +}); + +const ConfigurationV2_0 = z.object({ + name: z.string(), + directories: z.array(DirectoryV2_0), + projects: z.array(Project), + targets: z.array(Target), +}); + +const ConfigurationV2_3 = ConfigurationV2_0.extend({ + directories: z.array(DirectoryV2_3), +}); + +export const CodemodelV2_0 = z.object({ + kind: z.literal("codemodel"), + version: z.object({ + major: z.literal(2), + minor: z.number().max(2), + }), + paths: z.object({ + source: z.string(), + build: z.string(), + }), + configurations: z.array(ConfigurationV2_0), +}); + +export const CodemodelV2_3 = CodemodelV2_0.extend({ + version: z.object({ + major: z.literal(2), + minor: z.number().min(3), + }), + configurations: z.array(ConfigurationV2_3), +}); + +export const CodemodelV2 = z.union([CodemodelV2_0, CodemodelV2_3]); + +export const codemodelFilesSchemaPerVersion = { + "2.0": CodemodelV2_0, + "2.3": CodemodelV2_3, +} as const satisfies Record; diff --git a/packages/cmake-file-api/src/schemas/objects/ConfigureLogV1.ts b/packages/cmake-file-api/src/schemas/objects/ConfigureLogV1.ts new file mode 100644 index 00000000..6f6688ef --- /dev/null +++ b/packages/cmake-file-api/src/schemas/objects/ConfigureLogV1.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export const ConfigureLogV1_0 = z.object({ + kind: z.literal("configureLog"), + version: z.object({ + major: z.literal(1), + minor: z.literal(0), + }), + path: z.string(), + eventKindNames: z.array(z.string()), +}); + +export const ConfigureLogV1 = z.union([ConfigureLogV1_0]); + +export const configureLogSchemaPerVersion = { + "1.0": ConfigureLogV1_0, +} as const satisfies Record; diff --git a/packages/cmake-file-api/src/schemas/objects/TargetV2.ts b/packages/cmake-file-api/src/schemas/objects/TargetV2.ts new file mode 100644 index 00000000..da34e479 --- /dev/null +++ b/packages/cmake-file-api/src/schemas/objects/TargetV2.ts @@ -0,0 +1,253 @@ +import * as z from "zod"; + +const index = z.number().int().nonnegative(); + +const Artifact = z.object({ + path: z.string(), +}); + +const Folder = z.object({ + name: z.string(), +}); + +const InstallPrefix = z.object({ + path: z.string(), +}); + +const InstallDestination = z.object({ + path: z.string(), + backtrace: index.optional(), +}); + +const Install = z.object({ + prefix: InstallPrefix, + destinations: z.array(InstallDestination), +}); + +const Launcher = z.object({ + command: z.string(), + arguments: z.array(z.string()).optional(), + type: z.enum(["emulator", "test"]), +}); + +const LinkCommandFragment = z.object({ + fragment: z.string(), + role: z.enum(["flags", "libraries", "libraryPath", "frameworkPath"]), + backtrace: index.optional(), +}); + +const Sysroot = z.object({ + path: z.string(), +}); + +const Link = z.object({ + language: z.string(), + commandFragments: z.array(LinkCommandFragment).optional(), + lto: z.boolean().optional(), + sysroot: Sysroot.optional(), +}); + +const ArchiveCommandFragment = z.object({ + fragment: z.string(), + role: z.enum(["flags"]), +}); + +const Archive = z.object({ + commandFragments: z.array(ArchiveCommandFragment).optional(), + lto: z.boolean().optional(), +}); + +const Debugger = z.object({ + workingDirectory: z.string().optional(), +}); + +const Dependency = z.object({ + id: z.string(), + backtrace: index.optional(), +}); + +const FileSet = z.object({ + name: z.string(), + type: z.string(), + visibility: z.enum(["PUBLIC", "PRIVATE", "INTERFACE"]), + baseDirectories: z.array(z.string()), +}); + +const SourceGroup = z.object({ + name: z.string(), + sourceIndexes: z.array(index), +}); + +const LanguageStandard = z.object({ + backtraces: z.array(index).optional(), + standard: z.string(), +}); + +const CompileCommandFragment = z.object({ + fragment: z.string(), + backtrace: index.optional(), +}); + +const Include = z.object({ + path: z.string(), + isSystem: z.boolean().optional(), + backtrace: index.optional(), +}); + +const Framework = z.object({ + path: z.string(), + isSystem: z.boolean().optional(), + backtrace: index.optional(), +}); + +const PrecompileHeader = z.object({ + header: z.string(), + backtrace: index.optional(), +}); + +const Define = z.object({ + define: z.string(), + backtrace: index.optional(), +}); + +const BacktraceNode = z.object({ + file: index, + line: z.number().int().positive().optional(), + command: index.optional(), + parent: index.optional(), +}); + +const BacktraceGraph = z.object({ + nodes: z.array(BacktraceNode), + commands: z.array(z.string()), + files: z.array(z.string()), +}); + +// Versioned nested schemas +const SourceV2_0 = z.object({ + path: z.string(), + compileGroupIndex: index.optional(), + sourceGroupIndex: index.optional(), + isGenerated: z.boolean().optional(), + backtrace: index.optional(), +}); + +const SourceV2_5 = SourceV2_0.extend({ + fileSetIndex: index.optional(), +}); + +const CompileGroupV2_0 = z.object({ + sourceIndexes: z.array(index), + language: z.string(), + compileCommandFragments: z.array(CompileCommandFragment).optional(), + includes: z.array(Include).optional(), + defines: z.array(Define).optional(), + sysroot: Sysroot.optional(), +}); + +const CompileGroupV2_1 = CompileGroupV2_0.extend({ + precompileHeaders: z.array(PrecompileHeader).optional(), +}); + +const CompileGroupV2_2 = CompileGroupV2_1.extend({ + languageStandard: LanguageStandard.optional(), +}); + +const CompileGroupV2_6 = CompileGroupV2_2.extend({ + frameworks: z.array(Framework).optional(), +}); + +// Base version (v2.0) - Original target fields +const TargetV2_0 = z.object({ + name: z.string(), + id: z.string(), + type: z.enum([ + "EXECUTABLE", + "STATIC_LIBRARY", + "SHARED_LIBRARY", + "MODULE_LIBRARY", + "OBJECT_LIBRARY", + "INTERFACE_LIBRARY", + "UTILITY", + ]), + backtrace: index.optional(), + folder: Folder.optional(), + paths: z.object({ + source: z.string(), + build: z.string(), + }), + nameOnDisk: z.string().optional(), + artifacts: z.array(Artifact).optional(), + isGeneratorProvided: z.boolean().optional(), + install: Install.optional(), + link: Link.optional(), + archive: Archive.optional(), + dependencies: z.array(Dependency).optional(), + sources: z.array(SourceV2_0).optional(), + sourceGroups: z.array(SourceGroup).optional(), + compileGroups: z.array(CompileGroupV2_0).optional(), + backtraceGraph: BacktraceGraph.optional(), +}); + +// v2.1+ - Added precompileHeaders +const TargetV2_1 = TargetV2_0.extend({ + compileGroups: z.array(CompileGroupV2_1).optional(), +}); + +// v2.2+ - Added languageStandard +const TargetV2_2 = TargetV2_1.extend({ + compileGroups: z.array(CompileGroupV2_2).optional(), +}); + +// v2.5+ - Added fileSets and fileSetIndex in sources +const TargetV2_5 = TargetV2_2.extend({ + fileSets: z.array(FileSet).optional(), + sources: z.array(SourceV2_5).optional(), +}); + +// v2.6+ - Added frameworks +const TargetV2_6 = TargetV2_5.extend({ + compileGroups: z.array(CompileGroupV2_6).optional(), +}); + +// v2.7+ - Added launchers +const TargetV2_7 = TargetV2_6.extend({ + launchers: z.array(Launcher).optional(), +}); + +// v2.8+ - Added debugger +const TargetV2_8 = TargetV2_7.extend({ + debugger: Debugger.optional(), +}); + +// Export union of all versions for flexible validation +export const TargetV2 = z.union([ + TargetV2_0, + TargetV2_1, + TargetV2_2, + TargetV2_5, + TargetV2_6, + TargetV2_7, + TargetV2_8, +]); + +// Also export individual versions for specific use cases +export { + TargetV2_0, + TargetV2_1, + TargetV2_2, + TargetV2_5, + TargetV2_6, + TargetV2_7, + TargetV2_8, +}; + +export const targetSchemaPerVersion = { + "2.0": TargetV2_0, + "2.1": TargetV2_1, + "2.2": TargetV2_2, + "2.5": TargetV2_5, + "2.6": TargetV2_6, + "2.7": TargetV2_7, + "2.8": TargetV2_8, +} as const satisfies Record; diff --git a/packages/cmake-file-api/src/schemas/objects/ToolchainsV1.ts b/packages/cmake-file-api/src/schemas/objects/ToolchainsV1.ts new file mode 100644 index 00000000..1e3f79c2 --- /dev/null +++ b/packages/cmake-file-api/src/schemas/objects/ToolchainsV1.ts @@ -0,0 +1,37 @@ +import * as z from "zod"; + +const ToolchainCompilerImplicit = z.object({ + includeDirectories: z.array(z.string()).optional(), + linkDirectories: z.array(z.string()).optional(), + linkFrameworkDirectories: z.array(z.string()).optional(), + linkLibraries: z.array(z.string()).optional(), +}); + +const ToolchainCompiler = z.object({ + path: z.string().optional(), + id: z.string().optional(), + version: z.string().optional(), + target: z.string().optional(), + implicit: ToolchainCompilerImplicit, +}); + +const Toolchain = z.object({ + language: z.string(), + compiler: ToolchainCompiler, + sourceFileExtensions: z.array(z.string()).optional(), +}); + +export const ToolchainsV1_0 = z.object({ + kind: z.literal("toolchains"), + version: z.object({ + major: z.literal(1), + minor: z.number().int().nonnegative(), + }), + toolchains: z.array(Toolchain), +}); + +export const ToolchainsV1 = z.union([ToolchainsV1_0]); + +export const toolchainsSchemaPerVersion = { + "1.0": ToolchainsV1_0, +} as const satisfies Record; diff --git a/packages/cmake-file-api/tsconfig.json b/packages/cmake-file-api/tsconfig.json new file mode 100644 index 00000000..f183b9a9 --- /dev/null +++ b/packages/cmake-file-api/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../configs/tsconfig.node.json" +} diff --git a/packages/cmake-file-api/tsconfig.tests.json b/packages/cmake-file-api/tsconfig.tests.json new file mode 100644 index 00000000..c203a437 --- /dev/null +++ b/packages/cmake-file-api/tsconfig.tests.json @@ -0,0 +1,8 @@ +{ + "extends": "../../configs/tsconfig.node-tests.json", + "references": [ + { + "path": "./tsconfig.json" + } + ] +} diff --git a/packages/cmake-rn/CHANGELOG.md b/packages/cmake-rn/CHANGELOG.md index 90871c10..8748261e 100644 --- a/packages/cmake-rn/CHANGELOG.md +++ b/packages/cmake-rn/CHANGELOG.md @@ -1,5 +1,127 @@ # cmake-rn +## 0.6.3 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts ๐Ÿ™ˆ +- Updated dependencies [1dee80f] + - @react-native-node-api/cli-utils@0.1.4 + - cmake-file-api@0.1.2 + - react-native-node-api@1.0.1 + - weak-node-api@0.1.1 + +## 0.6.2 + +### Patch Changes + +- Updated dependencies [441dcc4] +- Updated dependencies [3d2e03e] + - @react-native-node-api/cli-utils@0.1.3 + - weak-node-api@0.1.0 + - react-native-node-api@1.0.0 + +## 0.6.1 + +### Patch Changes + +- Updated dependencies [7ff2c2b] +- Updated dependencies [7ff2c2b] + - cmake-file-api@0.1.1 + - weak-node-api@0.0.3 + - @react-native-node-api/cli-utils@0.1.2 + - react-native-node-api@0.7.1 + +## 0.6.0 + +### Minor Changes + +- 60fae96: Use `find_package` instead of `include` to locate "weak-node-api" + +### Patch Changes + +- 61fff3f: Allow passing --apple-bundle-identifier to specify the bundle identifiers used when creating Apple frameworks. +- Updated dependencies [60fae96] +- Updated dependencies [61fff3f] +- Updated dependencies [61fff3f] +- Updated dependencies [5dea205] +- Updated dependencies [60fae96] +- Updated dependencies [60fae96] +- Updated dependencies [eca721e] +- Updated dependencies [60fae96] + - react-native-node-api@0.7.0 + - weak-node-api@0.0.2 + +## 0.5.2 + +### Patch Changes + +- 07ea9dc: Add x86_64 and universal simulator triplets +- Updated dependencies [07ea9dc] +- Updated dependencies [7536c6c] +- Updated dependencies [c698698] +- Updated dependencies [a2fd422] +- Updated dependencies [bdc172e] +- Updated dependencies [4672e01] + - react-native-node-api@0.6.2 + +## 0.5.1 + +### Patch Changes + +- 5c9321b: Add `--strip` option to strip debug symbols from outputs +- 5c3de89: Locate and include debug symbols when creating an Xcframework. +- 5c3de89: Allow passing "RelWithDebInfo" and "MinSizeRel" as --configuration +- Updated dependencies [5c3de89] +- Updated dependencies [bb9a78c] + - react-native-node-api@0.6.1 + +## 0.5.0 + +### Minor Changes + +- 5156d35: Use of CMake targets producing Apple frameworks instead of free dylibs is now supported + +### Patch Changes + +- d8e90a8: Filter CMake targets by target name when passed +- 0c3e8ba: Fix expansion of options in --build and --out +- 5156d35: Refactored moving prettyPath util to CLI utils package +- Updated dependencies [acd06f2] +- Updated dependencies [5156d35] +- Updated dependencies [9f1a301] +- Updated dependencies [5016ed2] +- Updated dependencies [5156d35] + - react-native-node-api@0.6.0 + - @react-native-node-api/cli-utils@0.1.1 + +## 0.4.1 + +### Patch Changes + +- a23af5a: Use CMake file API to read shared library target paths +- Updated dependencies [2b9a538] + - react-native-node-api@0.5.2 + +## 0.4.0 + +### Minor Changes + +- ff34c45: Breaking: `CMAKE_JS_*` defines are no longer injected by default (use --cmake-js to opt-in) +- a336f07: Breaking: Renamed --target to --triplet to free up --target for passing CMake targets +- 2ecf894: Add passing of definitions (-D) to cmake when configuring +- 633dc34: Pass --target to CMake +- ff34c45: Expose includable WEAK_NODE_API_CONFIG to CMake projects + +### Patch Changes + +- 2a30d8d: Refactored CLIs to use a shared utility package +- f82239c: Pretty print spawn errors instead of simply rethrowing to commander. +- 9861bad: Assert the existence of CMakeList.txt before passing control to CMake +- Updated dependencies [2a30d8d] +- Updated dependencies [c72970f] + - react-native-node-api@0.5.1 + ## 0.3.2 ### Patch Changes @@ -22,7 +144,7 @@ ### Minor Changes -- 8557768: Derive default targets from the CMAKE_RN_TARGETS environment variable +- 8557768: Derive default targets from the CMAKE_RN_TRIPLETS environment variable ### Patch Changes diff --git a/packages/cmake-rn/README.md b/packages/cmake-rn/README.md index e4c64b33..61772ce2 100644 --- a/packages/cmake-rn/README.md +++ b/packages/cmake-rn/README.md @@ -3,3 +3,41 @@ A wrapper around Cmake making it easier to produce prebuilt binaries targeting iOS and Android matching [the prebuilt binary specification](https://github.com/callstackincubator/react-native-node-api/blob/main/docs/PREBUILDS.md). Serves the same purpose as `cmake-js` does for the Node.js community and could potentially be upstreamed into `cmake-js` eventually. + +## Linking against Node-API + +Android's dynamic linker imposes restrictions on the access to global symbols (such as the Node-API free functions): A dynamic library must explicitly declare any dependency bringing symbols it needs as `DT_NEEDED`. + +The implementation of Node-API is split between Hermes and our host package and to avoid addons having to explicitly link against either, we've introduced a `weak-node-api` library (published in `react-native-node-api` package). This library exposes only Node-API and will have its implementation injected by the host. + +To link against `weak-node-api` just use `find_package` to import the `weak-node-api` target and add it to the `target_link_libraries` of the addon's library target. + +```cmake +cmake_minimum_required(VERSION 3.15...3.31) +project(tests-buffers) + +# Defines the "weak-node-api" target +find_package(weak-node-api REQUIRED CONFIG) + +add_library(addon SHARED addon.c) +target_link_libraries(addon PRIVATE weak-node-api) +target_compile_features(addon PRIVATE cxx_std_20) + +if(APPLE) + # Build frameworks when building for Apple (optional) + set_target_properties(addon PROPERTIES + FRAMEWORK TRUE + MACOSX_FRAMEWORK_IDENTIFIER async_test.addon + MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 + MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 + XCODE_ATTRIBUTE_SKIP_INSTALL NO + ) +else() + set_target_properties(addon PROPERTIES + PREFIX "" + SUFFIX .node + ) +endif() +``` + +This is different from how `cmake-js` "injects" the Node-API for linking (via `${CMAKE_JS_INC}`, `${CMAKE_JS_SRC}` and `${CMAKE_JS_LIB}`). To allow for interoperability between these tools, we inject these when you pass `--cmake-js` to `cmake-rn`. diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 694cfe84..b766bfc1 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -1,6 +1,6 @@ { "name": "cmake-rn", - "version": "0.3.2", + "version": "0.6.3", "description": "Build React Native Node API modules with CMake", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -14,7 +14,9 @@ }, "files": [ "bin", - "dist" + "dist", + "!dist/**/*.test.d.ts", + "!dist/**/*.test.d.ts.map" ], "scripts": { "build": "tsc", @@ -22,12 +24,11 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "dependencies": { - "@commander-js/extra-typings": "^13.1.0", - "bufout": "^0.3.2", - "chalk": "^5.4.1", - "commander": "^13.1.0", - "ora": "^8.2.0", - "react-native-node-api": "0.5.0" + "@react-native-node-api/cli-utils": "0.1.4", + "cmake-file-api": "0.1.2", + "react-native-node-api": "1.0.1", + "zod": "^4.1.11", + "weak-node-api": "0.1.1" }, "peerDependencies": { "node-addon-api": "^8.3.1", diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index b9070c31..a96279ed 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -3,20 +3,23 @@ import path from "node:path"; import fs from "node:fs"; import { EventEmitter } from "node:events"; -import { Command, Option } from "@commander-js/extra-typings"; -import { spawn, SpawnFailure } from "bufout"; -import { oraPromise } from "ora"; -import chalk from "chalk"; +import { + chalk, + Command, + Option, + spawn, + oraPromise, + assertFixable, + wrapAction, +} from "@react-native-node-api/cli-utils"; -import { getWeakNodeApiVariables } from "./weak-node-api.js"; import { platforms, - allTargets, - findPlatformForTarget, - platformHasTarget, + allTriplets as allTriplets, + findPlatformForTriplet, + platformHasTriplet, } from "./platforms.js"; -import { BaseOpts, TargetContext, Platform } from "./platforms/types.js"; -import { isSupportedTriplet } from "react-native-node-api"; +import { Platform } from "./platforms/types.js"; // We're attaching a lot of listeners when spawning in parallel EventEmitter.defaultMaxListeners = 100; @@ -33,36 +36,38 @@ const sourcePathOption = new Option( "Specify the source directory containing a CMakeLists.txt file", ).default(process.cwd()); -// TODO: Add "MinSizeRel" and "RelWithDebInfo" const configurationOption = new Option("--configuration ") - .choices(["Release", "Debug"] as const) + .choices(["Release", "Debug", "RelWithDebInfo", "MinSizeRel"] as const) .default("Release"); -// TODO: Derive default targets +// TODO: Derive default build triplets // This is especially important when driving the build from within a React Native app package. -const { CMAKE_RN_TARGETS } = process.env; +const { CMAKE_RN_TRIPLETS } = process.env; -const defaultTargets = CMAKE_RN_TARGETS ? CMAKE_RN_TARGETS.split(",") : []; +const defaultTriplets = CMAKE_RN_TRIPLETS ? CMAKE_RN_TRIPLETS.split(",") : []; -for (const target of defaultTargets) { +for (const triplet of defaultTriplets) { assert( - (allTargets as string[]).includes(target), - `Unexpected target in CMAKE_RN_TARGETS: ${target}`, + (allTriplets as string[]).includes(triplet), + `Unexpected triplet in CMAKE_RN_TRIPLETS: ${triplet}`, ); } -const targetOption = new Option("--target ", "Targets to build for") - .choices(allTargets) +const tripletOption = new Option( + "--triplet ", + "Triplets to build for", +) + .choices(allTriplets) .default( - defaultTargets, - "CMAKE_RN_TARGETS environment variable split by ','", + defaultTriplets, + "CMAKE_RN_TRIPLETS environment variable split by ','", ); const buildPathOption = new Option( "--build ", "Specify the build directory to store the configured CMake project", -); +).default("{source}/build"); const cleanOption = new Option( "--clean", @@ -72,7 +77,38 @@ const cleanOption = new Option( const outPathOption = new Option( "--out ", "Specify the output directory to store the final build artifacts", -).default(false, "./{build}/{configuration}"); +).default("{build}/{configuration}"); + +const defineOption = new Option( + "-D,--define ", + "Define cache variables passed when configuring projects", +) + .argParser[]>((input, previous = []) => { + // TODO: Implement splitting of value using a regular expression (using named groups) for the format [:]= + // and return an object keyed by variable name with the string value as value or alternatively an array of [value, type] + const match = input.match( + /^(?[^:=]+)(:(?[^=]+))?=(?.+)$/, + ); + if (!match || !match.groups) { + throw new Error( + `Invalid format for -D/--define argument: ${input}. Expected [:]=`, + ); + } + const { name, type, value } = match.groups; + previous.push({ [type ? `${name}:${type}` : name]: value }); + return previous; + }) + .default([]); + +const targetOption = new Option( + "--target ", + "CMake targets to build", +).default([] as string[], "Build all targets of the CMake project"); + +const stripOption = new Option( + "--strip", + "Strip debug symbols from the final binaries", +).default(false); const noAutoLinkOption = new Option( "--no-auto-link", @@ -84,17 +120,26 @@ const noWeakNodeApiLinkageOption = new Option( "Don't pass the path of the weak-node-api library from react-native-node-api", ); +const cmakeJsOption = new Option( + "--cmake-js", + "Define CMAKE_JS_* variables used for compatibility with cmake-js", +).default(false); + let program = new Command("cmake-rn") .description("Build React Native Node API modules with CMake") - .addOption(targetOption) + .addOption(tripletOption) .addOption(verboseOption) .addOption(sourcePathOption) .addOption(buildPathOption) .addOption(outPathOption) .addOption(configurationOption) + .addOption(defineOption) .addOption(cleanOption) + .addOption(targetOption) + .addOption(stripOption) .addOption(noAutoLinkOption) - .addOption(noWeakNodeApiLinkageOption); + .addOption(noWeakNodeApiLinkageOption) + .addOption(cmakeJsOption); for (const platform of platforms) { const allOption = new Option( @@ -105,220 +150,179 @@ for (const platform of platforms) { program = platform.amendCommand(program); } +function expandTemplate( + input: string, + values: Record, +): string { + return input.replaceAll(/{([^}]+)}/g, (_, key: string) => + typeof values[key] === "string" ? values[key] : "", + ); +} + program = program.action( - async ({ target: requestedTargets, ...baseOptions }) => { - try { - const buildPath = getBuildPath(baseOptions); - if (baseOptions.clean) { - await fs.promises.rm(buildPath, { recursive: true, force: true }); - } - const targets = new Set(requestedTargets); + wrapAction(async ({ triplet: requestedTriplets, ...baseOptions }) => { + baseOptions.build = path.resolve( + process.cwd(), + expandTemplate(baseOptions.build, baseOptions), + ); + baseOptions.out = path.resolve( + process.cwd(), + expandTemplate(baseOptions.out, baseOptions), + ); + const { verbose, clean, source, out, build: buildPath } = baseOptions; + + assertFixable( + fs.existsSync(path.join(source, "CMakeLists.txt")), + `No CMakeLists.txt found in source directory: ${chalk.dim(source)}`, + { + instructions: `Change working directory into a directory with a CMakeLists.txt, create one or specify the correct source directory using --source`, + }, + ); + + if (clean) { + await fs.promises.rm(buildPath, { recursive: true, force: true }); + } + const triplets = new Set(requestedTriplets); - for (const platform of Object.values(platforms)) { - // Forcing the types a bit here, since the platform id option is dynamically added - if ((baseOptions as Record)[platform.id]) { - for (const target of platform.targets) { - targets.add(target); - } + for (const platform of Object.values(platforms)) { + // Forcing the types a bit here, since the platform id option is dynamically added + if ((baseOptions as Record)[platform.id]) { + for (const triplet of await platform.defaultTriplets("all")) { + triplets.add(triplet); } } + } - if (targets.size === 0) { - for (const platform of Object.values(platforms)) { - if (platform.isSupportedByHost()) { - for (const target of await platform.defaultTargets()) { - targets.add(target); - } + if (triplets.size === 0) { + for (const platform of Object.values(platforms)) { + if (platform.isSupportedByHost()) { + for (const triplet of await platform.defaultTriplets( + "current-development", + )) { + triplets.add(triplet); } } - if (targets.size === 0) { - throw new Error( - "Found no default targets: Install some platform specific build tools", - ); - } else { - console.error( - chalk.yellowBright("โ„น"), - "Using default targets", - chalk.dim("(" + [...targets].join(", ") + ")"), - ); - } } - - if (!baseOptions.out) { - baseOptions.out = path.join(buildPath, baseOptions.configuration); + if (triplets.size === 0) { + throw new Error( + "Found no default build triplets: Install some platform specific build tools", + ); + } else { + console.error( + chalk.yellowBright("โ„น"), + "Using default build triplets", + chalk.dim("(" + [...triplets].join(", ") + ")"), + ); } + } - const targetContexts = [...targets].map((target) => { - const platform = findPlatformForTarget(target); - const targetBuildPath = getTargetBuildPath(buildPath, target); - return { - target, - platform, - buildPath: targetBuildPath, - outputPath: path.join(targetBuildPath, "out"), - options: baseOptions, - }; - }); - - // Configure every triplet project - const targetsSummary = chalk.dim( - `(${getTargetsSummary(targetContexts)})`, - ); - await oraPromise( - Promise.all( - targetContexts.map(({ platform, ...context }) => - configureProject(platform, context, baseOptions), - ), - ), - { - text: `Configuring projects ${targetsSummary}`, - isSilent: baseOptions.verbose, - successText: `Configured projects ${targetsSummary}`, - failText: ({ message }) => `Failed to configure projects: ${message}`, - }, + const tripletContexts = [...triplets].map((triplet) => { + const platform = findPlatformForTriplet(triplet); + + assert( + platform.isSupportedByHost(), + `Triplet '${triplet}' cannot be built, as the '${platform.name}' platform is not supported on a '${process.platform}' host.`, ); - // Build every triplet project - await oraPromise( - Promise.all( - targetContexts.map(async ({ platform, ...context }) => { - // Delete any stale build artifacts before building - // This is important, since we might rename the output files - await fs.promises.rm(context.outputPath, { - recursive: true, - force: true, - }); - await buildProject(platform, context, baseOptions); - }), - ), - { - text: "Building projects", - isSilent: baseOptions.verbose, - successText: "Built projects", - failText: ({ message }) => `Failed to build projects: ${message}`, + return { + triplet, + platform, + async spawn(command: string, args: string[], cwd?: string) { + await spawn(command, args, { + outputMode: verbose ? "inherit" : "buffered", + outputPrefix: verbose ? chalk.dim(`[${triplet}] `) : undefined, + cwd, + }); }, + }; + }); + + // Configure every triplet project + const tripletsSummary = chalk.dim( + `(${getTripletsSummary(tripletContexts)})`, + ); + + // Perform configure steps for each platform in sequence + await oraPromise( + Promise.all( + platforms.map(async (platform) => { + const relevantTriplets = tripletContexts.filter(({ triplet }) => + platformHasTriplet(platform, triplet), + ); + if (relevantTriplets.length > 0) { + await platform.configure( + relevantTriplets, + baseOptions, + (command, args, cwd) => + spawn(command, args, { + outputMode: verbose ? "inherit" : "buffered", + outputPrefix: verbose + ? chalk.dim(`[${platform.name}] `) + : undefined, + cwd, + }), + ); + } + }), + ), + { + text: `Configuring projects ${tripletsSummary}`, + isSilent: baseOptions.verbose, + successText: `Configured projects ${tripletsSummary}`, + failText: ({ message }) => `Failed to configure projects: ${message}`, + }, + ); + + // Build every triplet project + await oraPromise( + Promise.all( + tripletContexts.map(async ({ platform, ...context }) => { + // TODO: Consider if this is still important ๐Ÿ˜ฌ + // // Delete any stale build artifacts before building + // // This is important, since we might rename the output files + // await fs.promises.rm(context.outputPath, { + // recursive: true, + // force: true, + // }); + await platform.build(context, baseOptions); + }), + ), + { + text: "Building projects", + isSilent: baseOptions.verbose, + successText: "Built projects", + failText: ({ message }) => `Failed to build projects: ${message}`, + }, + ); + + // Perform post-build steps for each platform in sequence + for (const platform of platforms) { + const relevantTriplets = tripletContexts.filter(({ triplet }) => + platformHasTriplet(platform, triplet), ); - - // Perform post-build steps for each platform in sequence - for (const platform of platforms) { - const relevantTargets = targetContexts.filter(({ target }) => - platformHasTarget(platform, target), - ); - if (relevantTargets.length == 0) { - continue; - } - await platform.postBuild( - { - outputPath: baseOptions.out || baseOptions.source, - targets: relevantTargets, - }, - baseOptions, - ); - } - } catch (error) { - if (error instanceof SpawnFailure) { - error.flushOutput("both"); + if (relevantTriplets.length == 0) { + continue; } - throw error; + await platform.postBuild(out, relevantTriplets, baseOptions); } - }, + }), ); -function getTargetsSummary( - targetContexts: { target: string; platform: Platform }[], +function getTripletsSummary( + tripletContexts: { triplet: string; platform: Platform }[], ) { - const targetsPerPlatform: Record = {}; - for (const { target, platform } of targetContexts) { - if (!targetsPerPlatform[platform.id]) { - targetsPerPlatform[platform.id] = []; + const tripletsPerPlatform: Record = {}; + for (const { triplet, platform } of tripletContexts) { + if (!tripletsPerPlatform[platform.id]) { + tripletsPerPlatform[platform.id] = []; } - targetsPerPlatform[platform.id].push(target); + tripletsPerPlatform[platform.id].push(triplet); } - return Object.entries(targetsPerPlatform) - .map(([platformId, targets]) => { - return `${platformId}: ${targets.join(", ")}`; + return Object.entries(tripletsPerPlatform) + .map(([platformId, triplets]) => { + return `${platformId}: ${triplets.join(", ")}`; }) .join(" / "); } -function getBuildPath({ build, source }: BaseOpts) { - // TODO: Add configuration (debug vs release) - return path.resolve(process.cwd(), build || path.join(source, "build")); -} - -/** - * Namespaces the output path with a target name - */ -function getTargetBuildPath(buildPath: string, target: unknown) { - assert(typeof target === "string", "Expected target to be a string"); - return path.join(buildPath, target.replace(/;/g, "_")); -} - -async function configureProject( - platform: Platform>, - context: TargetContext, - options: BaseOpts, -) { - const { target, buildPath, outputPath } = context; - const { verbose, source, weakNodeApiLinkage } = options; - - const nodeApiVariables = - weakNodeApiLinkage && isSupportedTriplet(target) - ? getWeakNodeApiVariables(target) - : // TODO: Make this a part of the platform definition - {}; - - const declarations = { - ...nodeApiVariables, - CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath, - }; - - await spawn( - "cmake", - [ - "-S", - source, - "-B", - buildPath, - ...platform.configureArgs(context, options), - ...toDeclarationArguments(declarations), - ], - { - outputMode: verbose ? "inherit" : "buffered", - outputPrefix: verbose ? chalk.dim(`[${target}] `) : undefined, - }, - ); -} - -async function buildProject( - platform: Platform>, - context: TargetContext, - options: BaseOpts, -) { - const { target, buildPath } = context; - const { verbose, configuration } = options; - await spawn( - "cmake", - [ - "--build", - buildPath, - "--config", - configuration, - "--", - ...platform.buildArgs(context, options), - ], - { - outputMode: verbose ? "inherit" : "buffered", - outputPrefix: verbose ? chalk.dim(`[${target}] `) : undefined, - }, - ); -} - -function toDeclarationArguments(declarations: Record) { - return Object.entries(declarations).flatMap(([key, value]) => [ - "-D", - `${key}=${value}`, - ]); -} - export { program }; diff --git a/packages/cmake-rn/src/helpers.ts b/packages/cmake-rn/src/helpers.ts new file mode 100644 index 00000000..ac88cc92 --- /dev/null +++ b/packages/cmake-rn/src/helpers.ts @@ -0,0 +1,8 @@ +export function toDefineArguments(declarations: Array>) { + return declarations.flatMap((values) => + Object.entries(values).flatMap(([key, definition]) => [ + "-D", + `${key}=${definition}`, + ]), + ); +} diff --git a/packages/cmake-rn/src/platforms.test.ts b/packages/cmake-rn/src/platforms.test.ts index 2f8c95f7..1d21c4ef 100644 --- a/packages/cmake-rn/src/platforms.test.ts +++ b/packages/cmake-rn/src/platforms.test.ts @@ -3,28 +3,30 @@ import { describe, it } from "node:test"; import { platforms, - platformHasTarget, - findPlatformForTarget, + platformHasTriplet, + findPlatformForTriplet, } from "./platforms.js"; import { Platform } from "./platforms/types.js"; -const mockPlatform = { targets: ["target1", "target2"] } as unknown as Platform; +const mockPlatform = { + triplets: ["triplet1", "triplet2"], +} as unknown as Platform; -describe("platformHasTarget", () => { - it("returns true when platform has target", () => { - assert.equal(platformHasTarget(mockPlatform, "target1"), true); +describe("platformHasTriplet", () => { + it("returns true when platform has triplet", () => { + assert.equal(platformHasTriplet(mockPlatform, "triplet1"), true); }); - it("returns false when platform doesn't have target", () => { - assert.equal(platformHasTarget(mockPlatform, "target3"), false); + it("returns false when platform doesn't have triplet", () => { + assert.equal(platformHasTriplet(mockPlatform, "triplet3"), false); }); }); -describe("findPlatformForTarget", () => { - it("returns platform when target is found", () => { +describe("findPlatformForTriplet", () => { + it("returns platform when triplet is found", () => { assert(platforms.length >= 2, "Expects at least two platforms"); const [platform1, platform2] = platforms; - const platform = findPlatformForTarget(platform1.targets[0]); + const platform = findPlatformForTriplet(platform1.triplets[0]); assert.equal(platform, platform1); assert.notEqual(platform, platform2); }); diff --git a/packages/cmake-rn/src/platforms.ts b/packages/cmake-rn/src/platforms.ts index cf892263..ed82cabb 100644 --- a/packages/cmake-rn/src/platforms.ts +++ b/packages/cmake-rn/src/platforms.ts @@ -5,23 +5,23 @@ import { platform as apple } from "./platforms/apple.js"; import { Platform } from "./platforms/types.js"; export const platforms: Platform[] = [android, apple] as const; -export const allTargets = [...android.targets, ...apple.targets] as const; +export const allTriplets = [...android.triplets, ...apple.triplets] as const; -export function platformHasTarget

( +export function platformHasTriplet

( platform: P, - target: unknown, -): target is P["targets"][number] { - return (platform.targets as unknown[]).includes(target); + triplet: unknown, +): triplet is P["triplets"][number] { + return (platform.triplets as unknown[]).includes(triplet); } -export function findPlatformForTarget(target: unknown) { +export function findPlatformForTriplet(triplet: unknown) { const platform = Object.values(platforms).find((platform) => - platformHasTarget(platform, target), + platformHasTriplet(platform, triplet), ); assert( platform, - `Unable to determine platform from target: ${ - typeof target === "string" ? target : JSON.stringify(target) + `Unable to determine platform from triplet: ${ + typeof triplet === "string" ? triplet : JSON.stringify(triplet) }`, ); return platform; diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index 63397aa0..4e505ca8 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -2,16 +2,23 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { Option } from "@commander-js/extra-typings"; +import { + Option, + oraPromise, + prettyPath, +} from "@react-native-node-api/cli-utils"; import { createAndroidLibsDirectory, - determineAndroidLibsFilename, - AndroidTriplet as Target, + AndroidTriplet as Triplet, } from "react-native-node-api"; +import * as cmakeFileApi from "cmake-file-api"; -import type { Platform } from "./types.js"; -import { oraPromise } from "ora"; -import chalk from "chalk"; +import type { BaseOpts, Platform } from "./types.js"; +import { toDefineArguments } from "../helpers.js"; +import { + getCmakeJSVariables, + getWeakNodeApiVariables, +} from "../weak-node-api.js"; // This should match https://github.com/react-native-community/template/blob/main/template/android/build.gradle#L7 const DEFAULT_NDK_VERSION = "27.1.12297006"; @@ -24,7 +31,7 @@ export const ANDROID_ARCHITECTURES = { "aarch64-linux-android": "arm64-v8a", "i686-linux-android": "x86", "x86_64-linux-android": "x86_64", -} satisfies Record; +} satisfies Record; const ndkVersionOption = new Option( "--ndk-version ", @@ -38,22 +45,76 @@ const androidSdkVersionOption = new Option( type AndroidOpts = { ndkVersion: string; androidSdkVersion: string }; -export const platform: Platform = { +function getBuildPath( + baseBuildPath: string, + triplet: Triplet, + configuration: BaseOpts["configuration"], +) { + return path.join(baseBuildPath, triplet + "-" + configuration); +} + +function getNdkPath(ndkVersion: string) { + const { ANDROID_HOME } = process.env; + assert(typeof ANDROID_HOME === "string", "Missing env variable ANDROID_HOME"); + assert( + fs.existsSync(ANDROID_HOME), + `Expected the Android SDK at ${ANDROID_HOME}`, + ); + const installNdkCommand = `sdkmanager --install "ndk;${ndkVersion}"`; + const ndkPath = path.resolve(ANDROID_HOME, "ndk", ndkVersion); + assert( + fs.existsSync(ndkPath), + `Missing Android NDK v${ndkVersion} (at ${ndkPath}) - run: ${installNdkCommand}`, + ); + return ndkPath; +} + +function getNdkToolchainPath(ndkPath: string) { + const toolchainPath = path.join( + ndkPath, + "build/cmake/android.toolchain.cmake", + ); + assert( + fs.existsSync(toolchainPath), + `No CMake toolchain found in ${toolchainPath}`, + ); + return toolchainPath; +} + +function getNdkLlvmBinPath(ndkPath: string) { + const prebuiltPath = path.join(ndkPath, "toolchains/llvm/prebuilt"); + const platforms = fs.readdirSync(prebuiltPath); + assert( + platforms.length === 1, + `Expected a single llvm prebuilt toolchain in ${prebuiltPath}`, + ); + return path.join(prebuiltPath, platforms[0], "bin"); +} + +export const platform: Platform = { id: "android", name: "Android", - targets: [ + triplets: [ "aarch64-linux-android", "armv7a-linux-androideabi", "i686-linux-android", "x86_64-linux-android", ], - defaultTargets() { - if (process.arch === "arm64") { - return ["aarch64-linux-android"]; - } else if (process.arch === "x64") { - return ["x86_64-linux-android"]; + defaultTriplets(mode) { + if (mode === "all") { + return [...this.triplets]; + } else if (mode === "current-development") { + // We're applying a heuristic to determine the current simulators + // TODO: Run a command to probe the currently running emulators instead + if (process.arch === "arm64") { + return ["aarch64-linux-android"]; + } else if (process.arch === "x64") { + return ["x86_64-linux-android"]; + } else { + return []; + } } else { - return []; + throw new Error(`Unexpected mode: ${mode as string}`); } }, amendCommand(command) { @@ -61,112 +122,167 @@ export const platform: Platform = { .addOption(ndkVersionOption) .addOption(androidSdkVersionOption); }, - configureArgs({ target }, { ndkVersion, androidSdkVersion }) { - const { ANDROID_HOME } = process.env; - assert( - typeof ANDROID_HOME === "string", - "Missing env variable ANDROID_HOME", - ); - assert( - fs.existsSync(ANDROID_HOME), - `Expected the Android SDK at ${ANDROID_HOME}`, - ); - const installNdkCommand = `sdkmanager --install "ndk;${ndkVersion}"`; - const ndkPath = path.resolve(ANDROID_HOME, "ndk", ndkVersion); - assert( - fs.existsSync(ndkPath), - `Missing Android NDK v${ndkVersion} (at ${ndkPath}) - run: ${installNdkCommand}`, - ); + async configure( + triplets, + { + configuration, + ndkVersion, + androidSdkVersion, + source, + define, + build, + weakNodeApiLinkage, + cmakeJs, + }, + ) { + const ndkPath = getNdkPath(ndkVersion); + const toolchainPath = getNdkToolchainPath(ndkPath); - const toolchainPath = path.join( - ndkPath, - "build/cmake/android.toolchain.cmake", - ); - const architecture = ANDROID_ARCHITECTURES[target]; - - return [ - "-G", - "Ninja", - "--toolchain", - toolchainPath, - "-D", - "CMAKE_SYSTEM_NAME=Android", - // "-D", - // `CPACK_SYSTEM_NAME=Android-${architecture}`, - // "-D", - // `CMAKE_INSTALL_PREFIX=${installPath}`, - // "-D", - // `CMAKE_BUILD_TYPE=${configuration}`, - "-D", - "CMAKE_MAKE_PROGRAM=ninja", - // "-D", - // "CMAKE_C_COMPILER_LAUNCHER=ccache", - // "-D", - // "CMAKE_CXX_COMPILER_LAUNCHER=ccache", - "-D", - `ANDROID_NDK=${ndkPath}`, - "-D", - `ANDROID_ABI=${architecture}`, - "-D", - "ANDROID_TOOLCHAIN=clang", - "-D", - `ANDROID_PLATFORM=${androidSdkVersion}`, - "-D", - // TODO: Make this configurable - "ANDROID_STL=c++_shared", + const commonDefinitions = [ + ...define, + { + CMAKE_BUILD_TYPE: configuration, + CMAKE_SYSTEM_NAME: "Android", + // "CMAKE_INSTALL_PREFIX": installPath, + CMAKE_MAKE_PROGRAM: "ninja", + // "-D", + // "CMAKE_C_COMPILER_LAUNCHER=ccache", + // "-D", + // "CMAKE_CXX_COMPILER_LAUNCHER=ccache", + ANDROID_NDK: ndkPath, + ANDROID_TOOLCHAIN: "clang", + ANDROID_PLATFORM: androidSdkVersion, + // TODO: Make this configurable + ANDROID_STL: "c++_shared", + }, ]; + + await Promise.all( + triplets.map(async ({ triplet, spawn }) => { + const buildPath = getBuildPath(build, triplet, configuration); + const outputPath = path.join(buildPath, "out"); + // We want to use the CMake File API to query information later + await cmakeFileApi.createSharedStatelessQuery( + buildPath, + "codemodel", + "2", + ); + + await spawn("cmake", [ + "-S", + source, + "-B", + buildPath, + // Ideally, we would use the "Ninja Multi-Config" generator here, + // but it doesn't support the "RelWithDebInfo" configuration on Android. + "-G", + "Ninja", + "--toolchain", + toolchainPath, + ...toDefineArguments([ + ...(weakNodeApiLinkage ? [getWeakNodeApiVariables(triplet)] : []), + ...(cmakeJs ? [getCmakeJSVariables(triplet)] : []), + ...commonDefinitions, + { + // "CPACK_SYSTEM_NAME": `Android-${architecture}`, + CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath, + ANDROID_ABI: ANDROID_ARCHITECTURES[triplet], + }, + ]), + ]); + }), + ); }, - buildArgs() { - return []; + async build({ triplet, spawn }, { target, build, configuration }) { + const buildPath = getBuildPath(build, triplet, configuration); + await spawn("cmake", [ + "--build", + buildPath, + ...(target.length > 0 ? ["--target", ...target] : []), + ]); }, isSupportedByHost() { const { ANDROID_HOME } = process.env; return typeof ANDROID_HOME === "string" && fs.existsSync(ANDROID_HOME); }, - async postBuild({ outputPath, targets }, { autoLink }) { - // TODO: Include `configuration` in the output path - const libraryPathByTriplet = Object.fromEntries( - await Promise.all( - targets.map(async ({ target, outputPath }) => { - assert( - fs.existsSync(outputPath), - `Expected a directory at ${outputPath}`, - ); - // Expect binary file(s), either .node or .so - const dirents = await fs.promises.readdir(outputPath, { - withFileTypes: true, - }); - const result = dirents - .filter( - (dirent) => - dirent.isFile() && - (dirent.name.endsWith(".so") || dirent.name.endsWith(".node")), - ) - .map((dirent) => path.join(dirent.parentPath, dirent.name)); - assert.equal(result.length, 1, "Expected exactly one library file"); - return [target, result[0]] as const; - }), - ), - ) as Record; - const androidLibsFilename = determineAndroidLibsFilename( - Object.values(libraryPathByTriplet), - ); - const androidLibsOutputPath = path.resolve(outputPath, androidLibsFilename); + async postBuild( + outputPath, + triplets, + { autoLink, configuration, target, build, strip, ndkVersion }, + ) { + const prebuilds: Record< + string, + { triplet: Triplet; libraryPath: string }[] + > = {}; - await oraPromise( - createAndroidLibsDirectory({ - outputPath: androidLibsOutputPath, - libraryPathByTriplet, - autoLink, - }), - { - text: "Assembling Android libs directory", - successText: `Android libs directory assembled into ${chalk.dim( - path.relative(process.cwd(), androidLibsOutputPath), - )}`, - failText: ({ message }) => - `Failed to assemble Android libs directory: ${message}`, - }, - ); + for (const { triplet, spawn } of triplets) { + const buildPath = getBuildPath(build, triplet, configuration); + assert(fs.existsSync(buildPath), `Expected a directory at ${buildPath}`); + const targets = await cmakeFileApi.readCurrentTargetsDeep( + buildPath, + configuration, + "2.0", + ); + const sharedLibraries = targets.filter( + ({ type, name }) => + type === "SHARED_LIBRARY" && + (target.length === 0 || target.includes(name)), + ); + assert.equal( + sharedLibraries.length, + 1, + "Expected exactly one shared library", + ); + const [sharedLibrary] = sharedLibraries; + const { artifacts } = sharedLibrary; + assert( + artifacts && artifacts.length === 1, + "Expected exactly one artifact", + ); + const [artifact] = artifacts; + // Add prebuild entry, creating a new entry if needed + if (!(sharedLibrary.name in prebuilds)) { + prebuilds[sharedLibrary.name] = []; + } + const libraryPath = path.join(buildPath, artifact.path); + assert( + fs.existsSync(libraryPath), + `Expected built library at ${libraryPath}`, + ); + + if (strip) { + const llvmBinPath = getNdkLlvmBinPath(getNdkPath(ndkVersion)); + const stripToolPath = path.join(llvmBinPath, `llvm-strip`); + assert( + fs.existsSync(stripToolPath), + `Expected llvm-strip to exist at ${stripToolPath}`, + ); + await spawn(stripToolPath, [libraryPath]); + } + prebuilds[sharedLibrary.name].push({ + triplet, + libraryPath, + }); + } + + for (const [libraryName, libraries] of Object.entries(prebuilds)) { + const prebuildOutputPath = path.resolve( + outputPath, + `${libraryName}.android.node`, + ); + await oraPromise( + createAndroidLibsDirectory({ + outputPath: prebuildOutputPath, + libraries, + autoLink, + }), + { + text: `Assembling Android libs directory (${libraryName})`, + successText: `Android libs directory (${libraryName}) assembled into ${prettyPath(prebuildOutputPath)}`, + failText: ({ message }) => + `Failed to assemble Android libs directory (${libraryName}): ${message}`, + }, + ); + } }, }; diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index ce091dc4..cb2e7f1b 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -1,18 +1,52 @@ import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs"; +import cp from "node:child_process"; -import { Option } from "@commander-js/extra-typings"; -import { oraPromise } from "ora"; import { - AppleTriplet as Target, + Option, + oraPromise, + prettyPath, +} from "@react-native-node-api/cli-utils"; +import { + AppleTriplet as Triplet, createAppleFramework, createXCframework, - determineXCFrameworkFilename, + dereferenceDirectory, } from "react-native-node-api"; import type { Platform } from "./types.js"; -import chalk from "chalk"; +import * as cmakeFileApi from "cmake-file-api"; +import { toDefineArguments } from "../helpers.js"; +import { + getCmakeJSVariables, + getWeakNodeApiVariables, +} from "../weak-node-api.js"; + +import * as z from "zod"; + +const XcodeListOutput = z.object({ + project: z.object({ + configurations: z.array(z.string()), + name: z.string(), + schemes: z.array(z.string()), + targets: z.array(z.string()), + }), +}); + +function listXcodeProject(cwd: string): z.infer { + const result = cp.spawnSync("xcodebuild", ["-list", "-json"], { + encoding: "utf-8", + cwd, + }); + assert.equal( + result.status, + 0, + `Failed to run xcodebuild -list: ${result.stderr}`, + ); + const parsed = JSON.parse(result.stdout) as unknown; + return XcodeListOutput.parse(parsed); +} type XcodeSDKName = | "iphoneos" @@ -28,14 +62,23 @@ const XCODE_SDK_NAMES = { "x86_64-apple-darwin": "macosx", "arm64-apple-darwin": "macosx", "arm64;x86_64-apple-darwin": "macosx", + "arm64-apple-ios": "iphoneos", "arm64-apple-ios-sim": "iphonesimulator", - "arm64-apple-tvos": "appletvos", + "x86_64-apple-ios-sim": "iphonesimulator", + "arm64;x86_64-apple-ios-sim": "iphonesimulator", + // "x86_64-apple-tvos": "appletvos", + "arm64-apple-tvos": "appletvos", + "x86_64-apple-tvos-sim": "appletvsimulator", "arm64-apple-tvos-sim": "appletvsimulator", + "arm64;x86_64-apple-tvos-sim": "appletvsimulator", + "arm64-apple-visionos": "xros", "arm64-apple-visionos-sim": "xrsimulator", -} satisfies Record; + "x86_64-apple-visionos-sim": "xrsimulator", + "arm64;x86_64-apple-visionos-sim": "xrsimulator", +} satisfies Record; type CMakeSystemName = "Darwin" | "iOS" | "tvOS" | "watchOS" | "visionOS"; @@ -43,14 +86,45 @@ const CMAKE_SYSTEM_NAMES = { "x86_64-apple-darwin": "Darwin", "arm64-apple-darwin": "Darwin", "arm64;x86_64-apple-darwin": "Darwin", + "arm64-apple-ios": "iOS", "arm64-apple-ios-sim": "iOS", - "arm64-apple-tvos": "tvOS", + "x86_64-apple-ios-sim": "iOS", + "arm64;x86_64-apple-ios-sim": "iOS", + // "x86_64-apple-tvos": "appletvos", + "arm64-apple-tvos": "tvOS", "arm64-apple-tvos-sim": "tvOS", + "x86_64-apple-tvos-sim": "tvOS", + "arm64;x86_64-apple-tvos-sim": "tvOS", + "arm64-apple-visionos": "visionOS", + "x86_64-apple-visionos-sim": "visionOS", "arm64-apple-visionos-sim": "visionOS", -} satisfies Record; + "arm64;x86_64-apple-visionos-sim": "visionOS", +} satisfies Record; + +const DESTINATION_BY_TRIPLET = { + "x86_64-apple-darwin": "generic/platform=macOS", + "arm64-apple-darwin": "generic/platform=macOS", + "arm64;x86_64-apple-darwin": "generic/platform=macOS", + + "arm64-apple-ios": "generic/platform=iOS", + "arm64-apple-ios-sim": "generic/platform=iOS Simulator", + "x86_64-apple-ios-sim": "generic/platform=iOS Simulator", + "arm64;x86_64-apple-ios-sim": "generic/platform=iOS Simulator", + + "arm64-apple-tvos": "generic/platform=tvOS", + // "x86_64-apple-tvos": "generic/platform=tvOS", + "x86_64-apple-tvos-sim": "generic/platform=tvOS Simulator", + "arm64-apple-tvos-sim": "generic/platform=tvOS Simulator", + "arm64;x86_64-apple-tvos-sim": "generic/platform=tvOS Simulator", + + "arm64-apple-visionos": "generic/platform=visionOS", + "arm64-apple-visionos-sim": "generic/platform=visionOS Simulator", + "x86_64-apple-visionos-sim": "generic/platform=visionOS Simulator", + "arm64;x86_64-apple-visionos-sim": "generic/platform=visionOS Simulator", +} satisfies Record; type AppleArchitecture = "arm64" | "x86_64" | "arm64;x86_64"; @@ -58,121 +132,344 @@ export const APPLE_ARCHITECTURES = { "x86_64-apple-darwin": "x86_64", "arm64-apple-darwin": "arm64", "arm64;x86_64-apple-darwin": "arm64;x86_64", + "arm64-apple-ios": "arm64", "arm64-apple-ios-sim": "arm64", - "arm64-apple-tvos": "arm64", + "x86_64-apple-ios-sim": "x86_64", + "arm64;x86_64-apple-ios-sim": "arm64;x86_64", + // "x86_64-apple-tvos": "x86_64", + "arm64-apple-tvos": "arm64", "arm64-apple-tvos-sim": "arm64", + "x86_64-apple-tvos-sim": "x86_64", + "arm64;x86_64-apple-tvos-sim": "arm64;x86_64", + "arm64-apple-visionos": "arm64", + "x86_64-apple-visionos-sim": "x86_64", "arm64-apple-visionos-sim": "arm64", -} satisfies Record; - -export function createPlistContent(values: Record) { - return [ - '', - '', - '', - "", - ...Object.entries(values).flatMap(([key, value]) => [ - `${key}`, - `${value}`, - ]), - "", - "", - ].join("\n"); -} - -export function getAppleBuildArgs() { - // We expect the final application to sign these binaries - return ["CODE_SIGNING_ALLOWED=NO"]; -} + "arm64;x86_64-apple-visionos-sim": "arm64;x86_64", +} satisfies Record; const xcframeworkExtensionOption = new Option( "--xcframework-extension", "Don't rename the xcframework to .apple.node", ).default(false); +const appleBundleIdentifierOption = new Option( + "--apple-bundle-identifier ", + "Unique CFBundleIdentifier used for Apple framework artifacts", +).default(undefined, "com.callstackincubator.node-api.{libraryName}"); + type AppleOpts = { xcframeworkExtension: boolean; + appleBundleIdentifier?: string; }; -export const platform: Platform = { +function getBuildPath(baseBuildPath: string, triplet: Triplet) { + return path.join(baseBuildPath, triplet.replace(/;/g, "_")); +} + +async function readCmakeSharedLibraryTarget( + buildPath: string, + configuration: string, + target: string[], +) { + const targets = await cmakeFileApi.readCurrentTargetsDeep( + buildPath, + configuration, + "2.0", + ); + const sharedLibraries = targets.filter( + ({ type, name }) => + type === "SHARED_LIBRARY" && + (target.length === 0 || target.includes(name)), + ); + assert.equal( + sharedLibraries.length, + 1, + "Expected exactly one shared library", + ); + const [sharedLibrary] = sharedLibraries; + return sharedLibrary; +} + +export const platform: Platform = { id: "apple", name: "Apple", - targets: [ + triplets: [ + "arm64-apple-darwin", + "x86_64-apple-darwin", "arm64;x86_64-apple-darwin", + "arm64-apple-ios", "arm64-apple-ios-sim", + "x86_64-apple-ios-sim", + "arm64;x86_64-apple-ios-sim", + "arm64-apple-tvos", + "x86_64-apple-tvos-sim", "arm64-apple-tvos-sim", + "arm64;x86_64-apple-tvos-sim", + "arm64-apple-visionos", + "x86_64-apple-visionos-sim", "arm64-apple-visionos-sim", + "arm64;x86_64-apple-visionos-sim", ], - defaultTargets() { - return process.arch === "arm64" ? ["arm64-apple-ios-sim"] : []; + defaultTriplets(mode) { + if (mode === "all") { + return [ + "arm64;x86_64-apple-darwin", + + "arm64-apple-ios", + "arm64;x86_64-apple-ios-sim", + + "arm64-apple-tvos", + "arm64;x86_64-apple-tvos-sim", + + "arm64-apple-visionos", + "arm64;x86_64-apple-visionos-sim", + ]; + } else if (mode === "current-development") { + // We're applying a heuristic to determine the current simulators + // TODO: Run a command to probe the currently running simulators instead + return ["arm64;x86_64-apple-ios-sim"]; + } else { + throw new Error(`Unexpected mode: ${mode as string}`); + } }, amendCommand(command) { - return command.addOption(xcframeworkExtensionOption); + return command + .addOption(xcframeworkExtensionOption) + .addOption(appleBundleIdentifierOption); }, - configureArgs({ target }) { - return [ - "-G", - "Xcode", - "-D", - `CMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAMES[target]}`, - "-D", - `CMAKE_OSX_SYSROOT=${XCODE_SDK_NAMES[target]}`, - "-D", - `CMAKE_OSX_ARCHITECTURES=${APPLE_ARCHITECTURES[target]}`, - ]; + async configure( + triplets, + { source, build, define, weakNodeApiLinkage, cmakeJs }, + spawn, + ) { + // Ideally, we would generate a single Xcode project supporting all architectures / platforms + // However, CMake's Xcode generator does not support that well, so we generate one project per triplet + // Specifically, the linking of weak-node-api breaks, since the sdk / arch specific framework + // from the xcframework is picked at configure time, not at build time. + // See https://gitlab.kitware.com/cmake/cmake/-/issues/21752#note_1717047 for more information. + await Promise.all( + triplets.map(async ({ triplet }) => { + const buildPath = getBuildPath(build, triplet); + // We want to use the CMake File API to query information later + // TODO: Or do we? + await cmakeFileApi.createSharedStatelessQuery( + buildPath, + "codemodel", + "2", + ); + await spawn("cmake", [ + "-S", + source, + "-B", + buildPath, + "-G", + "Xcode", + ...toDefineArguments([ + ...define, + ...(weakNodeApiLinkage ? [getWeakNodeApiVariables("apple")] : []), + ...(cmakeJs ? [getCmakeJSVariables("apple")] : []), + { + CMAKE_SYSTEM_NAME: CMAKE_SYSTEM_NAMES[triplet], + CMAKE_OSX_SYSROOT: XCODE_SDK_NAMES[triplet], + CMAKE_OSX_ARCHITECTURES: APPLE_ARCHITECTURES[triplet], + // Passing a linker flag to increase the header pad size to allow renaming the install name when linking it into the app. + CMAKE_SHARED_LINKER_FLAGS: "-Wl,-headerpad_max_install_names", + }, + { + // Setting the output directories works around an issue with Xcode generator + // where an unexpanded variable would emitted in the artifact paths. + // This is okay, since we're generating per triplet build directories anyway. + // https://gitlab.kitware.com/cmake/cmake/-/issues/24161 + CMAKE_LIBRARY_OUTPUT_DIRECTORY: path.join(buildPath, "out"), + CMAKE_ARCHIVE_OUTPUT_DIRECTORY: path.join(buildPath, "out"), + }, + ]), + ]); + }), + ); }, - buildArgs() { + async build( + { spawn, triplet }, + { build, target, configuration, appleBundleIdentifier }, + ) { // We expect the final application to sign these binaries - return ["CODE_SIGNING_ALLOWED=NO"]; + if (target.length > 1) { + throw new Error("Building for multiple targets is not supported yet"); + } + + const buildPath = getBuildPath(build, triplet); + + const sharedLibrary = await readCmakeSharedLibraryTarget( + buildPath, + configuration, + target, + ); + + const isFramework = sharedLibrary.nameOnDisk?.includes(".framework/"); + + if (isFramework) { + const { project } = listXcodeProject(buildPath); + + const schemes = project.schemes.filter( + (scheme) => scheme !== "ALL_BUILD" && scheme !== "ZERO_CHECK", + ); + + assert( + schemes.length === 1, + `Expected exactly one buildable scheme, got ${schemes.join(", ")}`, + ); + + const [scheme] = schemes; + + if (target.length === 1) { + assert.equal( + scheme, + target[0], + "Expected the only scheme to match the requested target", + ); + } + + await spawn( + "xcodebuild", + [ + "archive", + "-scheme", + scheme, + "-configuration", + configuration, + "-destination", + DESTINATION_BY_TRIPLET[triplet], + ], + buildPath, + ); + await spawn( + "xcodebuild", + [ + "install", + "-scheme", + scheme, + "-configuration", + configuration, + "-destination", + DESTINATION_BY_TRIPLET[triplet], + ], + buildPath, + ); + } else { + await spawn("cmake", [ + "--build", + buildPath, + "--config", + configuration, + ...(target.length > 0 ? ["--target", ...target] : []), + "--", + + // Skip code-signing (needed when building free dynamic libraries) + // TODO: Make this configurable + "CODE_SIGNING_ALLOWED=NO", + ]); + // Create a framework + const { artifacts } = sharedLibrary; + assert( + artifacts && artifacts.length === 1, + "Expected exactly one artifact", + ); + const [artifact] = artifacts; + await createAppleFramework({ + libraryPath: path.join(buildPath, artifact.path), + versioned: triplet.endsWith("-darwin"), + bundleIdentifier: appleBundleIdentifier, + }); + } }, isSupportedByHost: function (): boolean | Promise { return process.platform === "darwin"; }, async postBuild( - { outputPath, targets }, - { configuration, autoLink, xcframeworkExtension }, + outputPath, + triplets, + { configuration, autoLink, xcframeworkExtension, target, build, strip }, ) { - const libraryPaths = await Promise.all( - targets.map(async ({ outputPath }) => { - const configSpecificPath = path.join(outputPath, configuration); + const libraryNames = new Set(); + const frameworkPaths: string[] = []; + for (const { spawn, triplet } of triplets) { + const buildPath = getBuildPath(build, triplet); + assert(fs.existsSync(buildPath), `Expected a directory at ${buildPath}`); + const sharedLibrary = await readCmakeSharedLibraryTarget( + buildPath, + configuration, + target, + ); + const { artifacts } = sharedLibrary; + assert( + artifacts && artifacts.length === 1, + "Expected exactly one artifact", + ); + const [artifact] = artifacts; + + const artifactPath = path.join(buildPath, artifact.path); + + if (strip) { + // -r: All relocation entries. + // -S: All symbol table entries. + // -T: All text relocation entries. + // -x: All local symbols. + await spawn("strip", ["-rSTx", artifactPath]); + } + + libraryNames.add(sharedLibrary.name); + // Locate the path of the framework, if a free dynamic library was built + if (artifact.path.includes(".framework/")) { + frameworkPaths.push(path.dirname(artifactPath)); + } else { + const libraryName = path.basename( + artifact.path, + path.extname(artifact.path), + ); + const frameworkPath = path.join( + buildPath, + path.dirname(artifact.path), + `${libraryName}.framework`, + ); assert( - fs.existsSync(configSpecificPath), - `Expected a directory at ${configSpecificPath}`, + fs.existsSync(frameworkPath), + `Expected to find a framework at: ${frameworkPath}`, ); - // Expect binary file(s), either .node or .dylib - const files = await fs.promises.readdir(configSpecificPath); - const result = files.map(async (file) => { - const filePath = path.join(configSpecificPath, file); - if (filePath.endsWith(".dylib")) { - return filePath; - } else if (file.endsWith(".node")) { - // Rename the file to .dylib for xcodebuild to accept it - const newFilePath = filePath.replace(/\.node$/, ".dylib"); - await fs.promises.rename(filePath, newFilePath); - return newFilePath; - } else { - throw new Error( - `Expected a .node or .dylib file, but found ${file}`, - ); - } - }); - assert.equal(result.length, 1, "Expected exactly one library file"); - return await result[0]; + frameworkPaths.push(frameworkPath); + } + } + + // Make sure none of the frameworks are symlinks + // We do this before creating an xcframework to avoid symlink paths being invalidated + // as the xcframework might be moved to a different location + await Promise.all( + frameworkPaths.map(async (frameworkPath) => { + const stat = await fs.promises.lstat(frameworkPath); + if (stat.isSymbolicLink()) { + await dereferenceDirectory(frameworkPath); + } }), ); - const frameworkPaths = libraryPaths.map(createAppleFramework); - const xcframeworkFilename = determineXCFrameworkFilename( - frameworkPaths, - xcframeworkExtension ? ".xcframework" : ".apple.node", + + const extension = xcframeworkExtension ? ".xcframework" : ".apple.node"; + + assert( + libraryNames.size === 1, + "Expected all libraries to have the same name", ); + const [libraryName] = libraryNames; // Create the xcframework - const xcframeworkOutputPath = path.resolve(outputPath, xcframeworkFilename); + const xcframeworkOutputPath = path.resolve( + outputPath, + `${libraryName}${extension}`, + ); await oraPromise( createXCframework({ @@ -181,11 +478,10 @@ export const platform: Platform = { autoLink, }), { - text: "Assembling XCFramework", - successText: `XCFramework assembled into ${chalk.dim( - path.relative(process.cwd(), xcframeworkOutputPath), - )}`, - failText: ({ message }) => `Failed to assemble XCFramework: ${message}`, + text: `Assembling XCFramework (${libraryName})`, + successText: `XCFramework (${libraryName}) assembled into ${prettyPath(xcframeworkOutputPath)}`, + failText: ({ message }) => + `Failed to assemble XCFramework (${libraryName}): ${message}`, }, ); }, diff --git a/packages/cmake-rn/src/platforms/types.ts b/packages/cmake-rn/src/platforms/types.ts index fb7c8f5f..6944d4bf 100644 --- a/packages/cmake-rn/src/platforms/types.ts +++ b/packages/cmake-rn/src/platforms/types.ts @@ -1,29 +1,39 @@ -import * as commander from "@commander-js/extra-typings"; +import * as cli from "@react-native-node-api/cli-utils"; + import type { program } from "../cli.js"; -type InferOptionValues = ReturnType< +type InferOptionValues = ReturnType< Command["opts"] >; type BaseCommand = typeof program; -type ExtendedCommand = commander.Command< +type ExtendedCommand = cli.Command< [], Opts & InferOptionValues, Record // Global opts are not supported >; -export type BaseOpts = Omit, "target">; +export type BaseOpts = Omit, "triplet">; -export type TargetContext = { - target: Target; - buildPath: string; - outputPath: string; +export type TripletContext = { + triplet: Triplet; + /** + * Spawn a command in the context of this triplet + */ + spawn: Spawn; }; +export type Spawn = ( + command: string, + args: string[], + cwd?: string, +) => Promise; + export type Platform< - Targets extends string[] = string[], - Opts extends commander.OptionValues = Record, + Triplets extends string[] = string[], + Opts extends cli.OptionValues = Record, Command = ExtendedCommand, + Triplet extends string = Triplets[number], > = { /** * Used to identify the platform in the CLI. @@ -34,13 +44,15 @@ export type Platform< */ name: string; /** - * All the targets supported by this platform. + * All the triplets supported by this platform. */ - targets: Readonly; + triplets: Readonly; /** - * Get the limited subset of targets that should be built by default for this platform, to support a development workflow. + * Get the limited subset of triplets that should be built by default for this platform. */ - defaultTargets(): Targets[number][] | Promise; + defaultTriplets( + mode: "current-development" | "all", + ): Triplet[] | Promise; /** * Implement this to add any platform specific options to the command. */ @@ -50,30 +62,29 @@ export type Platform< */ isSupportedByHost(): boolean | Promise; /** - * Platform specific arguments passed to CMake to configure a target project. + * Configure all projects for this platform. */ - configureArgs( - context: TargetContext, + configure( + triplets: TripletContext[], options: BaseOpts & Opts, - ): string[]; + spawn: Spawn, + ): Promise; /** - * Platform specific arguments passed to CMake to build a target project. + * Platform specific command to build a triplet project. */ - buildArgs( - context: TargetContext, + build( + context: TripletContext, options: BaseOpts & Opts, - ): string[]; + ): Promise; /** - * Called to combine multiple targets into a single prebuilt artefact. + * Called to combine multiple triplets into a single prebuilt artefact. */ postBuild( - context: { - /** - * Location of the final prebuilt artefact. - */ - outputPath: string; - targets: TargetContext[]; - }, + /** + * Location of the final prebuilt artefact. + */ + outputPath: string, + triplets: TripletContext[], options: BaseOpts & Opts, ): Promise; }; diff --git a/packages/cmake-rn/src/weak-node-api.ts b/packages/cmake-rn/src/weak-node-api.ts index 496905d7..44b2e79c 100644 --- a/packages/cmake-rn/src/weak-node-api.ts +++ b/packages/cmake-rn/src/weak-node-api.ts @@ -6,9 +6,14 @@ import { isAndroidTriplet, isAppleTriplet, SupportedTriplet, - weakNodeApiPath, } from "react-native-node-api"; +import { + applePrebuildPath, + androidPrebuildPath, + weakNodeApiCmakePath, +} from "weak-node-api"; + import { ANDROID_ARCHITECTURES } from "./platforms/android.js"; import { getNodeAddonHeadersPath, getNodeApiHeadersPath } from "./headers.js"; @@ -16,21 +21,18 @@ export function toCmakePath(input: string) { return input.split(path.win32.sep).join(path.posix.sep); } -export function getWeakNodeApiPath(triplet: SupportedTriplet): string { - if (isAppleTriplet(triplet)) { - const xcframeworkPath = path.join( - weakNodeApiPath, - "weak-node-api.xcframework", - ); +export function getWeakNodeApiPath( + triplet: SupportedTriplet | "apple", +): string { + if (triplet === "apple" || isAppleTriplet(triplet)) { assert( - fs.existsSync(xcframeworkPath), - `Expected an XCFramework at ${xcframeworkPath}`, + fs.existsSync(applePrebuildPath), + `Expected an XCFramework at ${applePrebuildPath}`, ); - return xcframeworkPath; + return applePrebuildPath; } else if (isAndroidTriplet(triplet)) { const libraryPath = path.join( - weakNodeApiPath, - "weak-node-api.android.node", + androidPrebuildPath, ANDROID_ARCHITECTURES[triplet], "libweak-node-api.so", ); @@ -41,7 +43,7 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string { } } -export function getWeakNodeApiVariables(triplet: SupportedTriplet) { +function getNodeApiIncludePaths() { const includePaths = [getNodeApiHeadersPath(), getNodeAddonHeadersPath()]; for (const includePath of includePaths) { assert( @@ -49,8 +51,30 @@ export function getWeakNodeApiVariables(triplet: SupportedTriplet) { `Include path with a ';' is not supported: ${includePath}`, ); } + return includePaths; +} + +export function getWeakNodeApiVariables( + triplet: SupportedTriplet | "apple", +): Record { + return { + // Enable use of `find_package(weak-node-api REQUIRED CONFIG)` + "weak-node-api_DIR": path.dirname(weakNodeApiCmakePath), + // Enable use of `include(${WEAK_NODE_API_CONFIG})` + WEAK_NODE_API_CONFIG: weakNodeApiCmakePath, + WEAK_NODE_API_INC: getNodeApiIncludePaths().join(";"), + WEAK_NODE_API_LIB: getWeakNodeApiPath(triplet), + }; +} + +/** + * For compatibility with cmake-js + */ +export function getCmakeJSVariables( + triplet: SupportedTriplet | "apple", +): Record { return { - CMAKE_JS_INC: includePaths.join(";"), + CMAKE_JS_INC: getNodeApiIncludePaths().join(";"), CMAKE_JS_LIB: getWeakNodeApiPath(triplet), }; } diff --git a/packages/ferric-example/CHANGELOG.md b/packages/ferric-example/CHANGELOG.md index 66f2007e..3818f6fd 100644 --- a/packages/ferric-example/CHANGELOG.md +++ b/packages/ferric-example/CHANGELOG.md @@ -1,5 +1,11 @@ # @react-native-node-api/ferric-example +## 0.1.2 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts ๐Ÿ™ˆ + ## 0.1.1 ### Patch Changes diff --git a/packages/ferric-example/Cargo.toml b/packages/ferric-example/Cargo.toml index 2524a0e8..a301bce0 100644 --- a/packages/ferric-example/Cargo.toml +++ b/packages/ferric-example/Cargo.toml @@ -8,17 +8,21 @@ license = "MIT" crate-type = ["cdylib"] [dependencies.napi] -version = "3.1" -default-features = false +version = "=3.4.0" # see https://nodejs.org/api/n-api.html#node-api-version-matrix +default-features = false features = ["napi3"] [dependencies.napi-derive] -version = "3.1" +version = "3.3.0" features = ["type-def"] +# See https://github.com/callstackincubator/react-native-node-api/issues/331 +[dependencies.napi-sys] +version = "=3.0.1" + [build-dependencies] -napi-build = "2" +napi-build = "2.2.4" [profile.release] lto = true diff --git a/packages/ferric-example/package.json b/packages/ferric-example/package.json index 73678bb3..af2d5ada 100644 --- a/packages/ferric-example/package.json +++ b/packages/ferric-example/package.json @@ -1,8 +1,8 @@ { "name": "@react-native-node-api/ferric-example", + "version": "0.1.2", "private": true, "type": "commonjs", - "version": "0.1.1", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { "type": "git", @@ -11,6 +11,12 @@ }, "main": "ferric_example.js", "types": "ferric_example.d.ts", + "files": [ + "ferric_example.js", + "ferric_example.d.ts", + "ferric_example.apple.node", + "ferric_example.android.node" + ], "scripts": { "build": "ferric build", "bootstrap": "node --run build" diff --git a/packages/ferric/CHANGELOG.md b/packages/ferric/CHANGELOG.md index bfcae846..1518b83c 100644 --- a/packages/ferric/CHANGELOG.md +++ b/packages/ferric/CHANGELOG.md @@ -1,5 +1,104 @@ # ferric-cli +## 0.3.11 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts ๐Ÿ™ˆ +- Updated dependencies [1dee80f] + - @react-native-node-api/cli-utils@0.1.4 + - react-native-node-api@1.0.1 + - weak-node-api@0.1.1 + +## 0.3.10 + +### Patch Changes + +- 441dcc4: Add --verbose, --concurrency, --clean options +- Updated dependencies [441dcc4] +- Updated dependencies [3d2e03e] + - @react-native-node-api/cli-utils@0.1.3 + - weak-node-api@0.1.0 + - react-native-node-api@1.0.0 + +## 0.3.9 + +### Patch Changes + +- Updated dependencies [7ff2c2b] +- Updated dependencies [7ff2c2b] + - weak-node-api@0.0.3 + - @react-native-node-api/cli-utils@0.1.2 + - react-native-node-api@0.7.1 + +## 0.3.8 + +### Patch Changes + +- 61fff3f: Allow passing --apple-bundle-identifier to specify the bundle identifiers used when creating Apple frameworks. +- Updated dependencies [60fae96] +- Updated dependencies [61fff3f] +- Updated dependencies [61fff3f] +- Updated dependencies [5dea205] +- Updated dependencies [60fae96] +- Updated dependencies [60fae96] +- Updated dependencies [eca721e] +- Updated dependencies [60fae96] + - react-native-node-api@0.7.0 + - weak-node-api@0.0.2 + +## 0.3.7 + +### Patch Changes + +- 9411a8c: Add x86_64 ios simulator target and output universal libraries for iOS simulators. +- 9411a8c: It's no longer required to pass "build" to ferric, as this is default now +- b661176: Add support for visionOS and tvOS targets +- Updated dependencies [07ea9dc] +- Updated dependencies [7536c6c] +- Updated dependencies [c698698] +- Updated dependencies [a2fd422] +- Updated dependencies [bdc172e] +- Updated dependencies [4672e01] + - react-native-node-api@0.6.2 + +## 0.3.6 + +### Patch Changes + +- Updated dependencies [5c3de89] +- Updated dependencies [bb9a78c] + - react-native-node-api@0.6.1 + +## 0.3.5 + +### Patch Changes + +- 5156d35: Refactored moving prettyPath util to CLI utils package +- Updated dependencies [acd06f2] +- Updated dependencies [5156d35] +- Updated dependencies [9f1a301] +- Updated dependencies [5016ed2] +- Updated dependencies [5156d35] + - react-native-node-api@0.6.0 + - @react-native-node-api/cli-utils@0.1.1 + +## 0.3.4 + +### Patch Changes + +- Updated dependencies [2b9a538] + - react-native-node-api@0.5.2 + +## 0.3.3 + +### Patch Changes + +- 2a30d8d: Refactored CLIs to use a shared utility package +- Updated dependencies [2a30d8d] +- Updated dependencies [c72970f] + - react-native-node-api@0.5.1 + ## 0.3.2 ### Patch Changes diff --git a/packages/ferric/package.json b/packages/ferric/package.json index 1db23db9..1bf810df 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -1,6 +1,6 @@ { "name": "ferric-cli", - "version": "0.3.2", + "version": "0.3.11", "description": "Rust Node-API Modules for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -9,6 +9,10 @@ "directory": "packages/ferric" }, "type": "module", + "files": [ + "bin", + "dist" + ], "bin": { "ferric": "./bin/ferric.js" }, @@ -17,11 +21,8 @@ }, "dependencies": { "@napi-rs/cli": "~3.0.3", - "@commander-js/extra-typings": "^13.1.0", - "bufout": "^0.3.2", - "chalk": "^5.4.1", - "commander": "^13.1.0", - "react-native-node-api": "0.5.0", - "ora": "^8.2.0" + "@react-native-node-api/cli-utils": "0.1.4", + "react-native-node-api": "1.0.1", + "weak-node-api": "0.1.1" } } diff --git a/packages/ferric/src/banner.ts b/packages/ferric/src/banner.ts index 820b681d..8bf44cc5 100644 --- a/packages/ferric/src/banner.ts +++ b/packages/ferric/src/banner.ts @@ -1,4 +1,4 @@ -import chalk from "chalk"; +import { chalk } from "@react-native-node-api/cli-utils"; const LINES = [ // Pagga on https://www.asciiart.eu/text-to-ascii-art diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index aaf97d77..36a673ad 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -1,10 +1,18 @@ import path from "node:path"; import fs from "node:fs"; +import os from "node:os"; -import { Command, Option } from "@commander-js/extra-typings"; -import chalk from "chalk"; -import { SpawnFailure } from "bufout"; -import { oraPromise } from "ora"; +import { + chalk, + Command, + Option, + oraPromise, + assertFixable, + wrapAction, + prettyPath, + pLimit, + spawn, +} from "@react-native-node-api/cli-utils"; import { determineAndroidLibsFilename, @@ -15,10 +23,8 @@ import { createXCframework, createUniversalAppleLibrary, determineLibraryBasename, - prettyPath, } from "react-native-node-api"; -import { UsageError, assertFixable } from "./errors.js"; import { ensureCargo, build } from "./cargo.js"; import { ALL_TARGETS, @@ -26,7 +32,7 @@ import { AndroidTargetName, APPLE_TARGETS, AppleTargetName, - ensureInstalledTargets, + ensureAvailableTargets, filterTargetsByPlatform, } from "./targets.js"; import { generateTypeScriptDeclarations } from "./napi-rs.js"; @@ -82,6 +88,10 @@ function getDefaultTargets() { const targetOption = new Option("--target ", "Target triple") .choices(ALL_TARGETS) .default(getDefaultTargets()); +const cleanOption = new Option( + "--clean", + "Delete the target directory before building", +).default(false); const appleTarget = new Option("--apple", "Use all Apple targets"); const androidTarget = new Option("--android", "Use all Android targets"); const ndkVersionOption = new Option( @@ -104,26 +114,74 @@ const configurationOption = new Option( .choices(["debug", "release"]) .default("debug"); +const appleBundleIdentifierOption = new Option( + "--apple-bundle-identifier ", + "Unique CFBundleIdentifier used for Apple framework artifacts", +).default(undefined, "com.callstackincubator.node-api.{libraryName}"); + +const concurrencyOption = new Option( + "--concurrency ", + "Limit the number of concurrent tasks", +) + .argParser((value) => parseInt(value, 10)) + .default( + os.availableParallelism(), + `${os.availableParallelism()} or 1 when verbose is enabled`, + ); + +const verboseOption = new Option( + "--verbose", + "Print more output from underlying compiler & tools", +).default(process.env.CI ? true : false, `false in general and true on CI`); + +function logNotice(message: string, ...params: string[]) { + console.log(`${chalk.yellow("โ„น๏ธŽ")} ${message}`, ...params); +} + export const buildCommand = new Command("build") .description("Build Rust Node-API module") .addOption(targetOption) + .addOption(cleanOption) .addOption(appleTarget) .addOption(androidTarget) .addOption(ndkVersionOption) .addOption(outputPathOption) .addOption(configurationOption) .addOption(xcframeworkExtensionOption) + .addOption(appleBundleIdentifierOption) + .addOption(concurrencyOption) + .addOption(verboseOption) .action( - async ({ - target: targetArg, - apple, - android, - ndkVersion, - output: outputPath, - configuration, - xcframeworkExtension, - }) => { - try { + wrapAction( + async ({ + target: targetArg, + clean, + apple, + android, + ndkVersion, + output: outputPath, + configuration, + xcframeworkExtension, + appleBundleIdentifier, + concurrency, + verbose, + }) => { + if (clean) { + await oraPromise( + () => spawn("cargo", ["clean"], { outputMode: "buffered" }), + { + text: "Cleaning target directory", + successText: "Cleaned target directory", + failText: (error) => `Failed to clean target directory: ${error}`, + }, + ); + } + if (verbose && concurrency > 1) { + logNotice( + `Consider passing ${chalk.blue("--concurrency")} 1 when running in verbose mode`, + ); + } + const limit = pLimit(concurrency); const targets = new Set([...targetArg]); if (apple) { for (const target of APPLE_TARGETS) { @@ -149,19 +207,16 @@ export const buildCommand = new Command("build") targets.add("aarch64-apple-ios-sim"); } } - console.error( - chalk.yellowBright("โ„น"), - chalk.dim( - `Using default targets, pass ${chalk.italic( - "--android", - )}, ${chalk.italic("--apple")} or individual ${chalk.italic( - "--target", - )} options, to avoid this.`, - ), + logNotice( + `Using default targets, pass ${chalk.blue( + "--android", + )}, ${chalk.blue("--apple")} or individual ${chalk.blue( + "--target", + )} options, choose exactly what to target`, ); } ensureCargo(); - ensureInstalledTargets(targets); + ensureAvailableTargets(targets); const appleTargets = filterTargetsByPlatform(targets, "apple"); const androidTargets = filterTargetsByPlatform(targets, "android"); @@ -170,30 +225,40 @@ export const buildCommand = new Command("build") targets.size + (targets.size === 1 ? " target" : " targets") + chalk.dim(" (" + [...targets].join(", ") + ")"); + const [appleLibraries, androidLibraries] = await oraPromise( Promise.all([ Promise.all( - appleTargets.map( - async (target) => - [target, await build({ configuration, target })] as const, + appleTargets.map((target) => + limit( + async () => + [ + target, + await build({ configuration, target, verbose }), + ] as const, + ), ), ), Promise.all( - androidTargets.map( - async (target) => - [ - target, - await build({ - configuration, + androidTargets.map((target) => + limit( + async () => + [ target, - ndkVersion, - androidApiLevel: ANDROID_API_LEVEL, - }), - ] as const, + await build({ + configuration, + target, + verbose, + ndkVersion, + androidApiLevel: ANDROID_API_LEVEL, + }), + ] as const, + ), ), ), ]), { + isSilent: verbose, text: `Building ${targetsDescription}`, successText: `Built ${targetsDescription}`, failText: (error: Error) => `Failed to build: ${error.message}`, @@ -201,15 +266,13 @@ export const buildCommand = new Command("build") ); if (androidLibraries.length > 0) { - const libraryPathByTriplet = Object.fromEntries( - androidLibraries.map(([target, outputPath]) => [ - ANDROID_TRIPLET_PER_TARGET[target], - outputPath, - ]), - ) as Record; + const libraries = androidLibraries.map(([target, outputPath]) => ({ + triplet: ANDROID_TRIPLET_PER_TARGET[target], + libraryPath: outputPath, + })); const androidLibsFilename = determineAndroidLibsFilename( - Object.values(libraryPathByTriplet), + libraries.map(({ libraryPath }) => libraryPath), ); const androidLibsOutputPath = path.resolve( outputPath, @@ -217,11 +280,13 @@ export const buildCommand = new Command("build") ); await oraPromise( - createAndroidLibsDirectory({ - outputPath: androidLibsOutputPath, - libraryPathByTriplet, - autoLink: true, - }), + limit(() => + createAndroidLibsDirectory({ + outputPath: androidLibsOutputPath, + libraries, + autoLink: true, + }), + ), { text: "Assembling Android libs directory", successText: `Android libs directory assembled into ${prettyPath( @@ -235,7 +300,26 @@ export const buildCommand = new Command("build") if (appleLibraries.length > 0) { const libraryPaths = await combineLibraries(appleLibraries); - const frameworkPaths = libraryPaths.map(createAppleFramework); + + const frameworkPaths = await oraPromise( + Promise.all( + libraryPaths.map((libraryPath) => + limit(() => + // TODO: Pass true as `versioned` argument for -darwin targets + createAppleFramework({ + libraryPath, + bundleIdentifier: appleBundleIdentifier, + }), + ), + ), + ), + { + text: "Creating Apple frameworks", + successText: `Created Apple frameworks`, + failText: ({ message }) => + `Failed to create Apple frameworks: ${message}`, + }, + ); const xcframeworkFilename = determineXCFrameworkFilename( frameworkPaths, xcframeworkExtension ? ".xcframework" : ".apple.node", @@ -255,9 +339,7 @@ export const buildCommand = new Command("build") }), { text: "Assembling XCFramework", - successText: `XCFramework assembled into ${chalk.dim( - path.relative(process.cwd(), xcframeworkOutputPath), - )}`, + successText: `XCFramework assembled into ${prettyPath(xcframeworkOutputPath)}`, failText: ({ message }) => `Failed to assemble XCFramework: ${message}`, }, @@ -303,59 +385,65 @@ export const buildCommand = new Command("build") `Failed to generate entrypoint: ${error.message}`, }, ); - } catch (error) { - process.exitCode = 1; - if (error instanceof SpawnFailure) { - error.flushOutput("both"); - } - if (error instanceof UsageError || error instanceof SpawnFailure) { - console.error(chalk.red("ERROR"), error.message); - if (error.cause instanceof Error) { - console.error(chalk.red("CAUSE"), error.cause.message); - } - if (error instanceof UsageError && error.fix) { - console.error( - chalk.green("FIX"), - error.fix.command - ? chalk.dim("Run: ") + error.fix.command - : error.fix.instructions, - ); - } + }, + ), + ); + +async function createUniversalAppleLibraries(libraryPathGroups: string[][]) { + const result = await oraPromise( + Promise.all( + libraryPathGroups.map(async (libraryPaths) => { + if (libraryPaths.length === 0) { + return []; + } else if (libraryPaths.length === 1) { + return libraryPaths; } else { - throw error; + return [await createUniversalAppleLibrary(libraryPaths)]; } - } + }), + ), + { + text: "Combining arch-specific libraries into universal libraries", + successText: "Combined arch-specific libraries into universal libraries", + failText: (error) => + `Failed to combine arch-specific libraries: ${error.message}`, }, ); + return result.flat(); +} async function combineLibraries( libraries: Readonly<[AppleTargetName, string]>[], ): Promise { const result = []; const darwinLibraries = []; + const iosSimulatorLibraries = []; + const tvosSimulatorLibraries = []; for (const [target, libraryPath] of libraries) { if (target.endsWith("-darwin")) { darwinLibraries.push(libraryPath); + } else if ( + target === "aarch64-apple-ios-sim" || + target === "x86_64-apple-ios" // Simulator despite name missing -sim suffix + ) { + iosSimulatorLibraries.push(libraryPath); + } else if ( + target === "aarch64-apple-tvos-sim" || + target === "x86_64-apple-tvos" // Simulator despite name missing -sim suffix + ) { + tvosSimulatorLibraries.push(libraryPath); } else { result.push(libraryPath); } } - if (darwinLibraries.length === 0) { - return result; - } else if (darwinLibraries.length === 1) { - return [...result, darwinLibraries[0]]; - } else { - const universalPath = await oraPromise( - createUniversalAppleLibrary(darwinLibraries), - { - text: "Combining Darwin libraries into a universal library", - successText: "Combined Darwin libraries into a universal library", - failText: (error) => - `Failed to combine Darwin libraries: ${error.message}`, - }, - ); - return [...result, universalPath]; - } + + const combinedLibraryPaths = await createUniversalAppleLibraries([ + darwinLibraries, + iosSimulatorLibraries, + tvosSimulatorLibraries, + ]); + + return [...result, ...combinedLibraryPaths]; } export function isAndroidSupported() { diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index da4e8be9..fc4fb2ac 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -3,31 +3,65 @@ import cp from "node:child_process"; import fs from "node:fs"; import path from "node:path"; -import { spawn } from "bufout"; -import chalk from "chalk"; +import { + chalk, + assertFixable, + UsageError, + spawn, +} from "@react-native-node-api/cli-utils"; +import { applePrebuildPath, androidPrebuildPath } from "weak-node-api"; -import { assertFixable, UsageError } from "./errors.js"; import { AndroidTargetName, AppleTargetName, isAndroidTarget, isAppleTarget, + isThirdTierTarget, } from "./targets.js"; -import { weakNodeApiPath } from "react-native-node-api"; +/** + * A per apple target mapping to a list of xcframework slices in order of priority + */ +const APPLE_XCFRAMEWORK_SLICES_PER_TARGET: Record = { + "aarch64-apple-darwin": [ + "macos-arm64_x86_64", // Universal + "macos-arm64", + ], + "x86_64-apple-darwin": [ + "macos-arm64_x86_64", // Universal + "macos-x86_64", + ], + + "aarch64-apple-ios": ["ios-arm64"], + "aarch64-apple-ios-sim": [ + "ios-arm64_x86_64-simulator", // Universal + "ios-arm64-simulator", + ], + "x86_64-apple-ios": [ + "ios-arm64_x86_64-simulator", // Universal + "ios-x86_64-simulator", + ], + + "aarch64-apple-visionos": ["xros-arm64"], + "aarch64-apple-visionos-sim": [ + "xros-arm64_x86_64-simulator", // Universal + "xros-arm64-simulator", + ], + // The x86_64 target for vision simulator isn't supported + // see https://doc.rust-lang.org/rustc/platform-support.html + + "aarch64-apple-tvos": ["tvos-arm64"], + "aarch64-apple-tvos-sim": [ + "tvos-arm64_x86_64-simulator", // Universal + "tvos-arm64-simulator", + ], + "x86_64-apple-tvos": [ + "tvos-arm64_x86_64-simulator", // Universal + "tvos-x86_64-simulator", + ], -const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record = { - "aarch64-apple-darwin": "macos-arm64_x86_64", // Universal - "x86_64-apple-darwin": "macos-arm64_x86_64", // Universal - "aarch64-apple-ios": "ios-arm64", - "aarch64-apple-ios-sim": "ios-arm64-simulator", // "aarch64-apple-ios-macabi": "", // Catalyst - // "x86_64-apple-ios": "ios-x86_64", // "x86_64-apple-ios-macabi": "ios-x86_64-simulator", - // "aarch64-apple-tvos": "tvos-arm64", - // "aarch64-apple-tvos-sim": "tvos-arm64-simulator", - // "aarch64-apple-visionos": "xros-arm64", - // "aarch64-apple-visionos-sim": "xros-arm64-simulator", }; const ANDROID_ARCH_PR_TARGET: Record = { @@ -61,6 +95,7 @@ export function ensureCargo() { type BuildOptions = { configuration: "debug" | "release"; + verbose: boolean; } & ( | { target: AndroidTargetName; @@ -75,13 +110,22 @@ type BuildOptions = { ); export async function build(options: BuildOptions) { - const { target, configuration } = options; + const { target, configuration, verbose } = options; const args = ["build", "--target", target]; if (configuration.toLowerCase() === "release") { args.push("--release"); } + if (isThirdTierTarget(target)) { + // Use the nightly toolchain for third tier targets + args.splice(0, 0, "+nightly"); + // Passing the nightly "build-std" to + // > Enable Cargo to compile the standard library itself as part of a crate graph compilation + // See https://doc.rust-lang.org/rustc/platform-support/apple-visionos.html#building-the-target + args.push("-Z", "build-std=std,panic_abort"); + } await spawn("cargo", args, { - outputMode: "buffered", + outputMode: verbose ? "inherit" : "buffered", + outputPrefix: verbose ? chalk.dim(`[${target}]`) : undefined, env: { ...process.env, ...getTargetEnvironmentVariables(options), @@ -127,17 +171,20 @@ export function getTargetAndroidPlatform(target: AndroidTargetName) { } export function getWeakNodeApiFrameworkPath(target: AppleTargetName) { - return joinPathAndAssertExistence( - weakNodeApiPath, - "weak-node-api.xcframework", - APPLE_XCFRAMEWORK_CHILDS_PER_TARGET[target], + const result = APPLE_XCFRAMEWORK_SLICES_PER_TARGET[target].find((slice) => { + const candidatePath = path.join(applePrebuildPath, slice); + return fs.existsSync(candidatePath); + }); + assert( + result, + `No matching slice found in weak-node-api.xcframework for target ${target}`, ); + return joinPathAndAssertExistence(applePrebuildPath, result); } export function getWeakNodeApiAndroidLibraryPath(target: AndroidTargetName) { return joinPathAndAssertExistence( - weakNodeApiPath, - "weak-node-api.android.node", + androidPrebuildPath, ANDROID_ARCH_PR_TARGET[target], ); } @@ -174,8 +221,9 @@ export function getTargetEnvironmentVariables({ CARGO_ENCODED_RUSTFLAGS: [ "-L", weakNodeApiPath, - "-l", - "weak-node-api", + "-C", + // Passing --no-as-needed to prevent weak-node-api from being optimized away + "link-arg=-Wl,--push-state,--no-as-needed,-lweak-node-api,--pop-state", ].join(String.fromCharCode(0x1f)), CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: joinPathAndAssertExistence( toolchainBinPath, @@ -214,6 +262,10 @@ export function getTargetEnvironmentVariables({ }; } else if (isAppleTarget(target)) { const weakNodeApiFrameworkPath = getWeakNodeApiFrameworkPath(target); + assert( + fs.existsSync(weakNodeApiFrameworkPath), + `Expected weak-node-api framework at ${weakNodeApiFrameworkPath}`, + ); return { CARGO_ENCODED_RUSTFLAGS: [ "-L", diff --git a/packages/ferric/src/program.ts b/packages/ferric/src/program.ts index f8059c48..d47d0479 100644 --- a/packages/ferric/src/program.ts +++ b/packages/ferric/src/program.ts @@ -1,4 +1,4 @@ -import { Command } from "@commander-js/extra-typings"; +import { Command } from "@react-native-node-api/cli-utils"; import { printBanner } from "./banner.js"; import { buildCommand } from "./build.js"; @@ -6,4 +6,4 @@ import { buildCommand } from "./build.js"; export const program = new Command("ferric") .hook("preAction", () => printBanner()) .description("Rust Node-API Modules for React Native") - .addCommand(buildCommand); + .addCommand(buildCommand, { isDefault: true }); diff --git a/packages/ferric/src/run.ts b/packages/ferric/src/run.ts index 7b390d6c..01311284 100644 --- a/packages/ferric/src/run.ts +++ b/packages/ferric/src/run.ts @@ -1,4 +1,5 @@ import EventEmitter from "node:events"; + import { program } from "./program.js"; // We're attaching a lot of listeners when spawning in parallel diff --git a/packages/ferric/src/rustup.ts b/packages/ferric/src/rustup.ts index b246c0c0..11064dd1 100644 --- a/packages/ferric/src/rustup.ts +++ b/packages/ferric/src/rustup.ts @@ -1,6 +1,6 @@ -import cp from "child_process"; +import cp from "node:child_process"; -import { UsageError } from "./errors.js"; +import { UsageError } from "@react-native-node-api/cli-utils"; export function getInstalledTargets() { try { diff --git a/packages/ferric/src/targets.ts b/packages/ferric/src/targets.ts index 5cc7d80f..513c27e1 100644 --- a/packages/ferric/src/targets.ts +++ b/packages/ferric/src/targets.ts @@ -1,6 +1,6 @@ -import chalk from "chalk"; +import cp from "node:child_process"; -import { UsageError } from "./errors.js"; +import { assertFixable } from "@react-native-node-api/cli-utils"; import { getInstalledTargets } from "./rustup.js"; export const ANDROID_TARGETS = [ @@ -18,31 +18,31 @@ export type AndroidTargetName = (typeof ANDROID_TARGETS)[number]; export const APPLE_TARGETS = [ "aarch64-apple-darwin", "x86_64-apple-darwin", + "aarch64-apple-ios", "aarch64-apple-ios-sim", + "x86_64-apple-ios", // Simulator (despite the missing -sim suffix) + // "aarch64-apple-ios-macabi", // Catalyst - // "x86_64-apple-ios", // "x86_64-apple-ios-macabi", // Catalyst - // TODO: Re-enabled these when we know how to install them ๐Ÿ™ˆ - /* - "aarch64-apple-tvos", - "aarch64-apple-tvos-sim", "aarch64-apple-visionos", "aarch64-apple-visionos-sim", - */ + + "aarch64-apple-tvos", + // "arm64e-apple-tvos", + "aarch64-apple-tvos-sim", + "x86_64-apple-tvos", // Simulator (despite the missing -sim suffix) // "aarch64-apple-watchos", // "aarch64-apple-watchos-sim", // "arm64_32-apple-watchos", // "arm64e-apple-darwin", // "arm64e-apple-ios", - // "arm64e-apple-tvos", // "armv7k-apple-watchos", // "armv7s-apple-ios", // "i386-apple-ios", // "i686-apple-darwin", - // "x86_64-apple-tvos", // "x86_64-apple-watchos-sim", // "x86_64h-apple-darwin", ] as const; @@ -51,24 +51,72 @@ export type AppleTargetName = (typeof APPLE_TARGETS)[number]; export const ALL_TARGETS = [...ANDROID_TARGETS, ...APPLE_TARGETS] as const; export type TargetName = (typeof ALL_TARGETS)[number]; +const THIRD_TIER_TARGETS: Set = new Set([ + "aarch64-apple-visionos", + "aarch64-apple-visionos-sim", + + "aarch64-apple-tvos", + "aarch64-apple-tvos-sim", + "x86_64-apple-tvos", +]); + +export function assertNightlyToolchain() { + const toolchainLines = cp + .execFileSync("rustup", ["toolchain", "list"], { + encoding: "utf-8", + }) + .split("\n"); + + const nightlyLines = toolchainLines.filter((line) => + line.startsWith("nightly-"), + ); + assertFixable( + nightlyLines.length > 0, + "You need to use a nightly Rust toolchain", + { + command: "rustup toolchain install nightly --component rust-src", + }, + ); + + const componentLines = cp + .execFileSync("rustup", ["component", "list", "--toolchain", "nightly"], { + encoding: "utf-8", + }) + .split("\n"); + assertFixable( + componentLines.some((line) => line === "rust-src (installed)"), + "You need to install the rust-src component for the nightly Rust toolchain", + { + command: "rustup toolchain install nightly --component rust-src", + }, + ); +} + /** - * Ensure the targets are installed into the Rust toolchain + * Ensure the targets are either installed into the Rust toolchain or available via nightly Rust toolchain. * We do this up-front because the error message and fix is very unclear from the failure when missing. */ -export function ensureInstalledTargets(expectedTargets: Set) { +export function ensureAvailableTargets(expectedTargets: Set) { const installedTargets = getInstalledTargets(); - const missingTargets = new Set([ - ...[...expectedTargets].filter((target) => !installedTargets.has(target)), - ]); - if (missingTargets.size > 0) { - // TODO: Ask the user if they want to run this - throw new UsageError( - `You're missing ${ - missingTargets.size - } targets - to fix this, run:\n\n${chalk.italic( - `rustup target add ${[...missingTargets].join(" ")}`, - )}`, - ); + + const missingInstallableTargets = expectedTargets + .difference(installedTargets) + .difference(THIRD_TIER_TARGETS); + + assertFixable( + missingInstallableTargets.size === 0, + `You need to add these targets to your toolchain: ${[ + ...missingInstallableTargets, + ].join(", ")}`, + { + command: `rustup target add ${[...missingInstallableTargets].join(" ")}`, + }, + ); + + const expectedThirdTierTargets = + expectedTargets.intersection(THIRD_TIER_TARGETS); + if (expectedThirdTierTargets.size > 0) { + assertNightlyToolchain(); } } @@ -82,6 +130,10 @@ export function isAppleTarget(target: TargetName): target is AppleTargetName { return APPLE_TARGETS.includes(target as (typeof APPLE_TARGETS)[number]); } +export function isThirdTierTarget(target: TargetName): boolean { + return THIRD_TIER_TARGETS.has(target); +} + export function filterTargetsByPlatform( targets: Set, platform: "android", diff --git a/packages/gyp-to-cmake/CHANGELOG.md b/packages/gyp-to-cmake/CHANGELOG.md index bd29ad20..9aec77d2 100644 --- a/packages/gyp-to-cmake/CHANGELOG.md +++ b/packages/gyp-to-cmake/CHANGELOG.md @@ -1,5 +1,55 @@ # gyp-to-cmake +## 0.5.3 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts ๐Ÿ™ˆ +- Updated dependencies [1dee80f] + - @react-native-node-api/cli-utils@0.1.4 + +## 0.5.2 + +### Patch Changes + +- Updated dependencies [441dcc4] + - @react-native-node-api/cli-utils@0.1.3 + +## 0.5.1 + +### Patch Changes + +- Updated dependencies [7ff2c2b] + - @react-native-node-api/cli-utils@0.1.2 + +## 0.5.0 + +### Minor Changes + +- 60fae96: Use `find_package` instead of `include` to locate "weak-node-api" + +## 0.4.0 + +### Minor Changes + +- 5156d35: Use of CMake targets producing Apple frameworks instead of free dylibs is now supported + +### Patch Changes + +- 5156d35: Refactored moving prettyPath util to CLI utils package +- Updated dependencies [5156d35] + - @react-native-node-api/cli-utils@0.1.1 + +## 0.3.0 + +### Minor Changes + +- ff34c45: Add --weak-node-api option to emit CMake configuration for use with cmake-rn's default way of Node-API linkage. + +### Patch Changes + +- 2a30d8d: Refactored CLIs to use a shared utility package + ## 0.2.0 ### Minor Changes diff --git a/packages/gyp-to-cmake/package.json b/packages/gyp-to-cmake/package.json index a45bbcc8..2a1bde89 100644 --- a/packages/gyp-to-cmake/package.json +++ b/packages/gyp-to-cmake/package.json @@ -1,6 +1,6 @@ { "name": "gyp-to-cmake", - "version": "0.2.0", + "version": "0.5.3", "description": "Convert binding.gyp files to CMakeLists.txt", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -22,8 +22,9 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "dependencies": { - "@commander-js/extra-typings": "^13.1.0", - "commander": "^13.1.0", - "gyp-parser": "^1.0.4" + "@react-native-node-api/cli-utils": "0.1.4", + "gyp-parser": "^1.0.4", + "pkg-dir": "^8.0.0", + "read-pkg": "^9.0.1" } } diff --git a/packages/gyp-to-cmake/src/cli.ts b/packages/gyp-to-cmake/src/cli.ts index 133bd6f9..45cefbaa 100644 --- a/packages/gyp-to-cmake/src/cli.ts +++ b/packages/gyp-to-cmake/src/cli.ts @@ -1,6 +1,15 @@ +import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { Command } from "@commander-js/extra-typings"; +import { packageDirectorySync } from "pkg-dir"; +import { readPackageSync } from "read-pkg"; + +import { + Command, + Option, + prettyPath, + wrapAction, +} from "@react-native-node-api/cli-utils"; import { readBindingFile } from "./gyp.js"; import { @@ -8,41 +17,46 @@ import { type GypToCmakeListsOptions, } from "./transformer.js"; -export type TransformOptions = Omit< - GypToCmakeListsOptions, - "gyp" | "projectName" -> & { +export type TransformOptions = Omit & { disallowUnknownProperties: boolean; - projectName?: string; }; export function generateProjectName(gypPath: string) { - return path.dirname(gypPath).replaceAll(path.sep, "-"); + const packagePath = packageDirectorySync({ cwd: path.dirname(gypPath) }); + assert(packagePath, "Expected the binding.gyp file to be inside a package"); + const { name } = readPackageSync({ cwd: packagePath }); + return name + .replace(/^@/g, "") + .replace(/\//g, "--") + .replace(/[^a-zA-Z0-9_-]/g, "_"); } export function transformBindingGypFile( gypPath: string, { disallowUnknownProperties, - projectName = generateProjectName(gypPath), + projectName, ...restOfOptions }: TransformOptions, ) { - console.log("Transforming", gypPath); - const gyp = readBindingFile(gypPath, disallowUnknownProperties); const parentPath = path.dirname(gypPath); + const cmakeListsPath = path.join(parentPath, "CMakeLists.txt"); + console.log( + `Transforming ${prettyPath(gypPath)} โ†’ ${prettyPath(cmakeListsPath)}`, + ); + + const gyp = readBindingFile(gypPath, disallowUnknownProperties); const result = bindingGypToCmakeLists({ gyp, projectName, ...restOfOptions, }); - const cmakeListsPath = path.join(parentPath, "CMakeLists.txt"); fs.writeFileSync(cmakeListsPath, result, "utf-8"); } export function transformBindingGypsRecursively( directoryPath: string, - options: TransformOptions, + options: Omit, ) { const entries = fs.readdirSync(directoryPath, { withFileTypes: true }); for (const entry of entries) { @@ -50,34 +64,73 @@ export function transformBindingGypsRecursively( if (entry.isDirectory()) { transformBindingGypsRecursively(fullPath, options); } else if (entry.isFile() && entry.name === "binding.gyp") { - transformBindingGypFile(fullPath, options); + transformBindingGypFile(fullPath, { + ...options, + projectName: generateProjectName(fullPath), + }); } } } +const projectNameOption = new Option( + "--project-name ", + "Project name to use in CMakeLists.txt", +).default(undefined, "Uses name from the surrounding package.json"); + export const program = new Command("gyp-to-cmake") .description("Transform binding.gyp to CMakeLists.txt") .option( "--no-path-transforms", "Don't transform output from command expansions (replacing '\\' with '/')", ) + .option("--weak-node-api", "Link against the weak-node-api library", false) + .option("--define-napi-version", "Define NAPI_VERSION for all targets", false) + .option( + "--no-apple-framework", + "Disable emitting target properties to produce Apple frameworks", + ) + .option("--cpp ", "C++ standard version", "17") + .addOption(projectNameOption) .argument( "[path]", "Path to the binding.gyp file or directory to traverse recursively", process.cwd(), ) - .action((targetPath: string, { pathTransforms }) => { - const options: TransformOptions = { - unsupportedBehaviour: "throw", - disallowUnknownProperties: false, - transformWinPathsToPosix: pathTransforms, - }; - const stat = fs.statSync(targetPath); - if (stat.isFile()) { - transformBindingGypFile(targetPath, options); - } else if (stat.isDirectory()) { - transformBindingGypsRecursively(targetPath, options); - } else { - throw new Error(`Expected either a file or a directory: ${targetPath}`); - } - }); + .action( + wrapAction( + ( + targetPath: string, + { + pathTransforms, + cpp, + defineNapiVersion, + weakNodeApi, + appleFramework, + projectName, + }, + ) => { + const options: Omit = { + unsupportedBehaviour: "throw", + disallowUnknownProperties: false, + transformWinPathsToPosix: pathTransforms, + compileFeatures: cpp ? [`cxx_std_${cpp}`] : [], + defineNapiVersion, + weakNodeApi, + appleFramework, + }; + const stat = fs.statSync(targetPath); + if (stat.isFile()) { + transformBindingGypFile(targetPath, { + ...options, + projectName: projectName ?? generateProjectName(targetPath), + }); + } else if (stat.isDirectory()) { + transformBindingGypsRecursively(targetPath, options); + } else { + throw new Error( + `Expected either a file or a directory: ${targetPath}`, + ); + } + }, + ), + ); diff --git a/packages/gyp-to-cmake/src/run.ts b/packages/gyp-to-cmake/src/run.ts index c64a70b0..feff7eb2 100644 --- a/packages/gyp-to-cmake/src/run.ts +++ b/packages/gyp-to-cmake/src/run.ts @@ -1,2 +1,3 @@ import { program } from "./cli.js"; -program.parse(process.argv); + +program.parseAsync(process.argv).catch(console.error); diff --git a/packages/gyp-to-cmake/src/transformer.ts b/packages/gyp-to-cmake/src/transformer.ts index 01096501..4901df4f 100644 --- a/packages/gyp-to-cmake/src/transformer.ts +++ b/packages/gyp-to-cmake/src/transformer.ts @@ -12,6 +12,10 @@ export type GypToCmakeListsOptions = { executeCmdExpansions?: boolean; unsupportedBehaviour?: "skip" | "warn" | "throw"; transformWinPathsToPosix?: boolean; + compileFeatures?: string[]; + defineNapiVersion?: boolean; + weakNodeApi?: boolean; + appleFramework?: boolean; }; function isCmdExpansion(value: string) { @@ -23,6 +27,14 @@ function escapeSpaces(source: string) { return source.replace(/ /g, "\\ "); } +/** + * Escapes any input to match a CFBundleIdentifier + * See https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleidentifier + */ +export function escapeBundleIdentifier(input: string) { + return input.replaceAll("__", ".").replace(/[^A-Za-z0-9-.]/g, "-"); +} + /** * @see {@link https://github.com/cmake-js/cmake-js?tab=readme-ov-file#usage} for details on the template used * @returns The contents of a CMakeLists.txt file @@ -34,6 +46,10 @@ export function bindingGypToCmakeLists({ executeCmdExpansions = true, unsupportedBehaviour = "skip", transformWinPathsToPosix = true, + defineNapiVersion = true, + weakNodeApi = false, + appleFramework = true, + compileFeatures = [], }: GypToCmakeListsOptions): string { function mapExpansion(value: string): string[] { if (!isCmdExpansion(value)) { @@ -60,65 +76,145 @@ export function bindingGypToCmakeLists({ } const lines: string[] = [ - "cmake_minimum_required(VERSION 3.15)", + "cmake_minimum_required(VERSION 3.15...3.31)", //"cmake_policy(SET CMP0091 NEW)", //"cmake_policy(SET CMP0042 NEW)", `project(${projectName})`, "", // Declaring a project-wide NAPI_VERSION as a fallback for targets that don't explicitly set it - `add_compile_definitions(NAPI_VERSION=${napiVersion})`, + // This is only needed when using cmake-js, as it is injected by cmake-rn + ...(defineNapiVersion + ? [`add_compile_definitions(NAPI_VERSION=${napiVersion})`] + : []), ]; + if (weakNodeApi) { + lines.push(`find_package(weak-node-api REQUIRED CONFIG)`, ""); + } + for (const target of gyp.targets) { - const { target_name: targetName } = target; + const { target_name: targetName, defines = [] } = target; // TODO: Handle "conditions" // TODO: Handle "cflags" // TODO: Handle "ldflags" - const escapedJoinedSources = target.sources + const escapedSources = target.sources .flatMap(mapExpansion) .map(transformPath) - .map(escapeSpaces) - .join(" "); + .map(escapeSpaces); - const escapedJoinedIncludes = (target.include_dirs || []) + const escapedIncludes = (target.include_dirs || []) .flatMap(mapExpansion) .map(transformPath) - .map(escapeSpaces) - .join(" "); + .map(escapeSpaces); - const escapedJoinedDefines = (target.defines || []) + const escapedDefines = defines .flatMap(mapExpansion) .map(transformPath) - .map(escapeSpaces) - .join(" "); + .map(escapeSpaces); + + const libraries = []; + if (weakNodeApi) { + libraries.push("weak-node-api"); + } else { + libraries.push("${CMAKE_JS_LIB}"); + escapedSources.push("${CMAKE_JS_SRC}"); + escapedIncludes.push("${CMAKE_JS_INC}"); + } + + function setTargetPropertiesLines( + properties: Record, + indent = "", + ): string[] { + return [ + `${indent}set_target_properties(${targetName} PROPERTIES`, + ...Object.entries(properties).map( + ([key, value]) => `${indent} ${key} ${value ? value : '""'}`, + ), + `${indent} )`, + ]; + } + + lines.push(`add_library(${targetName} SHARED ${escapedSources.join(" ")})`); + + if (appleFramework) { + lines.push( + "", + 'option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON)', + "", + "if(APPLE AND BUILD_APPLE_FRAMEWORK)", + ...setTargetPropertiesLines( + { + FRAMEWORK: "TRUE", + MACOSX_FRAMEWORK_IDENTIFIER: escapeBundleIdentifier( + `${projectName}.${targetName}`, + ), + MACOSX_FRAMEWORK_SHORT_VERSION_STRING: "1.0", + MACOSX_FRAMEWORK_BUNDLE_VERSION: "1.0", + XCODE_ATTRIBUTE_SKIP_INSTALL: "NO", + }, + " ", + ), + "else()", + ...setTargetPropertiesLines( + { + PREFIX: "", + SUFFIX: ".node", + }, + " ", + ), + "endif()", + "", + ); + } else { + lines.push( + ...setTargetPropertiesLines({ + PREFIX: "", + SUFFIX: ".node", + }), + ); + } + + if (libraries.length > 0) { + lines.push( + `target_link_libraries(${targetName} PRIVATE ${libraries.join(" ")})`, + ); + } + + if (escapedIncludes.length > 0) { + lines.push( + `target_include_directories(${targetName} PRIVATE ${escapedIncludes.join( + " ", + )})`, + ); + } + + if (escapedDefines.length > 0) { + lines.push( + `target_compile_definitions(${targetName} PRIVATE ${escapedDefines.join(" ")})`, + ); + } + if (compileFeatures.length > 0) { + lines.push( + `target_compile_features(${targetName} PRIVATE ${compileFeatures.join(" ")})`, + ); + } + + // `set_target_properties(${targetName} PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO)`, + } + + if (!weakNodeApi) { + // This is required by cmake-js to generate the import library for node.lib on Windows lines.push( "", - `add_library(${targetName} SHARED ${escapedJoinedSources} \${CMAKE_JS_SRC})`, - `set_target_properties(${targetName} PROPERTIES PREFIX "" SUFFIX ".node")`, - `target_include_directories(${targetName} PRIVATE ${escapedJoinedIncludes} \${CMAKE_JS_INC})`, - `target_link_libraries(${targetName} PRIVATE \${CMAKE_JS_LIB})`, - `target_compile_features(${targetName} PRIVATE cxx_std_17)`, - ...(escapedJoinedDefines - ? [ - `target_compile_definitions(${targetName} PRIVATE ${escapedJoinedDefines})`, - ] - : []), - // or - // `set_target_properties(${targetName} PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO)`, + "if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)", + " # Generate node.lib", + " execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})", + "endif()", ); } - // Adding this post-amble from the template, although not used by react-native-node-api - lines.push( - "", - "if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)", - " # Generate node.lib", - " execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})", - "endif()", - ); - return lines.join("\n"); } diff --git a/packages/host/.gitignore b/packages/host/.gitignore index b3f12e5b..5ba3e2fe 100644 --- a/packages/host/.gitignore +++ b/packages/host/.gitignore @@ -16,12 +16,5 @@ include/ android/.cxx/ android/build/ -# Everything in weak-node-api is generated, except for the configurations -# Generated and built bia `npm run build-weak-node-api-injector` -/weak-node-api/build/ -/weak-node-api/*.xcframework -/weak-node-api/*.android.node -/weak-node-api/weak_node_api.cpp -/weak-node-api/weak_node_api.hpp # Generated via `npm run generate-weak-node-api-injector` /cpp/WeakNodeApiInjector.cpp diff --git a/packages/host/CHANGELOG.md b/packages/host/CHANGELOG.md index 4dbf62bb..13464f93 100644 --- a/packages/host/CHANGELOG.md +++ b/packages/host/CHANGELOG.md @@ -1,5 +1,96 @@ # react-native-node-api +## 1.0.1 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts ๐Ÿ™ˆ +- Updated dependencies [1dee80f] + - @react-native-node-api/cli-utils@0.1.4 + - weak-node-api@0.1.1 + +## 1.0.0 + +### Patch Changes + +- Updated dependencies [441dcc4] +- Updated dependencies [3d2e03e] + - @react-native-node-api/cli-utils@0.1.3 + - weak-node-api@0.1.0 + +## 0.7.1 + +### Patch Changes + +- 7ff2c2b: Fix minor package issues. +- Updated dependencies [7ff2c2b] +- Updated dependencies [7ff2c2b] + - weak-node-api@0.0.3 + - @react-native-node-api/cli-utils@0.1.2 + +## 0.7.0 + +### Minor Changes + +- 61fff3f: Ensure proper escaping when generating a bundle identifier while creating an Apple framework +- 60fae96: Use `find_package` instead of `include` to locate "weak-node-api" +- 60fae96: No longer exporting weakNodeApiPath, import from "weak-node-api" instead + +### Patch Changes + +- 60fae96: Moved weak-node-api into a separate "weak-node-api" package. +- 61fff3f: Allow passing --apple-bundle-identifier to specify the bundle identifiers used when creating Apple frameworks. +- 5dea205: Add "apple" folder into the package (follow-up to #301) +- eca721e: Don't instruct users to pass --force when vendoring hermes +- Updated dependencies [60fae96] + - weak-node-api@0.0.2 + +## 0.6.2 + +### Patch Changes + +- 07ea9dc: Add x86_64 and universal simulator triplets +- 7536c6c: Add --react-native-package option to "vendor-hermes" command, allowing caller to choose the package to download hermes into +- c698698: Moved and simplify Apple host TurboModule +- a2fd422: Detects "pod install" from React Native MacOS apps and vendors Hermes accordingly +- bdc172e: Add explicit support for React Native v0.79.7 +- 4672e01: Warn on "pod install" with the new architecture disabled + +## 0.6.1 + +### Patch Changes + +- 5c3de89: Rebuild any dSYM directory when linking frameworks. +- bb9a78c: Fixed visualizing duplicate library names + +## 0.6.0 + +### Minor Changes + +- 5156d35: Use of CMake targets producing Apple frameworks instead of free dylibs is now supported +- 5016ed2: Scope is now stripped from package names when renaming libraries while linking + +### Patch Changes + +- acd06f2: Linking Node-API addons for Apple platforms is no longer re-creating Xcframeworks +- 9f1a301: Fix requireNodeAddon return type +- 5156d35: Refactored moving prettyPath util to CLI utils package +- Updated dependencies [5156d35] + - @react-native-node-api/cli-utils@0.1.1 + +## 0.5.2 + +### Patch Changes + +- 2b9a538: Handle Info.plist lookup in versioned frameworks + +## 0.5.1 + +### Patch Changes + +- 2a30d8d: Refactored CLIs to use a shared utility package +- c72970f: Move REACT_NATIVE_OVERRIDE_HERMES_DIR out of tasks to fail earlier + ## 0.5.0 ### Minor Changes diff --git a/packages/host/android/CMakeLists.txt b/packages/host/android/CMakeLists.txt index 3e9fd392..e4c183f7 100644 --- a/packages/host/android/CMakeLists.txt +++ b/packages/host/android/CMakeLists.txt @@ -5,12 +5,7 @@ set(CMAKE_CXX_STANDARD 20) find_package(ReactAndroid REQUIRED CONFIG) find_package(hermes-engine REQUIRED CONFIG) - -add_library(weak-node-api INTERFACE) -target_include_directories(weak-node-api INTERFACE - ../weak-node-api - ../weak-node-api/include -) +find_package(weak-node-api REQUIRED CONFIG) add_library(node-api-host SHARED src/main/cpp/OnLoad.cpp diff --git a/packages/host/android/build.gradle b/packages/host/android/build.gradle index f4b3ba4c..39204133 100644 --- a/packages/host/android/build.gradle +++ b/packages/host/android/build.gradle @@ -2,6 +2,29 @@ import java.nio.file.Paths import groovy.json.JsonSlurper import org.gradle.internal.os.OperatingSystem +if (!System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR")) { + throw new GradleException([ + "React Native Node-API needs a custom version of Hermes with Node-API enabled.", + "Run the following in your Bash- or Zsh-compatible terminal, to clone Hermes and instruct React Native to use it:", + "", + "export REACT_NATIVE_OVERRIDE_HERMES_DIR=\$(npx react-native-node-api vendor-hermes --silent)", + "", + "And follow this guide to build React Native from source:", + "https://reactnative.dev/contributing/how-to-build-from-source#update-your-project-to-build-from-source" + ].join('\n')) +} + +def findWeakNodeApiDir() { + def searchDir = rootDir.toPath() + do { + def p = searchDir.resolve("node_modules/weak-node-api") + if (p.toFile().exists()) { + return p.toRealPath().toString() + } + } while (searchDir = searchDir.getParent()) + throw new GradleException("Could not find `weak-node-api`"); +} + buildscript { ext.getExtOrDefault = {name -> return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['NodeApiModules_' + name] @@ -44,13 +67,16 @@ def supportsNamespace() { android { if (supportsNamespace()) { - namespace "com.callstack.node_api_modules" + namespace "com.callstack.react_native_node_api" sourceSets { main { manifest.srcFile "src/main/AndroidManifestNew.xml" - // Include the weak-node-api to enable a dynamic load - jniLibs.srcDirs += ["../weak-node-api/weak-node-api.android.node"] + // Include the weak-node-api native libraries directly + jniLibs.srcDirs += [ + "../../weak-node-api/build/Debug/weak-node-api.android.node", + "../../weak-node-api/build/Release/weak-node-api.android.node" + ] } } } @@ -59,7 +85,8 @@ android { compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") buildFeatures { - prefab = true + buildConfig true + prefab true } defaultConfig { @@ -70,7 +97,7 @@ android { cmake { targets "node-api-host" cppFlags "-frtti -fexceptions -Wall -fstack-protector-all" - arguments "-DANDROID_STL=c++_shared" + arguments "-DANDROID_STL=c++_shared", "-Dweak-node-api_DIR=${findWeakNodeApiDir()}" abiFilters (*reactNativeArchitectures()) buildTypes { @@ -91,15 +118,11 @@ android { } } - buildFeatures { - buildConfig true - } - buildTypes { debug { jniDebuggable true packagingOptions { - doNotStrip "**/libnode-api-host.so" + doNotStrip "**/*.so" } } release { @@ -135,22 +158,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } -task checkHermesOverride { - doFirst { - if (!System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR")) { - throw new GradleException([ - "React Native Node-API needs a custom version of Hermes with Node-API enabled.", - "Run the following in your terminal, to clone Hermes and instruct React Native to use it:", - "", - "export REACT_NATIVE_OVERRIDE_HERMES_DIR=\$(npx react-native-node-api vendor-hermes --silent --force)", - "", - "And follow this guide to build React Native from source:", - "https://reactnative.dev/contributing/how-to-build-from-source#update-your-project-to-build-from-source" - ].join('\n')) - } - } -} - def commandLinePrefix = OperatingSystem.current().isWindows() ? ["cmd", "/c", "node"] : [] def cliPath = file("../bin/react-native-node-api.mjs") @@ -169,5 +176,5 @@ task linkNodeApiModules { } } -preBuild.dependsOn checkHermesOverride, linkNodeApiModules +preBuild.dependsOn linkNodeApiModules diff --git a/packages/host/android/src/main/AndroidManifest.xml b/packages/host/android/src/main/AndroidManifest.xml index 99212aac..1b416675 100644 --- a/packages/host/android/src/main/AndroidManifest.xml +++ b/packages/host/android/src/main/AndroidManifest.xml @@ -1,3 +1,3 @@ + package="com.callstack.react_native_node_api"> diff --git a/packages/host/android/src/main/cpp/OnLoad.cpp b/packages/host/android/src/main/cpp/OnLoad.cpp index 35e27128..3cd6f306 100644 --- a/packages/host/android/src/main/cpp/OnLoad.cpp +++ b/packages/host/android/src/main/cpp/OnLoad.cpp @@ -7,13 +7,13 @@ // Called when the library is loaded jint JNI_OnLoad(JavaVM *vm, void *reserved) { - callstack::nodeapihost::injectIntoWeakNodeApi(); + callstack::react_native_node_api::injectIntoWeakNodeApi(); // Register the C++ TurboModule facebook::react::registerCxxModuleToGlobalModuleMap( - callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, + callstack::react_native_node_api::CxxNodeApiHostModule::kModuleName, [](std::shared_ptr jsInvoker) { - return std::make_shared( - jsInvoker); + return std::make_shared< + callstack::react_native_node_api::CxxNodeApiHostModule>(jsInvoker); }); return JNI_VERSION_1_6; } diff --git a/packages/host/android/src/main/java/com/callstack/node_api_modules/NodeApiModulesPackage.kt b/packages/host/android/src/main/java/com/callstack/react_native_node_api/NodeApiHostPackage.kt similarity index 89% rename from packages/host/android/src/main/java/com/callstack/node_api_modules/NodeApiModulesPackage.kt rename to packages/host/android/src/main/java/com/callstack/react_native_node_api/NodeApiHostPackage.kt index 105d347e..38426367 100644 --- a/packages/host/android/src/main/java/com/callstack/node_api_modules/NodeApiModulesPackage.kt +++ b/packages/host/android/src/main/java/com/callstack/react_native_node_api/NodeApiHostPackage.kt @@ -1,4 +1,4 @@ -package com.callstack.node_api_modules +package com.callstack.react_native_node_api import com.facebook.hermes.reactexecutor.HermesExecutor import com.facebook.react.BaseReactPackage @@ -10,7 +10,7 @@ import com.facebook.soloader.SoLoader import java.util.HashMap -class NodeApiModulesPackage : BaseReactPackage() { +class NodeApiHostPackage : BaseReactPackage() { init { SoLoader.loadLibrary("node-api-host") } diff --git a/packages/host/apple/NodeApiHostModuleProvider.mm b/packages/host/apple/NodeApiHostModuleProvider.mm new file mode 100644 index 00000000..1ea633fe --- /dev/null +++ b/packages/host/apple/NodeApiHostModuleProvider.mm @@ -0,0 +1,21 @@ +#import "CxxNodeApiHostModule.hpp" +#import "WeakNodeApiInjector.hpp" + +#import +@interface NodeApiHostPackage : NSObject + +@end + +@implementation NodeApiHostPackage ++ (void)load { + callstack::react_native_node_api::injectIntoWeakNodeApi(); + + facebook::react::registerCxxModuleToGlobalModuleMap( + callstack::react_native_node_api::CxxNodeApiHostModule::kModuleName, + [](std::shared_ptr jsInvoker) { + return std::make_shared< + callstack::react_native_node_api::CxxNodeApiHostModule>(jsInvoker); + }); +} + +@end \ No newline at end of file diff --git a/packages/host/cpp/AddonLoaders.hpp b/packages/host/cpp/AddonLoaders.hpp index d0d5d269..2836bdfa 100644 --- a/packages/host/cpp/AddonLoaders.hpp +++ b/packages/host/cpp/AddonLoaders.hpp @@ -7,7 +7,7 @@ #include #include -using callstack::nodeapihost::log_debug; +using callstack::react_native_node_api::log_debug; struct PosixLoader { using Module = void *; diff --git a/packages/host/cpp/CxxNodeApiHostModule.cpp b/packages/host/cpp/CxxNodeApiHostModule.cpp index 950c7af6..0b1961ec 100644 --- a/packages/host/cpp/CxxNodeApiHostModule.cpp +++ b/packages/host/cpp/CxxNodeApiHostModule.cpp @@ -4,7 +4,7 @@ using namespace facebook; -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { CxxNodeApiHostModule::CxxNodeApiHostModule( std::shared_ptr jsInvoker) @@ -127,8 +127,8 @@ bool CxxNodeApiHostModule::initializeNodeModule(jsi::Runtime &rt, napi_set_named_property(env, global, addon.generatedName.data(), exports); assert(status == napi_ok); - callstack::nodeapihost::setCallInvoker(env, callInvoker_); + callstack::react_native_node_api::setCallInvoker(env, callInvoker_); return true; } -} // namespace callstack::nodeapihost +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/CxxNodeApiHostModule.hpp b/packages/host/cpp/CxxNodeApiHostModule.hpp index 9445cdaf..4c753cfe 100644 --- a/packages/host/cpp/CxxNodeApiHostModule.hpp +++ b/packages/host/cpp/CxxNodeApiHostModule.hpp @@ -6,7 +6,7 @@ #include "AddonLoaders.hpp" -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { class JSI_EXPORT CxxNodeApiHostModule : public facebook::react::TurboModule { public: @@ -37,4 +37,4 @@ class JSI_EXPORT CxxNodeApiHostModule : public facebook::react::TurboModule { bool initializeNodeModule(facebook::jsi::Runtime &rt, NodeAddon &addon); }; -} // namespace callstack::nodeapihost +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/Logger.cpp b/packages/host/cpp/Logger.cpp index 0d2a9c9f..b863fcdf 100644 --- a/packages/host/cpp/Logger.cpp +++ b/packages/host/cpp/Logger.cpp @@ -16,33 +16,33 @@ enum class LogLevel { Debug, Warning, Error }; constexpr std::string_view levelToString(LogLevel level) { switch (level) { - case LogLevel::Debug: - return "DEBUG"; - case LogLevel::Warning: - return "WARNING"; - case LogLevel::Error: - return "ERROR"; - default: - return "UNKNOWN"; + case LogLevel::Debug: + return "DEBUG"; + case LogLevel::Warning: + return "WARNING"; + case LogLevel::Error: + return "ERROR"; + default: + return "UNKNOWN"; } } #if defined(__ANDROID__) constexpr int androidLogLevel(LogLevel level) { switch (level) { - case LogLevel::Debug: - return ANDROID_LOG_DEBUG; - case LogLevel::Warning: - return ANDROID_LOG_WARN; - case LogLevel::Error: - return ANDROID_LOG_ERROR; - default: - return ANDROID_LOG_UNKNOWN; + case LogLevel::Debug: + return ANDROID_LOG_DEBUG; + case LogLevel::Warning: + return ANDROID_LOG_WARN; + case LogLevel::Error: + return ANDROID_LOG_ERROR; + default: + return ANDROID_LOG_UNKNOWN; } } #endif -void log_message_internal(LogLevel level, const char* format, va_list args) { +void log_message_internal(LogLevel level, const char *format, va_list args) { #if defined(__ANDROID__) __android_log_vprint(androidLogLevel(level), LOG_TAG, format, args); #elif defined(__APPLE__) @@ -59,27 +59,27 @@ void log_message_internal(LogLevel level, const char* format, va_list args) { fprintf(stdout, "\n"); #endif } -} // anonymous namespace +} // anonymous namespace -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { -void log_debug(const char* format, ...) { +void log_debug(const char *format, ...) { // TODO: Disable logging in release builds va_list args; va_start(args, format); log_message_internal(LogLevel::Debug, format, args); va_end(args); } -void log_warning(const char* format, ...) { +void log_warning(const char *format, ...) { va_list args; va_start(args, format); log_message_internal(LogLevel::Warning, format, args); va_end(args); } -void log_error(const char* format, ...) { +void log_error(const char *format, ...) { va_list args; va_start(args, format); log_message_internal(LogLevel::Error, format, args); va_end(args); } -} // namespace callstack::nodeapihost +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/Logger.hpp b/packages/host/cpp/Logger.hpp index 350e3f21..c064e7da 100644 --- a/packages/host/cpp/Logger.hpp +++ b/packages/host/cpp/Logger.hpp @@ -2,8 +2,8 @@ #include -namespace callstack::nodeapihost { -void log_debug(const char* format, ...); -void log_warning(const char* format, ...); -void log_error(const char* format, ...); -} // namespace callstack::nodeapihost +namespace callstack::react_native_node_api { +void log_debug(const char *format, ...); +void log_warning(const char *format, ...); +void log_error(const char *format, ...); +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/RuntimeNodeApi.cpp b/packages/host/cpp/RuntimeNodeApi.cpp index 3c4d773c..c4e1c44c 100644 --- a/packages/host/cpp/RuntimeNodeApi.cpp +++ b/packages/host/cpp/RuntimeNodeApi.cpp @@ -1,14 +1,14 @@ #include "RuntimeNodeApi.hpp" -#include #include "Logger.hpp" #include "Versions.hpp" +#include auto ArrayType = napi_uint8_array; -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { -napi_status napi_create_buffer( - napi_env env, size_t length, void** data, napi_value* result) { +napi_status napi_create_buffer(napi_env env, size_t length, void **data, + napi_value *result) { napi_value buffer; if (const auto status = napi_create_arraybuffer(env, length, data, &buffer); status != napi_ok) { @@ -22,17 +22,15 @@ napi_status napi_create_buffer( return napi_create_typedarray(env, ArrayType, length, buffer, 0, result); } -napi_status napi_create_buffer_copy(napi_env env, - size_t length, - const void* data, - void** result_data, - napi_value* result) { +napi_status napi_create_buffer_copy(napi_env env, size_t length, + const void *data, void **result_data, + napi_value *result) { if (!length || !data || !result) { return napi_invalid_arg; } void *buffer = nullptr; - if (const auto status = callstack::nodeapihost::napi_create_buffer( + if (const auto status = callstack::react_native_node_api::napi_create_buffer( env, length, &buffer, result); status != napi_ok) { return status; @@ -42,7 +40,7 @@ napi_status napi_create_buffer_copy(napi_env env, return napi_ok; } -napi_status napi_is_buffer(napi_env env, napi_value value, bool* result) { +napi_status napi_is_buffer(napi_env env, napi_value value, bool *result) { if (!result) { return napi_invalid_arg; } @@ -77,8 +75,8 @@ napi_status napi_is_buffer(napi_env env, napi_value value, bool* result) { return napi_ok; } -napi_status napi_get_buffer_info( - napi_env env, napi_value value, void** data, size_t* length) { +napi_status napi_get_buffer_info(napi_env env, napi_value value, void **data, + size_t *length) { if (!data || !length) { return napi_invalid_arg; } @@ -97,19 +95,17 @@ napi_status napi_get_buffer_info( auto isTypedArray{false}; if (const auto status = napi_is_typedarray(env, value, &isTypedArray); status == napi_ok && isTypedArray) { - return napi_get_typedarray_info( - env, value, &ArrayType, length, data, nullptr, nullptr); + return napi_get_typedarray_info(env, value, &ArrayType, length, data, + nullptr, nullptr); } return napi_ok; } -napi_status napi_create_external_buffer(napi_env env, - size_t length, - void* data, - node_api_basic_finalize basic_finalize_cb, - void* finalize_hint, - napi_value* result) { +napi_status +napi_create_external_buffer(napi_env env, size_t length, void *data, + node_api_basic_finalize basic_finalize_cb, + void *finalize_hint, napi_value *result) { napi_value buffer; if (const auto status = napi_create_external_arraybuffer( env, data, length, basic_finalize_cb, finalize_hint, &buffer); @@ -124,25 +120,20 @@ napi_status napi_create_external_buffer(napi_env env, return napi_create_typedarray(env, ArrayType, length, buffer, 0, result); } -void napi_fatal_error(const char* location, - size_t location_len, - const char* message, - size_t message_len) { +void napi_fatal_error(const char *location, size_t location_len, + const char *message, size_t message_len) { if (location && location_len) { - log_error("Fatal Node-API error: %.*s %.*s", - static_cast(location_len), - location, - static_cast(message_len), - message); + log_error("Fatal Node-API error: %.*s %.*s", static_cast(location_len), + location, static_cast(message_len), message); } else { - log_error( - "Fatal Node-API error: %.*s", static_cast(message_len), message); + log_error("Fatal Node-API error: %.*s", static_cast(message_len), + message); } abort(); } -napi_status napi_get_node_version( - node_api_basic_env env, const napi_node_version** result) { +napi_status napi_get_node_version(node_api_basic_env env, + const napi_node_version **result) { if (!result) { return napi_invalid_arg; } @@ -151,7 +142,7 @@ napi_status napi_get_node_version( return napi_generic_failure; } -napi_status napi_get_version(node_api_basic_env env, uint32_t* result) { +napi_status napi_get_version(node_api_basic_env env, uint32_t *result) { if (!result) { return napi_invalid_arg; } @@ -160,4 +151,4 @@ napi_status napi_get_version(node_api_basic_env env, uint32_t* result) { return napi_ok; } -} // namespace callstack::nodeapihost +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/RuntimeNodeApi.hpp b/packages/host/cpp/RuntimeNodeApi.hpp index 67da7856..1a5e62ea 100644 --- a/packages/host/cpp/RuntimeNodeApi.hpp +++ b/packages/host/cpp/RuntimeNodeApi.hpp @@ -2,35 +2,31 @@ #include "node_api.h" -namespace callstack::nodeapihost { -napi_status napi_create_buffer( - napi_env env, size_t length, void** data, napi_value* result); - -napi_status napi_create_buffer_copy(napi_env env, - size_t length, - const void* data, - void** result_data, - napi_value* result); - -napi_status napi_is_buffer(napi_env env, napi_value value, bool* result); - -napi_status napi_get_buffer_info( - napi_env env, napi_value value, void** data, size_t* length); - -napi_status napi_create_external_buffer(napi_env env, - size_t length, - void* data, - node_api_basic_finalize basic_finalize_cb, - void* finalize_hint, - napi_value* result); - -void __attribute__((noreturn)) napi_fatal_error(const char* location, - size_t location_len, - const char* message, - size_t message_len); - -napi_status napi_get_node_version( - node_api_basic_env env, const napi_node_version** result); - -napi_status napi_get_version(node_api_basic_env env, uint32_t* result); -} // namespace callstack::nodeapihost +namespace callstack::react_native_node_api { +napi_status napi_create_buffer(napi_env env, size_t length, void **data, + napi_value *result); + +napi_status napi_create_buffer_copy(napi_env env, size_t length, + const void *data, void **result_data, + napi_value *result); + +napi_status napi_is_buffer(napi_env env, napi_value value, bool *result); + +napi_status napi_get_buffer_info(napi_env env, napi_value value, void **data, + size_t *length); + +napi_status +napi_create_external_buffer(napi_env env, size_t length, void *data, + node_api_basic_finalize basic_finalize_cb, + void *finalize_hint, napi_value *result); + +void __attribute__((noreturn)) napi_fatal_error(const char *location, + size_t location_len, + const char *message, + size_t message_len); + +napi_status napi_get_node_version(node_api_basic_env env, + const napi_node_version **result); + +napi_status napi_get_version(node_api_basic_env env, uint32_t *result); +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/RuntimeNodeApiAsync.cpp b/packages/host/cpp/RuntimeNodeApiAsync.cpp index dd4c87c3..bee380a7 100644 --- a/packages/host/cpp/RuntimeNodeApiAsync.cpp +++ b/packages/host/cpp/RuntimeNodeApiAsync.cpp @@ -1,6 +1,6 @@ #include "RuntimeNodeApiAsync.hpp" -#include #include "Logger.hpp" +#include struct AsyncJob { using IdType = uint64_t; @@ -13,26 +13,25 @@ struct AsyncJob { napi_value async_resource_name; napi_async_execute_callback execute; napi_async_complete_callback complete; - void* data{nullptr}; + void *data{nullptr}; - static AsyncJob* fromWork(napi_async_work work) { - return reinterpret_cast(work); + static AsyncJob *fromWork(napi_async_work work) { + return reinterpret_cast(work); } - static napi_async_work toWork(AsyncJob* job) { + static napi_async_work toWork(AsyncJob *job) { return reinterpret_cast(job); } }; class AsyncWorkRegistry { - public: +public: using IdType = AsyncJob::IdType; - std::shared_ptr create(napi_env env, - napi_value async_resource, - napi_value async_resource_name, - napi_async_execute_callback execute, - napi_async_complete_callback complete, - void* data) { + std::shared_ptr create(napi_env env, napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void *data) { const auto job = std::shared_ptr(new AsyncJob{ .id = next_id(), .state = AsyncJob::State::Created, @@ -68,7 +67,7 @@ class AsyncWorkRegistry { return false; } - private: +private: IdType next_id() { if (current_id_ == std::numeric_limits::max()) [[unlikely]] { current_id_ = 0; @@ -84,10 +83,11 @@ static std::unordered_map> callInvokers; static AsyncWorkRegistry asyncWorkRegistry; -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { -void setCallInvoker(napi_env env, - const std::shared_ptr& invoker) { +void setCallInvoker( + napi_env env, + const std::shared_ptr &invoker) { callInvokers[env] = invoker; } @@ -97,13 +97,11 @@ std::weak_ptr getCallInvoker(napi_env env) { : std::weak_ptr{}; } -napi_status napi_create_async_work(napi_env env, - napi_value async_resource, - napi_value async_resource_name, - napi_async_execute_callback execute, - napi_async_complete_callback complete, - void* data, - napi_async_work* result) { +napi_status napi_create_async_work(napi_env env, napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void *data, napi_async_work *result) { const auto job = asyncWorkRegistry.create( env, async_resource, async_resource_name, execute, complete, data); if (!job) { @@ -115,8 +113,8 @@ napi_status napi_create_async_work(napi_env env, return napi_ok; } -napi_status napi_queue_async_work( - node_api_basic_env env, napi_async_work work) { +napi_status napi_queue_async_work(node_api_basic_env env, + napi_async_work work) { const auto job = asyncWorkRegistry.get(work); if (!job) { log_debug("Error: Received null job in napi_queue_async_work"); @@ -140,8 +138,9 @@ napi_status napi_queue_async_work( } job->complete(env, - job->state == AsyncJob::State::Cancelled ? napi_cancelled : napi_ok, - job->data); + job->state == AsyncJob::State::Cancelled ? napi_cancelled + : napi_ok, + job->data); job->state = AsyncJob::State::Completed; }); @@ -149,8 +148,8 @@ napi_status napi_queue_async_work( return napi_ok; } -napi_status napi_delete_async_work( - node_api_basic_env env, napi_async_work work) { +napi_status napi_delete_async_work(node_api_basic_env env, + napi_async_work work) { const auto job = asyncWorkRegistry.get(work); if (!job) { log_debug("Error: Received non-existent job in napi_delete_async_work"); @@ -165,26 +164,26 @@ napi_status napi_delete_async_work( return napi_ok; } -napi_status napi_cancel_async_work( - node_api_basic_env env, napi_async_work work) { +napi_status napi_cancel_async_work(node_api_basic_env env, + napi_async_work work) { const auto job = asyncWorkRegistry.get(work); if (!job) { log_debug("Error: Received null job in napi_cancel_async_work"); return napi_invalid_arg; } switch (job->state) { - case AsyncJob::State::Completed: - log_debug("Error: Cannot cancel async work that is already completed"); - return napi_generic_failure; - case AsyncJob::State::Deleted: - log_debug("Warning: Async work job is already deleted"); - return napi_generic_failure; - case AsyncJob::State::Cancelled: - log_debug("Warning: Async work job is already cancelled"); - return napi_ok; + case AsyncJob::State::Completed: + log_debug("Error: Cannot cancel async work that is already completed"); + return napi_generic_failure; + case AsyncJob::State::Deleted: + log_debug("Warning: Async work job is already deleted"); + return napi_generic_failure; + case AsyncJob::State::Cancelled: + log_debug("Warning: Async work job is already cancelled"); + return napi_ok; } job->state = AsyncJob::State::Cancelled; return napi_ok; } -} // namespace callstack::nodeapihost +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/RuntimeNodeApiAsync.hpp b/packages/host/cpp/RuntimeNodeApiAsync.hpp index f0108e6d..be20128c 100644 --- a/packages/host/cpp/RuntimeNodeApiAsync.hpp +++ b/packages/host/cpp/RuntimeNodeApiAsync.hpp @@ -1,26 +1,24 @@ #pragma once +#include "node_api.h" #include #include -#include "node_api.h" -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { void setCallInvoker( - napi_env env, const std::shared_ptr& invoker); + napi_env env, const std::shared_ptr &invoker); -napi_status napi_create_async_work(napi_env env, - napi_value async_resource, - napi_value async_resource_name, - napi_async_execute_callback execute, - napi_async_complete_callback complete, - void* data, - napi_async_work* result); +napi_status napi_create_async_work(napi_env env, napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void *data, napi_async_work *result); napi_status napi_queue_async_work(node_api_basic_env env, napi_async_work work); -napi_status napi_delete_async_work( - node_api_basic_env env, napi_async_work work); +napi_status napi_delete_async_work(node_api_basic_env env, + napi_async_work work); -napi_status napi_cancel_async_work( - node_api_basic_env env, napi_async_work work); -} // namespace callstack::nodeapihost +napi_status napi_cancel_async_work(node_api_basic_env env, + napi_async_work work); +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/WeakNodeApiInjector.hpp b/packages/host/cpp/WeakNodeApiInjector.hpp index 1b0a718d..52f9fd60 100644 --- a/packages/host/cpp/WeakNodeApiInjector.hpp +++ b/packages/host/cpp/WeakNodeApiInjector.hpp @@ -1,5 +1,5 @@ #include -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { void injectIntoWeakNodeApi(); } diff --git a/packages/host/ios/NodeApiHostModuleProvider.mm b/packages/host/ios/NodeApiHostModuleProvider.mm deleted file mode 100644 index d4ecd94f..00000000 --- a/packages/host/ios/NodeApiHostModuleProvider.mm +++ /dev/null @@ -1,44 +0,0 @@ -#import "CxxNodeApiHostModule.hpp" -#import "WeakNodeApiInjector.hpp" - -#define USE_CXX_TURBO_MODULE_UTILS 0 -#if defined(__has_include) -#if __has_include() -#undef USE_CXX_TURBO_MODULE_UTILS -#define USE_CXX_TURBO_MODULE_UTILS 1 -#endif -#endif - -#if USE_CXX_TURBO_MODULE_UTILS -#import -@interface NodeApiHost : NSObject -#else -#import -@interface NodeApiHost : NSObject -#endif // USE_CXX_TURBO_MODULE_UTILS - -@end - -@implementation NodeApiHost -#if USE_CXX_TURBO_MODULE_UTILS -+ (void)load { - callstack::nodeapihost::injectIntoWeakNodeApi(); - - facebook::react::registerCxxModuleToGlobalModuleMap( - callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, - [](std::shared_ptr jsInvoker) { - return std::make_shared( - jsInvoker); - }); -} -#else -RCT_EXPORT_MODULE() - -- (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared( - params.jsInvoker); -} -#endif // USE_CXX_TURBO_MODULE_UTILS - -@end \ No newline at end of file diff --git a/packages/host/package.json b/packages/host/package.json index 9ddc3c4a..401ba6f1 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -1,6 +1,6 @@ { "name": "react-native-node-api", - "version": "0.5.0", + "version": "1.0.1", "description": "Node-API for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -19,18 +19,19 @@ "react-native": "./dist/react-native/index.js" }, "./babel-plugin": "./dist/node/babel-plugin/index.js", - "./cli": "./dist/node/cli/run.js", - "./weak-node-api": "./dist/node/weak-node-api.js" + "./cli": "./dist/node/cli/run.js" }, "files": [ "logo.svg", "bin", "dist", + "!dist/**/*.test.d.ts", + "!dist/**/*.test.d.ts.map", "cpp", "android", "!android/.cxx", "!android/build", - "ios", + "apple", "include", "babel-plugin.js", "scripts/patch-hermes.rb", @@ -41,30 +42,18 @@ ], "scripts": { "build": "tsc --build", - "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", - "generate-weak-node-api": "tsx scripts/generate-weak-node-api.ts", - "generate-weak-node-api-injector": "tsx scripts/generate-weak-node-api-injector.ts", - "build-weak-node-api": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api --out ./weak-node-api", - "build-weak-node-api:all-triplets": "cmake-rn --android --apple --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api --out ./weak-node-api", + "injector:generate": "node scripts/generate-injector.mts", "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", "test:gradle": "ENABLE_GRADLE_TESTS=true node --run test", - "bootstrap": "node --run copy-node-api-headers && node --run generate-weak-node-api-injector && node --run generate-weak-node-api && node --run build-weak-node-api", - "prerelease": "node --run copy-node-api-headers && node --run generate-weak-node-api-injector && node --run generate-weak-node-api && node --run build-weak-node-api:all-triplets" + "bootstrap": "node --run injector:generate", + "prerelease": "node --run injector:generate" }, "keywords": [ - "react-native", "node-api", "napi", - "node-api", "node-addon-api", "native", - "addon", - "module", - "c", - "c++", - "bindings", - "buildtools", - "cmake" + "addon" ], "author": { "name": "Callstack", @@ -78,23 +67,20 @@ ], "license": "MIT", "dependencies": { - "@commander-js/extra-typings": "^13.1.0", - "bufout": "^0.3.2", - "chalk": "^5.4.1", - "commander": "^13.1.0", - "ora": "^8.2.0", + "@expo/plist": "^0.4.7", + "@react-native-node-api/cli-utils": "0.1.4", "pkg-dir": "^8.0.0", - "read-pkg": "^9.0.1" + "read-pkg": "^9.0.1", + "zod": "^4.1.11" }, "devDependencies": { "@babel/core": "^7.26.10", "@babel/types": "^7.27.0", - "fswin": "^3.24.829", - "node-api-headers": "^1.5.0", - "zod": "^3.24.3" + "fswin": "^3.24.829" }, "peerDependencies": { "@babel/core": "^7.26.10", - "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4" + "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.79.7 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4 || 0.81.5", + "weak-node-api": "0.1.1" } } diff --git a/packages/host/react-native-node-api.podspec b/packages/host/react-native-node-api.podspec index 5066e82d..ab4b0e75 100644 --- a/packages/host/react-native-node-api.podspec +++ b/packages/host/react-native-node-api.podspec @@ -17,6 +17,10 @@ unless defined?(@xcframeworks_copied) @xcframeworks_copied = true end +if ENV['RCT_NEW_ARCH_ENABLED'] == '0' + Pod::UI.warn "React Native Node-API doesn't support the legacy architecture (but RCT_NEW_ARCH_ENABLED == '0')" +end + Pod::Spec.new do |s| s.name = package["name"] s.version = package["version"] @@ -25,13 +29,13 @@ Pod::Spec.new do |s| s.license = package["license"] s.authors = package["author"] - s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "weak-node-api/include/*.h", "weak-node-api/*.hpp" - s.public_header_files = "weak-node-api/include/*.h" + s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}" + + s.dependency "weak-node-api" - s.vendored_frameworks = "auto-linked/apple/*.xcframework", "weak-node-api/weak-node-api.xcframework" + s.vendored_frameworks = "auto-linked/apple/*.xcframework" s.script_phase = { :name => 'Copy Node-API xcframeworks', :execution_position => :before_compile, diff --git a/packages/host/scripts/generate-weak-node-api-injector.ts b/packages/host/scripts/generate-injector.mts similarity index 87% rename from packages/host/scripts/generate-weak-node-api-injector.ts rename to packages/host/scripts/generate-injector.mts index d5adfd83..bfd6a150 100644 --- a/packages/host/scripts/generate-weak-node-api-injector.ts +++ b/packages/host/scripts/generate-injector.mts @@ -2,9 +2,9 @@ import fs from "node:fs"; import path from "node:path"; import cp from "node:child_process"; -import { FunctionDecl, getNodeApiFunctions } from "./node-api-functions"; +import { type FunctionDecl, getNodeApiFunctions } from "weak-node-api"; -export const CPP_SOURCE_PATH = path.join(__dirname, "../cpp"); +export const CPP_SOURCE_PATH = path.join(import.meta.dirname, "../cpp"); // TODO: Remove when all runtime Node API functions are implemented const IMPLEMENTED_RUNTIME_FUNCTIONS = [ @@ -28,9 +28,10 @@ const IMPLEMENTED_RUNTIME_FUNCTIONS = [ export function generateSource(functions: FunctionDecl[]) { return ` // This file is generated by react-native-node-api - #include #include #include + + #include #include #include @@ -42,7 +43,7 @@ export function generateSource(functions: FunctionDecl[]) { #error "WEAK_NODE_API_LIBRARY_NAME cannot be defined for this platform" #endif - namespace callstack::nodeapihost { + namespace callstack::react_native_node_api { void injectIntoWeakNodeApi() { void *module = dlopen(WEAK_NODE_API_LIBRARY_NAME, RTLD_NOW | RTLD_LOCAL); @@ -58,8 +59,8 @@ export function generateSource(functions: FunctionDecl[]) { abort(); } - log_debug("Injecting WeakNodeApiHost"); - inject_weak_node_api_host(WeakNodeApiHost { + log_debug("Injecting NodeApiHost"); + inject_weak_node_api_host(NodeApiHost { ${functions .filter( ({ kind, name }) => @@ -69,7 +70,7 @@ export function generateSource(functions: FunctionDecl[]) { .join("\n")} }); } - } // namespace callstack::nodeapihost + } // namespace callstack::react_native_node_api `; } diff --git a/packages/host/scripts/generate-weak-node-api.ts b/packages/host/scripts/generate-weak-node-api.ts deleted file mode 100644 index 1090de41..00000000 --- a/packages/host/scripts/generate-weak-node-api.ts +++ /dev/null @@ -1,88 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import cp from "node:child_process"; - -import { FunctionDecl, getNodeApiFunctions } from "./node-api-functions"; - -export const WEAK_NODE_API_PATH = path.join(__dirname, "../weak-node-api"); - -/** - * Generates source code for a version script for the given Node API version. - */ -export function generateHeader(functions: FunctionDecl[]) { - return [ - "// This file is generated by react-native-node-api", - "#include ", // Node-API - "#include ", // fprintf() - "#include ", // abort() - // Generate the struct of function pointers - "struct WeakNodeApiHost {", - ...functions.map( - ({ returnType, noReturn, name, argumentTypes }) => - `${returnType} ${ - noReturn ? " __attribute__((noreturn))" : "" - }(*${name})(${argumentTypes.join(", ")});`, - ), - "};", - "typedef void(*InjectHostFunction)(const WeakNodeApiHost&);", - `extern "C" void inject_weak_node_api_host(const WeakNodeApiHost& host);`, - ].join("\n"); -} - -/** - * Generates source code for a version script for the given Node API version. - */ -export function generateSource(functions: FunctionDecl[]) { - return [ - "// This file is generated by react-native-node-api", - `#include "weak_node_api.hpp"`, // Generated header - // Generate the struct of function pointers - "WeakNodeApiHost g_host;", - "void inject_weak_node_api_host(const WeakNodeApiHost& host) {", - " g_host = host;", - "};", - ``, - // Generate function calling into the host - ...functions.flatMap(({ returnType, noReturn, name, argumentTypes }) => { - return [ - `extern "C" ${returnType} ${ - noReturn ? " __attribute__((noreturn))" : "" - }${name}(${argumentTypes - .map((type, index) => `${type} arg${index}`) - .join(", ")}) {`, - `if (g_host.${name} == nullptr) {`, - ` fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n");`, - " abort();", - "}", - (returnType === "void" ? "" : "return ") + - "g_host." + - name + - "(" + - argumentTypes.map((_, index) => `arg${index}`).join(", ") + - ");", - "};", - ]; - }), - ].join("\n"); -} - -async function run() { - await fs.promises.mkdir(WEAK_NODE_API_PATH, { recursive: true }); - - const nodeApiFunctions = getNodeApiFunctions(); - - const header = generateHeader(nodeApiFunctions); - const headerPath = path.join(WEAK_NODE_API_PATH, "weak_node_api.hpp"); - await fs.promises.writeFile(headerPath, header, "utf-8"); - cp.spawnSync("clang-format", ["-i", headerPath], { stdio: "inherit" }); - - const source = generateSource(nodeApiFunctions); - const sourcePath = path.join(WEAK_NODE_API_PATH, "weak_node_api.cpp"); - await fs.promises.writeFile(sourcePath, source, "utf-8"); - cp.spawnSync("clang-format", ["-i", sourcePath], { stdio: "inherit" }); -} - -run().catch((err) => { - console.error(err); - process.exitCode = 1; -}); diff --git a/packages/host/scripts/patch-hermes.rb b/packages/host/scripts/patch-hermes.rb index 9e1faaf5..76252154 100644 --- a/packages/host/scripts/patch-hermes.rb +++ b/packages/host/scripts/patch-hermes.rb @@ -4,13 +4,30 @@ raise "React Native Node-API cannot reliably patch JSI when React Native Core is prebuilt." end -VENDORED_HERMES_DIR ||= `npx react-native-node-api vendor-hermes --silent '#{Pod::Config.instance.installation_root}'`.strip -if Dir.exist?(VENDORED_HERMES_DIR) - Pod::UI.info "Hermes vendored into #{VENDORED_HERMES_DIR.inspect}" -else - raise "Hermes patching failed. Please check the output above for errors." +def get_react_native_package + if caller.any? { |frame| frame.include?("node_modules/react-native-macos/") } + return "react-native-macos" + elsif caller.any? { |frame| frame.include?("node_modules/react-native/") } + return "react-native" + else + raise "Unable to determine React Native package from call stack." + end end -# Signal the patched Hermes to React Native -ENV['BUILD_FROM_SOURCE'] = 'true' -ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] = VENDORED_HERMES_DIR +if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].nil? + VENDORED_HERMES_DIR ||= `npx react-native-node-api vendor-hermes --react-native-package '#{get_react_native_package()}' --silent '#{Pod::Config.instance.installation_root}'`.strip + # Signal the patched Hermes to React Native + ENV['BUILD_FROM_SOURCE'] = 'true' + ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] = VENDORED_HERMES_DIR +elsif Dir.exist?(ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR']) + # Setting an override path implies building from source + ENV['BUILD_FROM_SOURCE'] = 'true' +end + +if !ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].empty? + if Dir.exist?(ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR']) + Pod::UI.info "[Node-API] Using overridden Hermes in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}" + else + raise "Hermes patching failed: Expected override to exist in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}" + end +end diff --git a/packages/host/src/node/babel-plugin/plugin.ts b/packages/host/src/node/babel-plugin/plugin.ts index 6b9937cb..45e269e9 100644 --- a/packages/host/src/node/babel-plugin/plugin.ts +++ b/packages/host/src/node/babel-plugin/plugin.ts @@ -9,11 +9,22 @@ import { isNodeApiModule, findNodeAddonForBindings, NamingStrategy, - PathSuffixChoice, - assertPathSuffix, + LibraryNamingChoice, + assertLibraryNamingChoice, } from "../path-utils"; export type PluginOptions = { + /** + * Controls how the package name is transformed into a library name. + * The transformation is needed to disambiguate and avoid conflicts between addons with the same name (but in different sub-paths or packages). + * + * As an example, if the package name is `@my-org/my-pkg` and the path of the addon within the package is `build/Release/my-addon.node` (and `pathSuffix` is set to `"strip"`): + * - `"omit"`: Only the path within the package is used and the library name will be `my-addon`. + * - `"strip"`: Scope / org gets stripped and the library name will be `my-pkg--my-addon`. + * - `"keep"`: The org and name is kept and the library name will be `my-org--my-pkg--my-addon`. + */ + packageName?: LibraryNamingChoice; + /** * Controls how the path of the addon inside a package is transformed into a library name. * The transformation is needed to disambiguate and avoid conflicts between addons with the same name (but in different sub-paths or packages). @@ -23,13 +34,16 @@ export type PluginOptions = { * - `"strip"` (default): Path gets stripped to its basename and the library name will be `my-pkg--my-addon`. * - `"keep"`: The full path is kept and the library name will be `my-pkg--build-Release-my-addon`. */ - pathSuffix?: PathSuffixChoice; + pathSuffix?: LibraryNamingChoice; }; function assertOptions(opts: unknown): asserts opts is PluginOptions { assert(typeof opts === "object" && opts !== null, "Expected an object"); if ("pathSuffix" in opts) { - assertPathSuffix(opts.pathSuffix); + assertLibraryNamingChoice(opts.pathSuffix); + } + if ("packageName" in opts) { + assertLibraryNamingChoice(opts.packageName); } } @@ -57,7 +71,7 @@ export function plugin(): PluginObj { visitor: { CallExpression(p) { assertOptions(this.opts); - const { pathSuffix = "strip" } = this.opts; + const { pathSuffix = "strip", packageName = "strip" } = this.opts; if (typeof this.filename !== "string") { // This transformation only works when the filename is known return; @@ -80,6 +94,7 @@ export function plugin(): PluginObj { const resolvedPath = findNodeAddonForBindings(id, from); if (typeof resolvedPath === "string") { replaceWithRequireNodeAddon(p.parentPath, resolvedPath, { + packageName, pathSuffix, }); } @@ -89,7 +104,10 @@ export function plugin(): PluginObj { isNodeApiModule(path.join(from, id)) ) { const relativePath = path.join(from, id); - replaceWithRequireNodeAddon(p, relativePath, { pathSuffix }); + replaceWithRequireNodeAddon(p, relativePath, { + packageName, + pathSuffix, + }); } } }, diff --git a/packages/host/src/node/cli/apple.test.ts b/packages/host/src/node/cli/apple.test.ts new file mode 100644 index 00000000..797ce905 --- /dev/null +++ b/packages/host/src/node/cli/apple.test.ts @@ -0,0 +1,398 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import path from "node:path"; +import fs from "node:fs"; +import cp from "node:child_process"; + +import { + linkFlatFramework, + readAndParsePlist, + readFrameworkInfo, + readXcframeworkInfo, + restoreVersionedFrameworkSymlinks, +} from "./apple"; +import { setupTempDirectory } from "../test-utils"; + +describe("apple", { skip: process.platform !== "darwin" }, () => { + describe("readInfoPlist", () => { + it("should read Info.plist contents, plus extra keys not in schema", async (context) => { + const infoPlistContents = ` + + + + + CFBundleExecutable + ExecutableFileName + CFBundlePackageType + FMWK + CFBundleInfoDictionaryVersion + 6.0 + + + `; + const infoPlistSubPath = "Info.plist"; + const tempDirectoryPath = setupTempDirectory(context, { + [infoPlistSubPath]: infoPlistContents, + }); + const infoPlistPath = path.join(tempDirectoryPath, infoPlistSubPath); + + const contents = await readAndParsePlist(infoPlistPath); + assert.deepEqual(contents, { + CFBundleExecutable: "ExecutableFileName", + CFBundlePackageType: "FMWK", + CFBundleInfoDictionaryVersion: "6.0", + }); + }); + + it("should throw if Info.plist doesn't exist", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, {}); + const infoPlistPath = path.join(tempDirectoryPath, "Info.plist"); + + await assert.rejects( + () => readAndParsePlist(infoPlistPath), + /Expected an Info.plist/, + ); + }); + }); + + describe("readXcframeworkInfo", () => { + it("should read xcframework Info.plist contents, plus extra keys not in schema", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "foo.xcframework": { + "Info.plist": ` + + + + + AvailableLibraries + + + BinaryPath + hello.framework/hello + LibraryIdentifier + tvos-arm64 + LibraryPath + hello.framework + SupportedArchitectures + + arm64 + + SupportedPlatform + tvos + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + + `, + }, + }); + + const result = await readXcframeworkInfo( + path.join(tempDirectoryPath, "foo.xcframework", "Info.plist"), + ); + + assert.deepEqual(result, { + AvailableLibraries: [ + { + BinaryPath: "hello.framework/hello", + LibraryIdentifier: "tvos-arm64", + LibraryPath: "hello.framework", + SupportedArchitectures: ["arm64"], + SupportedPlatform: "tvos", + }, + ], + CFBundlePackageType: "XFWK", + XCFrameworkFormatVersion: "1.0", + }); + }); + }); + + describe("readFrameworkInfo", () => { + it("should read framework Info.plist contents, plus extra keys not in schema", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "foo.framework": { + "Info.plist": ` + + + + + CFBundlePackageType + FMWK + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + example-0--hello + CFBundleIdentifier + example_0.hello + CFBundleSupportedPlatforms + + XRSimulator + + + + `, + }, + }); + + const result = await readFrameworkInfo( + path.join(tempDirectoryPath, "foo.framework", "Info.plist"), + ); + + assert.deepEqual(result, { + CFBundlePackageType: "FMWK", + CFBundleInfoDictionaryVersion: "6.0", + CFBundleExecutable: "example-0--hello", + CFBundleIdentifier: "example_0.hello", + CFBundleSupportedPlatforms: ["XRSimulator"], + }); + }); + }); + + describe("linkFlatFramework", () => { + it("updates an xml plist, preserving extra keys", async (context) => { + const infoPlistSubPath = "Info.plist"; + const tempDirectoryPath = setupTempDirectory(context, { + "foo.framework": { + [infoPlistSubPath]: ` + + + + + CFBundleExecutable + addon + CFBundlePackageType + FMWK + CFBundleInfoDictionaryVersion + 6.0 + MyExtraKey + MyExtraValue + + + `, + }, + }); + + // Create a dummy binary file + cp.spawnSync("clang", [ + "-dynamiclib", + "-o", + path.join(tempDirectoryPath, "foo.framework", "addon"), + "-xc", + "/dev/null", + ]); + + await linkFlatFramework({ + frameworkPath: path.join(tempDirectoryPath, "foo.framework"), + newLibraryName: "new-addon-name", + }); + + const contents = await fs.promises.readFile( + path.join( + tempDirectoryPath, + "new-addon-name.framework", + infoPlistSubPath, + ), + "utf-8", + ); + assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/); + assert.match( + contents, + /CFBundleExecutable<\/key>\s*new-addon-name<\/string>/, + ); + + // Assert the install name was updated correctly + const { stdout: otoolOutput } = cp.spawnSync( + "otool", + [ + "-L", + path.join( + tempDirectoryPath, + "new-addon-name.framework", + "new-addon-name", + ), + ], + { encoding: "utf-8" }, + ); + assert.match( + otoolOutput, + /@rpath\/new-addon-name.framework\/new-addon-name/, + ); + + // It should preserve extra keys + assert.match( + contents, + /MyExtraKey<\/key>\s*MyExtraValue<\/string>/, + ); + }); + + it("converts a binary plist to xml", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, {}); + await fs.promises.mkdir(path.join(tempDirectoryPath, "foo.framework")); + // Write a binary plist file + const binaryPlistContents = Buffer.from( + // Generated running "base64 -i " on a plist file from a framework in the node-examples package + "YnBsaXN0MDDfEBUBAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4cICEiIyQiJSYnJChfEBNCdWlsZE1hY2hpbmVPU0J1aWxkXxAZQ0ZCdW5kbGVEZXZlbG9wbWVudFJlZ2lvbl8QEkNGQnVuZGxlRXhlY3V0YWJsZV8QEkNGQnVuZGxlSWRlbnRpZmllcl8QHUNGQnVuZGxlSW5mb0RpY3Rpb25hcnlWZXJzaW9uXxATQ0ZCdW5kbGVQYWNrYWdlVHlwZV8QGkNGQnVuZGxlU2hvcnRWZXJzaW9uU3RyaW5nXxARQ0ZCdW5kbGVTaWduYXR1cmVfEBpDRkJ1bmRsZVN1cHBvcnRlZFBsYXRmb3Jtc18QD0NGQnVuZGxlVmVyc2lvbl8QFUNTUmVzb3VyY2VzRmlsZU1hcHBlZFpEVENvbXBpbGVyXxAPRFRQbGF0Zm9ybUJ1aWxkXkRUUGxhdGZvcm1OYW1lXxARRFRQbGF0Zm9ybVZlcnNpb25aRFRTREtCdWlsZFlEVFNES05hbWVXRFRYY29kZVxEVFhjb2RlQnVpbGRfEBBNaW5pbXVtT1NWZXJzaW9uXlVJRGV2aWNlRmFtaWx5VjI0RzIzMVdFbmdsaXNoVWFkZG9uXxAPZXhhbXBsZV82LmFkZG9uUzYuMFRGTVdLUzEuMFQ/Pz8/oR9fEA9pUGhvbmVTaW11bGF0b3IJXxAiY29tLmFwcGxlLmNvbXBpbGVycy5sbHZtLmNsYW5nLjFfMFYyMkMxNDZfEA9pcGhvbmVzaW11bGF0b3JUMTguMl8QE2lwaG9uZXNpbXVsYXRvcjE4LjJUMTYyMFgxNkM1MDMyYaEpEAEACAA1AEsAZwB8AJEAsQDHAOQA+AEVAScBPwFKAVwBawF/AYoBlAGcAakBvAHLAdIB2gHgAfIB9gH7Af8CBAIGAhgCGQI+AkUCVwJcAnICdwKAAoIAAAAAAAACAQAAAAAAAAAqAAAAAAAAAAAAAAAAAAAChA==", + "base64", + ); + await fs.promises.writeFile( + path.join(tempDirectoryPath, "foo.framework", "Info.plist"), + binaryPlistContents, + ); + + // Create a dummy binary file + cp.spawnSync("clang", [ + "-dynamiclib", + "-o", + path.join(tempDirectoryPath, "foo.framework", "addon"), + "-xc", + "/dev/null", + ]); + + await linkFlatFramework({ + frameworkPath: path.join(tempDirectoryPath, "foo.framework"), + newLibraryName: "new-addon-name", + }); + + const contents = await fs.promises.readFile( + path.join(tempDirectoryPath, "new-addon-name.framework", "Info.plist"), + "utf-8", + ); + assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/); + assert.match( + contents, + /CFBundleExecutable<\/key>\s*new-addon-name<\/string>/, + ); + }); + }); + + describe("restoreVersionedFrameworkSymlinks", () => { + it("restores a versioned framework", async (context) => { + const infoPlistContents = ` + + + + + CFBundlePackageType + FMWK + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + example-addon + + + `; + + const tempDirectoryPath = setupTempDirectory(context, { + "foo.framework": { + Versions: { + A: { + Resources: { + "Info.plist": infoPlistContents, + }, + "example-addon": "", + }, + }, + }, + }); + + const frameworkPath = path.join(tempDirectoryPath, "foo.framework"); + const currentVersionPath = path.join( + frameworkPath, + "Versions", + "Current", + ); + const binaryLinkPath = path.join(frameworkPath, "example-addon"); + const realBinaryPath = path.join( + frameworkPath, + "Versions", + "A", + "example-addon", + ); + + async function assertVersionedFramework() { + const currentStat = await fs.promises.lstat(currentVersionPath); + assert( + currentStat.isSymbolicLink(), + "Expected Current symlink to be restored", + ); + assert.equal( + await fs.promises.realpath(currentVersionPath), + path.join(frameworkPath, "Versions", "A"), + ); + + const binaryStat = await fs.promises.lstat(binaryLinkPath); + assert( + binaryStat.isSymbolicLink(), + "Expected binary symlink to be restored", + ); + assert.equal( + await fs.promises.realpath(binaryLinkPath), + realBinaryPath, + ); + } + + await restoreVersionedFrameworkSymlinks(frameworkPath); + await assertVersionedFramework(); + + // Calling again to expect a no-op + await restoreVersionedFrameworkSymlinks(frameworkPath); + await assertVersionedFramework(); + }); + + it("throws on a flat framework", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "foo.framework": { + "Info.plist": ` + + + + + CFBundlePackageType + FMWK + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + example-addon + + + `, + }, + }); + + const frameworkPath = path.join(tempDirectoryPath, "foo.framework"); + + await assert.rejects( + () => restoreVersionedFrameworkSymlinks(frameworkPath), + /Expected 'Versions' directory inside versioned framework/, + ); + }); + }); +}); + +describe("apple on non-darwin", { skip: process.platform === "darwin" }, () => { + it("throws", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + ["Info.plist"]: '', + }); + + await assert.rejects( + () => + linkFlatFramework({ + frameworkPath: path.join(tempDirectoryPath, "Info.plist"), + newLibraryName: "new-addon-name", + }), + (err) => { + assert(err instanceof Error); + assert.match( + err.message, + /Linking Apple addons are only supported on macOS/, + ); + return true; + }, + ); + }); +}); diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index a04405cb..31aa6779 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -1,9 +1,11 @@ import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs"; -import os from "node:os"; -import { spawn } from "bufout"; +import plist from "@expo/plist"; +import * as zod from "zod"; + +import { spawn } from "@react-native-node-api/cli-utils"; import { getLatestMtime, getLibraryName } from "../path-utils.js"; import { @@ -12,27 +14,318 @@ import { LinkModuleResult, } from "./link-modules.js"; -type UpdateInfoPlistOptions = { - filePath: string; - oldLibraryName: string; +/** + * Reads and parses a plist file, converting it to XML format if needed. + */ +export async function readAndParsePlist(plistPath: string): Promise { + assert(fs.existsSync(plistPath), `Expected an Info.plist: ${plistPath}`); + // Try reading the file to see if it is already in XML format + try { + const contents = await fs.promises.readFile(plistPath, "utf-8"); + if (contents.startsWith(", +) { + const infoPlistPath = path.join(xcframeworkPath, "Info.plist"); + const infoPlistXml = plist.build(info); + await fs.promises.writeFile(infoPlistPath, infoPlistXml, "utf-8"); +} + +const FrameworkInfoSchema = zod.looseObject({ + CFBundlePackageType: zod.literal("FMWK"), + CFBundleInfoDictionaryVersion: zod.literal("6.0"), + CFBundleExecutable: zod.string(), +}); + +export async function readFrameworkInfo(infoPlistPath: string) { + const infoPlist = await readAndParsePlist(infoPlistPath); + return FrameworkInfoSchema.parse(infoPlist); +} + +export async function writeFrameworkInfo( + infoPlistPath: string, + info: zod.infer, +) { + const infoPlistXml = plist.build(info); + await fs.promises.writeFile(infoPlistPath, infoPlistXml, "utf-8"); +} + +type LinkFrameworkOptions = { + frameworkPath: string; + debugSymbolsPath?: string; newLibraryName: string; }; +export async function linkFramework(options: LinkFrameworkOptions) { + const { frameworkPath } = options; + assert.equal( + process.platform, + "darwin", + "Linking Apple frameworks are only supported on macOS", + ); + assert( + fs.existsSync(frameworkPath), + `Expected framework at '${frameworkPath}'`, + ); + if (fs.existsSync(path.join(frameworkPath, "Versions"))) { + await linkVersionedFramework(options); + } else { + await linkFlatFramework(options); + } +} + +export async function linkFlatFramework({ + frameworkPath, + debugSymbolsPath, + newLibraryName, +}: LinkFrameworkOptions) { + assert.equal( + process.platform, + "darwin", + "Linking Apple addons are only supported on macOS", + ); + const frameworkInfoPath = path.join(frameworkPath, "Info.plist"); + const frameworkInfo = await readFrameworkInfo(frameworkInfoPath); + // Update install name + await spawn( + "install_name_tool", + [ + "-id", + `@rpath/${newLibraryName}.framework/${newLibraryName}`, + frameworkInfo.CFBundleExecutable, + ], + { + outputMode: "buffered", + cwd: frameworkPath, + }, + ); + await writeFrameworkInfo(frameworkInfoPath, { + ...frameworkInfo, + CFBundleExecutable: newLibraryName, + }); + + // Rename the actual binary + await fs.promises.rename( + path.join(frameworkPath, frameworkInfo.CFBundleExecutable), + path.join(frameworkPath, newLibraryName), + ); + // Rename the framework directory + const newFrameworkPath = path.join( + path.dirname(frameworkPath), + `${newLibraryName}.framework`, + ); + await fs.promises.rename(frameworkPath, newFrameworkPath); + + if (debugSymbolsPath) { + const frameworkDebugSymbolsPath = path.join( + debugSymbolsPath, + `${path.basename(frameworkPath)}.dSYM`, + ); + if (fs.existsSync(frameworkDebugSymbolsPath)) { + // Remove existing DWARF data + await fs.promises.rm(frameworkDebugSymbolsPath, { + recursive: true, + force: true, + }); + // Rebuild DWARF data + await spawn( + "dsymutil", + [ + path.join(newFrameworkPath, newLibraryName), + "-o", + path.join(debugSymbolsPath, newLibraryName + ".dSYM"), + ], + { + outputMode: "buffered", + }, + ); + } + } +} + +async function restoreSymlink(target: string, linkPath: string) { + if ( + !fs.existsSync(linkPath) && + fs.existsSync(path.resolve(path.dirname(linkPath), target)) + ) { + await fs.promises.symlink(target, linkPath); + } +} + +async function guessCurrentFrameworkVersion(frameworkPath: string) { + const versionsPath = path.join(frameworkPath, "Versions"); + assert( + fs.existsSync(versionsPath), + "Expected 'Versions' directory inside versioned framework", + ); + + const versionDirectoryEntries = await fs.promises.readdir(versionsPath, { + withFileTypes: true, + }); + const versions = versionDirectoryEntries + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + assert.equal( + versions.length, + 1, + `Expected exactly one directory in ${versionsPath}, found ${JSON.stringify(versions)}`, + ); + const [version] = versions; + return version; +} + /** - * Update the Info.plist file of an xcframework to use the new library name. + * NPM packages aren't preserving internal symlinks inside versioned frameworks. + * This function attempts to restore those. */ -export async function updateInfoPlist({ - filePath, - oldLibraryName, +export async function restoreVersionedFrameworkSymlinks(frameworkPath: string) { + const currentVersionName = await guessCurrentFrameworkVersion(frameworkPath); + const currentVersionPath = path.join(frameworkPath, "Versions", "Current"); + await restoreSymlink(currentVersionName, currentVersionPath); + await restoreSymlink( + "Versions/Current/Resources", + path.join(frameworkPath, "Resources"), + ); + await restoreSymlink( + "Versions/Current/Headers", + path.join(frameworkPath, "Headers"), + ); + + const { CFBundleExecutable: executableName } = await readFrameworkInfo( + path.join(currentVersionPath, "Resources", "Info.plist"), + ); + + await restoreSymlink( + path.join("Versions", "Current", executableName), + path.join(frameworkPath, executableName), + ); +} + +export async function linkVersionedFramework({ + frameworkPath, newLibraryName, -}: UpdateInfoPlistOptions) { - const infoPlistContents = await fs.promises.readFile(filePath, "utf-8"); - // TODO: Use a proper plist parser - const updatedContents = infoPlistContents.replaceAll( - oldLibraryName, +}: LinkFrameworkOptions) { + assert.equal( + process.platform, + "darwin", + "Linking Apple addons are only supported on macOS", + ); + + await restoreVersionedFrameworkSymlinks(frameworkPath); + + const frameworkInfoPath = path.join( + frameworkPath, + "Versions", + "Current", + "Resources", + "Info.plist", + ); + const frameworkInfo = await readFrameworkInfo(frameworkInfoPath); + // Update install name + await spawn( + "install_name_tool", + [ + "-id", + `@rpath/${newLibraryName}.framework/${newLibraryName}`, + frameworkInfo.CFBundleExecutable, + ], + { + outputMode: "buffered", + cwd: frameworkPath, + }, + ); + await writeFrameworkInfo(frameworkInfoPath, { + ...frameworkInfo, + CFBundleExecutable: newLibraryName, + }); + // Rename the actual binary + const existingBinaryPath = path.join( + frameworkPath, + frameworkInfo.CFBundleExecutable, + ); + const stat = await fs.promises.lstat(existingBinaryPath); + assert( + stat.isSymbolicLink(), + `Expected binary to be a symlink: ${existingBinaryPath}`, + ); + const realBinaryPath = await fs.promises.realpath(existingBinaryPath); + const newRealBinaryPath = path.join( + path.dirname(realBinaryPath), newLibraryName, ); - await fs.promises.writeFile(filePath, updatedContents, "utf-8"); + // Rename the real binary file + await fs.promises.rename(realBinaryPath, newRealBinaryPath); + // Remove the old binary symlink + await fs.promises.unlink(existingBinaryPath); + // Create a new symlink with the new name + const newBinarySymlinkTarget = path.join( + "Versions", + "Current", + newLibraryName, + ); + assert( + fs.existsSync(path.join(frameworkPath, newBinarySymlinkTarget)), + "Expected new binary to exist", + ); + await fs.promises.symlink( + newBinarySymlinkTarget, + path.join(frameworkPath, newLibraryName), + ); + + // Rename the framework directory + await fs.promises.rename( + frameworkPath, + path.join(path.dirname(frameworkPath), `${newLibraryName}.framework`), + ); } export async function linkXcframework({ @@ -41,123 +334,78 @@ export async function linkXcframework({ incremental, naming, }: LinkModuleOptions): Promise { + assert.equal( + process.platform, + "darwin", + "Linking Apple addons are only supported on macOS", + ); // Copy the xcframework to the output directory and rename the framework and binary const newLibraryName = getLibraryName(modulePath, naming); const outputPath = getLinkedModuleOutputPath(platform, modulePath, naming); - const tempPath = await fs.promises.mkdtemp( - path.join(os.tmpdir(), `react-native-node-api-${newLibraryName}-`), - ); - try { - if (incremental && fs.existsSync(outputPath)) { - const moduleModified = getLatestMtime(modulePath); - const outputModified = getLatestMtime(outputPath); - if (moduleModified < outputModified) { - return { - originalPath: modulePath, - libraryName: newLibraryName, - outputPath, - skipped: true, - }; - } - } - // Delete any existing xcframework (or xcodebuild will try to amend it) - await fs.promises.rm(outputPath, { recursive: true, force: true }); - await fs.promises.cp(modulePath, tempPath, { recursive: true }); - - // Following extracted function mimics `glob("*/*.framework/")` - function globFrameworkDirs( - startPath: string, - fn: (parentPath: string, name: string) => Promise, - ) { - return fs - .readdirSync(startPath, { withFileTypes: true }) - .filter((tripletEntry) => tripletEntry.isDirectory()) - .flatMap((tripletEntry) => { - const tripletPath = path.join(startPath, tripletEntry.name); - return fs - .readdirSync(tripletPath, { withFileTypes: true }) - .filter( - (frameworkEntry) => - frameworkEntry.isDirectory() && - path.extname(frameworkEntry.name) === ".framework", - ) - .flatMap( - async (frameworkEntry) => - await fn(tripletPath, frameworkEntry.name), - ); - }); + + if (incremental && fs.existsSync(outputPath)) { + const moduleModified = getLatestMtime(modulePath); + const outputModified = getLatestMtime(outputPath); + if (moduleModified < outputModified) { + return { + originalPath: modulePath, + libraryName: newLibraryName, + outputPath, + skipped: true, + }; } + } + // Delete any existing xcframework (or xcodebuild will try to amend it) + await fs.promises.rm(outputPath, { recursive: true, force: true }); + // Copy the existing xcframework to the output path + await fs.promises.cp(modulePath, outputPath, { + recursive: true, + verbatimSymlinks: true, + }); - const frameworkPaths = await Promise.all( - globFrameworkDirs(tempPath, async (tripletPath, frameworkEntryName) => { - const frameworkPath = path.join(tripletPath, frameworkEntryName); - const oldLibraryName = path.basename(frameworkEntryName, ".framework"); - const oldLibraryPath = path.join(frameworkPath, oldLibraryName); - const newFrameworkPath = path.join( - tripletPath, - `${newLibraryName}.framework`, - ); - const newLibraryPath = path.join(newFrameworkPath, newLibraryName); - assert( - fs.existsSync(oldLibraryPath), - `Expected a library at '${oldLibraryPath}'`, - ); - // Rename the library - await fs.promises.rename( - oldLibraryPath, - // Cannot use newLibraryPath here, because the framework isn't renamed yet - path.join(frameworkPath, newLibraryName), - ); - // Rename the framework - await fs.promises.rename(frameworkPath, newFrameworkPath); - // Expect the library in the new location - assert(fs.existsSync(newLibraryPath)); - // Update the binary - await spawn( - "install_name_tool", - [ - "-id", - `@rpath/${newLibraryName}.framework/${newLibraryName}`, - newLibraryPath, - ], - { - outputMode: "buffered", - }, - ); - // Update the Info.plist file for the framework - await updateInfoPlist({ - filePath: path.join(newFrameworkPath, "Info.plist"), - oldLibraryName, - newLibraryName, - }); - return newFrameworkPath; - }), - ); + const info = await readXcframeworkInfo(path.join(outputPath, "Info.plist")); - // Create a new xcframework from the renamed frameworks - await spawn( - "xcodebuild", - [ - "-create-xcframework", - ...frameworkPaths.flatMap((frameworkPath) => [ - "-framework", - frameworkPath, - ]), - "-output", + await Promise.all( + info.AvailableLibraries.map(async (framework) => { + const frameworkPath = path.join( outputPath, - ], - { - outputMode: "buffered", - }, - ); + framework.LibraryIdentifier, + framework.LibraryPath, + ); + await linkFramework({ + frameworkPath, + newLibraryName, + debugSymbolsPath: framework.DebugSymbolsPath + ? path.join( + outputPath, + framework.LibraryIdentifier, + framework.DebugSymbolsPath, + ) + : undefined, + }); + }), + ); - return { - originalPath: modulePath, - libraryName: newLibraryName, - outputPath, - skipped: false, - }; - } finally { - await fs.promises.rm(tempPath, { recursive: true, force: true }); - } + await writeXcframeworkInfo(outputPath, { + ...info, + AvailableLibraries: info.AvailableLibraries.map((library) => { + return { + ...library, + LibraryPath: `${newLibraryName}.framework`, + BinaryPath: `${newLibraryName}.framework/${newLibraryName}`, + }; + }), + }); + + // Delete any leftover "magic file" + await fs.promises.rm(path.join(outputPath, "react-native-node-api-module"), { + force: true, + }); + + return { + originalPath: modulePath, + libraryName: newLibraryName, + outputPath, + skipped: false, + }; } diff --git a/packages/host/src/node/cli/hermes.ts b/packages/host/src/node/cli/hermes.ts index 4fddebe6..4b41692c 100644 --- a/packages/host/src/node/cli/hermes.ts +++ b/packages/host/src/node/cli/hermes.ts @@ -2,17 +2,53 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { Command } from "@commander-js/extra-typings"; -import { spawn, SpawnFailure } from "bufout"; -import { oraPromise } from "ora"; -import { packageDirectorySync } from "pkg-dir"; +import { + chalk, + Command, + Option, + oraPromise, + spawn, + UsageError, + wrapAction, + prettyPath, +} from "@react-native-node-api/cli-utils"; +import { packageDirectory } from "pkg-dir"; +import { readPackage } from "read-pkg"; -import { prettyPath } from "../path-utils"; - -const HOST_PACKAGE_ROOT = path.resolve(__dirname, "../../.."); // FIXME: make this configurable with reasonable fallback before public release const HERMES_GIT_URL = "https://github.com/kraenhansen/hermes.git"; +const platformOption = new Option( + "--react-native-package ", + "The React Native package to vendor Hermes into", +).default("react-native"); + +type PatchJSIHeadersOptions = { + reactNativePath: string; + hermesJsiPath: string; + silent: boolean; +}; + +async function patchJsiHeaders({ + reactNativePath, + hermesJsiPath, + silent, +}: PatchJSIHeadersOptions) { + const reactNativeJsiPath = path.join(reactNativePath, "ReactCommon/jsi/jsi/"); + await oraPromise( + fs.promises.cp(hermesJsiPath, reactNativeJsiPath, { + recursive: true, + }), + { + text: `Copying JSI from patched Hermes to React Native`, + successText: "Copied JSI from patched Hermes to React Native", + failText: (err) => + `Failed to copy JSI from Hermes to React Native: ${err.message}`, + isEnabled: !silent, + }, + ); +} + export const command = new Command("vendor-hermes") .argument("[from]", "Path to a file inside the app package", process.cwd()) .option("--silent", "Don't print anything except the final path", false) @@ -21,12 +57,20 @@ export const command = new Command("vendor-hermes") "Don't check timestamps of input files to skip unnecessary rebuilds", false, ) - .action(async (from, { force, silent }) => { - try { - const appPackageRoot = packageDirectorySync({ cwd: from }); + .addOption(platformOption) + .action( + wrapAction(async (from, { force, silent, reactNativePackage }) => { + const appPackageRoot = await packageDirectory({ cwd: from }); assert(appPackageRoot, "Failed to find package root"); + + const { dependencies = {} } = await readPackage({ cwd: appPackageRoot }); + assert( + Object.keys(dependencies).includes(reactNativePackage), + `Expected app to have a dependency on the '${reactNativePackage}' package`, + ); + const reactNativePath = path.dirname( - require.resolve("react-native/package.json", { + require.resolve(reactNativePackage + "/package.json", { // Ensures we'll be patching the React Native package actually used by the app paths: [appPackageRoot], }), @@ -36,17 +80,17 @@ export const command = new Command("vendor-hermes") "sdks", ".hermesversion", ); + assert( + fs.existsSync(hermesVersionPath), + `Expected a file with a Hermes version at ${prettyPath(hermesVersionPath)}`, + ); + const hermesVersion = fs.readFileSync(hermesVersionPath, "utf8").trim(); if (!silent) { console.log(`Using Hermes version: ${hermesVersion}`); } - const reactNativeJsiPath = path.join( - reactNativePath, - "ReactCommon/jsi/jsi/", - ); - - const hermesPath = path.join(HOST_PACKAGE_ROOT, "hermes"); + const hermesPath = path.join(reactNativePath, "sdks", "node-api-hermes"); if (force && fs.existsSync(hermesPath)) { await oraPromise( fs.promises.rm(hermesPath, { recursive: true, force: true }), @@ -88,17 +132,12 @@ export const command = new Command("vendor-hermes") }, ); } catch (error) { - if (error instanceof SpawnFailure) { - error.flushOutput("both"); - console.error( - `\n๐Ÿ›‘ React Native uses the ${hermesVersion} tag and cloning our fork failed.`, - `Please see the Node-API package's peer dependency on "react-native" for supported versions.`, - ); - process.exitCode = 1; - return; - } else { - throw error; - } + throw new UsageError("Failed to clone custom Hermes", { + cause: error, + fix: { + instructions: `Check the network connection and ensure this ${chalk.bold("react-native")} version is supported by ${chalk.bold("react-native-node-api")}.`, + }, + }); } } const hermesJsiPath = path.join(hermesPath, "API/jsi/jsi"); @@ -107,25 +146,11 @@ export const command = new Command("vendor-hermes") fs.existsSync(hermesJsiPath), `Hermes JSI path does not exist: ${hermesJsiPath}`, ); - - await oraPromise( - fs.promises.cp(hermesJsiPath, reactNativeJsiPath, { - recursive: true, - }), - { - text: `Copying JSI from patched Hermes to React Native`, - successText: "Copied JSI from patched Hermes to React Native", - failText: (err) => - `Failed to copy JSI from Hermes to React Native: ${err.message}`, - isEnabled: !silent, - }, - ); + await patchJsiHeaders({ + reactNativePath, + hermesJsiPath, + silent, + }); console.log(hermesPath); - } catch (error) { - process.exitCode = 1; - if (error instanceof SpawnFailure) { - error.flushOutput("both"); - } - throw error; - } - }); + }), + ); diff --git a/packages/host/src/node/cli/link-modules.ts b/packages/host/src/node/cli/link-modules.ts index 6053fc68..a2d274f6 100644 --- a/packages/host/src/node/cli/link-modules.ts +++ b/packages/host/src/node/cli/link-modules.ts @@ -1,17 +1,21 @@ import path from "node:path"; import fs from "node:fs"; -import { SpawnFailure } from "bufout"; +import { + chalk, + SpawnFailure, + prettyPath, +} from "@react-native-node-api/cli-utils"; + import { findNodeApiModulePathsByDependency, getAutolinkPath, getLibraryName, - logModulePaths, + visualizeLibraryMap, NamingStrategy, PlatformName, - prettyPath, + getLibraryMap, } from "../path-utils"; -import chalk from "chalk"; export type ModuleLinker = ( options: LinkModuleOptions, @@ -75,9 +79,14 @@ export async function linkModules({ ), ); - if (hasDuplicateLibraryNames(absoluteModulePaths, naming)) { - logModulePaths(absoluteModulePaths, naming); - throw new Error("Found conflicting library names"); + const libraryMap = getLibraryMap(absoluteModulePaths, naming); + const duplicates = new Map( + Array.from(libraryMap.entries()).filter(([, paths]) => paths.length > 1), + ); + + if (duplicates.size > 0) { + const visualized = visualizeLibraryMap(duplicates); + throw new Error("Found conflicting library names:\n" + visualized); } return Promise.all( @@ -130,17 +139,6 @@ export async function pruneLinkedModules( ); } -export function hasDuplicateLibraryNames( - modulePaths: string[], - naming: NamingStrategy, -): boolean { - const libraryNames = modulePaths.map((modulePath) => { - return getLibraryName(modulePath, naming); - }); - const uniqueNames = new Set(libraryNames); - return uniqueNames.size !== libraryNames.length; -} - export function getLinkedModuleOutputPath( platform: PlatformName, modulePath: string, diff --git a/packages/host/src/node/cli/options.ts b/packages/host/src/node/cli/options.ts index 1f81f306..eb059b42 100644 --- a/packages/host/src/node/cli/options.ts +++ b/packages/host/src/node/cli/options.ts @@ -1,15 +1,28 @@ -import { Option } from "@commander-js/extra-typings"; +import { Option } from "@react-native-node-api/cli-utils"; -import { assertPathSuffix, PATH_SUFFIX_CHOICES } from "../path-utils"; +import { + assertLibraryNamingChoice, + LIBRARY_NAMING_CHOICES, +} from "../path-utils"; -const { NODE_API_PATH_SUFFIX } = process.env; +const { NODE_API_PACKAGE_NAME, NODE_API_PATH_SUFFIX } = process.env; +if (typeof NODE_API_PACKAGE_NAME === "string") { + assertLibraryNamingChoice(NODE_API_PACKAGE_NAME); +} if (typeof NODE_API_PATH_SUFFIX === "string") { - assertPathSuffix(NODE_API_PATH_SUFFIX); + assertLibraryNamingChoice(NODE_API_PATH_SUFFIX); } +export const packageNameOption = new Option( + "--package-name ", + "Controls how the package name is transformed into a library name", +) + .choices(LIBRARY_NAMING_CHOICES) + .default(NODE_API_PACKAGE_NAME || "strip"); + export const pathSuffixOption = new Option( "--path-suffix ", "Controls how the path of the addon inside a package is transformed into a library name", ) - .choices(PATH_SUFFIX_CHOICES) + .choices(LIBRARY_NAMING_CHOICES) .default(NODE_API_PATH_SUFFIX || "strip"); diff --git a/packages/host/src/node/cli/program.ts b/packages/host/src/node/cli/program.ts index 4747a23e..169ca85b 100644 --- a/packages/host/src/node/cli/program.ts +++ b/packages/host/src/node/cli/program.ts @@ -2,25 +2,29 @@ import assert from "node:assert/strict"; import path from "node:path"; import { EventEmitter } from "node:stream"; -import { Command } from "@commander-js/extra-typings"; -import { SpawnFailure } from "bufout"; -import chalk from "chalk"; -import { oraPromise } from "ora"; +import { + Command, + chalk, + SpawnFailure, + oraPromise, + wrapAction, + prettyPath, +} from "@react-native-node-api/cli-utils"; import { determineModuleContext, findNodeApiModulePathsByDependency, getAutolinkPath, getLibraryName, - logModulePaths, + visualizeLibraryMap, normalizeModulePath, PlatformName, PLATFORMS, - prettyPath, + getLibraryMap, } from "../path-utils"; import { command as vendorHermes } from "./hermes"; -import { pathSuffixOption } from "./options"; +import { packageNameOption, pathSuffixOption } from "./options"; import { linkModules, pruneLinkedModules, ModuleLinker } from "./link-modules"; import { linkXcframework } from "./apple"; import { linkAndroidDir } from "./android"; @@ -67,164 +71,181 @@ program ) .option("--android", "Link Android modules") .option("--apple", "Link Apple modules") + .addOption(packageNameOption) .addOption(pathSuffixOption) - .action(async (pathArg, { force, prune, pathSuffix, android, apple }) => { - console.log("Auto-linking Node-API modules from", chalk.dim(pathArg)); - const platforms: PlatformName[] = []; - if (android) { - platforms.push("android"); - } - if (apple) { - platforms.push("apple"); - } - - if (platforms.length === 0) { - console.error( - `No platform specified, pass one or more of:`, - ...PLATFORMS.map((platform) => chalk.bold(`\n --${platform}`)), - ); - process.exitCode = 1; - return; - } - - for (const platform of platforms) { - const platformDisplayName = getPlatformDisplayName(platform); - const platformOutputPath = getAutolinkPath(platform); - const modules = await oraPromise( - () => - linkModules({ - platform, - fromPath: path.resolve(pathArg), - incremental: !force, - naming: { pathSuffix }, - linker: getLinker(platform), - }), - { - text: `Linking ${platformDisplayName} Node-API modules into ${prettyPath( - platformOutputPath, - )}`, - successText: `Linked ${platformDisplayName} Node-API modules into ${prettyPath( - platformOutputPath, - )}`, - failText: (error) => - `Failed to link ${platformDisplayName} Node-API modules into ${prettyPath( - platformOutputPath, - )}: ${error.message}`, - }, - ); - - if (modules.length === 0) { - console.log("Found no Node-API modules ๐Ÿคท"); - } - - const failures = modules.filter((result) => "failure" in result); - const linked = modules.filter((result) => "outputPath" in result); + .action( + wrapAction( + async ( + pathArg, + { force, prune, pathSuffix, android, apple, packageName }, + ) => { + console.log("Auto-linking Node-API modules from", chalk.dim(pathArg)); + const platforms: PlatformName[] = []; + if (android) { + platforms.push("android"); + } + if (apple) { + platforms.push("apple"); + } - for (const { originalPath, outputPath, skipped } of linked) { - const prettyOutputPath = outputPath - ? "โ†’ " + prettyPath(path.basename(outputPath)) - : ""; - if (skipped) { - console.log( - chalk.greenBright("-"), - "Skipped", - prettyPath(originalPath), - prettyOutputPath, - "(up to date)", - ); - } else { - console.log( - chalk.greenBright("โšญ"), - "Linked", - prettyPath(originalPath), - prettyOutputPath, + if (platforms.length === 0) { + console.error( + `No platform specified, pass one or more of:`, + ...PLATFORMS.map((platform) => chalk.bold(`\n --${platform}`)), ); + process.exitCode = 1; + return; } - } - for (const { originalPath, failure } of failures) { - assert(failure instanceof SpawnFailure); - console.error( - "\n", - chalk.redBright("โœ–"), - "Failed to copy", - prettyPath(originalPath), - ); - console.error(failure.message); - failure.flushOutput("both"); - process.exitCode = 1; - } + for (const platform of platforms) { + const platformDisplayName = getPlatformDisplayName(platform); + const platformOutputPath = getAutolinkPath(platform); + const modules = await oraPromise( + () => + linkModules({ + platform, + fromPath: path.resolve(pathArg), + incremental: !force, + naming: { packageName, pathSuffix }, + linker: getLinker(platform), + }), + { + text: `Linking ${platformDisplayName} Node-API modules into ${prettyPath( + platformOutputPath, + )}`, + successText: `Linked ${platformDisplayName} Node-API modules into ${prettyPath( + platformOutputPath, + )}`, + failText: () => + `Failed to link ${platformDisplayName} Node-API modules into ${prettyPath( + platformOutputPath, + )}`, + }, + ); - if (prune) { - await pruneLinkedModules(platform, modules); - } - } - }); + if (modules.length === 0) { + console.log("Found no Node-API modules ๐Ÿคท"); + } + + const failures = modules.filter((result) => "failure" in result); + const linked = modules.filter((result) => "outputPath" in result); + + for (const { originalPath, outputPath, skipped } of linked) { + const prettyOutputPath = outputPath + ? "โ†’ " + prettyPath(path.basename(outputPath)) + : ""; + if (skipped) { + console.log( + chalk.greenBright("-"), + "Skipped", + prettyPath(originalPath), + prettyOutputPath, + "(up to date)", + ); + } else { + console.log( + chalk.greenBright("โšญ"), + "Linked", + prettyPath(originalPath), + prettyOutputPath, + ); + } + } + + for (const { originalPath, failure } of failures) { + assert(failure instanceof SpawnFailure); + console.error( + "\n", + chalk.redBright("โœ–"), + "Failed to copy", + prettyPath(originalPath), + ); + console.error(failure.message); + failure.flushOutput("both"); + process.exitCode = 1; + } + + if (prune) { + await pruneLinkedModules(platform, modules); + } + } + }, + ), + ); program .command("list") .description("Lists Node-API modules") .argument("[from-path]", "Some path inside the app package", process.cwd()) .option("--json", "Output as JSON", false) + .addOption(packageNameOption) .addOption(pathSuffixOption) - .action(async (fromArg, { json, pathSuffix }) => { - const rootPath = path.resolve(fromArg); - const dependencies = await findNodeApiModulePathsByDependency({ - fromPath: rootPath, - platform: PLATFORMS, - includeSelf: true, - }); - - if (json) { - console.log(JSON.stringify(dependencies, null, 2)); - } else { - const dependencyCount = Object.keys(dependencies).length; - const xframeworkCount = Object.values(dependencies).reduce( - (acc, { modulePaths }) => acc + modulePaths.length, - 0, - ); - console.log( - "Found", - chalk.greenBright(xframeworkCount), - "Node-API modules in", - chalk.greenBright(dependencyCount), - dependencyCount === 1 ? "package" : "packages", - "from", - prettyPath(rootPath), - ); - for (const [dependencyName, dependency] of Object.entries(dependencies)) { - console.log( - chalk.blueBright(dependencyName), - "โ†’", - prettyPath(dependency.path), + .action( + wrapAction(async (fromArg, { json, pathSuffix, packageName }) => { + const rootPath = path.resolve(fromArg); + const dependencies = await findNodeApiModulePathsByDependency({ + fromPath: rootPath, + platform: PLATFORMS, + includeSelf: true, + }); + + if (json) { + console.log(JSON.stringify(dependencies, null, 2)); + } else { + const dependencyCount = Object.keys(dependencies).length; + const xframeworkCount = Object.values(dependencies).reduce( + (acc, { modulePaths }) => acc + modulePaths.length, + 0, ); - logModulePaths( - dependency.modulePaths.map((p) => path.join(dependency.path, p)), - { pathSuffix }, + console.log( + "Found", + chalk.greenBright(xframeworkCount), + "Node-API modules in", + chalk.greenBright(dependencyCount), + dependencyCount === 1 ? "package" : "packages", + "from", + prettyPath(rootPath), ); + for (const [dependencyName, dependency] of Object.entries( + dependencies, + )) { + console.log( + "\n" + chalk.blueBright(dependencyName), + "โ†’", + prettyPath(dependency.path), + ); + const libraryMap = getLibraryMap( + dependency.modulePaths.map((p) => path.join(dependency.path, p)), + { packageName, pathSuffix }, + ); + console.log(visualizeLibraryMap(libraryMap)); + } } - } - }); + }), + ); program .command("info ") .description( "Utility to print, module path, the hash of a single Android library", ) + .addOption(packageNameOption) .addOption(pathSuffixOption) - .action((pathInput, { pathSuffix }) => { - const resolvedModulePath = path.resolve(pathInput); - const normalizedModulePath = normalizeModulePath(resolvedModulePath); - const { packageName, relativePath } = - determineModuleContext(resolvedModulePath); - const libraryName = getLibraryName(resolvedModulePath, { - pathSuffix, - }); - console.log({ - resolvedModulePath, - normalizedModulePath, - packageName, - relativePath, - libraryName, - }); - }); + .action( + wrapAction((pathInput, { pathSuffix, packageName }) => { + const resolvedModulePath = path.resolve(pathInput); + const normalizedModulePath = normalizeModulePath(resolvedModulePath); + const context = determineModuleContext(resolvedModulePath); + const libraryName = getLibraryName(resolvedModulePath, { + packageName, + pathSuffix, + }); + console.log({ + resolvedModulePath, + normalizedModulePath, + packageName: context.packageName, + relativePath: context.relativePath, + libraryName, + }); + }), + ); diff --git a/packages/host/src/node/cli/run.ts b/packages/host/src/node/cli/run.ts index ee4c5620..da5566fe 100644 --- a/packages/host/src/node/cli/run.ts +++ b/packages/host/src/node/cli/run.ts @@ -1,2 +1,3 @@ import { program } from "./program"; + program.parseAsync(process.argv).catch(console.error); diff --git a/packages/host/src/node/duplicates.ts b/packages/host/src/node/duplicates.ts deleted file mode 100644 index 5e8be7f2..00000000 --- a/packages/host/src/node/duplicates.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function findDuplicates(values: string[]) { - const seen = new Set(); - const duplicates = new Set(); - for (const value of values) { - if (seen.has(value)) { - duplicates.add(value); - } else { - seen.add(value); - } - } - return duplicates; -} diff --git a/packages/host/src/node/gradle.test.ts b/packages/host/src/node/gradle.test.ts index 45c6f8e0..e08ee16e 100644 --- a/packages/host/src/node/gradle.test.ts +++ b/packages/host/src/node/gradle.test.ts @@ -12,11 +12,11 @@ describe( // Skipping these tests by default, as they download a lot and takes a long time { skip: process.env.ENABLE_GRADLE_TESTS !== "true" }, () => { - describe("checkHermesOverride task", () => { + describe("linkNodeApiModules task", () => { it("should fail if REACT_NATIVE_OVERRIDE_HERMES_DIR is not set", () => { const { status, stdout, stderr } = cp.spawnSync( "sh", - ["gradlew", "react-native-node-api:checkHermesOverride"], + ["gradlew", "react-native-node-api:linkNodeApiModules"], { cwd: TEST_APP_ANDROID_PATH, env: { @@ -34,26 +34,29 @@ describe( ); assert.match( stderr, - /Run the following in your terminal, to clone Hermes and instruct React Native to use it/, + /Run the following in your Bash- or Zsh-compatible terminal, to clone Hermes and instruct React Native to use it/, ); assert.match( stderr, - /export REACT_NATIVE_OVERRIDE_HERMES_DIR=\$\(npx react-native-node-api vendor-hermes --silent --force\)/, + /export REACT_NATIVE_OVERRIDE_HERMES_DIR=\$\(npx react-native-node-api vendor-hermes --silent\)/, ); assert.match( stderr, /And follow this guide to build React Native from source/, ); }); - }); - describe("linkNodeApiModules task", () => { it("should call the CLI to autolink", () => { const { status, stdout, stderr } = cp.spawnSync( "sh", ["gradlew", "react-native-node-api:linkNodeApiModules"], { cwd: TEST_APP_ANDROID_PATH, + env: { + ...process.env, + // We're passing some directory which exists + REACT_NATIVE_OVERRIDE_HERMES_DIR: __dirname, + }, encoding: "utf-8", }, ); diff --git a/packages/host/src/node/index.ts b/packages/host/src/node/index.ts index 878eeb01..1c4c69a9 100644 --- a/packages/host/src/node/index.ts +++ b/packages/host/src/node/index.ts @@ -22,6 +22,7 @@ export { determineXCFrameworkFilename, } from "./prebuilds/apple.js"; -export { determineLibraryBasename, prettyPath } from "./path-utils.js"; - -export { weakNodeApiPath } from "./weak-node-api.js"; +export { + determineLibraryBasename, + dereferenceDirectory, +} from "./path-utils.js"; diff --git a/packages/host/src/node/path-utils.test.ts b/packages/host/src/node/path-utils.test.ts index cc6b307b..7a65147b 100644 --- a/packages/host/src/node/path-utils.test.ts +++ b/packages/host/src/node/path-utils.test.ts @@ -13,6 +13,7 @@ import { isNodeApiModule, stripExtension, findNodeApiModulePathsByDependency, + dereferenceDirectory, } from "./path-utils.js"; import { setupTempDirectory } from "./test-utils.js"; @@ -208,6 +209,7 @@ describe("getLibraryName", () => { }); assert.equal( getLibraryName(path.join(tempDirectoryPath, "addon"), { + packageName: "keep", pathSuffix: "keep", }), "my-package--addon", @@ -215,6 +217,7 @@ describe("getLibraryName", () => { assert.equal( getLibraryName(path.join(tempDirectoryPath, "sub-directory/addon"), { + packageName: "keep", pathSuffix: "keep", }), "my-package--sub-directory-addon", @@ -230,6 +233,7 @@ describe("getLibraryName", () => { }); assert.equal( getLibraryName(path.join(tempDirectoryPath, "addon"), { + packageName: "keep", pathSuffix: "strip", }), "my-package--addon", @@ -237,6 +241,7 @@ describe("getLibraryName", () => { assert.equal( getLibraryName(path.join(tempDirectoryPath, "sub-directory", "addon"), { + packageName: "keep", pathSuffix: "strip", }), "my-package--addon", @@ -252,6 +257,7 @@ describe("getLibraryName", () => { }); assert.equal( getLibraryName(path.join(tempDirectoryPath, "addon"), { + packageName: "keep", pathSuffix: "omit", }), "my-package", @@ -259,11 +265,54 @@ describe("getLibraryName", () => { assert.equal( getLibraryName(path.join(tempDirectoryPath, "sub-directory", "addon"), { + packageName: "keep", pathSuffix: "omit", }), "my-package", ); }); + + it("keeps and escapes scope from package name", (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "package.json": `{ "name": "@my-org/my-package" }`, + "addon.apple.node/addon.node": "// This is supposed to be a binary file", + }); + assert.equal( + getLibraryName(path.join(tempDirectoryPath, "addon"), { + packageName: "keep", + pathSuffix: "strip", + }), + "my-org__my-package--addon", + ); + }); + + it("strips scope from package name", (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "package.json": `{ "name": "@my-org/my-package" }`, + "addon.apple.node/addon.node": "// This is supposed to be a binary file", + }); + assert.equal( + getLibraryName(path.join(tempDirectoryPath, "addon"), { + packageName: "strip", + pathSuffix: "strip", + }), + "my-package--addon", + ); + }); + + it("omits scope from package name", (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "package.json": `{ "name": "@my-org/my-package" }`, + "addon.apple.node/addon.node": "// This is supposed to be a binary file", + }); + assert.equal( + getLibraryName(path.join(tempDirectoryPath, "addon"), { + packageName: "omit", + pathSuffix: "strip", + }), + "addon", + ); + }); }); describe("findPackageDependencyPaths", () => { @@ -502,3 +551,45 @@ describe("findNodeAddonForBindings()", () => { }); } }); + +describe("dereferenceDirectory", () => { + describe("when directory contains symlinks", () => { + it("should dereference symlinks", async (context) => { + // Create a temp directory with a symlink + const tempDir = setupTempDirectory(context, { + "original/file.txt": "Hello, world!", + }); + const originalPath = path.join(tempDir, "original"); + const symlinkPath = path.join(tempDir, "link"); + // Create a link to the original directory + fs.symlinkSync(originalPath, symlinkPath, "dir"); + // And an internal link + fs.symlinkSync( + path.join(originalPath, "file.txt"), + path.join(originalPath, "linked-file.txt"), + "file", + ); + + { + // Verify that outer link is no longer a link + const stat = await fs.promises.lstat(symlinkPath); + assert(stat.isSymbolicLink()); + } + + await dereferenceDirectory(symlinkPath); + + { + // Verify that outer link is no longer a link + const stat = await fs.promises.lstat(symlinkPath); + assert(!stat.isSymbolicLink()); + } + + // Verify that the internal link is still a link to a readable file + const internalLinkPath = path.join(symlinkPath, "linked-file.txt"); + const internalLinkStat = await fs.promises.lstat(internalLinkPath); + assert(internalLinkStat.isSymbolicLink()); + const content = await fs.promises.readFile(internalLinkPath, "utf8"); + assert.equal(content, "Hello, world!"); + }); + }); +}); diff --git a/packages/host/src/node/path-utils.ts b/packages/host/src/node/path-utils.ts index ffec330a..a2a954a6 100644 --- a/packages/host/src/node/path-utils.ts +++ b/packages/host/src/node/path-utils.ts @@ -1,12 +1,12 @@ import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs"; -import { findDuplicates } from "./duplicates"; -import chalk from "chalk"; import { packageDirectorySync } from "pkg-dir"; import { readPackageSync } from "read-pkg"; import { createRequire } from "node:module"; +import { chalk, prettyPath } from "@react-native-node-api/cli-utils"; + // TODO: Change to .apple.node export const PLATFORMS = ["android", "apple"] as const; export type PlatformName = "android" | "apple"; @@ -16,22 +16,33 @@ export const PLATFORM_EXTENSIONS = { apple: ".apple.node", } as const satisfies Record; -export type PlatformExtentions = (typeof PLATFORM_EXTENSIONS)[PlatformName]; +export type PlatformExtensions = (typeof PLATFORM_EXTENSIONS)[PlatformName]; -export const PATH_SUFFIX_CHOICES = ["strip", "keep", "omit"] as const; -export type PathSuffixChoice = (typeof PATH_SUFFIX_CHOICES)[number]; +export const LIBRARY_NAMING_CHOICES = ["strip", "keep", "omit"] as const; +export type LibraryNamingChoice = (typeof LIBRARY_NAMING_CHOICES)[number]; -export function assertPathSuffix( +export function assertLibraryNamingChoice( value: unknown, -): asserts value is PathSuffixChoice { +): asserts value is LibraryNamingChoice { assert(typeof value === "string", `Expected a string, got ${typeof value}`); assert( - (PATH_SUFFIX_CHOICES as readonly string[]).includes(value), - `Expected one of ${PATH_SUFFIX_CHOICES.join(", ")}`, + (LIBRARY_NAMING_CHOICES as readonly string[]).includes(value), + `Expected one of ${LIBRARY_NAMING_CHOICES.join(", ")}`, ); } export type NamingStrategy = { + /** + * Controls how the package name is transformed into a library name. + * The transformation is needed to disambiguate and avoid conflicts between addons with the same name (but in different sub-paths or packages). + * + * As an example, if the package name is `@my-org/my-pkg` and the path of the addon within the package is `build/Release/my-addon.node` (and `pathSuffix` is set to `"strip"`): + * - `"omit"`: Only the path within the package is used and the library name will be `my-addon`. + * - `"strip"`: Scope / org gets stripped and the library name will be `my-pkg--my-addon`. + * - `"keep"`: The org and name is kept and the library name will be `my-org--my-pkg--my-addon`. + */ + packageName: LibraryNamingChoice; + /** * Controls how the path of the addon inside a package is transformed into a library name. * The transformation is needed to disambiguate and avoid conflicts between addons with the same name (but in different sub-paths or packages). @@ -41,7 +52,7 @@ export type NamingStrategy = { * - `"strip"`: Path gets stripped to its basename and the library name will be `my-pkg--my-addon`. * - `"keep"`: The full path is kept and the library name will be `my-pkg--build-Release-my-addon`. */ - pathSuffix: PathSuffixChoice; + pathSuffix: LibraryNamingChoice; }; // Cache mapping package directory to package name across calls @@ -174,28 +185,71 @@ export function normalizeModulePath(modulePath: string) { } export function escapePath(modulePath: string) { - return modulePath.replace(/[^a-zA-Z0-9]/g, "-"); + return ( + modulePath + // Replace any non-alphanumeric character with a dash + .replace(/[^a-zA-Z0-9-_]/g, "-") + ); +} + +export function transformPackageName( + packageName: string, + strategy: LibraryNamingChoice, +) { + if (strategy === "omit") { + return ""; + } else if (packageName.startsWith("@")) { + const [first, ...rest] = packageName.split("/"); + assert(rest.length > 0, `Invalid scoped package name (${packageName})`); + if (strategy === "strip") { + return escapePath(rest.join("/")); + } else { + // Stripping away the @ and using double underscore to separate scope and name is common practice in other projects (like DefinitelyTyped) + return escapePath(`${first.replace(/^@/, "")}__${rest.join("/")}`); + } + } else { + return escapePath(packageName); + } +} + +export function transformPathSuffix( + relativePath: string, + strategy: LibraryNamingChoice, +) { + if (strategy === "omit") { + return ""; + } else if (strategy === "strip") { + return escapePath(path.basename(relativePath)); + } else { + return escapePath(relativePath.replaceAll(/[/\\]/g, "-")); + } } /** * Get the name of the library which will be used when the module is linked in. */ export function getLibraryName(modulePath: string, naming: NamingStrategy) { + assert( + naming.packageName !== "omit" || naming.pathSuffix !== "omit", + "Both packageName and pathSuffix cannot be 'omit' at the same time", + ); const { packageName, relativePath } = determineModuleContext(modulePath); - const escapedPackageName = escapePath(packageName); - return naming.pathSuffix === "omit" - ? escapedPackageName - : `${escapedPackageName}--${escapePath( - naming.pathSuffix === "strip" - ? path.basename(relativePath) - : relativePath, - )}`; -} - -export function prettyPath(p: string) { - return chalk.dim( - path.relative(process.cwd(), p) || chalk.italic("current directory"), + const transformedPackageName = transformPackageName( + packageName, + naming.packageName, ); + const transformedRelativePath = transformPathSuffix( + relativePath, + naming.pathSuffix, + ); + const parts = []; + if (transformedPackageName) { + parts.push(transformedPackageName); + } + if (transformedRelativePath) { + parts.push(transformedRelativePath); + } + return parts.join("--"); } export function resolvePackageRoot( @@ -211,32 +265,33 @@ export function resolvePackageRoot( } } -export function logModulePaths( - modulePaths: string[], - // TODO: Default to iterating and printing for all supported naming strategies - naming: NamingStrategy, -) { - const pathsPerName = new Map(); +/** + * Module paths per library name. + */ +export type LibraryMap = Map; + +export function getLibraryMap(modulePaths: string[], naming: NamingStrategy) { + const result = new Map(); for (const modulePath of modulePaths) { const libraryName = getLibraryName(modulePath, naming); - const existingPaths = pathsPerName.get(libraryName) ?? []; + const existingPaths = result.get(libraryName) ?? []; existingPaths.push(modulePath); - pathsPerName.set(libraryName, existingPaths); + result.set(libraryName, existingPaths); } + return result; +} - const allModulePaths = modulePaths.map((modulePath) => modulePath); - const duplicatePaths = findDuplicates(allModulePaths); - for (const [libraryName, modulePaths] of pathsPerName) { - console.log( +export function visualizeLibraryMap(libraryMap: LibraryMap) { + const result = []; + for (const [libraryName, modulePaths] of libraryMap) { + result.push( chalk.greenBright(`${libraryName}`), ...modulePaths.flatMap((modulePath) => { - const line = duplicatePaths.has(modulePath) - ? chalk.redBright(prettyPath(modulePath)) - : prettyPath(modulePath); - return `\n โ†ณ ${line}`; + return ` โ†ณ ${prettyPath(modulePath)}`; }), ); } + return result.join("\n"); } /** @@ -466,11 +521,17 @@ export function getLatestMtime(fromPath: string): number { // https://github.com/TooTallNate/node-bindings/blob/v1.3.0/bindings.js#L21 const nodeBindingsSubdirs = [ "./", + "./build/MinSizeRel", + "./build/RelWithDebInfo", "./build/Release", "./build/Debug", "./build", + "./out/MinSizeRel", + "./out/RelWithDebInfo", "./out/Release", "./out/Debug", + "./MinSizeRel", + "./RelWithDebInfo", "./Release", "./Debug", ]; @@ -487,3 +548,18 @@ export function findNodeAddonForBindings(id: string, fromDir: string) { } return undefined; } + +export async function dereferenceDirectory(dirPath: string) { + const tempPath = dirPath + ".tmp"; + const stat = await fs.promises.lstat(dirPath); + assert(stat.isSymbolicLink(), `Expected a symbolic link at: ${dirPath}`); + // Move the existing framework out of the way + await fs.promises.rename(dirPath, tempPath); + // Only dereference the symlink at tempPath (not recursively) + const realPath = await fs.promises.realpath(tempPath); + await fs.promises.cp(realPath, dirPath, { + recursive: true, + verbatimSymlinks: true, + }); + await fs.promises.unlink(tempPath); +} diff --git a/packages/host/src/node/prebuilds/android.ts b/packages/host/src/node/prebuilds/android.ts index b24b422b..ec26408b 100644 --- a/packages/host/src/node/prebuilds/android.ts +++ b/packages/host/src/node/prebuilds/android.ts @@ -32,24 +32,24 @@ export function determineAndroidLibsFilename(libraryPaths: string[]) { type AndroidLibsDirectoryOptions = { outputPath: string; - libraryPathByTriplet: Record; + libraries: { triplet: AndroidTriplet; libraryPath: string }[]; autoLink: boolean; }; export async function createAndroidLibsDirectory({ outputPath, - libraryPathByTriplet, + libraries, autoLink, }: AndroidLibsDirectoryOptions) { // Delete and recreate any existing output directory await fs.promises.rm(outputPath, { recursive: true, force: true }); await fs.promises.mkdir(outputPath, { recursive: true }); - for (const [triplet, libraryPath] of Object.entries(libraryPathByTriplet)) { + for (const { triplet, libraryPath } of libraries) { assert( fs.existsSync(libraryPath), `Library not found: ${libraryPath} for triplet ${triplet}`, ); - const arch = ANDROID_ARCHITECTURES[triplet as AndroidTriplet]; + const arch = ANDROID_ARCHITECTURES[triplet]; const archOutputPath = path.join(outputPath, arch); await fs.promises.mkdir(archOutputPath, { recursive: true }); // Strip the ".node" extension from the library name diff --git a/packages/host/src/node/prebuilds/apple.test.ts b/packages/host/src/node/prebuilds/apple.test.ts new file mode 100644 index 00000000..4139831e --- /dev/null +++ b/packages/host/src/node/prebuilds/apple.test.ts @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { escapeBundleIdentifier } from "./apple"; + +describe("escapeBundleIdentifier", () => { + it("escapes and passes through values as expected", () => { + assert.equal( + escapeBundleIdentifier("abc-def-123-789.-"), + "abc-def-123-789.-", + ); + assert.equal(escapeBundleIdentifier("abc_def"), "abc-def"); + assert.equal(escapeBundleIdentifier("abc\ndef"), "abc-def"); + assert.equal(escapeBundleIdentifier("\0abc"), "-abc"); + assert.equal(escapeBundleIdentifier("๐Ÿคท"), "--"); // An emoji takes up two chars + }); +}); diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index 9b7765fb..23f0848b 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -2,50 +2,41 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; -import cp from "node:child_process"; -import { spawn } from "bufout"; +import plist from "@expo/plist"; +import { spawn } from "@react-native-node-api/cli-utils"; -import { AppleTriplet } from "./triplets.js"; import { determineLibraryBasename } from "../path-utils.js"; -type AppleArchitecture = "arm64" | "x86_64" | "arm64;x86_64"; - -export const APPLE_ARCHITECTURES = { - "x86_64-apple-darwin": "x86_64", - "arm64-apple-darwin": "arm64", - "arm64;x86_64-apple-darwin": "arm64;x86_64", - "arm64-apple-ios": "arm64", - "arm64-apple-ios-sim": "arm64", - "arm64-apple-tvos": "arm64", - // "x86_64-apple-tvos": "x86_64", - "arm64-apple-tvos-sim": "arm64", - "arm64-apple-visionos": "arm64", - "arm64-apple-visionos-sim": "arm64", -} satisfies Record; - -export function createPlistContent(values: Record) { - return [ - '', - '', - '', - " ", - ...Object.entries(values).flatMap(([key, value]) => [ - ` ${key}`, - ` ${value}`, - ]), - " ", - "", - ].join("\n"); -} - type XCframeworkOptions = { frameworkPaths: string[]; outputPath: string; autoLink: boolean; }; -export function createAppleFramework(libraryPath: string) { +/** + * Escapes any input to match a CFBundleIdentifier + * See https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleidentifier + */ +export function escapeBundleIdentifier(input: string) { + return input.replace(/[^A-Za-z0-9-.]/g, "-"); +} + +type CreateAppleFrameworkOptions = { + libraryPath: string; + versioned?: boolean; + bundleIdentifier?: string; +}; + +export async function createAppleFramework({ + libraryPath, + versioned = false, + bundleIdentifier, +}: CreateAppleFrameworkOptions) { + if (versioned) { + // TODO: Add support for generating a Versions/Current/Resources/Info.plist convention framework + throw new Error("Creating versioned frameworks is not supported yet"); + } assert(fs.existsSync(libraryPath), `Library not found: ${libraryPath}`); // Write a info.plist file to the framework const libraryName = path.basename(libraryPath, path.extname(libraryPath)); @@ -54,16 +45,18 @@ export function createAppleFramework(libraryPath: string) { `${libraryName}.framework`, ); // Create the framework from scratch - fs.rmSync(frameworkPath, { recursive: true, force: true }); - fs.mkdirSync(frameworkPath); - fs.mkdirSync(path.join(frameworkPath, "Headers")); + await fs.promises.rm(frameworkPath, { recursive: true, force: true }); + await fs.promises.mkdir(frameworkPath); + await fs.promises.mkdir(path.join(frameworkPath, "Headers")); // Create an empty Info.plist file - fs.writeFileSync( + await fs.promises.writeFile( path.join(frameworkPath, "Info.plist"), - createPlistContent({ + plist.build({ CFBundleDevelopmentRegion: "en", CFBundleExecutable: libraryName, - CFBundleIdentifier: `com.callstackincubator.node-api.${libraryName}`, + CFBundleIdentifier: escapeBundleIdentifier( + bundleIdentifier ?? `com.callstackincubator.node-api.${libraryName}`, + ), CFBundleInfoDictionaryVersion: "6.0", CFBundleName: libraryName, CFBundlePackageType: "FMWK", @@ -75,13 +68,15 @@ export function createAppleFramework(libraryPath: string) { ); const newLibraryPath = path.join(frameworkPath, libraryName); // TODO: Consider copying the library instead of renaming it - fs.renameSync(libraryPath, newLibraryPath); + await fs.promises.rename(libraryPath, newLibraryPath); // Update the name of the library - cp.spawnSync("install_name_tool", [ - "-id", - `@rpath/${libraryName}.framework/${libraryName}`, - newLibraryPath, - ]); + await spawn( + "install_name_tool", + ["-id", `@rpath/${libraryName}.framework/${libraryName}`, newLibraryPath], + { + outputMode: "buffered", + }, + ); return frameworkPath; } @@ -105,7 +100,19 @@ export async function createXCframework({ "xcodebuild", [ "-create-xcframework", - ...frameworkPaths.flatMap((p) => ["-framework", p]), + ...frameworkPaths.flatMap((frameworkPath) => { + const debugSymbolPath = frameworkPath + ".dSYM"; + if (fs.existsSync(debugSymbolPath)) { + return [ + "-framework", + frameworkPath, + "-debug-symbols", + debugSymbolPath, + ]; + } else { + return ["-framework", frameworkPath]; + } + }), "-output", xcodeOutputPath, ], @@ -141,11 +148,17 @@ export function determineXCFrameworkFilename( } export async function createUniversalAppleLibrary(libraryPaths: string[]) { + assert( + libraryPaths.length > 0, + "Expected at least one library to create a universal library", + ); // Determine the output path const filenames = new Set(libraryPaths.map((p) => path.basename(p))); assert( filenames.size === 1, - "Expected all darwin libraries to have the same name", + `Expected libraries to have the same name, but got: ${[...filenames].join( + ", ", + )}`, ); const [filename] = filenames; const lipoParentPath = fs.realpathSync( diff --git a/packages/host/src/node/prebuilds/triplets.ts b/packages/host/src/node/prebuilds/triplets.ts index 7471b15b..1d361c0a 100644 --- a/packages/host/src/node/prebuilds/triplets.ts +++ b/packages/host/src/node/prebuilds/triplets.ts @@ -11,16 +11,25 @@ export const ANDROID_TRIPLETS = [ export type AndroidTriplet = (typeof ANDROID_TRIPLETS)[number]; export const APPLE_TRIPLETS = [ - "arm64;x86_64-apple-darwin", "x86_64-apple-darwin", "arm64-apple-darwin", + "arm64;x86_64-apple-darwin", + "arm64-apple-ios", + "x86_64-apple-ios-sim", "arm64-apple-ios-sim", + "arm64;x86_64-apple-ios-sim", + "arm64-apple-tvos", - "arm64-apple-tvos-sim", // "x86_64-apple-tvos", + "x86_64-apple-tvos-sim", + "arm64-apple-tvos-sim", + "arm64;x86_64-apple-tvos-sim", + "arm64-apple-visionos", + "x86_64-apple-visionos-sim", "arm64-apple-visionos-sim", + "arm64;x86_64-apple-visionos-sim", ] as const; export type AppleTriplet = (typeof APPLE_TRIPLETS)[number]; diff --git a/packages/host/src/node/test-utils.ts b/packages/host/src/node/test-utils.ts index fb7abaf5..78b4d9a1 100644 --- a/packages/host/src/node/test-utils.ts +++ b/packages/host/src/node/test-utils.ts @@ -25,7 +25,9 @@ export function setupTempDirectory(context: TestContext, files: FileMap) { ); context.after(() => { - fs.rmSync(tempDirectoryPath, { recursive: true, force: true }); + if (!process.env.KEEP_TEMP_DIRS) { + fs.rmSync(tempDirectoryPath, { recursive: true, force: true }); + } }); writeFiles(tempDirectoryPath, files); diff --git a/packages/host/src/node/weak-node-api.ts b/packages/host/src/node/weak-node-api.ts deleted file mode 100644 index 02e3befe..00000000 --- a/packages/host/src/node/weak-node-api.ts +++ /dev/null @@ -1,10 +0,0 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import path from "node:path"; - -export const weakNodeApiPath = path.resolve(__dirname, "../../weak-node-api"); - -assert( - fs.existsSync(weakNodeApiPath), - `Expected Weak Node API path to exist: ${weakNodeApiPath}`, -); diff --git a/packages/host/src/react-native/NativeNodeApiHost.ts b/packages/host/src/react-native/NativeNodeApiHost.ts deleted file mode 100644 index fdf04dc8..00000000 --- a/packages/host/src/react-native/NativeNodeApiHost.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { TurboModule } from "react-native"; -import { TurboModuleRegistry } from "react-native"; - -export interface Spec extends TurboModule { - requireNodeAddon(libraryName: string): void; -} - -export default TurboModuleRegistry.getEnforcing("NodeApiHost"); diff --git a/packages/host/src/react-native/index.ts b/packages/host/src/react-native/index.ts index c34ae8b7..0a077c75 100644 --- a/packages/host/src/react-native/index.ts +++ b/packages/host/src/react-native/index.ts @@ -1,3 +1,14 @@ -import native from "./NativeNodeApiHost"; +import { type TurboModule, TurboModuleRegistry } from "react-native"; -export const requireNodeAddon = native.requireNodeAddon.bind(native); +export interface Spec extends TurboModule { + requireNodeAddon(libraryName: string): T; +} + +const native = TurboModuleRegistry.getEnforcing("NodeApiHost"); + +/** + * Loads a native Node-API addon by filename. + */ +export function requireNodeAddon(libraryName: string): T { + return native.requireNodeAddon(libraryName); +} diff --git a/packages/host/tsconfig.node-scripts.json b/packages/host/tsconfig.node-scripts.json index 4e11d816..b3771a38 100644 --- a/packages/host/tsconfig.node-scripts.json +++ b/packages/host/tsconfig.node-scripts.json @@ -7,6 +7,11 @@ "rootDir": "scripts", "types": ["node"] }, - "include": ["scripts/**/*.ts", "types/**/*.d.ts"], - "exclude": [] + "include": ["scripts/**/*.mts", "types/**/*.d.ts"], + "exclude": [], + "references": [ + { + "path": "../weak-node-api/tsconfig.node.json" + } + ] } diff --git a/packages/host/tsconfig.node.json b/packages/host/tsconfig.node.json index bf847c8c..e0982db2 100644 --- a/packages/host/tsconfig.node.json +++ b/packages/host/tsconfig.node.json @@ -8,5 +8,10 @@ "types": ["node"] }, "include": ["src/node/**/*.ts", "types/**/*.d.ts"], - "exclude": ["**/*.test.ts"] + "exclude": ["**/*.test.ts"], + "references": [ + { + "path": "../weak-node-api/tsconfig.node.json" + } + ] } diff --git a/packages/host/types/node-api-headers/index.d.ts b/packages/host/types/node-api-headers/index.d.ts deleted file mode 100644 index dc6ab254..00000000 --- a/packages/host/types/node-api-headers/index.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -module "node-api-headers" { - type SymbolsPerInterface = { - js_native_api_symbols: string[]; - node_api_symbols: string[]; - }; - type Exported = { - include_dir: string; - def_paths: { - js_native_api_def: string; - node_api_def: string; - }; - symbols: { - v1: SymbolsPerInterface; - v2: SymbolsPerInterface; - v3: SymbolsPerInterface; - v4: SymbolsPerInterface; - v5: SymbolsPerInterface; - v6: SymbolsPerInterface; - v7: SymbolsPerInterface; - v8: SymbolsPerInterface; - v9: SymbolsPerInterface; - v10: SymbolsPerInterface; - }; - }; - export type NodeApiVersion = keyof Exported["symbols"]; - - const exported: Exported; - export = exported; -} diff --git a/packages/host/weak-node-api/CMakeLists.txt b/packages/host/weak-node-api/CMakeLists.txt deleted file mode 100644 index 49e9bf85..00000000 --- a/packages/host/weak-node-api/CMakeLists.txt +++ /dev/null @@ -1,25 +0,0 @@ -cmake_minimum_required(VERSION 3.15) -project(weak-node-api) - -add_library(${PROJECT_NAME} SHARED - weak_node_api.cpp - ${CMAKE_JS_SRC} -) - -# Stripping the prefix from the library name -# to make sure the name of the XCFramework will match the name of the library -if(APPLE) - set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") -endif() - -target_include_directories(${PROJECT_NAME} - PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include -) -target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) -target_compile_definitions(${PROJECT_NAME} PRIVATE NAPI_VERSION=8) - -target_compile_options(${PROJECT_NAME} PRIVATE - $<$:/W4 /WX> - $<$>:-Wall -Wextra -Werror> -) diff --git a/packages/node-addon-examples/.gitignore b/packages/node-addon-examples/.gitignore index d838da98..7470cb91 100644 --- a/packages/node-addon-examples/.gitignore +++ b/packages/node-addon-examples/.gitignore @@ -1 +1,2 @@ examples/ +build/ diff --git a/packages/node-addon-examples/CHANGELOG.md b/packages/node-addon-examples/CHANGELOG.md new file mode 100644 index 00000000..25a0bd9d --- /dev/null +++ b/packages/node-addon-examples/CHANGELOG.md @@ -0,0 +1,7 @@ +# @react-native-node-api/node-addon-examples + +## 0.1.1 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts ๐Ÿ™ˆ diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index adae7591..6063927b 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -1,7 +1,17 @@ { "name": "@react-native-node-api/node-addon-examples", + "version": "0.1.1", "type": "commonjs", "main": "dist/index.js", + "files": [ + "dist", + "examples/**/package.json", + "examples/**/*.js", + "tests/**/package.json", + "tests/**/*.js", + "**/*.apple.node/**", + "**/*.android.node/**" + ], "private": true, "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -11,7 +21,7 @@ }, "scripts": { "copy-examples": "tsx scripts/copy-examples.mts", - "gyp-to-cmake": "gyp-to-cmake .", + "gyp-to-cmake": "gyp-to-cmake --weak-node-api .", "build": "tsx scripts/build-examples.mts", "copy-and-build": "node --run copy-examples && node --run gyp-to-cmake && node --run build", "verify": "tsx scripts/verify-prebuilds.mts", @@ -20,7 +30,7 @@ }, "devDependencies": { "cmake-rn": "*", - "node-addon-examples": "github:nodejs/node-addon-examples#4213d4c9d07996ae68629c67926251e117f8e52a", + "node-addon-examples": "github:nodejs/node-addon-examples#4b7dd86a85644610e6de80154df9acac9329b509", "gyp-to-cmake": "*", "read-pkg": "^9.0.1" }, diff --git a/packages/node-addon-examples/scripts/build-examples.mts b/packages/node-addon-examples/scripts/build-examples.mts index fd590eb6..bc447e71 100644 --- a/packages/node-addon-examples/scripts/build-examples.mts +++ b/packages/node-addon-examples/scripts/build-examples.mts @@ -6,14 +6,9 @@ const projectDirectories = findCMakeProjects(); for (const projectDirectory of projectDirectories) { console.log(`Running "cmake-rn" in ${projectDirectory}`); - execSync( - "cmake-rn", - // "cmake-rn --android --apple", - // "cmake-rn --triplet aarch64-linux-android --triplet arm64-apple-ios-sim", - { - cwd: projectDirectory, - stdio: "inherit", - }, - ); + execSync("cmake-rn --configuration RelWithDebInfo", { + cwd: projectDirectory, + stdio: "inherit", + }); console.log(); } diff --git a/packages/node-addon-examples/tests/.gitignore b/packages/node-addon-examples/tests/.gitignore deleted file mode 100644 index 378eac25..00000000 --- a/packages/node-addon-examples/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt index 31d513c0..67e5448b 100644 --- a/packages/node-addon-examples/tests/async/CMakeLists.txt +++ b/packages/node-addon-examples/tests/async/CMakeLists.txt @@ -1,15 +1,26 @@ -cmake_minimum_required(VERSION 3.15) -project(tests-async) +cmake_minimum_required(VERSION 3.15...3.31) +project(async-test) -add_compile_definitions(NAPI_VERSION=8) +find_package(weak-node-api REQUIRED CONFIG) -add_library(addon SHARED addon.c ${CMAKE_JS_SRC}) -set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node") -target_include_directories(addon PRIVATE ${CMAKE_JS_INC}) -target_link_libraries(addon PRIVATE ${CMAKE_JS_LIB}) -target_compile_features(addon PRIVATE cxx_std_17) +add_library(addon SHARED addon.c) -if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) - # Generate node.lib - execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) -endif() \ No newline at end of file +option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) + +if(APPLE AND BUILD_APPLE_FRAMEWORK) + set_target_properties(addon PROPERTIES + FRAMEWORK TRUE + MACOSX_FRAMEWORK_IDENTIFIER async-test.addon + MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 + MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 + XCODE_ATTRIBUTE_SKIP_INSTALL NO + ) +else() + set_target_properties(addon PROPERTIES + PREFIX "" + SUFFIX .node + ) +endif() + +target_link_libraries(addon PRIVATE weak-node-api) +target_compile_features(addon PRIVATE cxx_std_17) \ No newline at end of file diff --git a/packages/node-addon-examples/tests/buffers/CMakeLists.txt b/packages/node-addon-examples/tests/buffers/CMakeLists.txt index 8e8cc950..da615db2 100644 --- a/packages/node-addon-examples/tests/buffers/CMakeLists.txt +++ b/packages/node-addon-examples/tests/buffers/CMakeLists.txt @@ -1,15 +1,26 @@ -cmake_minimum_required(VERSION 3.15) -project(tests-buffers) +cmake_minimum_required(VERSION 3.15...3.31) +project(buffers-test) -add_compile_definitions(NAPI_VERSION=8) +find_package(weak-node-api REQUIRED CONFIG) -add_library(addon SHARED addon.c ${CMAKE_JS_SRC}) -set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node") -target_include_directories(addon PRIVATE ${CMAKE_JS_INC}) -target_link_libraries(addon PRIVATE ${CMAKE_JS_LIB}) -target_compile_features(addon PRIVATE cxx_std_17) +add_library(addon SHARED addon.c) -if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) - # Generate node.lib - execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) -endif() \ No newline at end of file +option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) + +if(APPLE AND BUILD_APPLE_FRAMEWORK) + set_target_properties(addon PROPERTIES + FRAMEWORK TRUE + MACOSX_FRAMEWORK_IDENTIFIER buffers-test.addon + MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 + MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 + XCODE_ATTRIBUTE_SKIP_INSTALL NO + ) +else() + set_target_properties(addon PROPERTIES + PREFIX "" + SUFFIX .node + ) +endif() + +target_link_libraries(addon PRIVATE weak-node-api) +target_compile_features(addon PRIVATE cxx_std_17) \ No newline at end of file diff --git a/packages/node-tests/CHANGELOG.md b/packages/node-tests/CHANGELOG.md new file mode 100644 index 00000000..4c05ad01 --- /dev/null +++ b/packages/node-tests/CHANGELOG.md @@ -0,0 +1,7 @@ +# @react-native-node-api/node-tests + +## 0.1.1 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts ๐Ÿ™ˆ diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json index c708c5ee..642432ee 100644 --- a/packages/node-tests/package.json +++ b/packages/node-tests/package.json @@ -1,8 +1,15 @@ { "name": "@react-native-node-api/node-tests", + "version": "0.1.1", "description": "Harness for running the Node.js tests from https://github.com/nodejs/node/tree/main/test", "type": "commonjs", "main": "tests.generated.js", + "files": [ + "dist", + "tests/**/*.js", + "**/*.apple.node/**", + "**/*.android.node/**" + ], "private": true, "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -21,8 +28,7 @@ "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", - "prebuildify": "^6.0.1", - "react-native-node-api": "^0.5.0", + "react-native-node-api": "^1.0.1", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } diff --git a/packages/node-tests/rolldown.config.mts b/packages/node-tests/rolldown.config.mts index c1b7f327..884b18c7 100644 --- a/packages/node-tests/rolldown.config.mts +++ b/packages/node-tests/rolldown.config.mts @@ -64,6 +64,18 @@ function testSuiteConfig(suitePath: string): RolldownOptions[] { delimiters: ["", ""], }, ), + replacePlugin( + { + // Replace the default export to return a function instead of initializing the addon immediately + // This allows the test runner to intercept any errors which would normally be thrown when importing + // to work around Metro's `guardedLoadModule` swallowing errors during module initialization + // See https://github.com/facebook/metro/blob/34bb8913ec4b5b02690b39d2246599faf094f721/packages/metro-runtime/src/polyfills/require.js#L348-L353 + "export default require_test();": "export default require_test;", + }, + { + delimiters: ["", ""], + }, + ), aliasPlugin({ entries: [ { diff --git a/packages/node-tests/scripts/build-tests.mts b/packages/node-tests/scripts/build-tests.mts index de879e8c..1c38cfb8 100644 --- a/packages/node-tests/scripts/build-tests.mts +++ b/packages/node-tests/scripts/build-tests.mts @@ -1,5 +1,5 @@ import path from "node:path"; -import { spawnSync } from "node:child_process"; +import { execSync } from "node:child_process"; import { findCMakeProjects } from "./utils.mjs"; @@ -13,5 +13,8 @@ for (const projectPath of projectPaths) { projectPath, )} to build for React Native`, ); - spawnSync("cmake-rn", [], { cwd: projectPath, stdio: "inherit" }); + execSync("cmake-rn --cmake-js", { + cwd: projectPath, + stdio: "inherit", + }); } diff --git a/packages/node-tests/scripts/generate-entrypoint.mts b/packages/node-tests/scripts/generate-entrypoint.mts index a0cf8c8d..49e6478a 100644 --- a/packages/node-tests/scripts/generate-entrypoint.mts +++ b/packages/node-tests/scripts/generate-entrypoint.mts @@ -36,7 +36,7 @@ function suiteToString(suite: TestSuite, indent = 1): string { return Object.entries(suite) .map(([key, value]) => { if (typeof value === "string") { - return `${padding}"${key}": () => require("./${value}")`; + return `${padding}"${key}": require("./${value}")`; } else { return `${padding}"${key}": {\n${suiteToString( value, diff --git a/packages/weak-node-api/.gitignore b/packages/weak-node-api/.gitignore new file mode 100644 index 00000000..83e62995 --- /dev/null +++ b/packages/weak-node-api/.gitignore @@ -0,0 +1,14 @@ + +# Everything in weak-node-api is generated, except for the configurations +# Generated and built via `npm run bootstrap` +/build/ +/build-tests/ +/*.xcframework +/*.android.node +/generated/ + +# Copied from node-api-headers by scripts/copy-node-api-headers.ts +/include/ + +# Clang cache +/.cache/ diff --git a/packages/weak-node-api/CHANGELOG.md b/packages/weak-node-api/CHANGELOG.md new file mode 100644 index 00000000..90dec646 --- /dev/null +++ b/packages/weak-node-api/CHANGELOG.md @@ -0,0 +1,26 @@ +# weak-node-api + +## 0.1.1 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts ๐Ÿ™ˆ + +## 0.1.0 + +### Minor Changes + +- 3d2e03e: Renamed WeakNodeApiHost to NodeApiHost + +## 0.0.3 + +### Patch Changes + +- 7ff2c2b: Fix minor package issues. +- 7ff2c2b: Add missing "generated" directory + +## 0.0.2 + +### Patch Changes + +- 60fae96: Initial release! diff --git a/packages/weak-node-api/CMakeLists.txt b/packages/weak-node-api/CMakeLists.txt new file mode 100644 index 00000000..90525434 --- /dev/null +++ b/packages/weak-node-api/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 3.19) +project(weak-node-api) + +# Read version from package.json +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/package.json" PACKAGE_JSON) +string(JSON PACKAGE_VERSION GET ${PACKAGE_JSON} version) + +add_library(${PROJECT_NAME} SHARED) + +set(INCLUDE_DIR "include") +set(GENERATED_SOURCE_DIR "generated") + +target_sources(${PROJECT_NAME} + PUBLIC + ${GENERATED_SOURCE_DIR}/weak_node_api.cpp + PUBLIC FILE_SET HEADERS + BASE_DIRS ${GENERATED_SOURCE_DIR} ${INCLUDE_DIR} FILES + ${GENERATED_SOURCE_DIR}/weak_node_api.hpp + ${GENERATED_SOURCE_DIR}/NodeApiHost.hpp + ${INCLUDE_DIR}/js_native_api_types.h + ${INCLUDE_DIR}/js_native_api.h + ${INCLUDE_DIR}/node_api_types.h + ${INCLUDE_DIR}/node_api.h +) + +get_target_property(PUBLIC_HEADER_FILES ${PROJECT_NAME} HEADER_SET) + +# Stripping the prefix from the library name +# to make sure the name of the XCFramework will match the name of the library +if(APPLE) + set_target_properties(${PROJECT_NAME} PROPERTIES + FRAMEWORK TRUE + MACOSX_FRAMEWORK_IDENTIFIER com.callstack.${PROJECT_NAME} + MACOSX_FRAMEWORK_SHORT_VERSION_STRING ${PACKAGE_VERSION} + MACOSX_FRAMEWORK_BUNDLE_VERSION ${PACKAGE_VERSION} + VERSION ${PACKAGE_VERSION} + XCODE_ATTRIBUTE_SKIP_INSTALL NO + PUBLIC_HEADER "${PUBLIC_HEADER_FILES}" + ) +endif() + +# C++20 is needed to use designated initializers +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_20) +target_compile_definitions(${PROJECT_NAME} PRIVATE NAPI_VERSION=8) + +target_compile_options(${PROJECT_NAME} PRIVATE + $<$:/W4 /WX> + $<$>:-Wall -Wextra -Werror> +) + +option(BUILD_TESTS "Build the tests" OFF) +if(BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/packages/weak-node-api/README.md b/packages/weak-node-api/README.md new file mode 100644 index 00000000..073c9121 --- /dev/null +++ b/packages/weak-node-api/README.md @@ -0,0 +1,19 @@ +# Weak Node-API + +A clean linkable interface for Node-API and with runtime-injectable implementation. + +This package is part of the [Node-API for React Native](https://github.com/callstackincubator/react-native-node-api) project, which brings Node-API support to React Native applications. However, it can be used independently in any context where an indirect / weak Node-API implementation is needed. + +## Why is this needed? + +Android's dynamic linker restricts access to global symbolsโ€”dynamic libraries must explicitly declare dependencies as `DT_NEEDED` to access symbols. In the context of React Native, the Node-API implementation is split between Hermes and a host runtime, native addons built for Android would otherwise need to explicitly link against both - which is not ideal for multiple reasons. + +This library provides a solution by: + +- Exposing only Node-API functions without implementation +- Allowing runtime injection of the actual implementation by the host +- Eliminating the need for addons to suppress undefined symbol errors + +## Is this usable in the context of Node.js? + +While originally designed for React Native's split Node-API implementation, this approach could potentially be adapted for Node.js scenarios where addons need to link with undefined symbols allowed. Usage patterns and examples for Node.js contexts are being explored and this pattern could eventually be upstreamed to Node.js itself, benefiting the broader Node-API ecosystem. diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json new file mode 100644 index 00000000..fa557f28 --- /dev/null +++ b/packages/weak-node-api/package.json @@ -0,0 +1,73 @@ +{ + "name": "weak-node-api", + "version": "0.1.1", + "description": "A linkable and runtime-injectable Node-API", + "homepage": "https://github.com/callstackincubator/react-native-node-api", + "repository": { + "type": "git", + "url": "git+https://github.com/callstackincubator/react-native-node-api.git", + "directory": "packages/weak-node-api" + }, + "type": "module", + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist", + "!dist/**/*.test.d.ts", + "!dist/**/*.test.d.ts.map", + "include", + "generated", + "build/Debug", + "build/Release", + "*.podspec", + "*.cmake" + ], + "scripts": { + "build": "tsc --build", + "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", + "generate": "tsx scripts/generate.ts", + "prebuild:prepare": "node --run copy-node-api-headers && node --run generate", + "prebuild:build": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension", + "prebuild:build:android": "node --run prebuild:build -- --android", + "prebuild:build:apple": "node --run prebuild:build -- --apple", + "prebuild:build:all": "node --run prebuild:build -- --android --apple", + "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", + "test:configure": "cmake -S . -B build-tests -DBUILD_TESTS=ON", + "test:build": "cmake --build build-tests", + "test:run": "ctest --test-dir build-tests --output-on-failure", + "bootstrap": "node --run prebuild:prepare && node --run prebuild:build", + "prerelease": "node --run prebuild:prepare && node --run prebuild:build:all" + }, + "keywords": [ + "react-native", + "napi", + "node-api", + "node-addon-api", + "native", + "addon", + "module", + "c", + "c++", + "bindings", + "buildtools", + "cmake" + ], + "author": { + "name": "Callstack", + "url": "https://github.com/callstackincubator" + }, + "contributors": [ + { + "name": "Krรฆn Hansen", + "url": "https://github.com/kraenhansen" + } + ], + "license": "MIT", + "dependencies": { + "node-api-headers": "^1.5.0" + }, + "devDependencies": { + "zod": "^4.1.11" + } +} diff --git a/packages/host/scripts/copy-node-api-headers.ts b/packages/weak-node-api/scripts/copy-node-api-headers.ts similarity index 61% rename from packages/host/scripts/copy-node-api-headers.ts rename to packages/weak-node-api/scripts/copy-node-api-headers.ts index 9632627e..8d90174b 100644 --- a/packages/host/scripts/copy-node-api-headers.ts +++ b/packages/weak-node-api/scripts/copy-node-api-headers.ts @@ -1,9 +1,11 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { include_dir as includeSourcePath } from "node-api-headers"; -const includeDestinationPath = path.join(__dirname, "../weak-node-api/include"); +import { nodeApiHeaders } from "../src/node-api-functions.js"; +const { include_dir: includeSourcePath } = nodeApiHeaders; + +const includeDestinationPath = path.join(import.meta.dirname, "../include"); assert(fs.existsSync(includeSourcePath), `Expected ${includeSourcePath}`); console.log(`Copying ${includeSourcePath} to ${includeDestinationPath}`); fs.cpSync(includeSourcePath, includeDestinationPath, { recursive: true }); diff --git a/packages/weak-node-api/scripts/generate.ts b/packages/weak-node-api/scripts/generate.ts new file mode 100644 index 00000000..fc21e485 --- /dev/null +++ b/packages/weak-node-api/scripts/generate.ts @@ -0,0 +1,95 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import cp from "node:child_process"; + +import { + FunctionDecl, + getNodeApiFunctions, +} from "../src/node-api-functions.js"; + +import * as weakNodeApiGenerator from "./generators/weak-node-api.js"; +import * as hostGenerator from "./generators/NodeApiHost.js"; + +export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); + +type GenerateFileOptions = { + functions: FunctionDecl[]; + fileName: string; + generator: (functions: FunctionDecl[]) => string; + headingComment?: string; +}; + +async function generateFile({ + functions, + fileName, + generator, + headingComment = "", +}: GenerateFileOptions) { + const generated = generator(functions); + const output = ` + /** + * @file ${fileName} + * ${headingComment + .trim() + .split("\n") + .map((l) => l.trim()) + .join("\n* ")} + * + * @note This file is generated - don't edit it directly + */ + + ${generated} + `; + const outputPath = path.join(OUTPUT_PATH, fileName); + await fs.promises.writeFile(outputPath, output.trim(), "utf-8"); + const { status, stderr = "No error output" } = cp.spawnSync( + "clang-format", + ["-i", outputPath], + { + encoding: "utf8", + }, + ); + assert.equal(status, 0, `Failed to format ${fileName}: ${stderr}`); +} + +async function run() { + await fs.promises.mkdir(OUTPUT_PATH, { recursive: true }); + + const functions = getNodeApiFunctions(); + await generateFile({ + functions, + fileName: "NodeApiHost.hpp", + generator: hostGenerator.generateHeader, + headingComment: ` + @brief NodeApiHost struct. + + This header provides a struct of Node-API functions implemented by a host to inject its implementations. + `, + }); + await generateFile({ + functions, + fileName: "weak_node_api.hpp", + generator: weakNodeApiGenerator.generateHeader, + headingComment: ` + @brief Weak Node-API host injection interface. + + This header provides the struct and injection function for deferring Node-API function calls from addons into a Node-API host. + `, + }); + await generateFile({ + functions, + fileName: "weak_node_api.cpp", + generator: weakNodeApiGenerator.generateSource, + headingComment: ` + @brief Weak Node-API host injection implementation. + + Provides the implementation for deferring Node-API function calls from addons into a Node-API host. + `, + }); +} + +run().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/packages/weak-node-api/scripts/generators/NodeApiHost.ts b/packages/weak-node-api/scripts/generators/NodeApiHost.ts new file mode 100644 index 00000000..c273cfe7 --- /dev/null +++ b/packages/weak-node-api/scripts/generators/NodeApiHost.ts @@ -0,0 +1,32 @@ +import type { FunctionDecl } from "../../src/node-api-functions.js"; + +export function generateFunctionDecl({ + returnType, + name, + argumentTypes, +}: FunctionDecl) { + return `${returnType} (*${name})(${argumentTypes.join(", ")});`; +} + +export function generateHeader(functions: FunctionDecl[]) { + return ` + #pragma once + + #include + + // Ideally we would have just used NAPI_NO_RETURN, but + // __declspec(noreturn) (when building with Microsoft Visual C++) cannot be used on members of a struct + // TODO: If we targeted C++23 we could use std::unreachable() + + #if defined(__GNUC__) + #define WEAK_NODE_API_UNREACHABLE __builtin_unreachable() + #else + #define WEAK_NODE_API_UNREACHABLE __assume(0) + #endif + + // Generate the struct of function pointers + struct NodeApiHost { + ${functions.map(generateFunctionDecl).join("\n")} + }; + `; +} diff --git a/packages/weak-node-api/scripts/generators/shared.ts b/packages/weak-node-api/scripts/generators/shared.ts new file mode 100644 index 00000000..88104e58 --- /dev/null +++ b/packages/weak-node-api/scripts/generators/shared.ts @@ -0,0 +1,28 @@ +import type { FunctionDecl } from "../../src/node-api-functions.js"; + +type FunctionOptions = FunctionDecl & { + extern?: true; + static?: true; + namespace?: string; + body?: string; + argumentNames?: string[]; +}; + +export function generateFunction({ + extern, + static: staticMember, + returnType, + namespace, + name, + argumentTypes, + argumentNames = [], + noReturn, + body, +}: FunctionOptions) { + return ` + ${staticMember ? "static " : ""}${extern ? 'extern "C" ' : ""}${returnType} ${namespace ? namespace + "::" : ""}${name}( + ${argumentTypes.map((type, index) => `${type} ` + (argumentNames[index] ?? `arg${index}`)).join(", ")} + ) ${body ? `{ ${body} ${noReturn ? "WEAK_NODE_API_UNREACHABLE;" : ""}\n}` : ""} + ; + `; +} diff --git a/packages/weak-node-api/scripts/generators/weak-node-api.ts b/packages/weak-node-api/scripts/generators/weak-node-api.ts new file mode 100644 index 00000000..62a1a026 --- /dev/null +++ b/packages/weak-node-api/scripts/generators/weak-node-api.ts @@ -0,0 +1,55 @@ +import type { FunctionDecl } from "../../src/node-api-functions.js"; +import { generateFunction } from "./shared.js"; + +export function generateHeader() { + return ` + #pragma once + + #include + #include // fprintf() + #include // abort() + + #include "NodeApiHost.hpp" + + typedef void(*InjectHostFunction)(const NodeApiHost&); + extern "C" void inject_weak_node_api_host(const NodeApiHost& host); + `; +} + +function generateFunctionImpl(fn: FunctionDecl) { + const { name, returnType, argumentTypes } = fn; + return generateFunction({ + ...fn, + extern: true, + body: ` + if (g_host.${name} == nullptr) { + fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n"); + abort(); + } + ${returnType === "void" ? "" : "return "} g_host.${name}( + ${argumentTypes.map((_, index) => `arg${index}`).join(", ")} + ); + `, + }); +} + +export function generateSource(functions: FunctionDecl[]) { + return ` + #include "weak_node_api.hpp" + + /** + * @brief Global instance of the injected Node-API host. + * + * This variable holds the function table for Node-API calls. + * It is set via inject_weak_node_api_host() before any Node-API function is dispatched. + * All Node-API calls are routed through this host. + */ + NodeApiHost g_host; + void inject_weak_node_api_host(const NodeApiHost& host) { + g_host = host; + }; + + // Generate function calling into the host + ${functions.map(generateFunctionImpl).join("\n")} + `; +} diff --git a/packages/weak-node-api/src/index.ts b/packages/weak-node-api/src/index.ts new file mode 100644 index 00000000..fbd5337a --- /dev/null +++ b/packages/weak-node-api/src/index.ts @@ -0,0 +1,2 @@ +export * from "./weak-node-api.js"; +export * from "./node-api-functions.js"; diff --git a/packages/host/scripts/node-api-functions.ts b/packages/weak-node-api/src/node-api-functions.ts similarity index 79% rename from packages/host/scripts/node-api-functions.ts rename to packages/weak-node-api/src/node-api-functions.ts index c554a053..f92bd956 100644 --- a/packages/host/scripts/node-api-functions.ts +++ b/packages/weak-node-api/src/node-api-functions.ts @@ -1,14 +1,39 @@ import assert from "node:assert/strict"; import path from "node:path"; import cp from "node:child_process"; - -import { - type NodeApiVersion, - symbols, - include_dir as nodeApiIncludePath, -} from "node-api-headers"; import { z } from "zod"; +import * as rawHeaders from "node-api-headers"; + +const SymbolsPerInterface = z.object({ + js_native_api_symbols: z.array(z.string()), + node_api_symbols: z.array(z.string()), +}); + +const NodeApiHeaders = z.object({ + include_dir: z.string(), + def_paths: z.object({ + js_native_api_def: z.string(), + node_api_def: z.string(), + }), + symbols: z.object({ + v1: SymbolsPerInterface, + v2: SymbolsPerInterface, + v3: SymbolsPerInterface, + v4: SymbolsPerInterface, + v5: SymbolsPerInterface, + v6: SymbolsPerInterface, + v7: SymbolsPerInterface, + v8: SymbolsPerInterface, + v9: SymbolsPerInterface, + v10: SymbolsPerInterface, + }), +}); + +type NodeApiVersion = keyof z.infer["symbols"]; + +export const nodeApiHeaders = NodeApiHeaders.parse(rawHeaders); + const clangAstDump = z.object({ kind: z.literal("TranslationUnitDecl"), inner: z.array( @@ -24,10 +49,6 @@ const clangAstDump = z.object({ ), }); -/** - * Generates source code for a version script for the given Node API version. - * @param version - */ export function getNodeApiHeaderAST(version: NodeApiVersion) { const output = cp.execFileSync( "clang", @@ -42,8 +63,8 @@ export function getNodeApiHeaderAST(version: NodeApiVersion) { // Parse and analyze the source file but not compile it "-fsyntax-only", // Include from the node-api-headers package - `-I${nodeApiIncludePath}`, - path.join(nodeApiIncludePath, "node_api.h"), + `-I${nodeApiHeaders.include_dir}`, + path.join(nodeApiHeaders.include_dir, "node_api.h"), ], { encoding: "utf-8", @@ -71,7 +92,7 @@ export function getNodeApiFunctions(version: NodeApiVersion = "v8") { assert(Array.isArray(root.inner)); const foundSymbols = new Set(); - const symbolsPerInterface = symbols[version]; + const symbolsPerInterface = nodeApiHeaders.symbols[version]; const engineSymbols = new Set(symbolsPerInterface.js_native_api_symbols); const runtimeSymbols = new Set(symbolsPerInterface.node_api_symbols); const allSymbols = new Set([...engineSymbols, ...runtimeSymbols]); diff --git a/packages/weak-node-api/src/restore-xcframework-symlinks.ts b/packages/weak-node-api/src/restore-xcframework-symlinks.ts new file mode 100644 index 00000000..575d9ce3 --- /dev/null +++ b/packages/weak-node-api/src/restore-xcframework-symlinks.ts @@ -0,0 +1,67 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +import { applePrebuildPath } from "./weak-node-api.js"; + +async function restoreSymlink(target: string, path: string) { + if (!fs.existsSync(path)) { + await fs.promises.symlink(target, path); + } +} + +async function guessCurrentFrameworkVersion(frameworkPath: string) { + const versionsPath = path.join(frameworkPath, "Versions"); + assert(fs.existsSync(versionsPath)); + + const versionDirectoryEntries = await fs.promises.readdir(versionsPath, { + withFileTypes: true, + }); + const versions = versionDirectoryEntries + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + assert.equal( + versions.length, + 1, + `Expected exactly one directory in ${versionsPath}, found ${JSON.stringify(versions)}`, + ); + const [version] = versions; + return version; +} + +async function restoreVersionedFrameworkSymlinks(frameworkPath: string) { + const currentVersionName = await guessCurrentFrameworkVersion(frameworkPath); + await restoreSymlink( + currentVersionName, + path.join(frameworkPath, "Versions", "Current"), + ); + await restoreSymlink( + "Versions/Current/weak-node-api", + path.join(frameworkPath, "weak-node-api"), + ); + await restoreSymlink( + "Versions/Current/Resources", + path.join(frameworkPath, "Resources"), + ); + await restoreSymlink( + "Versions/Current/Headers", + path.join(frameworkPath, "Headers"), + ); +} + +if (process.platform === "darwin") { + assert( + fs.existsSync(applePrebuildPath), + `Expected an Xcframework at ${applePrebuildPath}`, + ); + + const macosFrameworkPath = path.join( + applePrebuildPath, + "macos-arm64_x86_64", + "weak-node-api.framework", + ); + + if (fs.existsSync(macosFrameworkPath)) { + await restoreVersionedFrameworkSymlinks(macosFrameworkPath); + } +} diff --git a/packages/weak-node-api/src/weak-node-api.ts b/packages/weak-node-api/src/weak-node-api.ts new file mode 100644 index 00000000..87802461 --- /dev/null +++ b/packages/weak-node-api/src/weak-node-api.ts @@ -0,0 +1,26 @@ +import path from "node:path"; +import fs from "node:fs"; + +export const weakNodeApiPath = path.resolve(import.meta.dirname, ".."); + +const debugOutputPath = path.resolve(weakNodeApiPath, "build", "Debug"); +const releaseOutputPath = path.resolve(weakNodeApiPath, "build", "Release"); + +export const outputPath = fs.existsSync(debugOutputPath) + ? debugOutputPath + : releaseOutputPath; + +export const applePrebuildPath = path.resolve( + outputPath, + "weak-node-api.xcframework", +); + +export const androidPrebuildPath = path.resolve( + outputPath, + "weak-node-api.android.node", +); + +export const weakNodeApiCmakePath = path.resolve( + weakNodeApiPath, + "weak-node-api-config.cmake", +); diff --git a/packages/weak-node-api/tests/CMakeLists.txt b/packages/weak-node-api/tests/CMakeLists.txt new file mode 100644 index 00000000..89b19f84 --- /dev/null +++ b/packages/weak-node-api/tests/CMakeLists.txt @@ -0,0 +1,27 @@ +Include(FetchContent) + +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.11.0 +) + +FetchContent_MakeAvailable(Catch2) + +add_executable(weak-node-api-tests + test_inject.cpp +) +target_link_libraries(weak-node-api-tests + PRIVATE + weak-node-api + Catch2::Catch2WithMain +) + +target_compile_features(weak-node-api-tests PRIVATE cxx_std_20) +target_compile_definitions(weak-node-api-tests PRIVATE NAPI_VERSION=8) + +# As per https://github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md#catchcmake-and-catchaddtestscmake +list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) +include(CTest) +include(Catch) +catch_discover_tests(weak-node-api-tests) diff --git a/packages/weak-node-api/tests/test_inject.cpp b/packages/weak-node-api/tests/test_inject.cpp new file mode 100644 index 00000000..5b35f15e --- /dev/null +++ b/packages/weak-node-api/tests/test_inject.cpp @@ -0,0 +1,25 @@ +#include +#include + +TEST_CASE("inject_weak_node_api_host") { + SECTION("is callable") { + NodeApiHost host{}; + inject_weak_node_api_host(host); + } + + SECTION("propagates calls to napi_create_object") { + static bool called = false; + auto my_create_object = [](napi_env env, + napi_value *result) -> napi_status { + called = true; + return napi_status::napi_ok; + }; + NodeApiHost host{.napi_create_object = my_create_object}; + inject_weak_node_api_host(host); + + napi_value result; + napi_create_object({}, &result); + + REQUIRE(called); + } +} diff --git a/packages/weak-node-api/tsconfig.json b/packages/weak-node-api/tsconfig.json new file mode 100644 index 00000000..f08015f8 --- /dev/null +++ b/packages/weak-node-api/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true + }, + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.node-scripts.json" } + ] +} diff --git a/packages/weak-node-api/tsconfig.node-scripts.json b/packages/weak-node-api/tsconfig.node-scripts.json new file mode 100644 index 00000000..4bc586fa --- /dev/null +++ b/packages/weak-node-api/tsconfig.node-scripts.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "rootDir": "scripts", + "types": ["node"] + }, + "include": ["scripts/**/*.ts", "types/**/*.d.ts"], + "exclude": [], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/weak-node-api/tsconfig.node.json b/packages/weak-node-api/tsconfig.node.json new file mode 100644 index 00000000..0028899f --- /dev/null +++ b/packages/weak-node-api/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "composite": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*.ts", "types/**/*.d.ts"], + "exclude": ["**/*.test.ts"] +} diff --git a/packages/weak-node-api/types/node-api-headers/index.d.ts b/packages/weak-node-api/types/node-api-headers/index.d.ts new file mode 100644 index 00000000..4444171b --- /dev/null +++ b/packages/weak-node-api/types/node-api-headers/index.d.ts @@ -0,0 +1,4 @@ +module "node-api-headers" { + declare const exported: unknown; + export = exported; +} diff --git a/packages/weak-node-api/weak-node-api-config.cmake b/packages/weak-node-api/weak-node-api-config.cmake new file mode 100644 index 00000000..7d4d4a05 --- /dev/null +++ b/packages/weak-node-api/weak-node-api-config.cmake @@ -0,0 +1,39 @@ + +# Get the current file directory +get_filename_component(WEAK_NODE_API_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + +if(NOT DEFINED WEAK_NODE_API_LIB) + # Auto-detect library path for Android NDK builds + if(ANDROID) + # Define the library path pattern for Android + set(WEAK_NODE_API_LIB_PATH "weak-node-api.android.node/${ANDROID_ABI}/libweak-node-api.so") + + # Try Debug first, then Release using the packaged Android node structure + set(WEAK_NODE_API_LIB_DEBUG "${WEAK_NODE_API_CMAKE_DIR}/build/Debug/${WEAK_NODE_API_LIB_PATH}") + set(WEAK_NODE_API_LIB_RELEASE "${WEAK_NODE_API_CMAKE_DIR}/build/Release/${WEAK_NODE_API_LIB_PATH}") + + if(EXISTS "${WEAK_NODE_API_LIB_DEBUG}") + set(WEAK_NODE_API_LIB "${WEAK_NODE_API_LIB_DEBUG}") + message(STATUS "Using Debug weak-node-api library: ${WEAK_NODE_API_LIB}") + elseif(EXISTS "${WEAK_NODE_API_LIB_RELEASE}") + set(WEAK_NODE_API_LIB "${WEAK_NODE_API_LIB_RELEASE}") + message(STATUS "Using Release weak-node-api library: ${WEAK_NODE_API_LIB}") + else() + message(FATAL_ERROR "Could not find weak-node-api library for Android ABI ${ANDROID_ABI}. Expected at:\n ${WEAK_NODE_API_LIB_DEBUG}\n ${WEAK_NODE_API_LIB_RELEASE}") + endif() + else() + message(FATAL_ERROR "WEAK_NODE_API_LIB is not set") + endif() +endif() + +if(NOT DEFINED WEAK_NODE_API_INC) + set(WEAK_NODE_API_INC "${WEAK_NODE_API_CMAKE_DIR}/include;${WEAK_NODE_API_CMAKE_DIR}/generated") + message(STATUS "Using weak-node-api include directories: ${WEAK_NODE_API_INC}") +endif() + +add_library(weak-node-api SHARED IMPORTED) + +set_target_properties(weak-node-api PROPERTIES + IMPORTED_LOCATION "${WEAK_NODE_API_LIB}" + INTERFACE_INCLUDE_DIRECTORIES "${WEAK_NODE_API_INC}" +) diff --git a/packages/weak-node-api/weak-node-api.podspec b/packages/weak-node-api/weak-node-api.podspec new file mode 100644 index 00000000..26e69f74 --- /dev/null +++ b/packages/weak-node-api/weak-node-api.podspec @@ -0,0 +1,31 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +# We need to restore symlinks in the versioned framework directories, +# as these are not preserved when in the archive uploaded to NPM +unless defined?(@restored) + RESTORE_COMMAND = "node '#{File.join(__dir__, "dist/restore-xcframework-symlinks.js")}'" + Pod::UI.info("[weak-node-api] ".green + "Restoring symbolic links in Xcframework") + system(RESTORE_COMMAND) or raise "Failed to restore symlinks in Xcframework" + # Setting a flag to avoid running this command on every require + @restored = true +end + +Pod::Spec.new do |s| + s.name = package["name"] + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } + + s.source_files = "generated/*.hpp", "include/*.h" + s.public_header_files = "generated/*.hpp", "include/*.h" + s.vendored_frameworks = "build/*/weak-node-api.xcframework" + + # Avoiding the header dir to allow for idiomatic Node-API includes + s.header_dir = nil +end \ No newline at end of file diff --git a/scripts/depcheck.ts b/scripts/depcheck.ts new file mode 100644 index 00000000..a16097a5 --- /dev/null +++ b/scripts/depcheck.ts @@ -0,0 +1,81 @@ +import path from "node:path"; +import assert from "node:assert/strict"; +import cp from "node:child_process"; +import fs from "node:fs"; + +import depcheck from "depcheck"; + +function getWorkspaces() { + const workspaces = JSON.parse( + cp.execFileSync("npm", ["query", ".workspace"], { encoding: "utf8" }), + ) as unknown; + assert(Array.isArray(workspaces)); + for (const workspace of workspaces) { + assert(typeof workspace === "object" && workspace !== null); + } + return workspaces as Record[]; +} + +const rootDir = path.resolve(import.meta.dirname, ".."); +const root = await depcheck(rootDir, {}); + +const rootPackage = JSON.parse( + await fs.promises.readFile(path.join(rootDir, "package.json"), { + encoding: "utf8", + }), +) as unknown; + +assert( + typeof rootPackage === "object" && + rootPackage !== null && + "devDependencies" in rootPackage && + typeof rootPackage.devDependencies === "object" && + rootPackage.devDependencies !== null, +); + +const rootDevDependencies = new Set(Object.keys(rootPackage.devDependencies)); +for (const packageName of [...rootDevDependencies.values()]) { + rootDevDependencies.add(`@types/${packageName}`); +} + +for (const { + name: workspaceName, + path: workspacePath, + private: workspacePrivate, +} of getWorkspaces()) { + assert(typeof workspaceName === "string"); + assert(typeof workspacePath === "string"); + assert( + typeof workspacePrivate === "boolean" || + typeof workspacePrivate === "undefined", + ); + if (workspacePrivate) { + console.warn(`Skipping private package '${workspaceName}'`); + continue; + } + const result = await depcheck(workspacePath, { + ignoreMatches: [...rootDevDependencies], + }); + for (const [name, filePaths] of Object.entries(result.missing)) { + if (!rootDevDependencies.has(name)) { + console.error(`Missing '${name}' in '${workspaceName}':`); + for (const filePath of filePaths) { + console.error("โ†ณ", path.relative(workspacePath, filePath)); + } + console.error(); + process.exitCode = 1; + } + } + for (const name of result.dependencies) { + console.error(`Unused dependency '${name}' in '${workspaceName}'`); + console.error(); + process.exitCode = 1; + } + for (const name of result.devDependencies) { + console.error(`Unused dev-dependency '${name}' in '${workspaceName}'`); + console.error(); + process.exitCode = 1; + } +} + +assert.deepEqual(root.dependencies, [], "Found unused dependencies"); diff --git a/scripts/init-macos-test-app.ts b/scripts/init-macos-test-app.ts new file mode 100644 index 00000000..1f80b1a7 --- /dev/null +++ b/scripts/init-macos-test-app.ts @@ -0,0 +1,194 @@ +import assert from "node:assert/strict"; +import cp from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { readPackage } from "read-pkg"; + +const REACT_NATIVE_VERSION = "0.79.6"; +const ROOT_PATH = path.join(import.meta.dirname, ".."); +const APP_PATH = path.join(ROOT_PATH, "apps", "macos-test-app"); +const OTHER_APP_PATH = path.join(ROOT_PATH, "apps", "test-app"); + +function exec(command: string, args: string[], options: cp.SpawnOptions = {}) { + const { status } = cp.spawnSync(command, args, { + stdio: "inherit", + ...options, + }); + assert.equal(status, 0, `Failed to execute '${command}'`); +} + +async function deletePreviousApp() { + if (fs.existsSync(APP_PATH)) { + console.log("Deleting existing app directory"); + await fs.promises.rm(APP_PATH, { recursive: true, force: true }); + } +} + +async function initializeReactNativeTemplate() { + console.log("Initializing community template"); + exec("npx", [ + "@react-native-community/cli", + "init", + "MacOSTestApp", + "--skip-install", + "--skip-git-init", + // "--platform-name", + // "react-native-macos", + "--version", + REACT_NATIVE_VERSION, + "--directory", + APP_PATH, + ]); + + // Clean up + const CLEANUP_PATHS = [ + "ios", + "android", + "__tests__", + ".prettierrc.js", + ".gitignore", + ]; + + for (const cleanupPath of CLEANUP_PATHS) { + await fs.promises.rm(path.join(APP_PATH, cleanupPath), { + recursive: true, + force: true, + }); + } +} + +async function patchPackageJson() { + console.log("Patching package.json scripts"); + const packageJson = await readPackage({ cwd: APP_PATH }); + const otherPackageJson = await readPackage({ cwd: OTHER_APP_PATH }); + + packageJson.scripts = { + ...packageJson.scripts, + metro: "react-native start --reset-cache --no-interactive", + "mocha-and-metro": "mocha-remote --exit-on-error -- node --run metro", + premacos: "killall 'MacOSTestApp' || true", + macos: "react-native run-macos --no-packager", + test: "mocha-remote --exit-on-error -- concurrently --passthrough-arguments --kill-others-on-fail npm:metro 'npm:macos -- {@}' --", + "test:allTests": "MOCHA_REMOTE_CONTEXT=allTests node --run test -- ", + "test:nodeAddonExamples": + "MOCHA_REMOTE_CONTEXT=nodeAddonExamples node --run test -- ", + "test:nodeTests": "MOCHA_REMOTE_CONTEXT=nodeTests node --run test -- ", + "test:ferricExample": + "MOCHA_REMOTE_CONTEXT=ferricExample node --run test -- ", + }; + + const transferredDependencies = new Set([ + "@rnx-kit/metro-config", + "mocha-remote-cli", + "mocha-remote-react-native", + ]); + + const { dependencies: otherDependencies = {} } = otherPackageJson; + + packageJson.dependencies = { + ...packageJson.dependencies, + "react-native-macos-init": "^2.1.3", + "@react-native-node-api/node-addon-examples": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "node-addon-examples"), + ), + "@react-native-node-api/node-tests": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "node-tests"), + ), + "@react-native-node-api/ferric-example": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "ferric-example"), + ), + "react-native-node-api": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "host"), + ), + "weak-node-api": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "weak-node-api"), + ), + ...Object.fromEntries( + Object.entries(otherDependencies).filter(([name]) => + transferredDependencies.has(name), + ), + ), + }; + + await fs.promises.writeFile( + path.join(APP_PATH, "package.json"), + JSON.stringify(packageJson, null, 2), + "utf8", + ); +} + +function installDependencies() { + console.log("Installing dependencies"); + exec("npm", ["install", "--prefer-offline"], { + cwd: APP_PATH, + }); +} + +function initializeReactNativeMacOSTemplate() { + console.log("Initializing react-native-macos template"); + exec("npx", ["react-native-macos-init"], { + cwd: APP_PATH, + }); +} + +async function patchPodfile() { + console.log("Patching Podfile"); + const replacements = [ + [ + // As per https://github.com/microsoft/react-native-macos/issues/2723#issuecomment-3392930688 + "require_relative '../node_modules/react-native-macos/scripts/react_native_pods'\nrequire_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'", + "require_relative '../node_modules/react-native-macos/scripts/cocoapods/autolinking'", + ], + [ + ":hermes_enabled => false,", + // Adding the new_arch_enabled here as it's not a part of the template + ":hermes_enabled => true,\n :new_arch_enabled => true,", + ], + [ + ":fabric_enabled => ENV['RCT_NEW_ARCH_ENABLED'] == '1',", + ":fabric_enabled => true,", + ], + [ + "react_native_post_install(installer)", + "react_native_post_install(installer, '../node_modules/react-native-macos')", + ], + ]; + + const podfilePath = path.join(APP_PATH, "macos", "Podfile"); + let podfileContents = await fs.promises.readFile(podfilePath, "utf8"); + for (const [searchValue, replaceValue] of replacements) { + podfileContents = podfileContents.replace(searchValue, replaceValue); + } + await fs.promises.writeFile(podfilePath, podfileContents, "utf8"); +} + +async function copySourceFiles() { + console.log("Copying source files from test-app into macos-test-app:"); + const FILE_NAMES = [ + "App.tsx", + // Adds the babel plugin needed to transform require calls + "babel.config.js", + // Adds the ability to reference symlinked packages + "metro.config.js", + ]; + for (const fileName of FILE_NAMES) { + console.log(`โ†ณ ${fileName}`); + await fs.promises.copyFile( + path.join(OTHER_APP_PATH, fileName), + path.join(APP_PATH, fileName), + ); + } +} + +await deletePreviousApp(); +await initializeReactNativeTemplate(); +await patchPackageJson(); +installDependencies(); +initializeReactNativeMacOSTemplate(); +await patchPodfile(); +await copySourceFiles(); diff --git a/scripts/run-in-published.ts b/scripts/run-in-published.ts new file mode 100644 index 00000000..cc59253b --- /dev/null +++ b/scripts/run-in-published.ts @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import cp from "node:child_process"; + +console.log("Run command in all non-private packages of the monorepo"); + +function getWorkspaces() { + const workspaces = JSON.parse( + cp.execFileSync("npm", ["query", ".workspace"], { encoding: "utf8" }), + ) as unknown; + assert(Array.isArray(workspaces)); + for (const workspace of workspaces) { + assert(typeof workspace === "object" && workspace !== null); + } + return workspaces as Record[]; +} + +const publishedPackagePaths = getWorkspaces() + .filter((w) => !w.private) + .map((p) => { + assert(typeof p.path === "string"); + return p.path; + }); + +const [, , command, ...argv] = process.argv; + +for (const packagePath of publishedPackagePaths) { + const { status } = cp.spawnSync(command, argv, { + cwd: packagePath, + stdio: "inherit", + }); + assert.equal(status, 0, `Command failed (status = ${status})`); +} diff --git a/tsconfig.json b/tsconfig.json index 77eba035..2d49bdb1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,11 +6,16 @@ }, "files": ["prettier.config.js", "eslint.config.js"], "references": [ + { "path": "./tsconfig.scripts.json" }, + { "path": "./packages/cli-utils/tsconfig.json" }, + { "path": "./packages/cmake-file-api/tsconfig.json" }, + { "path": "./packages/cmake-file-api/tsconfig.tests.json" }, { "path": "./packages/host/tsconfig.json" }, { "path": "./packages/gyp-to-cmake/tsconfig.json" }, { "path": "./packages/cmake-rn/tsconfig.json" }, { "path": "./packages/ferric/tsconfig.json" }, { "path": "./packages/node-addon-examples/tsconfig.json" }, - { "path": "./packages/node-tests/tsconfig.json" } + { "path": "./packages/node-tests/tsconfig.json" }, + { "path": "./packages/weak-node-api/tsconfig.json" } ] } diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json new file mode 100644 index 00000000..88041106 --- /dev/null +++ b/tsconfig.scripts.json @@ -0,0 +1,10 @@ +{ + "extends": "./configs/tsconfig.node.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "declarationMap": false, + "rootDir": "scripts" + }, + "include": ["scripts"] +}