Skip to content

Default out path to {build}/{configuration} directory and refactored pathSuffix naming strategy #150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions packages/cmake-rn/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const cleanOption = new Option(
const outPathOption = new Option(
"--out <path>",
"Specify the output directory to store the final build artifacts"
);
).default(false, "./{build}/{configuration}");
Copy link
Preview

Copilot AI Jul 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .default(false, "./{build}/{configuration}") call will set the default to false rather than the intended path string. Use .default("./{build}/{configuration}") so --out defaults correctly.

Suggested change
).default(false, "./{build}/{configuration}");
).default("./{build}/{configuration}");

Copilot uses AI. Check for mistakes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is on purpose, since we need the values of build and configuration to construct the default value, we're setting it to false to defer defining its actual value.


const ndkVersionOption = new Option(
"--ndk-version <version>",
Expand Down Expand Up @@ -113,12 +113,12 @@ export const program = new Command("cmake-rn")
.description("Build React Native Node API modules with CMake")
.addOption(verboseOption)
.addOption(sourcePathOption)
.addOption(buildPathOption)
.addOption(outPathOption)
.addOption(configurationOption)
.addOption(tripletOption)
.addOption(androidOption)
.addOption(appleOption)
.addOption(buildPathOption)
.addOption(outPathOption)
.addOption(cleanOption)
.addOption(ndkVersionOption)
.addOption(androidSdkVersionOption)
Expand Down Expand Up @@ -169,6 +169,10 @@ export const program = new Command("cmake-rn")
}
}

if (!globalContext.out) {
globalContext.out = path.join(buildPath, globalContext.configuration);
}

