Skip to content
Open
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
98 changes: 98 additions & 0 deletions __test__/workspacePaths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, test } from "@jest/globals";
import fs from "fs";
import os from "os";
import path from "path";
import { resolveViroAndroidRelativePath } from "../plugins/withViroAndroid";
import { resolveViroIosRelativePath } from "../plugins/withViroIos";

function createPackageAt(packageRoot: string): void {
fs.mkdirSync(path.join(packageRoot, "android"), { recursive: true });
fs.mkdirSync(path.join(packageRoot, "ios"), { recursive: true });
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "@reactvision/react-viro", version: "0.0.0" })
);
}

describe("workspace path resolution", () => {
function tempRoot(): string {
// require.resolve resolves the real path; on macOS /var is a symlink to
// /private/var, so build under realpath to keep both sides consistent.
return fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "viro-plugin-"));
}

test("resolves android path for a hoisted monorepo layout", () => {
const root = tempRoot();
try {
// App lives at <root>/apps/assisteo, package hoisted to <root>/node_modules
const app = path.join(root, "apps", "assisteo");
createPackageAt(path.join(root, "node_modules", "@reactvision", "react-viro"));
expect(resolveViroAndroidRelativePath(app, path.join(app, "android"))).toBe(
"../../../node_modules/@reactvision/react-viro/android"
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});

test("resolves android path for a flat (non-workspace) layout", () => {
const root = tempRoot();
try {
const app = path.join(root, "app");
createPackageAt(path.join(app, "node_modules", "@reactvision", "react-viro"));
expect(resolveViroAndroidRelativePath(app, path.join(app, "android"))).toBe(
"../node_modules/@reactvision/react-viro/android"
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});

test("falls back when the package cannot be resolved", () => {
const root = tempRoot();
try {
expect(resolveViroAndroidRelativePath(root, path.join(root, "android"))).toBe(
"../node_modules/@reactvision/react-viro/android"
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});

test("resolves ios path for a hoisted monorepo layout", () => {
const root = tempRoot();
try {
const app = path.join(root, "apps", "assisteo");
createPackageAt(path.join(root, "node_modules", "@reactvision", "react-viro"));
expect(resolveViroIosRelativePath(app, path.join(app, "ios"))).toBe(
"../../../node_modules/@reactvision/react-viro/ios"
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});

test("resolves ios path for a flat (non-workspace) layout", () => {
const root = tempRoot();
try {
const app = path.join(root, "app");
createPackageAt(path.join(app, "node_modules", "@reactvision", "react-viro"));
expect(resolveViroIosRelativePath(app, path.join(app, "ios"))).toBe(
"../node_modules/@reactvision/react-viro/ios"
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});

test("falls back when the package cannot be resolved", () => {
const root = tempRoot();
try {
expect(resolveViroIosRelativePath(root, path.join(root, "ios"))).toBe(
"../node_modules/@reactvision/react-viro/ios"
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
});
1 change: 1 addition & 0 deletions dist/plugins/withViroAndroid.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ConfigPlugin } from "@expo/config-plugins";
import { ViroConfigurationOptions } from "./withViro";
export declare function resolveViroAndroidRelativePath(projectRoot: string, androidRoot: string): string;
export declare const withViroAndroid: ConfigPlugin<ViroConfigurationOptions>;
27 changes: 22 additions & 5 deletions dist/plugins/withViroAndroid.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.withViroAndroid = void 0;
exports.withViroAndroid = exports.resolveViroAndroidRelativePath = void 0;
const config_plugins_1 = require("@expo/config-plugins");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
Expand Down Expand Up @@ -135,15 +135,32 @@ const withViroAppBuildGradle = (config) => (0, config_plugins_1.withAppBuildGrad
return config;
});
const withViroSettingsGradle = (config) => (0, config_plugins_1.withSettingsGradle)(config, async (config) => {
const viroAndroidRoot = resolveViroAndroidRelativePath(config.modRequest.projectRoot, config.modRequest.platformProjectRoot);
config.modResults.contents += `
include ':react_viro', ':arcore_client', ':gvr_common', ':viro_renderer'
project(':arcore_client').projectDir = new File('../node_modules/@reactvision/react-viro/android/arcore_client')
project(':gvr_common').projectDir = new File('../node_modules/@reactvision/react-viro/android/gvr_common')
project(':viro_renderer').projectDir = new File('../node_modules/@reactvision/react-viro/android/viro_renderer')
project(':react_viro').projectDir = new File('../node_modules/@reactvision/react-viro/android/react_viro')
project(':arcore_client').projectDir = new File('${viroAndroidRoot}/arcore_client')
project(':gvr_common').projectDir = new File('${viroAndroidRoot}/gvr_common')
project(':viro_renderer').projectDir = new File('${viroAndroidRoot}/viro_renderer')
project(':react_viro').projectDir = new File('${viroAndroidRoot}/react_viro')
`;
return config;
});
function resolveViroAndroidRelativePath(projectRoot, androidRoot) {
const fallback = "../node_modules/@reactvision/react-viro/android";
try {
const pkgJson = require.resolve("@reactvision/react-viro/package.json", {
paths: [projectRoot],
});
const viroAndroidDir = path_1.default.join(path_1.default.dirname(pkgJson), "android");
// Gradle resolves relative File(...) against the settings.gradle dir (androidRoot).
// Always emit POSIX separators — Gradle accepts them on every platform.
return path_1.default.relative(androidRoot, viroAndroidDir).split(path_1.default.sep).join("/");
}
catch {
return fallback;
}
}
exports.resolveViroAndroidRelativePath = resolveViroAndroidRelativePath;
const withViroManifest = (config) => (0, config_plugins_1.withAndroidManifest)(config, async (newConfig) => {
const contents = newConfig.modResults;
contents.manifest.$["xmlns:tools"] = "http://schemas.android.com/tools";
Expand Down
1 change: 1 addition & 0 deletions dist/plugins/withViroIos.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ConfigPlugin } from "@expo/config-plugins";
import { ViroConfigurationOptions } from "./withViro";
export declare function resolveViroIosRelativePath(projectRoot: string, iosRoot: string): string;
export declare const withDefaultInfoPlist: ConfigPlugin<ViroConfigurationOptions>;
export declare const withViroIos: ConfigPlugin<ViroConfigurationOptions>;
25 changes: 22 additions & 3 deletions dist/plugins/withViroIos.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,34 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.withViroIos = exports.withDefaultInfoPlist = void 0;
exports.withViroIos = exports.withDefaultInfoPlist = exports.resolveViroIosRelativePath = void 0;
const config_plugins_1 = require("@expo/config-plugins");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const insertLinesHelper_1 = require("./util/insertLinesHelper");
const withViro_1 = require("./withViro");
function resolveViroIosRelativePath(projectRoot, iosRoot) {
const fallback = "../node_modules/@reactvision/react-viro/ios";
try {
const pkgJson = require.resolve("@reactvision/react-viro/package.json", {
paths: [projectRoot],
});
// CocoaPods resolves :path relative to the directory of the Podfile (iosRoot).
// Always emit POSIX separators (split on the current platform's sep and
// re-join with "/") so the generated paths are valid on Windows too.
return path_1.default.relative(iosRoot, path_1.default.join(path_1.default.dirname(pkgJson), "ios")).split(path_1.default.sep).join("/");
}
catch {
return fallback;
}
}
exports.resolveViroIosRelativePath = resolveViroIosRelativePath;
const withViroPods = (config) => {
config = (0, config_plugins_1.withDangerousMod)(config, [
"ios",
async (newConfig) => {
const root = newConfig.modRequest.platformProjectRoot;
const projectRoot = newConfig.modRequest.projectRoot;
// Check plugin configuration options
let cloudAnchorProvider;
let geospatialAnchorProvider;
Expand All @@ -36,10 +54,11 @@ const withViroPods = (config) => {
}
fs_1.default.readFile(`${root}/Podfile`, "utf-8", (err, data) => {
// ViroReact with integrated Fabric support
const viroIosRoot = resolveViroIosRelativePath(projectRoot, root);
let viroPods = ` # ViroReact with integrated New Architecture (Fabric) support\n` +
` # Automatically includes Fabric components when RCT_NEW_ARCH_ENABLED=1\n` +
` pod 'ViroReact', :path => '../node_modules/@reactvision/react-viro/ios'\n` +
` pod 'ViroKit', :path => '../node_modules/@reactvision/react-viro/ios/dist/ViroRenderer/'`;
` pod 'ViroReact', :path => '${viroIosRoot}'\n` +
` pod 'ViroKit', :path => '${viroIosRoot}/dist/ViroRenderer/'`;
// Add ARCore pods if enabled (explicitly via includeARCore/includeSemantics or implicitly via providers)
// ViroKit.podspec declares these as weak_frameworks, making ARCore optional at runtime
const needsARCoreForFeatures = cloudAnchorProvider === "arcore" || geospatialAnchorProvider === "arcore";
Expand Down
38 changes: 34 additions & 4 deletions plugins/withViroAndroid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,14 +211,44 @@ const withViroAppBuildGradle = (config: ExpoConfig) =>
return config;
});

/**
* Resolve the on-disk `@reactvision/react-viro/android` directory as a path
* relative to the Android project root (where `settings.gradle` lives).
*
* Hardcoding `../node_modules/...` assumes react-viro is installed in the app's
* own `node_modules`. That is false under pnpm/yarn workspaces (and npm
* workspaces), where the package is hoisted to the monorepo root or nested
* under `.pnpm`, so Gradle fails with "Configuring project ':gvr_common'
* without an existing directory is not allowed". Resolving via Node follows
* symlinks/hoisting and works in both flat and workspace layouts.
*/
export function resolveViroAndroidRelativePath(projectRoot: string, androidRoot: string): string {
const fallback = "../node_modules/@reactvision/react-viro/android";
try {
const pkgJson = require.resolve("@reactvision/react-viro/package.json", {
paths: [projectRoot],
});
const viroAndroidDir = path.join(path.dirname(pkgJson), "android");
// Gradle resolves relative File(...) against the settings.gradle dir (androidRoot).
// Always emit POSIX separators — Gradle accepts them on every platform.
return path.relative(androidRoot, viroAndroidDir).split(path.sep).join("/");
} catch {
return fallback;
}
}

const withViroSettingsGradle = (config: ExpoConfig) =>
withSettingsGradle(config, async (config) => {
const viroAndroidRoot = resolveViroAndroidRelativePath(
config.modRequest.projectRoot,
config.modRequest.platformProjectRoot
);
config.modResults.contents += `
include ':react_viro', ':arcore_client', ':gvr_common', ':viro_renderer'
project(':arcore_client').projectDir = new File('../node_modules/@reactvision/react-viro/android/arcore_client')
project(':gvr_common').projectDir = new File('../node_modules/@reactvision/react-viro/android/gvr_common')
project(':viro_renderer').projectDir = new File('../node_modules/@reactvision/react-viro/android/viro_renderer')
project(':react_viro').projectDir = new File('../node_modules/@reactvision/react-viro/android/react_viro')
project(':arcore_client').projectDir = new File('${viroAndroidRoot}/arcore_client')
project(':gvr_common').projectDir = new File('${viroAndroidRoot}/gvr_common')
project(':viro_renderer').projectDir = new File('${viroAndroidRoot}/viro_renderer')
project(':react_viro').projectDir = new File('${viroAndroidRoot}/react_viro')
`;
return config;
});
Expand Down
33 changes: 31 additions & 2 deletions plugins/withViroIos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,42 @@ import {
} from "@expo/config-plugins";
import { ExpoConfig } from "@expo/config-types";
import fs from "fs";
import path from "path";
import { insertLinesHelper } from "./util/insertLinesHelper";
import { DEFAULTS, ViroConfigurationOptions } from "./withViro";

/**
* Resolve the on-disk `@reactvision/react-viro/ios` directory as a path
* relative to the iOS project root (where the Podfile lives).
*
* Hardcoding `../node_modules/...` assumes react-viro is installed in the app's
* own `node_modules`. That is false under pnpm/yarn workspaces (and npm
* workspaces), where the package is hoisted to the monorepo root or nested
* under `.pnpm`, so `pod install` fails with "No podspec found for ViroKit".
* Resolving via Node follows symlinks/hoisting and works in both flat and
* workspace layouts.
*/
export function resolveViroIosRelativePath(projectRoot: string, iosRoot: string): string {
const fallback = "../node_modules/@reactvision/react-viro/ios";
try {
const pkgJson = require.resolve("@reactvision/react-viro/package.json", {
paths: [projectRoot],
});
// CocoaPods resolves :path relative to the directory of the Podfile (iosRoot).
// Always emit POSIX separators (split on the current platform's sep and
// re-join with "/") so the generated paths are valid on Windows too.
return path.relative(iosRoot, path.join(path.dirname(pkgJson), "ios")).split(path.sep).join("/");
} catch {
return fallback;
}
}

const withViroPods = (config: ExpoConfig) => {
config = withDangerousMod(config, [
"ios",
async (newConfig) => {
const root = newConfig.modRequest.platformProjectRoot;
const projectRoot = newConfig.modRequest.projectRoot;

// Check plugin configuration options
let cloudAnchorProvider: string | undefined;
Expand Down Expand Up @@ -44,11 +72,12 @@ const withViroPods = (config: ExpoConfig) => {

fs.readFile(`${root}/Podfile`, "utf-8", (err, data) => {
// ViroReact with integrated Fabric support
const viroIosRoot = resolveViroIosRelativePath(projectRoot, root);
let viroPods =
` # ViroReact with integrated New Architecture (Fabric) support\n` +
` # Automatically includes Fabric components when RCT_NEW_ARCH_ENABLED=1\n` +
` pod 'ViroReact', :path => '../node_modules/@reactvision/react-viro/ios'\n` +
` pod 'ViroKit', :path => '../node_modules/@reactvision/react-viro/ios/dist/ViroRenderer/'`;
` pod 'ViroReact', :path => '${viroIosRoot}'\n` +
` pod 'ViroKit', :path => '${viroIosRoot}/dist/ViroRenderer/'`;

// Add ARCore pods if enabled (explicitly via includeARCore/includeSemantics or implicitly via providers)
// ViroKit.podspec declares these as weak_frameworks, making ARCore optional at runtime
Expand Down