Skip to content
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

MultiRoot Workspace support #663

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions packages/vscode-extension/src/common/LaunchConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type LaunchConfigurationOptions = {

export interface LaunchConfigEventMap {
launchConfigChange: LaunchConfigurationOptions;
applicationRootsChanged: void;
}

export interface LaunchConfigEventListener<T> {
Expand All @@ -44,10 +45,14 @@ export type LaunchConfigUpdater = <K extends keyof LaunchConfigurationOptions>(
value: LaunchConfigurationOptions[K] | "Auto"
) => void;

export type AddCustomApplicationRoot = (appRoot: string) => void;

export interface LaunchConfig {
getConfig(): Promise<LaunchConfigurationOptions>;
update: LaunchConfigUpdater;
addCustomApplicationRoot: AddCustomApplicationRoot;
getAvailableXcodeSchemes(): Promise<string[]>;
getAvailableApplicationRoots(): Promise<string[]>;
addListener<K extends keyof LaunchConfigEventMap>(
eventType: K,
listener: LaunchConfigEventListener<LaunchConfigEventMap[K]>
Expand Down
182 changes: 34 additions & 148 deletions packages/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import fs from "fs";
import {
commands,
languages,
debug,
window,
workspace,
Uri,
ExtensionContext,
ExtensionMode,
ConfigurationChangeEvent,
Expand All @@ -20,16 +18,16 @@ import { DebugConfigProvider } from "./providers/DebugConfigProvider";
import { DebugAdapterDescriptorFactory } from "./debugging/DebugAdapterDescriptorFactory";
import { Logger, enableDevModeLogging } from "./Logger";
import {
configureAppRootFolder,
extensionContext,
setAppRootFolder,
getAppRootFolder,
getCurrentLaunchConfig,
setExtensionContext,
} from "./utilities/extensionContext";
import { setupPathEnv } from "./utilities/subprocess";
import { SidePanelViewProvider } from "./panels/SidepanelViewProvider";
import { PanelLocation } from "./common/WorkspaceConfig";
import { getLaunchConfiguration } from "./utilities/launchConfiguration";
import { Project } from "./project/project";
import { findFilesInWorkspace, isWorkspaceRoot } from "./utilities/common";
import { Platform } from "./utilities/platform";
import { migrateOldBuildCachesToNewStorage } from "./builders/BuildCache";

Expand Down Expand Up @@ -222,29 +220,44 @@ export async function activate(context: ExtensionContext) {
)
);

const setupAppRoot = async () => {
const appRoot = await configureAppRootFolder();
if (!appRoot) {
return;
}

if (Platform.OS === "macos") {
try {
await setupPathEnv(appRoot);
} catch (error) {
window.showWarningMessage(
"Error when setting up PATH environment variable, RN IDE may not work correctly.",
"Dismiss"
);
}
}
};

context.subscriptions.push(
workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
workspace.onDidChangeConfiguration(async (event: ConfigurationChangeEvent) => {
if (event.affectsConfiguration("RadonIDE.panelLocation")) {
showIDEPanel();
}
if (event.affectsConfiguration("launch")) {
const config = getCurrentLaunchConfig();
const oldAppRoot = getAppRootFolder();
if (config.appRoot === oldAppRoot) {
return;
}
await setupAppRoot();

// restart here
Project.currentProject?.reload("reboot");
}
})
);

const appRoot = await configureAppRootFolder();
if (!appRoot) {
return;
}

if (Platform.OS === "macos") {
try {
await setupPathEnv(appRoot);
} catch (error) {
window.showWarningMessage(
"Error when setting up PATH environment variable, RN IDE may not work correctly.",
"Dismiss"
);
}
}
await setupAppRoot();

// this needs to be run after app root is set
migrateOldBuildCachesToNewStorage();
Expand All @@ -269,133 +282,6 @@ function extensionActivated() {
}
}

async function configureAppRootFolder() {
const appRootFolder = await findAppRootFolder();
if (appRootFolder) {
Logger.info(`Found app root folder: ${appRootFolder}`);
setAppRootFolder(appRootFolder);
commands.executeCommand("setContext", "RNIDE.extensionIsActive", true);
}
return appRootFolder;
}

async function findAppRootCandidates(): Promise<string[]> {
const candidates: string[] = [];

const metroConfigUris = await findFilesInWorkspace("**/metro.config.{js,ts}", "**/node_modules");
metroConfigUris.forEach((metroConfigUri) => {
candidates.push(Uri.joinPath(metroConfigUri, "..").fsPath);
});

const appConfigUris = await findFilesInWorkspace("**/app.config.{js,ts}", "**/node_modules");
appConfigUris.forEach((appConfigUri) => {
const appRootFsPath = Uri.joinPath(appConfigUri, "..").fsPath;
if (!candidates.includes(appRootFsPath)) {
candidates.push(appRootFsPath);
}
});

// given that if the user uses workspaces his node_modules are installed not in the root of an application,
// but in the root of the workspace we need to detect workspaces root and exclude it.
let excludePattern = null;
workspace.workspaceFolders?.forEach((folder) => {
if (isWorkspaceRoot(folder.uri.fsPath)) {
excludePattern = "node_modules/react-native/package.json";
}
});

const rnPackageLocations = await findFilesInWorkspace(
"**/node_modules/react-native/package.json",
excludePattern
);
rnPackageLocations.forEach((rnPackageLocation) => {
const appRootFsPath = Uri.joinPath(rnPackageLocation, "../../..").fsPath;
if (!candidates.includes(appRootFsPath)) {
candidates.push(appRootFsPath);
}
});

// app json is often used in non react-native projects, but in worst case scenario we can use it as a fallback
const appJsonUris = await findFilesInWorkspace("**/app.json", "**/node_modules");
appJsonUris.forEach((appJsonUri) => {
const appRootFsPath = Uri.joinPath(appJsonUri, "..").fsPath;
if (!candidates.includes(appRootFsPath)) {
candidates.push(appRootFsPath);
}
});

return candidates;
}

async function findAppRootFolder() {
const launchConfiguration = getLaunchConfiguration();
const appRootFromLaunchConfig = launchConfiguration.appRoot;
if (appRootFromLaunchConfig) {
let appRoot: string | undefined;
workspace.workspaceFolders?.forEach((folder) => {
const possibleAppRoot = Uri.joinPath(folder.uri, appRootFromLaunchConfig).fsPath;
if (fs.existsSync(possibleAppRoot)) {
appRoot = possibleAppRoot;
}
});
if (!appRoot) {
// when relative app location setting is set, we expect app root exists
const openLaunchConfigButton = "Open Launch Configuration";
window
.showErrorMessage(
`The app root folder does not exist in the workspace at ${appRootFromLaunchConfig}.`,
openLaunchConfigButton
)
.then((item) => {
if (item === openLaunchConfigButton) {
commands.executeCommand("workbench.action.debug.configure");
}
});
return undefined;
}
return appRoot;
}

const appRootCandidates = await findAppRootCandidates();

if (appRootCandidates.length > 1) {
const openLaunchConfigButton = "Open Launch Configuration";
window
.showWarningMessage(
`Multiple react-native applications were detected in the workspace. "${appRootCandidates[0]}" was automatically chosen as your application root. To change that or remove this warning in the future, you can setup a permanent appRoot in Launch Configuration.`,
openLaunchConfigButton
)
.then((item) => {
if (item === openLaunchConfigButton) {
commands.executeCommand("workbench.action.debug.configure");
}
});
}

if (appRootCandidates.length > 0) {
return appRootCandidates[0];
}

const manageLaunchConfigButton = "Manage Launch Configuration";
window
.showErrorMessage(
`
Radon IDE couldn't find root application folder in this workspace.\n
Please make sure that the opened workspace contains a valid React Native or Expo project.\n
The way extension verifies the project is by looking for either: app.json, metro.config.js,
or node_modules/react-native folder. If your project structure is different, you can set the
app root using launch configuration.`,
manageLaunchConfigButton,
"Dismiss"
)
.then((item) => {
if (item === manageLaunchConfigButton) {
commands.executeCommand("debug.addConfiguration");
}
});
return undefined;
}

async function openDevMenu() {
Project.currentProject?.openDevMenu();
}
Expand Down
67 changes: 43 additions & 24 deletions packages/vscode-extension/src/panels/LaunchConfigController.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from "path";
import { EventEmitter } from "stream";
import { ConfigurationChangeEvent, workspace, Disposable } from "vscode";
import {
Expand All @@ -6,46 +7,32 @@ import {
LaunchConfigEventMap,
LaunchConfigurationOptions,
} from "../common/LaunchConfig";
import { getAppRootFolder } from "../utilities/extensionContext";
import {
extensionContext,
findAppRootCandidates,
getAppRootFolder,
getCurrentLaunchConfig,
} from "../utilities/extensionContext";
import { findXcodeProject, findXcodeScheme } from "../utilities/xcode";
import { Logger } from "../Logger";
import { getIosSourceDir } from "../builders/buildIOS";

const CUSTOM_APPLICATION_ROOTS_KEY = "custom_application_roots_key";

export class LaunchConfigController implements Disposable, LaunchConfig {
private config: LaunchConfigurationOptions;
private eventEmitter = new EventEmitter();
private configListener: Disposable;

constructor() {
const getCurrentConfig = (): LaunchConfigurationOptions => {
const launchConfiguration = workspace.getConfiguration(
"launch",
workspace.workspaceFolders![0].uri
);

const configurations = launchConfiguration.get<Array<Record<string, any>>>("configurations")!;

const RNIDEConfiguration = configurations.find(
({ type }) => type === "react-native-ide" || type === "radon-ide" // for compatibility we want to support old configuration type name
);

if (!RNIDEConfiguration) {
return {};
}

const { android, appRoot, ios, isExpo, metroConfigPath, env, eas } = RNIDEConfiguration;

return { android, appRoot, ios, isExpo, metroConfigPath, env, eas };
};

this.config = getCurrentConfig();
this.config = getCurrentLaunchConfig();

this.configListener = workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
if (!event.affectsConfiguration("launch")) {
return;
}

this.config = getCurrentConfig();
this.config = getCurrentLaunchConfig();

this.eventEmitter.emit("launchConfigChange", this.config);
});
Expand Down Expand Up @@ -88,6 +75,38 @@ export class LaunchConfigController implements Disposable, LaunchConfig {
await configurations.update("configurations", newConfigurations);
}

async addCustomApplicationRoot(appRoot: string) {
const oldCustomApplicationRoots =
extensionContext.workspaceState.get<string[] | undefined>(CUSTOM_APPLICATION_ROOTS_KEY) ?? [];

const newCustomApplicationRoots = [...oldCustomApplicationRoots, appRoot];

extensionContext.workspaceState.update(
CUSTOM_APPLICATION_ROOTS_KEY,
newCustomApplicationRoots
) ?? [];

this.eventEmitter.emit("applicationRootsChanged");
}

async getAvailableApplicationRoots() {
const workspacePath = workspace.workspaceFolders![0].uri.fsPath;
const applicationRootsCandidates = (await findAppRootCandidates()).map((candidate) => {
return "./" + path.relative(workspacePath, candidate);
});
const customApplicationRoots =
extensionContext.workspaceState.get<string[] | undefined>(CUSTOM_APPLICATION_ROOTS_KEY) ?? [];

const applicationRoots = [...applicationRootsCandidates, ...customApplicationRoots];

if (!applicationRoots) {
Logger.debug(`Could not find any application roots.`);
return [];
}

return applicationRoots;
}

async getAvailableXcodeSchemes() {
const appRootFolder = getAppRootFolder();
const sourceDir = getIosSourceDir(appRootFolder);
Expand Down
Loading
Loading