diff --git a/.changeset/shiny-hotels-wink.md b/.changeset/shiny-hotels-wink.md new file mode 100644 index 00000000..10ba1209 --- /dev/null +++ b/.changeset/shiny-hotels-wink.md @@ -0,0 +1,6 @@ +--- +"cmake-rn": patch +"react-native-node-api": patch +--- + +Refactor into a platform abstraction diff --git a/packages/cmake-rn/src/android.ts b/packages/cmake-rn/src/android.ts deleted file mode 100644 index af550cb1..00000000 --- a/packages/cmake-rn/src/android.ts +++ /dev/null @@ -1,103 +0,0 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import path from "node:path"; - -import { AndroidTriplet } from "react-native-node-api"; - -export const DEFAULT_ANDROID_TRIPLETS = [ - "aarch64-linux-android", - "armv7a-linux-androideabi", - "i686-linux-android", - "x86_64-linux-android", -] as const satisfies AndroidTriplet[]; - -type AndroidArchitecture = "armeabi-v7a" | "arm64-v8a" | "x86" | "x86_64"; - -export const ANDROID_ARCHITECTURES = { - "armv7a-linux-androideabi": "armeabi-v7a", - "aarch64-linux-android": "arm64-v8a", - "i686-linux-android": "x86", - "x86_64-linux-android": "x86_64", -} satisfies Record; - -type AndroidConfigureOptions = { - triplet: AndroidTriplet; - ndkVersion: string; - sdkVersion: string; -}; - -export function getAndroidConfigureCmakeArgs({ - triplet, - ndkVersion, - sdkVersion, -}: AndroidConfigureOptions) { - 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}` - ); - - const toolchainPath = path.join( - ndkPath, - "build/cmake/android.toolchain.cmake" - ); - const architecture = ANDROID_ARCHITECTURES[triplet]; - - const linkerFlags: string[] = [ - // `--no-version-undefined`, - // `--whole-archive`, - // `--no-whole-archive`, - ]; - - return [ - // Use the XCode as generator for Apple platforms - "-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=${sdkVersion}`, - "-D", - "ANDROID_STL=c++_shared", - // Pass linker flags to avoid errors from undefined symbols - // TODO: Link against a weak-node-api to avoid this (or whatever other lib which will be providing the symbols) - // "-D", - // `CMAKE_SHARED_LINKER_FLAGS="-Wl,--allow-shlib-undefined"`, - "-D", - `CMAKE_SHARED_LINKER_FLAGS=${linkerFlags - .map((flag) => `-Wl,${flag}`) - .join(" ")}`, - ]; -} - -export function isAndroidSupported() { - const { ANDROID_HOME } = process.env; - return typeof ANDROID_HOME === "string" && fs.existsSync(ANDROID_HOME); -} diff --git a/packages/cmake-rn/src/apple.ts b/packages/cmake-rn/src/apple.ts deleted file mode 100644 index 1b92409b..00000000 --- a/packages/cmake-rn/src/apple.ts +++ /dev/null @@ -1,113 +0,0 @@ -import assert from "node:assert/strict"; - -import { AppleTriplet, isAppleTriplet } from "react-native-node-api"; - -export const DEFAULT_APPLE_TRIPLETS = [ - "arm64;x86_64-apple-darwin", - "arm64-apple-ios", - "arm64-apple-ios-sim", - "arm64-apple-tvos", - "arm64-apple-tvos-sim", - "arm64-apple-visionos", - "arm64-apple-visionos-sim", -] as const satisfies AppleTriplet[]; - -type XcodeSDKName = - | "iphoneos" - | "iphonesimulator" - | "catalyst" - | "xros" - | "xrsimulator" - | "appletvos" - | "appletvsimulator" - | "macosx"; - -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-tvos": "appletvos", - "arm64-apple-tvos-sim": "appletvsimulator", - "arm64-apple-visionos": "xros", - "arm64-apple-visionos-sim": "xrsimulator", -} satisfies Record; - -type CMakeSystemName = "Darwin" | "iOS" | "tvOS" | "watchOS" | "visionOS"; - -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-tvos": "appletvos", - "arm64-apple-tvos-sim": "tvOS", - "arm64-apple-visionos": "visionOS", - "arm64-apple-visionos-sim": "visionOS", -} satisfies Record; - -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 AppleConfigureOptions = { - triplet: AppleTriplet; -}; - -export function getAppleConfigureCmakeArgs({ triplet }: AppleConfigureOptions) { - assert(isAppleTriplet(triplet)); - const systemName = CMAKE_SYSTEM_NAMES[triplet]; - - return [ - // Use the XCode as generator for Apple platforms - "-G", - "Xcode", - "-D", - `CMAKE_SYSTEM_NAME=${systemName}`, - // Set the SDK path for the target platform - "-D", - `CMAKE_OSX_SYSROOT=${XCODE_SDK_NAMES[triplet]}`, - // Set the target architecture - "-D", - `CMAKE_OSX_ARCHITECTURES=${APPLE_ARCHITECTURES[triplet]}`, - ]; -} - -export function getAppleBuildArgs() { - // We expect the final application to sign these binaries - return ["CODE_SIGNING_ALLOWED=NO"]; -} - -export function isAppleSupported() { - return process.platform === "darwin"; -} diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 86a1486b..39ffadc3 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -8,39 +8,19 @@ import { spawn, SpawnFailure } from "bufout"; import { oraPromise } from "ora"; import chalk from "chalk"; -import { - DEFAULT_APPLE_TRIPLETS, - isAppleSupported, - getAppleBuildArgs, - getAppleConfigureCmakeArgs, -} from "./apple.js"; -import { - DEFAULT_ANDROID_TRIPLETS, - isAndroidSupported, - getAndroidConfigureCmakeArgs, -} from "./android.js"; import { getWeakNodeApiVariables } from "./weak-node-api.js"; - import { - SUPPORTED_TRIPLETS, - SupportedTriplet, - AndroidTriplet, - isAndroidTriplet, - isAppleTriplet, - determineAndroidLibsFilename, - createAndroidLibsDirectory, - createAppleFramework, - createXCframework, - determineXCFrameworkFilename, -} from "react-native-node-api"; + platforms, + allTargets, + findPlatformForTarget, + platformHasTarget, +} from "./platforms.js"; +import { BaseOpts, TargetContext, Platform } from "./platforms/types.js"; +import { isSupportedTriplet } from "react-native-node-api"; // We're attaching a lot of listeners when spawning in parallel EventEmitter.defaultMaxListeners = 100; -// This should match https://github.com/react-native-community/template/blob/main/template/android/build.gradle#L7 -const DEFAULT_NDK_VERSION = "27.1.12297006"; -const DEFAULT_ANDROID_SDK_VERSION = "24"; - // TODO: Add automatic ccache support const verboseOption = new Option( @@ -58,16 +38,13 @@ const configurationOption = new Option("--configuration ") .choices(["Release", "Debug"] as const) .default("Release"); -// TODO: Derive default triplets +// TODO: Derive default targets // This is especially important when driving the build from within a React Native app package. -const tripletOption = new Option( - "--triplet ", - "Triplets to build for" -).choices(SUPPORTED_TRIPLETS); - -const androidOption = new Option("--android", "Enable all Android triplets"); -const appleOption = new Option("--apple", "Enable all Apple triplets"); +const targetOption = new Option( + "--target ", + "Targets to build for" +).choices(allTargets); const buildPathOption = new Option( "--build ", @@ -84,16 +61,6 @@ const outPathOption = new Option( "Specify the output directory to store the final build artifacts" ).default(false, "./{build}/{configuration}"); -const ndkVersionOption = new Option( - "--ndk-version ", - "The NDK version to use for Android builds" -).default(DEFAULT_NDK_VERSION); - -const androidSdkVersionOption = new Option( - "--android-sdk-version ", - "The Android SDK version to use for Android builds" -).default(DEFAULT_ANDROID_SDK_VERSION); - const noAutoLinkOption = new Option( "--no-auto-link", "Don't mark the output as auto-linkable by react-native-node-api" @@ -104,233 +71,132 @@ const noWeakNodeApiLinkageOption = new Option( "Don't pass the path of the weak-node-api library from react-native-node-api" ); -const xcframeworkExtensionOption = new Option( - "--xcframework-extension", - "Don't rename the xcframework to .apple.node" -).default(false); - -export const program = new Command("cmake-rn") +let program = new Command("cmake-rn") .description("Build React Native Node API modules with CMake") + .addOption(targetOption) .addOption(verboseOption) .addOption(sourcePathOption) .addOption(buildPathOption) .addOption(outPathOption) .addOption(configurationOption) - .addOption(tripletOption) - .addOption(androidOption) - .addOption(appleOption) .addOption(cleanOption) - .addOption(ndkVersionOption) - .addOption(androidSdkVersionOption) .addOption(noAutoLinkOption) - .addOption(noWeakNodeApiLinkageOption) - .addOption(xcframeworkExtensionOption) - .action(async ({ triplet: tripletValues, ...globalContext }) => { + .addOption(noWeakNodeApiLinkageOption); + +for (const platform of platforms) { + const allOption = new Option( + `--${platform.id}`, + `Enable all ${platform.name} triplets` + ); + program = program.addOption(allOption); + program = platform.amendCommand(program); +} + +program = program.action( + async ({ target: requestedTargets, ...baseOptions }) => { try { - const buildPath = getBuildPath(globalContext); - if (globalContext.clean) { + const buildPath = getBuildPath(baseOptions); + if (baseOptions.clean) { await fs.promises.rm(buildPath, { recursive: true, force: true }); } - const triplets = new Set(tripletValues); - if (globalContext.apple) { - for (const triplet of DEFAULT_APPLE_TRIPLETS) { - triplets.add(triplet); - } - } - if (globalContext.android) { - for (const triplet of DEFAULT_ANDROID_TRIPLETS) { - triplets.add(triplet); - } - } + const targets = new Set(requestedTargets); - if (triplets.size === 0) { - if (isAndroidSupported()) { - if (process.arch === "arm64") { - triplets.add("aarch64-linux-android"); - } else if (process.arch === "x64") { - triplets.add("x86_64-linux-android"); + 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); } } - if (isAppleSupported()) { - if (process.arch === "arm64") { - triplets.add("arm64-apple-ios-sim"); + } + + 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) { + if (targets.size === 0) { throw new Error( - "Found no default triplets: Install some platform specific build tools" + "Found no default targets: Install some platform specific build tools" ); } else { console.error( chalk.yellowBright("ℹ"), - "Using default triplets", - chalk.dim("(" + [...triplets].join(", ") + ")") + "Using default targets", + chalk.dim("(" + [...targets].join(", ") + ")") ); } } - if (!globalContext.out) { - globalContext.out = path.join(buildPath, globalContext.configuration); + if (!baseOptions.out) { + baseOptions.out = path.join(buildPath, baseOptions.configuration); } - const tripletContext = [...triplets].map((triplet) => { - const tripletBuildPath = getTripletBuildPath(buildPath, triplet); + const targetContexts = [...targets].map((target) => { + const platform = findPlatformForTarget(target); + const targetBuildPath = getTargetBuildPath(buildPath, target); return { - ...globalContext, - triplet, - tripletBuildPath, - tripletOutputPath: path.join(tripletBuildPath, "out"), + target, + platform, + buildPath: targetBuildPath, + outputPath: path.join(targetBuildPath, "out"), + options: baseOptions, }; }); // Configure every triplet project - await oraPromise(Promise.all(tripletContext.map(configureProject)), { - text: "Configuring projects", - isSilent: globalContext.verbose, - successText: "Configured projects", - failText: ({ message }) => `Failed to configure projects: ${message}`, - }); + await oraPromise( + Promise.all( + targetContexts.map(({ platform, ...context }) => + configureProject(platform, context, baseOptions) + ) + ), + { + text: "Configuring projects", + isSilent: baseOptions.verbose, + successText: "Configured projects", + failText: ({ message }) => `Failed to configure projects: ${message}`, + } + ); // Build every triplet project await oraPromise( Promise.all( - tripletContext.map(async (context) => { + 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.tripletOutputPath, { + await fs.promises.rm(context.outputPath, { recursive: true, force: true, }); - await buildProject(context); + await buildProject(platform, context, baseOptions); }) ), { text: "Building projects", - isSilent: globalContext.verbose, + isSilent: baseOptions.verbose, successText: "Built projects", failText: ({ message }) => `Failed to build projects: ${message}`, } ); - // Collect triplets in vendor specific containers - const appleTriplets = tripletContext.filter(({ triplet }) => - isAppleTriplet(triplet) - ); - if (appleTriplets.length > 0) { - const libraryPaths = await Promise.all( - appleTriplets.map(async ({ tripletOutputPath }) => { - const configSpecificPath = path.join( - tripletOutputPath, - globalContext.configuration - ); - assert( - fs.existsSync(configSpecificPath), - `Expected a directory at ${configSpecificPath}` - ); - // 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]; - }) - ); - const frameworkPaths = libraryPaths.map(createAppleFramework); - const xcframeworkFilename = determineXCFrameworkFilename( - frameworkPaths, - globalContext.xcframeworkExtension ? ".xcframework" : ".apple.node" - ); - - // Create the xcframework - const xcframeworkOutputPath = path.resolve( - globalContext.out || globalContext.source, - xcframeworkFilename - ); - - await oraPromise( - createXCframework({ - outputPath: xcframeworkOutputPath, - frameworkPaths, - autoLink: globalContext.autoLink, - }), - { - text: "Assembling XCFramework", - successText: `XCFramework assembled into ${chalk.dim( - path.relative(process.cwd(), xcframeworkOutputPath) - )}`, - failText: ({ message }) => - `Failed to assemble XCFramework: ${message}`, - } + // Perform post-build steps for each platform in sequence + for (const platform of platforms) { + const relevantTargets = targetContexts.filter(({ target }) => + platformHasTarget(platform, target) ); - } - - const androidTriplets = tripletContext.filter(({ triplet }) => - isAndroidTriplet(triplet) - ); - if (androidTriplets.length > 0) { - const libraryPathByTriplet = Object.fromEntries( - await Promise.all( - androidTriplets.map(async ({ tripletOutputPath, triplet }) => { - assert( - fs.existsSync(tripletOutputPath), - `Expected a directory at ${tripletOutputPath}` - ); - // Expect binary file(s), either .node or .so - const dirents = await fs.promises.readdir(tripletOutputPath, { - 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 [triplet, result[0]] as const; - }) - ) - ) as Record; - const androidLibsFilename = determineAndroidLibsFilename( - Object.values(libraryPathByTriplet) - ); - const androidLibsOutputPath = path.resolve( - globalContext.out || globalContext.source, - androidLibsFilename - ); - - await oraPromise( - createAndroidLibsDirectory({ - outputPath: androidLibsOutputPath, - libraryPathByTriplet, - autoLink: globalContext.autoLink, - }), + if (relevantTargets.length == 0) { + continue; + } + await platform.postBuild( { - 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}`, - } + outputPath: baseOptions.out || baseOptions.source, + targets: relevantTargets, + }, + baseOptions ); } } catch (error) { @@ -339,123 +205,87 @@ export const program = new Command("cmake-rn") } throw error; } - }); - -type GlobalContext = ReturnType; -type TripletScopedContext = Omit & { - triplet: SupportedTriplet; - tripletBuildPath: string; - tripletOutputPath: string; -}; + } +); -function getBuildPath(context: GlobalContext) { +function getBuildPath({ build, source }: BaseOpts) { // TODO: Add configuration (debug vs release) - return path.resolve( - process.cwd(), - context.build || path.join(context.source, "build") - ); + return path.resolve(process.cwd(), build || path.join(source, "build")); } /** - * Namespaces the output path with the triplet + * Namespaces the output path with a target name */ -function getTripletBuildPath(buildPath: string, triplet: SupportedTriplet) { - return path.join(buildPath, triplet.replace(/;/g, "_")); +function getTargetBuildPath(buildPath: string, target: unknown) { + assert(typeof target === "string", "Expected target to be a string"); + return path.join(buildPath, target.replace(/;/g, "_")); } -function getTripletConfigureCmakeArgs( - triplet: SupportedTriplet, - { - ndkVersion, - androidSdkVersion, - }: Pick< - GlobalContext, - "ndkVersion" | "androidSdkVersion" | "weakNodeApiLinkage" - > +async function configureProject( + platform: Platform>, + context: TargetContext, + options: BaseOpts ) { - if (isAndroidTriplet(triplet)) { - return getAndroidConfigureCmakeArgs({ - triplet, - ndkVersion, - sdkVersion: androidSdkVersion, - }); - } else if (isAppleTriplet(triplet)) { - return getAppleConfigureCmakeArgs({ triplet }); - } else { - throw new Error(`Support for '${triplet}' is not implemented yet`); - } -} - -function getBuildArgs(triplet: SupportedTriplet) { - if (isAndroidTriplet(triplet)) { - return []; - } else if (isAppleTriplet(triplet)) { - return getAppleBuildArgs(); - } else { - throw new Error(`Support for '${triplet}' is not implemented yet`); - } -} + 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, + }; -async function configureProject(context: TripletScopedContext) { - const { - verbose, - triplet, - tripletBuildPath, - source, - ndkVersion, - androidSdkVersion, - weakNodeApiLinkage, - } = context; await spawn( "cmake", [ "-S", source, "-B", - tripletBuildPath, - ...getVariablesArgs(getVariables(context)), - ...getTripletConfigureCmakeArgs(triplet, { - ndkVersion, - weakNodeApiLinkage, - androidSdkVersion, - }), + buildPath, + ...platform.configureArgs(context, options), + ...toDeclarationArguments(declarations), ], { outputMode: verbose ? "inherit" : "buffered", - outputPrefix: verbose ? chalk.dim(`[${triplet}] `) : undefined, + outputPrefix: verbose ? chalk.dim(`[${target}] `) : undefined, } ); } -async function buildProject(context: TripletScopedContext) { - const { verbose, triplet, tripletBuildPath, configuration } = context; +async function buildProject( + platform: Platform>, + context: TargetContext, + options: BaseOpts +) { + const { target, buildPath } = context; + const { verbose, configuration } = options; await spawn( "cmake", [ "--build", - tripletBuildPath, + buildPath, "--config", configuration, "--", - ...getBuildArgs(triplet), + ...platform.buildArgs(context, options), ], { outputMode: verbose ? "inherit" : "buffered", - outputPrefix: verbose ? chalk.dim(`[${triplet}] `) : undefined, + outputPrefix: verbose ? chalk.dim(`[${target}] `) : undefined, } ); } -function getVariables(context: TripletScopedContext): Record { - return { - ...(context.weakNodeApiLinkage && getWeakNodeApiVariables(context.triplet)), - CMAKE_LIBRARY_OUTPUT_DIRECTORY: context.tripletOutputPath, - }; -} - -function getVariablesArgs(variables: Record) { - return Object.entries(variables).flatMap(([key, value]) => [ +function toDeclarationArguments(declarations: Record) { + return Object.entries(declarations).flatMap(([key, value]) => [ "-D", `${key}=${value}`, ]); } + +export { program }; diff --git a/packages/cmake-rn/src/platforms.test.ts b/packages/cmake-rn/src/platforms.test.ts new file mode 100644 index 00000000..2f8c95f7 --- /dev/null +++ b/packages/cmake-rn/src/platforms.test.ts @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { + platforms, + platformHasTarget, + findPlatformForTarget, +} from "./platforms.js"; +import { Platform } from "./platforms/types.js"; + +const mockPlatform = { targets: ["target1", "target2"] } as unknown as Platform; + +describe("platformHasTarget", () => { + it("returns true when platform has target", () => { + assert.equal(platformHasTarget(mockPlatform, "target1"), true); + }); + + it("returns false when platform doesn't have target", () => { + assert.equal(platformHasTarget(mockPlatform, "target3"), false); + }); +}); + +describe("findPlatformForTarget", () => { + it("returns platform when target is found", () => { + assert(platforms.length >= 2, "Expects at least two platforms"); + const [platform1, platform2] = platforms; + const platform = findPlatformForTarget(platform1.targets[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 new file mode 100644 index 00000000..0692a3a4 --- /dev/null +++ b/packages/cmake-rn/src/platforms.ts @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; + +import { platform as android } from "./platforms/android.js"; +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 function platformHasTarget