const tripletContext = [...triplets].map((triplet) => {
const tripletBuildPath = getTripletBuildPath(buildPath, triplet);
return {
Expand Down
4 changes: 2 additions & 2 deletions packages/host/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
"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",
"build-weak-node-api:all-triplets": "cmake-rn --android --apple --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api",
"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",
"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",
"bootstrap": "npm run copy-node-api-headers && npm run generate-weak-node-api-injector && npm run generate-weak-node-api && npm run build-weak-node-api"
},
Expand Down
3 changes: 1 addition & 2 deletions packages/host/react-native-node-api.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ require_relative "./scripts/patch-hermes"

NODE_PATH ||= `which node`.strip
CLI_COMMAND ||= "'#{NODE_PATH}' '#{File.join(__dir__, "dist/node/cli/run.js")}'"
STRIP_PATH_SUFFIX ||= ENV['NODE_API_MODULES_STRIP_PATH_SUFFIX'] === "true"
COPY_FRAMEWORKS_COMMAND ||= "#{CLI_COMMAND} link --apple '#{Pod::Config.instance.installation_root}' #{STRIP_PATH_SUFFIX ? '--strip-path-suffix' : ''}"
COPY_FRAMEWORKS_COMMAND ||= "#{CLI_COMMAND} link --apple '#{Pod::Config.instance.installation_root}'"

# We need to run this now to ensure the xcframeworks are copied vendored_frameworks are considered
XCFRAMEWORKS_DIR ||= File.join(__dir__, "xcframeworks")
Expand Down
283 changes: 188 additions & 95 deletions packages/host/src/node/babel-plugin/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,115 +1,208 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { describe, it, TestContext } from "node:test";
import path from "node:path";

import { transformFileSync } from "@babel/core";

import { plugin } from "./plugin.js";
import { plugin, PluginOptions } from "./plugin.js";
import { setupTempDirectory } from "../test-utils.js";
import { getLibraryName } from "../path-utils.js";

describe("plugin", () => {
it("transforms require calls, regardless", (context) => {
const tempDirectoryPath = setupTempDirectory(context, {
"package.json": `{ "name": "my-package" }`,
"addon-1.apple.node/addon-1.node":
"// This is supposed to be a binary file",
"addon-2.apple.node/addon-2.node":
"// This is supposed to be a binary file",
"addon-1.js": `
const addon = require('./addon-1.node');
console.log(addon);
`,
"addon-2.js": `
const addon = require('./addon-2.node');
console.log(addon);
`,
"sub-directory/addon-1.js": `
const addon = require('../addon-1.node');
console.log(addon);
`,
"addon-1-bindings.js": `
const addon = require('bindings')('addon-1');
console.log(addon);
`,
"require-js-file.js": `
const addon = require('./addon-1.js');
console.log(addon);
`,
});
type TestTransformationOptions = {
files: Record<string, string>;
inputFilePath: string;
assertion(code: string): void;
options?: PluginOptions;
};

const ADDON_1_REQUIRE_ARG = getLibraryName(
path.join(tempDirectoryPath, "addon-1"),
{ stripPathSuffix: false }
);
const ADDON_2_REQUIRE_ARG = getLibraryName(
path.join(tempDirectoryPath, "addon-2"),
{ stripPathSuffix: false }
function itTransforms(
title: string,
{ files, inputFilePath, assertion, options = {} }: TestTransformationOptions
) {
it(`transforms ${title}`, (context: TestContext) => {
const tempDirectoryPath = setupTempDirectory(context, files);
const result = transformFileSync(
path.join(tempDirectoryPath, inputFilePath),
{ plugins: [[plugin, options]] }
);
assert(result, "Expected transformation to produce a result");
const { code } = result;
assert(code, "Expected transformation to produce code");
assertion(code);
});
}

{
const result = transformFileSync(
path.join(tempDirectoryPath, "./addon-1.js"),
{ plugins: [[plugin, { stripPathSuffix: false }]] }
);
assert(result);
const { code } = result;
function assertIncludes(needle: string, { reverse = false } = {}) {
return (code: string) => {
if (reverse) {
assert(
code && code.includes(`requireNodeAddon("${ADDON_1_REQUIRE_ARG}")`),
`Unexpected code: ${code}`
!code.includes(needle),
`Expected code to not include: ${needle}, got ${code}`
);
}

{
const result = transformFileSync(
path.join(tempDirectoryPath, "./addon-2.js"),
{ plugins: [[plugin, { naming: "hash" }]] }
);
assert(result);
const { code } = result;
} else {
assert(
code && code.includes(`requireNodeAddon("${ADDON_2_REQUIRE_ARG}")`),
`Unexpected code: ${code}`
code.includes(needle),
`Expected code to include: ${needle}, got ${code}`
);
}
};
}

{
const result = transformFileSync(
path.join(tempDirectoryPath, "./sub-directory/addon-1.js"),
{ plugins: [[plugin, { naming: "hash" }]] }
);
assert(result);
const { code } = result;
assert(
code && code.includes(`requireNodeAddon("${ADDON_1_REQUIRE_ARG}")`),
`Unexpected code: ${code}`
);
}
describe("plugin", () => {
describe("transforming require(...) calls", () => {
itTransforms("a simple call", {
files: {
"package.json": `{ "name": "my-package" }`,
"my-addon.apple.node/my-addon.node":
"// This is supposed to be a binary file",
"index.js": `
const addon = require('./my-addon.node');
console.log(addon);
`,
},
inputFilePath: "index.js",
assertion: assertIncludes(`requireNodeAddon("my-package--my-addon")`),
});

{
const result = transformFileSync(
path.join(tempDirectoryPath, "./addon-1-bindings.js"),
{ plugins: [[plugin, { naming: "hash" }]] }
);
assert(result);
const { code } = result;
assert(
code && code.includes(`requireNodeAddon("${ADDON_1_REQUIRE_ARG}")`),
`Unexpected code: ${code}`
);
}
itTransforms("from sub-directory", {
files: {
"package.json": `{ "name": "my-package" }`,
"my-addon.apple.node/my-addon.node":
"// This is supposed to be a binary file",
"sub-dir/index.js": `
const addon = require('../my-addon.node');
console.log(addon);
`,
},
inputFilePath: "sub-dir/index.js",
assertion: assertIncludes(`requireNodeAddon("my-package--my-addon")`),
});

{
const result = transformFileSync(
path.join(tempDirectoryPath, "./require-js-file.js"),
{ plugins: [[plugin, { naming: "hash" }]] }
);
assert(result);
const { code } = result;
assert(
code && !code.includes(`requireNodeAddon`),
`Unexpected code: ${code}`
);
}
describe("in 'sub-dir'", () => {
itTransforms("a nested addon (keeping suffix)", {
files: {
"package.json": `{ "name": "my-package" }`,
"sub-dir/my-addon.apple.node/my-addon.node":
"// This is supposed to be a binary file",
"index.js": `
const addon = require('./sub-dir/my-addon.node');
console.log(addon);
`,
},
inputFilePath: "index.js",
options: { pathSuffix: "keep" },
assertion: assertIncludes(
`requireNodeAddon("my-package--sub-dir-my-addon")`
),
});

itTransforms("a nested addon (stripping suffix)", {
files: {
"package.json": `{ "name": "my-package" }`,
"sub-dir/my-addon.apple.node/my-addon.node":
"// This is supposed to be a binary file",
"index.js": `
const addon = require('./sub-dir/my-addon.node');
console.log(addon);
`,
},
inputFilePath: "index.js",
options: { pathSuffix: "strip" },
assertion: assertIncludes(`requireNodeAddon("my-package--my-addon")`),
});

itTransforms("a nested addon (omitting suffix)", {
files: {
"package.json": `{ "name": "my-package" }`,
"sub-dir/my-addon.apple.node/my-addon.node":
"// This is supposed to be a binary file",
"index.js": `
const addon = require('./sub-dir/my-addon.node');
console.log(addon);
`,
},
inputFilePath: "index.js",
options: { pathSuffix: "omit" },
assertion: assertIncludes(`requireNodeAddon("my-package")`),
});
});

itTransforms("and does not touch required JS files", {
files: {
"package.json": `{ "name": "my-package" }`,
// TODO: Add a ./my-addon.node to make this test complete
"my-addon.js": "// Some JS file",
"index.js": `
const addon = require('./my-addon');
console.log(addon);
`,
},
inputFilePath: "index.js",
assertion: assertIncludes("requireNodeAddon", { reverse: true }),
});
});

describe("transforming require('binding')(...) calls", () => {
itTransforms("a simple call", {
files: {
"package.json": `{ "name": "my-package" }`,
"my-addon.apple.node/my-addon.node":
"// This is supposed to be a binary file",
"index.js": `
const addon = require('bindings')('my-addon');
console.log(addon);
`,
},
inputFilePath: "index.js",
assertion: assertIncludes(`requireNodeAddon("my-package--my-addon")`),
});

describe("in 'build/Release'", () => {
itTransforms("a nested addon (keeping suffix)", {
files: {
"package.json": `{ "name": "my-package" }`,
"build/Release/my-addon.apple.node/my-addon.node":
"// This is supposed to be a binary file",
"index.js": `
const addon = require('bindings')('my-addon');
console.log(addon);
`,
},
inputFilePath: "index.js",
options: { pathSuffix: "keep" },
assertion: assertIncludes(
`requireNodeAddon("my-package--build-Release-my-addon")`
),
});

itTransforms("a nested addon (stripping suffix)", {
files: {
"package.json": `{ "name": "my-package" }`,
"build/Release/my-addon.apple.node/my-addon.node":
"// This is supposed to be a binary file",
"index.js": `
const addon = require('bindings')('my-addon');
console.log(addon);
`,
},
inputFilePath: "index.js",
options: { pathSuffix: "strip" },
assertion: assertIncludes(`requireNodeAddon("my-package--my-addon")`),
});

itTransforms("a nested addon (omitting suffix)", {
files: {
"package.json": `{ "name": "my-package" }`,
"build/Release/my-addon.apple.node/my-addon.node":
"// This is supposed to be a binary file",
"index.js": `
const addon = require('bindings')('my-addon');
console.log(addon);
`,
},
inputFilePath: "index.js",
options: { pathSuffix: "omit" },
assertion: assertIncludes(`requireNodeAddon("my-package")`),
});
});
});
});
Loading
Loading