diff --git a/apps/test-app/App.tsx b/apps/test-app/App.tsx index 3fa20d0e..102e3c8c 100644 --- a/apps/test-app/App.tsx +++ b/apps/test-app/App.tsx @@ -9,6 +9,7 @@ import { } from "mocha-remote-react-native"; import { suites as nodeAddonExamplesSuites } from "@react-native-node-api/node-addon-examples"; +import { suites as nodeTestsSuites } from "@react-native-node-api/node-tests"; function describeIf( condition: boolean, @@ -21,12 +22,14 @@ function describeIf( type Context = { allTests?: boolean; nodeAddonExamples?: boolean; + nodeTests?: boolean; ferricExample?: boolean; }; function loadTests({ allTests = false, nodeAddonExamples = allTests, + nodeTests = allTests, ferricExample = allTests, }: Context) { describeIf(nodeAddonExamples, "Node Addon Examples", () => { @@ -46,6 +49,22 @@ function loadTests({ } }); + describeIf(nodeTests, "Node Tests", () => { + function registerTestSuite(suite: typeof nodeTestsSuites) { + for (const [name, suiteOrTest] of Object.entries(suite)) { + if (typeof suiteOrTest === "function") { + it(name, suiteOrTest); + } else { + describe(name, () => { + registerTestSuite(suiteOrTest); + }); + } + } + } + + registerTestSuite(nodeTestsSuites); + }); + describeIf(ferricExample, "ferric-example", () => { it("exports a callable sum function", () => { /* eslint-disable-next-line @typescript-eslint/no-require-imports -- TODO: Determine why a dynamic import doesn't work on Android */ diff --git a/apps/test-app/package.json b/apps/test-app/package.json index dc360b3a..f77dabf5 100644 --- a/apps/test-app/package.json +++ b/apps/test-app/package.json @@ -10,10 +10,12 @@ "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 npm run test:android -- ", "test:android:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples npm run test:android -- ", + "test:android:nodeTests": "MOCHA_REMOTE_CONTEXT=nodeTests npm run test:android -- ", "test:android:ferricExample": "MOCHA_REMOTE_CONTEXT=ferricExample npm run test:android -- ", "test:ios": "mocha-remote --exit-on-error -- concurrently --passthrough-arguments --kill-others-on-fail npm:metro 'npm:ios -- {@}' --", "test:ios:allTests": "MOCHA_REMOTE_CONTEXT=allTests npm run test:ios -- ", "test:ios:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples npm run test:ios -- ", + "test:ios:nodeTests": "MOCHA_REMOTE_CONTEXT=nodeTests npm run test:ios -- ", "test:ios:ferricExample": "MOCHA_REMOTE_CONTEXT=ferricExample npm run test:ios -- " }, "dependencies": { @@ -23,6 +25,8 @@ "@react-native-community/cli": "^18.0.0", "@react-native-community/cli-platform-android": "^18.0.0", "@react-native-community/cli-platform-ios": "^18.0.0", + "@react-native-node-api/node-addon-examples": "*", + "@react-native-node-api/node-tests": "*", "@react-native/babel-preset": "0.79.0", "@react-native/metro-config": "0.79.0", "@react-native/typescript-config": "0.79.0", @@ -36,7 +40,6 @@ "mocha-remote-react-native": "^1.13.2", "react": "19.0.0", "react-native": "0.79.5", - "react-native-node-addon-examples": "*", "react-native-node-api": "*", "react-native-test-app": "^4.3.3" } diff --git a/eslint.config.js b/eslint.config.js index 9b1b463e..794defe7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,6 +13,8 @@ export default tseslint.config( "packages/host/hermes/**", "packages/node-addon-examples/examples/**", "packages/ferric-example/ferric_example.d.ts", + "packages/node-tests/node/**", + "packages/node-tests/tests/**", ]), eslint.configs.recommended, tseslint.configs.recommended, @@ -23,6 +25,7 @@ export default tseslint.config( "packages/node-addon-examples/*.js", "packages/host/babel-plugin.js", "packages/host/react-native.config.js", + "packages/node-tests/tests.generated.js", ], languageOptions: { parserOptions: { diff --git a/package-lock.json b/package-lock.json index e3d811c9..5e84baf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "packages/ferric", "packages/host", "packages/node-addon-examples", + "packages/node-tests", "packages/ferric-example" ], "devDependencies": { @@ -43,6 +44,8 @@ "@react-native-community/cli": "^18.0.0", "@react-native-community/cli-platform-android": "^18.0.0", "@react-native-community/cli-platform-ios": "^18.0.0", + "@react-native-node-api/node-addon-examples": "*", + "@react-native-node-api/node-tests": "*", "@react-native/babel-preset": "0.79.0", "@react-native/metro-config": "0.79.0", "@react-native/typescript-config": "0.79.0", @@ -56,7 +59,6 @@ "mocha-remote-react-native": "^1.13.2", "react": "19.0.0", "react-native": "0.79.5", - "react-native-node-addon-examples": "*", "react-native-node-api": "*", "react-native-test-app": "^4.3.3" } @@ -5895,6 +5897,10 @@ "resolved": "packages/node-addon-examples", "link": true }, + "node_modules/@react-native-node-api/node-tests": { + "resolved": "packages/node-tests", + "link": true + }, "node_modules/@react-native-node-api/test-app": { "resolved": "apps/test-app", "link": true @@ -6234,6 +6240,236 @@ "node": ">=16.17" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.29.tgz", + "integrity": "sha512-pDv7gg59Gdy80eFmMkEqXEaoJi3Y9W/a9T3z9M4t8Ma8aVXNldvSy9UgtlX7AK7DPqF8tULnmIZ2Z3rvGMz/NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.29.tgz", + "integrity": "sha512-fPqR6TfTqbzgKKCQYtcCS+Dms91YcptTbdlwJ13DxOUgMe8LgDIVsLLlEykfm7ijJd5mM4zNw0Hr2CJb6kvQZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.29.tgz", + "integrity": "sha512-7Z4qosL0xN8i6++txHOEPCVP3/lcGLOvftUJOWATZ5aDkDskwcZDa66BGiJt/K1/DgW4kpRVmnGWUWAORHBbFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.29.tgz", + "integrity": "sha512-0HLTfPW5Glh608s76qgayN/nPsXPchNUumavf7W5nh1eMG6qBsOO7Q1QaK0v4un7qtsn3IA/1Tgq0ZgNc0dbeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.29.tgz", + "integrity": "sha512-QNboxdVTJOZS4zP8kA2+XUwAegejd5QNSH5zVR4neqG2AfbxRcMFzSVRkJHN6yDaaKweD/4sUvXfmef6p/7zsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.29.tgz", + "integrity": "sha512-hzBmOtYdC4369XxN2SNJ3oBlXKWNif3ieWBT+oh/qvAeox4fQR0ngqyh+kIGOufBnP5Zc2rqJf9LzIbJw3Tx/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.29.tgz", + "integrity": "sha512-6B35GmFJJ4RX88OgubrnUmuJBUgRh6/OTXIpy8m/VUnoc683lufIPo26HW/0LxLgxp2GM7KHr3LOULcVxbqq4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rolldown/binding-linux-arm64-ohos": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-ohos/-/binding-linux-arm64-ohos-1.0.0-beta.29.tgz", + "integrity": "sha512-z3ru8fUCunQM8q9I7RbDVMT5cxzxVVVBNNKM5/qAQQrdObd1u8g0LR5z0yLtaFWzybwLVdPtJDRcXtLm5tOBFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.29.tgz", + "integrity": "sha512-n6fs4L7j99MIiI6vKhQDdyScv4/uMAPtIMkB0zGbUX8MKWT1osym1hvWVdlENjnS/Phf0zzhjyOgoFDzdhI1cQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.29.tgz", + "integrity": "sha512-C5hcJgtDN4rp6/WsPTQSDVUWrdnIC//ynMGcUIh1O0anm9KnSy47zKQ5D9EqtlEKvO+2PPqmyUVJ2DTq18nlVA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.29.tgz", + "integrity": "sha512-lMN1IBItdZFO182Sdus9oVuNDqyIymn/bsR5KwgeGaiqLsrmpQHBSLwkS/nKJO1nzYlpGDRugFSpnrSJ5ZmihQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.1.tgz", + "integrity": "sha512-KVlQ/jgywZpixGCKMNwxStmmbYEMyokZpCf2YuIChhfJA2uqfAKNEM8INz7zzTo55iEXfBhIIs3VqYyqzDLj8g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.29.tgz", + "integrity": "sha512-0UrXCUAOrbWdyVJskzjtne/4d3YMMhhhpBnob3SeF4jAvbKYqPhCZJ71pP7yUpvbowGXXTnHWpKfitg4Sovmtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rolldown/binding-win32-ia32-msvc": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.29.tgz", + "integrity": "sha512-YX0OYL1dcB7rPnsndpEa68fytYyZZj1iaWzH7momFB2oBS2lXAe1UrrDWcdLoUXdzPIyzpvtBCiS2XcDgYG7ag==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.29.tgz", + "integrity": "sha512-azrPWbV+NZiCFNs59AgH9Y6vFKHoAI6T/XtKKsoLxkPyP1LpbdgL5eqRfeWz+GCAUY9qhDOC4hH1GjFG8PrZIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", + "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -6817,6 +7053,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansis": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.1.0.tgz", + "integrity": "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -8994,6 +9240,13 @@ "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": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -10966,6 +11219,13 @@ "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.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.0.tgz", @@ -11236,6 +11496,19 @@ "node": ">=12.0.0" } }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-addon-api": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.4.0.tgz", @@ -11817,6 +12090,37 @@ "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11897,6 +12201,17 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -12130,10 +12445,6 @@ } } }, - "node_modules/react-native-node-addon-examples": { - "resolved": "packages/node-addon-examples", - "link": true - }, "node_modules/react-native-node-api": { "resolved": "packages/host", "link": true @@ -12456,6 +12767,38 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.29.tgz", + "integrity": "sha512-EsoOi8moHN6CAYyTZipxDDVTJn0j2nBCWor4wRU45RQ8ER2qREDykXLr3Ulz6hBh6oBKCFTQIjo21i0FXNo/IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "=0.77.3", + "@oxc-project/types": "=0.77.3", + "@rolldown/pluginutils": "1.0.0-beta.29", + "ansis": "^4.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.29", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.29", + "@rolldown/binding-darwin-x64": "1.0.0-beta.29", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.29", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.29", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.29", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.29", + "@rolldown/binding-linux-arm64-ohos": "1.0.0-beta.29", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.29", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.29", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.29", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.29", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.29", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.29" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -13152,6 +13495,43 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "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/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -15013,6 +15393,17 @@ "react-native-node-api-cmake": "*" } }, + "packages/node-tests": { + "name": "@react-native-node-api/node-tests", + "devDependencies": { + "cmake-rn": "*", + "gyp-to-cmake": "*", + "prebuildify": "^6.0.1", + "react-native-node-api": "^0.3.2", + "read-pkg": "^9.0.1", + "rolldown": "1.0.0-beta.29" + } + }, "packages/react-native-node-api-cmake": { "version": "0.1.0", "extraneous": true, diff --git a/package.json b/package.json index c7ff7c2f..cac8f8d6 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "packages/ferric", "packages/host", "packages/node-addon-examples", + "packages/node-tests", "packages/ferric-example" ], "homepage": "https://github.com/callstackincubator/react-native-node-api#readme", diff --git a/packages/node-tests/.gitignore b/packages/node-tests/.gitignore new file mode 100644 index 00000000..b73ec41d --- /dev/null +++ b/packages/node-tests/.gitignore @@ -0,0 +1,4 @@ +node/ +tests/ + +tests.generated.js diff --git a/packages/node-tests/common.ts b/packages/node-tests/common.ts new file mode 100644 index 00000000..3264d808 --- /dev/null +++ b/packages/node-tests/common.ts @@ -0,0 +1 @@ +export const buildType = "Release"; diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json new file mode 100644 index 00000000..1cc6cd20 --- /dev/null +++ b/packages/node-tests/package.json @@ -0,0 +1,29 @@ +{ + "name": "@react-native-node-api/node-tests", + "description": "Harness for running the Node.js tests from https://github.com/nodejs/node/tree/main/test", + "type": "commonjs", + "main": "tests.generated.js", + "private": true, + "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/node-tests" + }, + "scripts": { + "copy-tests": "tsx scripts/copy-tests.mts", + "gyp-to-cmake": "gyp-to-cmake ./tests", + "build-tests": "tsx scripts/build-tests.mts", + "bundle": "rolldown -c rolldown.config.mts", + "generate-entrypoint": "tsx scripts/generate-entrypoint.mts", + "bootstrap": "node --run copy-tests && node --run gyp-to-cmake && node --run build-tests && node --run bundle && node --run generate-entrypoint" + }, + "devDependencies": { + "cmake-rn": "*", + "gyp-to-cmake": "*", + "prebuildify": "^6.0.1", + "react-native-node-api": "^0.3.2", + "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 new file mode 100644 index 00000000..c1b7f327 --- /dev/null +++ b/packages/node-tests/rolldown.config.mts @@ -0,0 +1,93 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +import { defineConfig, type RolldownOptions } from "rolldown"; +import { aliasPlugin, replacePlugin } from "rolldown/experimental"; + +function readGypTargetNames(gypFilePath: string): string[] { + const contents = JSON.parse(fs.readFileSync(gypFilePath, "utf-8")) as unknown; + assert( + typeof contents === "object" && contents !== null, + "Expected gyp file to contain a valid JSON object", + ); + assert("targets" in contents, "Expected targets in gyp file"); + const { targets } = contents; + assert(Array.isArray(targets), "Expected targets to be an array"); + return targets.map(({ target_name }) => { + assert( + typeof target_name === "string", + "Expected target_name to be a string", + ); + return target_name; + }); +} + +function testSuiteConfig(suitePath: string): RolldownOptions[] { + const testFiles = fs.globSync("*.js", { + cwd: suitePath, + exclude: ["*.bundle.js"], + }); + const gypFilePath = path.join(suitePath, "binding.gyp"); + const targetNames = readGypTargetNames(gypFilePath); + return testFiles.map((testFile) => ({ + input: path.join(suitePath, testFile), + output: { + file: path.join(suitePath, path.basename(testFile, ".js") + ".bundle.js"), + }, + resolve: { + conditionNames: ["react-native"], + }, + polyfillRequire: false, + plugins: [ + // Replace dynamic require statements for addon targets to allow the babel plugin to handle them correctly + replacePlugin( + Object.fromEntries( + targetNames.map((targetName) => [ + `require(\`./build/\${common.buildType}/${targetName}\`)`, + `require("./build/Release/${targetName}")`, + ]), + ), + { + delimiters: ["", ""], + }, + ), + replacePlugin( + Object.fromEntries( + targetNames.map((targetName) => [ + // Replace "__require" statement with a regular "require" to allow Metro to resolve it + `__require("./build/Release/${targetName}")`, + `require("./build/Release/${targetName}")`, + ]), + ), + { + delimiters: ["", ""], + }, + ), + aliasPlugin({ + entries: [ + { + find: "../../common", + replacement: "./common.ts", + }, + ], + }), + ], + external: targetNames.map((targetName) => `./build/Release/${targetName}`), + })); +} + +const suitePaths = fs + .globSync("tests/*/*", { + cwd: import.meta.dirname, + withFileTypes: true, + }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => + path.join( + path.relative(import.meta.dirname, dirent.parentPath), + dirent.name, + ), + ); + +export default defineConfig(suitePaths.flatMap(testSuiteConfig)); diff --git a/packages/node-tests/scripts/build-tests.mts b/packages/node-tests/scripts/build-tests.mts new file mode 100644 index 00000000..de879e8c --- /dev/null +++ b/packages/node-tests/scripts/build-tests.mts @@ -0,0 +1,17 @@ +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +import { findCMakeProjects } from "./utils.mjs"; + +const rootPath = path.join(import.meta.dirname, ".."); +const projectPaths = findCMakeProjects(); + +for (const projectPath of projectPaths) { + console.log( + `Running "cmake-rn" in ${path.relative( + rootPath, + projectPath, + )} to build for React Native`, + ); + spawnSync("cmake-rn", [], { cwd: projectPath, stdio: "inherit" }); +} diff --git a/packages/node-tests/scripts/copy-tests.mts b/packages/node-tests/scripts/copy-tests.mts new file mode 100644 index 00000000..0b14099b --- /dev/null +++ b/packages/node-tests/scripts/copy-tests.mts @@ -0,0 +1,71 @@ +import fs from "node:fs"; +import path from "node:path"; +import cp from "node:child_process"; + +import { TESTS_DIR } from "./utils.mjs"; + +const NODE_REPO_URL = "https://github.com/nodejs/node.git"; +const NODE_REPO_DIR = path.resolve(import.meta.dirname, "../node"); + +const ALLOW_LIST = [ + "js-native-api/common.h", + "js-native-api/common-inl.h", + "js-native-api/entry_point.h", + "js-native-api/2_function_arguments", + // "node-api/test_async", + // "node-api/test_buffer", +]; + +console.log("Copying files to", TESTS_DIR); + +// Clean up the destination directory before copying +// fs.rmSync(EXAMPLES_DIR, { recursive: true, force: true }); + +if (!fs.existsSync(NODE_REPO_DIR)) { + console.log( + "Sparse and shallow cloning Node.js repository to", + NODE_REPO_DIR, + ); + + // Init a new git repository + cp.execFileSync("git", ["init", NODE_REPO_DIR], { + stdio: "inherit", + }); + // Set the remote origin to the Node.js repository + cp.execFileSync("git", ["remote", "add", "origin", NODE_REPO_URL], { + stdio: "inherit", + cwd: NODE_REPO_DIR, + }); + // Enable sparse checkout + cp.execFileSync( + "git", + ["sparse-checkout", "set", "test/js-native-api", "test/node-api"], + { + stdio: "inherit", + cwd: NODE_REPO_DIR, + }, + ); + // Pull the latest changes from the master branch + console.log("Pulling latest changes from Node.js repository..."); + cp.execFileSync("git", ["pull", "--depth=1", "origin", "main"], { + stdio: "inherit", + cwd: NODE_REPO_DIR, + }); +} +const SRC_DIR = path.join(NODE_REPO_DIR, "test"); +console.log("Copying files from", SRC_DIR); + +for (const src of ALLOW_LIST) { + const srcPath = path.join(SRC_DIR, src); + const destPath = path.join(TESTS_DIR, src); + + if (fs.existsSync(destPath)) { + console.warn( + `Destination path ${destPath} already exists - skipping copy of ${src}.`, + ); + continue; + } + + console.log("Copying from", srcPath, "to", destPath); + fs.cpSync(srcPath, destPath, { recursive: true }); +} diff --git a/packages/node-tests/scripts/generate-entrypoint.mts b/packages/node-tests/scripts/generate-entrypoint.mts new file mode 100644 index 00000000..a0cf8c8d --- /dev/null +++ b/packages/node-tests/scripts/generate-entrypoint.mts @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +const packageRoot = path.join(import.meta.dirname, ".."); +const entrypointPath = path.join(packageRoot, "tests.generated.js"); + +const testPaths = fs.globSync("**/*.bundle.js", { + cwd: path.join(packageRoot, "tests"), +}); + +interface TestSuite { + [key: string]: string | TestSuite; +} + +const suites: TestSuite = {}; + +for (const testPath of testPaths) { + const paths = testPath.split(path.sep); + const testName = paths.pop(); + assert(typeof testName === "string"); + let parent: TestSuite = suites; + for (const part of paths) { + if (!parent[part]) { + // Init if missing + parent[part] = {}; + } + assert(typeof parent[part] === "object"); + parent = parent[part]; + } + parent[path.basename(testName, ".bundle.js")] = path.join("tests", testPath); +} + +function suiteToString(suite: TestSuite, indent = 1): string { + const padding = " ".repeat(indent); + return Object.entries(suite) + .map(([key, value]) => { + if (typeof value === "string") { + return `${padding}"${key}": () => require("./${value}")`; + } else { + return `${padding}"${key}": {\n${suiteToString( + value, + indent + 1, + )}\n${padding}}`; + } + }) + .join(", "); +} + +const comment = "Generated by ./scripts/generate-entrypoint.mts"; + +console.log( + `Writing entrypoint to ${path.relative( + import.meta.dirname, + entrypointPath, + )} for ${testPaths.length} tests ...`, +); + +fs.writeFileSync( + entrypointPath, + `/* ${comment} */\nmodule.exports.suites = {\n${suiteToString(suites)}\n};`, +); diff --git a/packages/node-tests/scripts/utils.mts b/packages/node-tests/scripts/utils.mts new file mode 100644 index 00000000..97e32b03 --- /dev/null +++ b/packages/node-tests/scripts/utils.mts @@ -0,0 +1,20 @@ +import { readdirSync, statSync } from "node:fs"; +import path from "node:path"; + +export const TESTS_DIR = path.resolve(import.meta.dirname, "../tests"); + +export function findCMakeProjects(dir = TESTS_DIR): string[] { + let results: string[] = []; + const files = readdirSync(dir); + + for (const file of files) { + const fullPath = path.join(dir, file); + if (statSync(fullPath).isDirectory()) { + results = results.concat(findCMakeProjects(fullPath)); + } else if (file === "CMakeLists.txt") { + results.push(dir); + } + } + + return results; +} diff --git a/packages/node-tests/tests.generated.d.ts b/packages/node-tests/tests.generated.d.ts new file mode 100644 index 00000000..e801e4b1 --- /dev/null +++ b/packages/node-tests/tests.generated.d.ts @@ -0,0 +1,7 @@ +// Despite the name, this file isn't generated. + +export interface TestSuite { + [key: string]: TestSuite | (() => void); +} + +export declare const suites: TestSuite; diff --git a/packages/node-tests/tsconfig.common.json b/packages/node-tests/tsconfig.common.json new file mode 100644 index 00000000..127dd86b --- /dev/null +++ b/packages/node-tests/tsconfig.common.json @@ -0,0 +1,11 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "composite": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "types": [] + }, + "include": ["common.ts"] +} diff --git a/packages/node-tests/tsconfig.json b/packages/node-tests/tsconfig.json new file mode 100644 index 00000000..ac70f799 --- /dev/null +++ b/packages/node-tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.common.json" }, + { "path": "./tsconfig.node-scripts.json" } + ] +} diff --git a/packages/node-tests/tsconfig.node-scripts.json b/packages/node-tests/tsconfig.node-scripts.json new file mode 100644 index 00000000..e4ca4a5b --- /dev/null +++ b/packages/node-tests/tsconfig.node-scripts.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "rootDir": "scripts", + "types": ["node", "read-pkg"] + }, + "include": ["scripts/**/*.mts"], + "exclude": [] +} diff --git a/tsconfig.json b/tsconfig.json index b9af5629..f5c80f57 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ { "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-addon-examples/tsconfig.json" }, + { "path": "./packages/node-tests/tsconfig.json" } ] }