From 831bdc7d2bb53a2f8efeb6d3bda57c6131f0a5ec Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:54:51 -0500 Subject: [PATCH] refactor: improve `findUp` utility correctness and performance This change modernizes the `findUp` utility across the codebase. Replaced `path.parse().root` with `path.dirname(dir) === dir` check. Introduced an asynchronous version (`findUp`) in the CLI utility and renamed the synchronous version to `findUpSync` to align with Node.js conventions. Updated `packages/angular/cli/src/utilities/config.ts` to use the asynchronous `findUp` for non-blocking configuration discovery. --- packages/angular/cli/src/utilities/config.ts | 14 ++--- packages/angular/cli/src/utilities/find-up.ts | 62 ++++++++++++++++--- packages/angular/cli/src/utilities/project.ts | 4 +- .../angular_devkit/architect/bin/architect.ts | 18 +++--- .../schematics_cli/bin/schematics.ts | 18 +++--- 5 files changed, 81 insertions(+), 35 deletions(-) diff --git a/packages/angular/cli/src/utilities/config.ts b/packages/angular/cli/src/utilities/config.ts index 25f8dfb2f896..dfe21fa96692 100644 --- a/packages/angular/cli/src/utilities/config.ts +++ b/packages/angular/cli/src/utilities/config.ts @@ -11,7 +11,7 @@ import { existsSync, promises as fs } from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { PackageManager } from '../../lib/config/workspace-schema'; -import { findUp } from './find-up'; +import { findUp, findUpSync } from './find-up'; import { JSONFile, readAndParseJson } from './json-file'; function isJsonObject(value: json.JsonValue | undefined): value is json.JsonObject { @@ -70,13 +70,13 @@ function xdgConfigHomeOld(home: string): string { return path.join(p, '.angular-config.json'); } -function projectFilePath(projectPath?: string): string | null { +async function projectFilePath(projectPath?: string): Promise { // Find the configuration, either where specified, in the Angular CLI project // (if it's in node_modules) or from the current process. return ( - (projectPath && findUp(configNames, projectPath)) || - findUp(configNames, process.cwd()) || - findUp(configNames, __dirname) + (projectPath && (await findUp(configNames, projectPath))) || + (await findUp(configNames, process.cwd())) || + (await findUp(configNames, __dirname)) ); } @@ -181,7 +181,7 @@ export async function getWorkspace( return cachedWorkspaces.get(level); } - const configPath = level === 'local' ? projectFilePath() : globalFilePath(); + const configPath = level === 'local' ? await projectFilePath() : globalFilePath(); if (!configPath) { if (level === 'global') { // Unlike a local config, a global config is not mandatory. @@ -223,7 +223,7 @@ export async function getWorkspace( export async function getWorkspaceRaw( level: 'local' | 'global' = 'local', ): Promise<[JSONFile | null, string | null]> { - let configPath = level === 'local' ? projectFilePath() : globalFilePath(); + let configPath = level === 'local' ? await projectFilePath() : globalFilePath(); if (!configPath) { if (level === 'global') { diff --git a/packages/angular/cli/src/utilities/find-up.ts b/packages/angular/cli/src/utilities/find-up.ts index 317c8d8497f5..f088105b0558 100644 --- a/packages/angular/cli/src/utilities/find-up.ts +++ b/packages/angular/cli/src/utilities/find-up.ts @@ -7,24 +7,66 @@ */ import { existsSync } from 'node:fs'; -import * as path from 'node:path'; +import { stat } from 'node:fs/promises'; +import { dirname, join, resolve } from 'node:path'; -export function findUp(names: string | string[], from: string) { - if (!Array.isArray(names)) { - names = [names]; +/** + * Find a file or directory by walking up the directory tree. + * @param names The name or names of the files or directories to find. + * @param from The directory to start the search from. + * @returns The path to the first match found, or `null` if no match was found. + */ +export async function findUp(names: string | string[], from: string): Promise { + const filenames = Array.isArray(names) ? names : [names]; + + let currentDir = resolve(from); + while (true) { + for (const name of filenames) { + const p = join(currentDir, name); + try { + await stat(p); + + return p; + } catch { + // Ignore errors (e.g. file not found). + } + } + + const parentDir = dirname(currentDir); + if (parentDir === currentDir) { + break; + } + + currentDir = parentDir; } - const root = path.parse(from).root; - let currentDir = from; - while (currentDir && currentDir !== root) { - for (const name of names) { - const p = path.join(currentDir, name); + return null; +} + +/** + * Synchronously find a file or directory by walking up the directory tree. + * @param names The name or names of the files or directories to find. + * @param from The directory to start the search from. + * @returns The path to the first match found, or `null` if no match was found. + */ +export function findUpSync(names: string | string[], from: string): string | null { + const filenames = Array.isArray(names) ? names : [names]; + + let currentDir = resolve(from); + while (true) { + for (const name of filenames) { + const p = join(currentDir, name); if (existsSync(p)) { return p; } } - currentDir = path.dirname(currentDir); + const parentDir = dirname(currentDir); + if (parentDir === currentDir) { + break; + } + + currentDir = parentDir; } return null; diff --git a/packages/angular/cli/src/utilities/project.ts b/packages/angular/cli/src/utilities/project.ts index 39ce2e6d3e83..12ca07545342 100644 --- a/packages/angular/cli/src/utilities/project.ts +++ b/packages/angular/cli/src/utilities/project.ts @@ -10,7 +10,7 @@ import { normalize } from '@angular-devkit/core'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; -import { findUp } from './find-up'; +import { findUpSync } from './find-up'; interface PackageDependencies { dependencies?: Record; @@ -19,7 +19,7 @@ interface PackageDependencies { export function findWorkspaceFile(currentDirectory = process.cwd()): string | null { const possibleConfigFiles = ['angular.json', '.angular.json']; - const configFilePath = findUp(possibleConfigFiles, currentDirectory); + const configFilePath = findUpSync(possibleConfigFiles, currentDirectory); if (configFilePath === null) { return null; } diff --git a/packages/angular_devkit/architect/bin/architect.ts b/packages/angular_devkit/architect/bin/architect.ts index acfd798b89a2..b4513721a1da 100644 --- a/packages/angular_devkit/architect/bin/architect.ts +++ b/packages/angular_devkit/architect/bin/architect.ts @@ -16,21 +16,23 @@ import { Architect } from '../index'; import { WorkspaceNodeModulesArchitectHost } from '../node/index'; function findUp(names: string | string[], from: string) { - if (!Array.isArray(names)) { - names = [names]; - } - const root = path.parse(from).root; + const filenames = Array.isArray(names) ? names : [names]; - let currentDir = from; - while (currentDir && currentDir !== root) { - for (const name of names) { + let currentDir = path.resolve(from); + while (true) { + for (const name of filenames) { const p = path.join(currentDir, name); if (existsSync(p)) { return p; } } - currentDir = path.dirname(currentDir); + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + break; + } + + currentDir = parentDir; } return null; diff --git a/packages/angular_devkit/schematics_cli/bin/schematics.ts b/packages/angular_devkit/schematics_cli/bin/schematics.ts index 8dc64ff5eae0..109497dd89e1 100644 --- a/packages/angular_devkit/schematics_cli/bin/schematics.ts +++ b/packages/angular_devkit/schematics_cli/bin/schematics.ts @@ -177,21 +177,23 @@ function _createPromptProvider(): schema.PromptProvider { } function findUp(names: string | string[], from: string) { - if (!Array.isArray(names)) { - names = [names]; - } - const root = path.parse(from).root; + const filenames = Array.isArray(names) ? names : [names]; - let currentDir = from; - while (currentDir && currentDir !== root) { - for (const name of names) { + let currentDir = path.resolve(from); + while (true) { + for (const name of filenames) { const p = path.join(currentDir, name); if (existsSync(p)) { return p; } } - currentDir = path.dirname(currentDir); + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + break; + } + + currentDir = parentDir; } return null;