Skip to content

Commit ad89f73

Browse files
committed
feat(core): add support for additional project directories
Add native workspace support for additional project roots alongside the main workspace root. This enables workspaces to define multiple project directories for better organization and flexibility. Changes include: - New Rust module for handling additional project directories - Updated workspace context to support multiple project roots - Extended project graph utilities to recognize additional directories - Updated package.json plugin creation with new API
1 parent 47c76de commit ad89f73

File tree

13 files changed

+180
-93
lines changed

13 files changed

+180
-93
lines changed

packages/devkit/src/utils/replace-project-configuration-with-plugin.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
updateProjectConfiguration,
1111
} from 'nx/src/devkit-exports';
1212
import { findProjectForPath, hashObject } from 'nx/src/devkit-internals';
13+
import { multiGlobInAdditionalProjectDirectories } from 'nx/src/native';
1314

1415
export async function replaceProjectConfigurationsWithPlugin<T = unknown>(
1516
tree: Tree,
@@ -37,9 +38,16 @@ export async function replaceProjectConfigurationsWithPlugin<T = unknown>(
3738
const [pluginGlob, createNodesFunction] = createNodes;
3839
const configFiles = glob(tree, [pluginGlob]);
3940

41+
const additionalProjectConfigurationFiles =
42+
multiGlobInAdditionalProjectDirectories(
43+
tree.root,
44+
nxJson.additionalProjectDirectories ?? [],
45+
[pluginGlob]
46+
)[0];
4047
const results = await createNodesFunction(configFiles, pluginOptions, {
4148
workspaceRoot: tree.root,
4249
nxJsonConfiguration: readNxJson(tree),
50+
additionalProjectConfigurationFiles,
4351
});
4452

4553
for (const [configFile, nodes] of results) {

packages/nx/bin/nx.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,8 @@ async function main() {
5959
process.env.NX_DAEMON = 'false';
6060
require('nx/src/command-line/nx-commands').commandsObject.argv;
6161
} else {
62-
const additionalProjectDirectories =
63-
readNxJson(workspace.dir).additionalProjectDirectories ?? [];
6462
if (!daemonClient.enabled() && workspace !== null) {
65-
setupWorkspaceContext(workspace.dir, additionalProjectDirectories);
63+
setupWorkspaceContext(workspace.dir);
6664
}
6765

6866
// polyfill rxjs observable to avoid issues with multiple version of Observable installed in node_modules

packages/nx/src/daemon/server/server.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -650,9 +650,7 @@ const handleOutputsChanges: FileWatcherCallback = async (err, changeEvents) => {
650650
};
651651

652652
export async function startServer(): Promise<Server> {
653-
const additionalProjectDirectories =
654-
readNxJson(workspaceRoot).additionalProjectDirectories ?? [];
655-
setupWorkspaceContext(workspaceRoot, additionalProjectDirectories);
653+
setupWorkspaceContext(workspaceRoot);
656654

657655
// Persist metadata about the background process so that it can be cleaned up later if needed
658656
await writeDaemonJsonProcessCache({

packages/nx/src/native/index.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export declare class Watcher {
135135

136136
export declare class WorkspaceContext {
137137
workspaceRoot: string
138-
constructor(workspaceRoot: string, additionalProjectDirectories: Array<string>, cacheDir: string)
138+
constructor(workspaceRoot: string, cacheDir: string)
139139
getWorkspaceFiles(projectRootMap: Record<string, string>): NxWorkspaceFiles
140140
glob(globs: Array<string>, exclude?: Array<string> | undefined | null): Array<string>
141141
/**
@@ -264,6 +264,10 @@ export declare export declare function isEditorInstalled(editor: SupportedEditor
264264

265265
export declare export declare function logDebug(message: string): void
266266

267+
export declare export declare function multiGlobInAdditionalProjectDirectories(workspaceRoot: string, additionalProjectDirectories: Array<string>, globs: Array<string>, exclude?: Array<string> | undefined | null): Array<Array<string>>
268+
269+
export declare export declare function multiHashGlobInAdditionalProjectDirectories(workspaceRoot: string, additionalProjectDirectories: Array<string>, globGroups: Array<Array<string>>): Array<string>
270+
267271
/** Stripped version of the NxJson interface for use in rust */
268272
export interface NxJson {
269273
namedInputs?: Record<string, Array<JsInputs>>

packages/nx/src/native/native-bindings.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,8 @@ module.exports.IS_WASM = nativeBinding.IS_WASM
397397
module.exports.isAiAgent = nativeBinding.isAiAgent
398398
module.exports.isEditorInstalled = nativeBinding.isEditorInstalled
399399
module.exports.logDebug = nativeBinding.logDebug
400+
module.exports.multiGlobInAdditionalProjectDirectories = nativeBinding.multiGlobInAdditionalProjectDirectories
401+
module.exports.multiHashGlobInAdditionalProjectDirectories = nativeBinding.multiHashGlobInAdditionalProjectDirectories
400402
module.exports.parseTaskStatus = nativeBinding.parseTaskStatus
401403
module.exports.remove = nativeBinding.remove
402404
module.exports.restoreTerminal = nativeBinding.restoreTerminal
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use std::path::{Path, PathBuf, Component};
2+
3+
use crate::native::glob::glob_files::glob_files;
4+
use crate::native::types::FileData;
5+
use crate::native::walker::{nx_walker, NxFile};
6+
use rayon::prelude::*;
7+
use xxhash_rust::xxh3;
8+
9+
fn join_paths(base: &Path, relative: &str) -> PathBuf {
10+
let mut path = base.to_path_buf();
11+
12+
for component in Path::new(relative).components() {
13+
match component {
14+
Component::ParentDir => {
15+
path.pop();
16+
}
17+
Component::CurDir => {}
18+
Component::Normal(c) => path.push(c),
19+
_ => {}
20+
}
21+
}
22+
23+
path
24+
}
25+
26+
pub fn get_files_in_additional_project_directories(
27+
workspace_root: String,
28+
additional_project_directories: Vec<String>,
29+
) -> Vec<Vec<NxFile>> {
30+
let root_path = PathBuf::from(&workspace_root);
31+
additional_project_directories
32+
.into_par_iter()
33+
.map(|additional_project_directory| {
34+
let joined_path = join_paths(&root_path, &additional_project_directory);
35+
let full_path = joined_path.to_string_lossy().to_string();
36+
nx_walker(&full_path, true).collect()
37+
})
38+
.collect()
39+
}
40+
41+
#[napi]
42+
pub fn multi_glob_in_additional_project_directories(
43+
workspace_root: String,
44+
additional_project_directories: Vec<String>,
45+
globs: Vec<String>,
46+
exclude: Option<Vec<String>>,
47+
) -> napi::Result<Vec<Vec<String>>> {
48+
let files = get_files_in_additional_project_directories(workspace_root, additional_project_directories);
49+
50+
globs
51+
.iter()
52+
.map(|glob| -> napi::Result<Vec<String>> {
53+
let mut result = Vec::new();
54+
for dir_files in &files {
55+
let file_data: Vec<FileData> = dir_files
56+
.iter()
57+
.map(|f| FileData {
58+
file: f.full_path.clone(),
59+
hash: String::new(),
60+
})
61+
.collect();
62+
let globbed_files: Vec<String> = glob_files(&file_data, vec![glob.clone()], exclude.clone())?
63+
.map(|f| f.file.to_owned())
64+
.collect();
65+
result.extend(globbed_files);
66+
}
67+
Ok(result)
68+
})
69+
.collect()
70+
}
71+
72+
#[napi]
73+
pub fn multi_hash_glob_in_additional_project_directories(
74+
workspace_root: String,
75+
additional_project_directories: Vec<String>,
76+
glob_groups: Vec<Vec<String>>,
77+
) -> napi::Result<Vec<String>> {
78+
let files = get_files_in_additional_project_directories(workspace_root, additional_project_directories);
79+
80+
glob_groups
81+
.iter()
82+
.map(|glob_group| -> napi::Result<String> {
83+
let mut hasher = xxh3::Xxh3::new();
84+
for dir_files in &files {
85+
let file_data: Vec<FileData> = dir_files
86+
.iter()
87+
.map(|f| FileData {
88+
file: f.full_path.clone(),
89+
hash: String::new(),
90+
})
91+
.collect();
92+
93+
let globbed_files: Vec<_> =
94+
glob_files(&file_data, glob_group.clone(), None)?.collect();
95+
for file in globbed_files {
96+
hasher.update(file.file.as_bytes());
97+
hasher.update(file.hash.as_bytes());
98+
}
99+
}
100+
101+
Ok(hasher.digest().to_string())
102+
})
103+
.collect()
104+
}

packages/nx/src/native/workspace/context.rs

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ pub struct WorkspaceContext {
3030

3131
type Files = Vec<(PathBuf, String)>;
3232

33-
fn gather_and_hash_files(directory: &Path, cache_dir: &String) -> Vec<(PathBuf, String)> {
34-
let archived_files = read_files_archive(cache_dir);
33+
fn gather_and_hash_files(workspace_root: &Path, cache_dir: String) -> Vec<(PathBuf, String)> {
34+
let archived_files = read_files_archive(&cache_dir);
3535

36-
trace!("Gathering files in {}", directory.display());
36+
trace!("Gathering files in {}", workspace_root.display());
3737
let now = std::time::Instant::now();
3838
let file_hashes = if let Some(archived_files) = archived_files {
39-
selective_files_hash(directory, archived_files)
39+
selective_files_hash(workspace_root, archived_files)
4040
} else {
41-
full_files_hash(directory)
41+
full_files_hash(workspace_root)
4242
};
4343

4444
let mut files = file_hashes
@@ -57,11 +57,7 @@ fn gather_and_hash_files(directory: &Path, cache_dir: &String) -> Vec<(PathBuf,
5757
struct FilesWorker(Option<Arc<(NxMutex<Files>, NxCondvar)>>);
5858
impl FilesWorker {
5959
#[cfg(not(target_arch = "wasm32"))]
60-
fn gather_files(
61-
workspace_root: &Path,
62-
additional_project_directories: Vec<PathBuf>,
63-
cache_dir: String,
64-
) -> Self {
60+
fn gather_files(workspace_root: &Path, cache_dir: String) -> Self {
6561
if !workspace_root.exists() {
6662
warn!(
6763
"workspace root does not exist: {}",
@@ -79,14 +75,7 @@ impl FilesWorker {
7975
trace!("Initially locking files");
8076
let mut workspace_files = lock.lock().expect("Should be the first time locking files");
8177

82-
let mut files = gather_and_hash_files(&workspace_root, &cache_dir);
83-
84-
for additional_project_directory in additional_project_directories {
85-
let additional_files =
86-
gather_and_hash_files(&additional_project_directory, &cache_dir);
87-
88-
files.extend(additional_files);
89-
}
78+
let files = gather_and_hash_files(&workspace_root, cache_dir);
9079

9180
*workspace_files = files;
9281
let files_len = workspace_files.len();
@@ -211,27 +200,15 @@ impl FilesWorker {
211200
#[napi]
212201
impl WorkspaceContext {
213202
#[napi(constructor)]
214-
pub fn new(
215-
workspace_root: String,
216-
additional_project_directories: Vec<String>,
217-
cache_dir: String,
218-
) -> Self {
203+
pub fn new(workspace_root: String, cache_dir: String) -> Self {
219204
enable_logger();
220205

221206
trace!(?workspace_root);
222207

223208
let workspace_root_path = PathBuf::from(&workspace_root);
224-
let additional_project_directories = additional_project_directories
225-
.iter()
226-
.map(|s| PathBuf::from(s))
227-
.collect::<Vec<PathBuf>>();
228209

229210
WorkspaceContext {
230-
files_worker: FilesWorker::gather_files(
231-
&workspace_root_path,
232-
additional_project_directories,
233-
cache_dir.clone(),
234-
),
211+
files_worker: FilesWorker::gather_files(&workspace_root_path, cache_dir.clone()),
235212
workspace_root,
236213
workspace_root_path,
237214
}

packages/nx/src/native/workspace/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod files_archive;
99
mod files_hashing;
1010
pub mod types;
1111
pub mod workspace_files;
12+
pub mod additional_project_directories;
1213

1314
#[napi]
1415
// should only be used in tests to transfer the file map from the JS world to the Rust world

packages/nx/src/plugins/package-json/create-nodes.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { minimatch } from 'minimatch';
22
import { existsSync } from 'node:fs';
3-
import { dirname, join } from 'node:path';
3+
import { dirname, join, basename, resolve, relative } from 'node:path';
44

55
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
66
import type { ProjectConfiguration } from '../../config/workspace-json-project-json';
@@ -21,7 +21,6 @@ import {
2121
createNodesFromFiles,
2222
CreateNodesV2,
2323
} from '../../project-graph/plugins';
24-
import { basename } from 'path';
2524
import { hashObject } from '../../hasher/file-hasher';
2625
import {
2726
PackageJsonConfigurationCache,
@@ -36,7 +35,9 @@ export const createNodesV2: CreateNodesV2 = [
3635
'**/project.json'
3736
),
3837
(configFiles, _, context) => {
39-
const { packageJsons, projectJsonRoots } = splitConfigFiles(configFiles);
38+
const { packageJsons, projectJsonRoots } = splitConfigFiles(
39+
configFiles.concat(context.additionalProjectConfigurationFiles)
40+
);
4041

4142
const readJson = (f) => readJsonFile(join(context.workspaceRoot, f));
4243
const isInPackageJsonWorkspaces =
@@ -52,8 +53,9 @@ export const createNodesV2: CreateNodesV2 = [
5253

5354
return createNodesFromFiles(
5455
(packageJsonPath, options, context) => {
55-
const isInPackageManagerWorkspaces =
56-
isInPackageJsonWorkspaces(packageJsonPath);
56+
const isInPackageManagerWorkspaces = isInPackageJsonWorkspaces(
57+
relative(context.workspaceRoot, packageJsonPath)
58+
);
5759
if (
5860
!isInPackageManagerWorkspaces &&
5961
!isNextToProjectJson(packageJsonPath)
@@ -137,7 +139,7 @@ export function createNodeFromPackageJson(
137139
cache: PackageJsonConfigurationCache,
138140
isInPackageManagerWorkspaces: boolean
139141
) {
140-
const json: PackageJson = readJsonFile(join(workspaceRoot, pkgJsonPath));
142+
const json: PackageJson = readJsonFile(resolve(workspaceRoot, pkgJsonPath));
141143

142144
const projectRoot = dirname(pkgJsonPath);
143145

@@ -180,8 +182,9 @@ export function buildProjectConfigurationFromPackageJson(
180182
nxJson: NxJsonConfiguration,
181183
isInPackageManagerWorkspaces: boolean
182184
): ProjectConfiguration & { name: string } {
183-
const normalizedPath = packageJsonPath.split('\\').join('/');
184-
const projectRoot = dirname(normalizedPath);
185+
const projectRoot = dirname(relative(workspaceRoot, packageJsonPath))
186+
.split('\\')
187+
.join('/');
185188

186189
const siblingProjectJson = tryReadJson<ProjectConfiguration>(
187190
join(workspaceRoot, projectRoot, 'project.json')
@@ -208,7 +211,7 @@ export function buildProjectConfigurationFromPackageJson(
208211
);
209212
}
210213

211-
let name = packageJson.name ?? toProjectName(normalizedPath);
214+
let name = packageJson.name ?? toProjectName(projectRoot);
212215

213216
const projectConfiguration: ProjectConfiguration & { name: string } = {
214217
root: projectRoot,

packages/nx/src/project-graph/plugins/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { TaskResults } from '../../tasks-runner/life-cycle';
1616
export interface CreateNodesContextV2 {
1717
readonly nxJsonConfiguration: NxJsonConfiguration;
1818
readonly workspaceRoot: string;
19+
readonly additionalProjectConfigurationFiles?: string[];
1920
}
2021

2122
export type CreateNodesResultV2 = Array<

0 commit comments

Comments
 (0)