( + platform: P, + target: unknown +): target is P["targets"][number] { + return (platform.targets as unknown[]).includes(target); +} + +export function findPlatformForTarget(target: unknown) { + const platform = Object.values(platforms).find((platform) => + platformHasTarget(platform, target) + ); + assert(platform, `Unable to determine platform from target: ${target}`); + return platform; +} diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts new file mode 100644 index 00000000..51e2ab76 --- /dev/null +++ b/packages/cmake-rn/src/platforms/android.ts @@ -0,0 +1,172 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +import { Option } from "@commander-js/extra-typings"; +import { + createAndroidLibsDirectory, + determineAndroidLibsFilename, + AndroidTriplet as Target, +} from "react-native-node-api"; + +import type { Platform } from "./types.js"; +import { oraPromise } from "ora"; +import chalk from "chalk"; + +// This should match https://github.com/react-native-community/template/blob/main/template/android/build.gradle#L7 +const DEFAULT_NDK_VERSION = "27.1.12297006"; +const DEFAULT_ANDROID_SDK_VERSION = "24"; + +type AndroidArchitecture = "armeabi-v7a" | "arm64-v8a" | "x86" | "x86_64"; + +export const ANDROID_ARCHITECTURES = { + "armv7a-linux-androideabi": "armeabi-v7a", + "aarch64-linux-android": "arm64-v8a", + "i686-linux-android": "x86", + "x86_64-linux-android": "x86_64", +} satisfies Record; + +const ndkVersionOption = new Option( + "--ndk-version ", + "The NDK version to use for Android builds" +).default(DEFAULT_NDK_VERSION); + +const androidSdkVersionOption = new Option( + "--android-sdk-version ", + "The Android SDK version to use for Android builds" +).default(DEFAULT_ANDROID_SDK_VERSION); + +type AndroidOpts = { ndkVersion: string; androidSdkVersion: string }; + +export const platform: Platform = { + id: "android", + name: "Android", + targets: [ + "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"]; + } else { + return []; + } + }, + amendCommand(command) { + return command + .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}` + ); + + 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", + ]; + }, + buildArgs() { + return []; + }, + 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); + + 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}`, + } + ); + }, +}; diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts new file mode 100644 index 00000000..ea66da98 --- /dev/null +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -0,0 +1,191 @@ +import assert from "node:assert/strict"; +import path from "node:path"; +import fs from "node:fs"; + +import { Option } from "@commander-js/extra-typings"; +import { oraPromise } from "ora"; +import { + AppleTriplet as Target, + createAppleFramework, + createXCframework, + determineXCFrameworkFilename, +} from "react-native-node-api"; + +import type { Platform } from "./types.js"; +import chalk from "chalk"; + +type XcodeSDKName = + | "iphoneos" + | "iphonesimulator" + | "catalyst" + | "xros" + | "xrsimulator" + | "appletvos" + | "appletvsimulator" + | "macosx"; + +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-tvos": "appletvos", + "arm64-apple-tvos-sim": "appletvsimulator", + "arm64-apple-visionos": "xros", + "arm64-apple-visionos-sim": "xrsimulator", +} satisfies Record; + +type CMakeSystemName = "Darwin" | "iOS" | "tvOS" | "watchOS" | "visionOS"; + +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-tvos": "appletvos", + "arm64-apple-tvos-sim": "tvOS", + "arm64-apple-visionos": "visionOS", + "arm64-apple-visionos-sim": "visionOS", +} satisfies Record; + +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"); +} + +export function getAppleBuildArgs() { + // We expect the final application to sign these binaries + return ["CODE_SIGNING_ALLOWED=NO"]; +} + +const xcframeworkExtensionOption = new Option( + "--xcframework-extension", + "Don't rename the xcframework to .apple.node" +).default(false); + +type AppleOpts = { + xcframeworkExtension: boolean; +}; + +export const platform: Platform = { + id: "apple", + name: "Apple", + targets: [ + "arm64;x86_64-apple-darwin", + "arm64-apple-ios", + "arm64-apple-ios-sim", + "arm64-apple-tvos", + "arm64-apple-tvos-sim", + "arm64-apple-visionos", + "arm64-apple-visionos-sim", + ], + defaultTargets() { + return process.arch === "arm64" ? ["arm64-apple-ios-sim"] : []; + }, + amendCommand(command) { + return command.addOption(xcframeworkExtensionOption); + }, + 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]}`, + ]; + }, + buildArgs() { + return []; + }, + isSupportedByHost: function (): boolean | Promise { + return process.platform === "darwin"; + }, + async postBuild( + { outputPath, targets }, + { configuration, autoLink, xcframeworkExtension } + ) { + const libraryPaths = await Promise.all( + targets.map(async ({ outputPath }) => { + const configSpecificPath = path.join(outputPath, configuration); + assert( + fs.existsSync(configSpecificPath), + `Expected a directory at ${configSpecificPath}` + ); + // 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]; + }) + ); + const frameworkPaths = libraryPaths.map(createAppleFramework); + const xcframeworkFilename = determineXCFrameworkFilename( + frameworkPaths, + xcframeworkExtension ? ".xcframework" : ".apple.node" + ); + + // Create the xcframework + const xcframeworkOutputPath = path.resolve(outputPath, xcframeworkFilename); + + await oraPromise( + createXCframework({ + outputPath: xcframeworkOutputPath, + frameworkPaths, + autoLink, + }), + { + text: "Assembling XCFramework", + successText: `XCFramework assembled into ${chalk.dim( + path.relative(process.cwd(), xcframeworkOutputPath) + )}`, + failText: ({ message }) => `Failed to assemble XCFramework: ${message}`, + } + ); + }, +}; diff --git a/packages/cmake-rn/src/platforms/types.ts b/packages/cmake-rn/src/platforms/types.ts new file mode 100644 index 00000000..5406cc48 --- /dev/null +++ b/packages/cmake-rn/src/platforms/types.ts @@ -0,0 +1,79 @@ +import * as commander from "@commander-js/extra-typings"; +import type { program } from "../cli.js"; + +type InferOptionValues = ReturnType< + Command["opts"] +>; + +type BaseCommand = typeof program; +type ExtendedCommand = commander.Command< + [], + Opts & InferOptionValues, + Record // Global opts are not supported +>; + +export type BaseOpts = Omit, "target">; + +export type TargetContext = { + target: Target; + buildPath: string; + outputPath: string; +}; + +export type Platform< + Targets extends string[] = string[], + Opts extends commander.OptionValues = Record, + Command = ExtendedCommand +> = { + /** + * Used to identify the platform in the CLI. + */ + id: string; + /** + * Name of the platform, used for display purposes. + */ + name: string; + /** + * All the targets supported by this platform. + */ + targets: Readonly; + /** + * Get the limited subset of targets that should be built by default for this platform, to support a development workflow. + */ + defaultTargets(): Targets[number][] | Promise; + /** + * Implement this to add any platform specific options to the command. + */ + amendCommand(command: BaseCommand): Command; + /** + * Check if the platform is supported by the host system, running the build. + */ + isSupportedByHost(): boolean | Promise; + /** + * Platform specific arguments passed to CMake to configure a target project. + */ + configureArgs( + context: TargetContext, + options: BaseOpts & Opts + ): string[]; + /** + * Platform specific arguments passed to CMake to build a target project. + */ + buildArgs( + context: TargetContext, + options: BaseOpts & Opts + ): string[]; + /** + * Called to combine multiple targets into a single prebuilt artefact. + */ + postBuild( + context: { + /** + * Location of the final prebuilt artefact. + */ + outputPath: string; + targets: TargetContext[]; + }, + 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 2f483c9b..4db8d667 100644 --- a/packages/cmake-rn/src/weak-node-api.ts +++ b/packages/cmake-rn/src/weak-node-api.ts @@ -1,15 +1,15 @@ import fs from "node:fs"; import assert from "node:assert/strict"; import path from "node:path"; -import { fileURLToPath } from "node:url"; import { isAndroidTriplet, isAppleTriplet, SupportedTriplet, + weakNodeApiPath, } from "react-native-node-api"; -import { ANDROID_ARCHITECTURES } from "./android.js"; +import { ANDROID_ARCHITECTURES } from "./platforms/android.js"; import { getNodeAddonHeadersPath, getNodeApiHeadersPath } from "./headers.js"; export function toCmakePath(input: string) { @@ -17,12 +17,11 @@ export function toCmakePath(input: string) { } export function getWeakNodeApiPath(triplet: SupportedTriplet): string { - const basePath = fileURLToPath( - import.meta.resolve("react-native-node-api/weak-node-api") - ); - assert(fs.existsSync(basePath), "Weak Node API path does not exist"); if (isAppleTriplet(triplet)) { - const xcframeworkPath = path.join(basePath, "weak-node-api.xcframework"); + const xcframeworkPath = path.join( + weakNodeApiPath, + "weak-node-api.xcframework" + ); assert( fs.existsSync(xcframeworkPath), `Expected an XCFramework at ${xcframeworkPath}` @@ -30,7 +29,7 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string { return xcframeworkPath; } else if (isAndroidTriplet(triplet)) { const libraryPath = path.join( - basePath, + weakNodeApiPath, "weak-node-api.android.node", ANDROID_ARCHITECTURES[triplet], "libweak-node-api.so" diff --git a/packages/cmake-rn/tsconfig.json b/packages/cmake-rn/tsconfig.json index 2f7da785..a1c18413 100644 --- a/packages/cmake-rn/tsconfig.json +++ b/packages/cmake-rn/tsconfig.json @@ -1,9 +1,7 @@ { - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "composite": true, - "outDir": "dist", - "rootDir": "src" - }, - "include": ["src"] + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.node-tests.json" } + ] } diff --git a/packages/cmake-rn/tsconfig.node-tests.json b/packages/cmake-rn/tsconfig.node-tests.json new file mode 100644 index 00000000..cabcdca7 --- /dev/null +++ b/packages/cmake-rn/tsconfig.node-tests.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true + }, + "include": ["src/**/*.test.ts"], + "exclude": [], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/packages/cmake-rn/tsconfig.node.json b/packages/cmake-rn/tsconfig.node.json new file mode 100644 index 00000000..d6472ce0 --- /dev/null +++ b/packages/cmake-rn/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"], + "exclude": ["**/*.test.ts"] +} diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index 26b06970..16d2f38e 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -2,7 +2,6 @@ import assert from "node:assert/strict"; import cp from "node:child_process"; import fs from "node:fs"; import path from "node:path"; -import { fileURLToPath } from "node:url"; import { spawn } from "bufout"; import chalk from "chalk"; @@ -15,9 +14,7 @@ import { isAppleTarget, } from "./targets.js"; -const WEAK_NODE_API_PATH = fileURLToPath( - import.meta.resolve("react-native-node-api/weak-node-api") -); +import { weakNodeApiPath } from "react-native-node-api"; const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record = { "aarch64-apple-darwin": "macos-arm64_x86_64", // Universal @@ -126,18 +123,16 @@ export function getTargetAndroidPlatform(target: AndroidTargetName) { } export function getWeakNodeApiFrameworkPath(target: AppleTargetName) { - assert(fs.existsSync(WEAK_NODE_API_PATH), "Expected weak-node-api to exist"); return joinPathAndAssertExistence( - WEAK_NODE_API_PATH, + weakNodeApiPath, "weak-node-api.xcframework", APPLE_XCFRAMEWORK_CHILDS_PER_TARGET[target] ); } export function getWeakNodeApiAndroidLibraryPath(target: AndroidTargetName) { - assert(fs.existsSync(WEAK_NODE_API_PATH), "Expected weak-node-api to exist"); return joinPathAndAssertExistence( - WEAK_NODE_API_PATH, + weakNodeApiPath, "weak-node-api.android.node", ANDROID_ARCH_PR_TARGET[target] ); @@ -202,7 +197,10 @@ export function getTargetEnvironmentVariables({ toolchainBinPath, `${targetArch}-linux-${targetPlatform}-clang++${cmdMaybe}` ), - TARGET_AR: joinPathAndAssertExistence(toolchainBinPath, `llvm-ar${exeMaybe}`), + TARGET_AR: joinPathAndAssertExistence( + toolchainBinPath, + `llvm-ar${exeMaybe}` + ), TARGET_RANLIB: joinPathAndAssertExistence( toolchainBinPath, `llvm-ranlib${exeMaybe}` diff --git a/packages/host/package.json b/packages/host/package.json index cd79a85b..ea5eb74b 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -20,7 +20,7 @@ }, "./babel-plugin": "./dist/node/babel-plugin/index.js", "./cli": "./dist/node/cli/run.js", - "./weak-node-api": "./weak-node-api" + "./weak-node-api": "./dist/node/weak-node-api.js" }, "files": [ "logo.svg", diff --git a/packages/host/src/node/index.ts b/packages/host/src/node/index.ts index 4a9c0207..878eeb01 100644 --- a/packages/host/src/node/index.ts +++ b/packages/host/src/node/index.ts @@ -1,10 +1,11 @@ export { + SUPPORTED_TRIPLETS, ANDROID_TRIPLETS, APPLE_TRIPLETS, - SUPPORTED_TRIPLETS, + type SupportedTriplet, type AndroidTriplet, type AppleTriplet, - type SupportedTriplet, + isSupportedTriplet, isAppleTriplet, isAndroidTriplet, } from "./prebuilds/triplets.js"; @@ -22,3 +23,5 @@ export { } from "./prebuilds/apple.js"; export { determineLibraryBasename, prettyPath } from "./path-utils.js"; + +export { weakNodeApiPath } from "./weak-node-api.js"; diff --git a/packages/host/src/node/path-utils.ts b/packages/host/src/node/path-utils.ts index 0ca2bfe9..c92115cc 100644 --- a/packages/host/src/node/path-utils.ts +++ b/packages/host/src/node/path-utils.ts @@ -420,12 +420,19 @@ export async function findNodeApiModulePathsByDependency({ * Errors if all framework paths doesn't produce the same basename. */ export function determineLibraryBasename(libraryPaths: string[]) { + assert( + libraryPaths.length > 0, + "Expected at least one library path to determine its basename" + ); const libraryNames = libraryPaths.map((p) => // Strip the "lib" prefix and any file extension path.basename(p, path.extname(p)).replace(/^lib/, "") ); const candidates = new Set(libraryNames); - assert(candidates.size === 1, "Expected all libraries to have the same name"); + assert( + candidates.size === 1, + `Expected all libraries to share name, got: ${[...candidates].join(", ")}` + ); const [name] = candidates; return name; } diff --git a/packages/host/src/node/prebuilds/triplets.ts b/packages/host/src/node/prebuilds/triplets.ts index fcfbf6a9..60bff46f 100644 --- a/packages/host/src/node/prebuilds/triplets.ts +++ b/packages/host/src/node/prebuilds/triplets.ts @@ -32,14 +32,20 @@ export const SUPPORTED_TRIPLETS = [ export type SupportedTriplet = (typeof SUPPORTED_TRIPLETS)[number]; +export function isSupportedTriplet( + triplet: unknown +): triplet is SupportedTriplet { + return (SUPPORTED_TRIPLETS as readonly unknown[]).includes(triplet); +} + export function isAndroidTriplet( triplet: SupportedTriplet ): triplet is AndroidTriplet { - return ANDROID_TRIPLETS.includes(triplet as AndroidTriplet); + return (ANDROID_TRIPLETS as readonly unknown[]).includes(triplet); } export function isAppleTriplet( triplet: SupportedTriplet ): triplet is AppleTriplet { - return APPLE_TRIPLETS.includes(triplet as AppleTriplet); + return (APPLE_TRIPLETS as readonly unknown[]).includes(triplet); } diff --git a/packages/host/src/node/weak-node-api.ts b/packages/host/src/node/weak-node-api.ts new file mode 100644 index 00000000..df29834f --- /dev/null +++ b/packages/host/src/node/weak-node-api.ts @@ -0,0 +1,10 @@ +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}` +);