From 8368e9e4af39a0cc7a8862919395c8c6a1ee2b02 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 29 Sep 2020 10:47:45 -0400 Subject: [PATCH] refactor(@angular/cli): discover/load workspace on startup Previously, the workspace configuration file was found and loaded by individual commands potentially multiple times. This change moves the initial workspace location discovery and loading of the workspace to the CLI startup. It also provides the workspace to each command so that the commands can reuse the already loaded and parsed workspace configuration. --- packages/angular/cli/commands/add-impl.ts | 8 ++-- packages/angular/cli/commands/update-impl.ts | 22 +++++----- packages/angular/cli/commands/version-impl.ts | 6 +-- packages/angular/cli/lib/cli/index.ts | 19 +++++--- .../angular/cli/models/architect-command.ts | 26 ++++++----- packages/angular/cli/models/command-runner.ts | 16 ++++--- packages/angular/cli/models/command.ts | 19 +++----- packages/angular/cli/models/interface.ts | 16 +++---- .../angular/cli/models/schematic-command.ts | 43 +++++-------------- packages/angular/cli/utilities/config.ts | 24 ++++++++++- packages/angular/cli/utilities/project.ts | 35 ++++++--------- 11 files changed, 113 insertions(+), 121 deletions(-) diff --git a/packages/angular/cli/commands/add-impl.ts b/packages/angular/cli/commands/add-impl.ts index 902c9782bd3f..321601e22e22 100644 --- a/packages/angular/cli/commands/add-impl.ts +++ b/packages/angular/cli/commands/add-impl.ts @@ -79,7 +79,7 @@ export class AddCommand extends SchematicCommand { } } - const packageManager = await getPackageManager(this.workspace.root); + const packageManager = await getPackageManager(this.context.root); const usingYarn = packageManager === PackageManager.Yarn; if (packageIdentifier.type === 'tag' && !packageIdentifier.rawSpec) { @@ -210,7 +210,7 @@ export class AddCommand extends SchematicCommand { private isPackageInstalled(name: string): boolean { try { - require.resolve(join(name, 'package.json'), { paths: [this.workspace.root] }); + require.resolve(join(name, 'package.json'), { paths: [this.context.root] }); return true; } catch (e) { @@ -254,7 +254,7 @@ export class AddCommand extends SchematicCommand { let installedPackage; try { installedPackage = require.resolve(join(name, 'package.json'), { - paths: [this.workspace.root], + paths: [this.context.root], }); } catch {} @@ -268,7 +268,7 @@ export class AddCommand extends SchematicCommand { let projectManifest; try { - projectManifest = await fetchPackageManifest(this.workspace.root, this.logger); + projectManifest = await fetchPackageManifest(this.context.root, this.logger); } catch {} if (projectManifest) { diff --git a/packages/angular/cli/commands/update-impl.ts b/packages/angular/cli/commands/update-impl.ts index b119baa1651a..adf2d3cdafe8 100644 --- a/packages/angular/cli/commands/update-impl.ts +++ b/packages/angular/cli/commands/update-impl.ts @@ -64,15 +64,15 @@ export class UpdateCommand extends Command { private packageManager = PackageManager.Npm; async initialize() { - this.packageManager = await getPackageManager(this.workspace.root); + this.packageManager = await getPackageManager(this.context.root); this.workflow = new NodeWorkflow( - new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(this.workspace.root)), + new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(this.context.root)), { packageManager: this.packageManager, - root: normalize(this.workspace.root), + root: normalize(this.context.root), // __dirname -> favor @schematics/update from this package // Otherwise, use packages from the active workspace (migrations) - resolvePaths: [__dirname, this.workspace.root], + resolvePaths: [__dirname, this.context.root], }, ); this.workflow.engineHost.registerOptionsTransform( @@ -274,7 +274,7 @@ export class UpdateCommand extends Command { // This works around issues with packages containing migrations that cannot directly depend on the package // This check can be removed once the schematic runtime handles this situation try { - require.resolve('@angular-devkit/schematics', { paths: [this.workspace.root] }); + require.resolve('@angular-devkit/schematics', { paths: [this.context.root] }); } catch (e) { if (e.code === 'MODULE_NOT_FOUND') { this.logger.fatal( @@ -386,8 +386,8 @@ export class UpdateCommand extends Command { options.from === undefined && packages.length === 1 && packages[0].name === '@angular/cli' && - this.workspace.configFile && - oldConfigFileNames.includes(this.workspace.configFile) + this.workspace && + oldConfigFileNames.includes(path.basename(this.workspace.filePath)) ) { options.migrateOnly = true; options.from = '1.0.0'; @@ -395,7 +395,7 @@ export class UpdateCommand extends Command { this.logger.info('Collecting installed dependencies...'); - const rootDependencies = await getProjectDependencies(this.workspace.root); + const rootDependencies = await getProjectDependencies(this.context.root); this.logger.info(`Found ${rootDependencies.size} dependencies.`); @@ -441,7 +441,7 @@ export class UpdateCommand extends Command { // Allow running migrations on transitively installed dependencies // There can technically be nested multiple versions // TODO: If multiple, this should find all versions and ask which one to use - const packageJson = findPackageJson(this.workspace.root, packageName); + const packageJson = findPackageJson(this.context.root, packageName); if (packageJson) { packagePath = path.dirname(packageJson); packageNode = await readPackageJson(packagePath); @@ -679,7 +679,7 @@ export class UpdateCommand extends Command { migration.package, // Resolve the collection from the workspace root, as otherwise it will be resolved from the temp // installed CLI version. - require.resolve(migration.collection, { paths: [this.workspace.root] }), + require.resolve(migration.collection, { paths: [this.context.root] }), new semver.Range('>' + migration.from + ' <=' + migration.to), options.createCommits, ); @@ -754,7 +754,7 @@ export class UpdateCommand extends Command { // Only files inside the workspace root are relevant for (const entry of result.split('\n')) { const relativeEntry = path.relative( - path.resolve(this.workspace.root), + path.resolve(this.context.root), path.resolve(topLevel.trim(), entry.slice(3).trim()), ); diff --git a/packages/angular/cli/commands/version-impl.ts b/packages/angular/cli/commands/version-impl.ts index 9d5b2637458c..669ce2d2ba54 100644 --- a/packages/angular/cli/commands/version-impl.ts +++ b/packages/angular/cli/commands/version-impl.ts @@ -26,7 +26,7 @@ export class VersionCommand extends Command { const cliPackage: PartialPackageInfo = require('../package.json'); let workspacePackage: PartialPackageInfo | undefined; try { - workspacePackage = require(path.resolve(this.workspace.root, 'package.json')); + workspacePackage = require(path.resolve(this.context.root, 'package.json')); } catch {} const patterns = [ @@ -144,7 +144,7 @@ export class VersionCommand extends Command { // Try to find the package in the workspace try { - packagePath = require.resolve(`${moduleName}/package.json`, { paths: [ this.workspace.root ]}); + packagePath = require.resolve(`${moduleName}/package.json`, { paths: [ this.context.root ]}); } catch {} // If not found, try to find within the CLI @@ -169,7 +169,7 @@ export class VersionCommand extends Command { private getIvyWorkspace(): string { try { - const content = fs.readFileSync(path.resolve(this.workspace.root, 'tsconfig.json'), 'utf-8'); + const content = fs.readFileSync(path.resolve(this.context.root, 'tsconfig.json'), 'utf-8'); const tsConfig = parseJson(content, JsonParseMode.Loose); if (!isJsonObject(tsConfig)) { return ''; diff --git a/packages/angular/cli/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts index cf0cacdb715d..d7e3d913260a 100644 --- a/packages/angular/cli/lib/cli/index.ts +++ b/packages/angular/cli/lib/cli/index.ts @@ -9,9 +9,9 @@ import { createConsoleLogger } from '@angular-devkit/core/node'; import { format } from 'util'; import { runCommand } from '../../models/command-runner'; import { colors, removeColor } from '../../utilities/color'; -import { getWorkspaceRaw } from '../../utilities/config'; +import { AngularWorkspace, getWorkspaceRaw } from '../../utilities/config'; import { writeErrorToLogFile } from '../../utilities/log-file'; -import { getWorkspaceDetails } from '../../utilities/project'; +import { findWorkspaceFile } from '../../utilities/project'; const debugEnv = process.env['NG_DEBUG']; const isDebug = @@ -67,8 +67,9 @@ export default async function(options: { testing?: boolean; cliArgs: string[] }) logger.error(format(...args)); }; - let projectDetails = getWorkspaceDetails(); - if (projectDetails === null) { + let workspace; + const workspaceFile = findWorkspaceFile(); + if (workspaceFile === null) { const [, localPath] = getWorkspaceRaw('local'); if (localPath !== null) { logger.fatal( @@ -78,12 +79,18 @@ export default async function(options: { testing?: boolean; cliArgs: string[] }) return 1; } + } else { + try { + workspace = await AngularWorkspace.load(workspaceFile); + } catch (e) { + logger.fatal(`Unable to read workspace file '${workspaceFile}': ${e.message}`); - projectDetails = { root: process.cwd() }; + return 1; + } } try { - const maybeExitCode = await runCommand(options.cliArgs, logger, projectDetails); + const maybeExitCode = await runCommand(options.cliArgs, logger, workspace); if (typeof maybeExitCode === 'number') { console.assert(Number.isInteger(maybeExitCode)); diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts index be263b4e9880..0976702b39a9 100644 --- a/packages/angular/cli/models/architect-command.ts +++ b/packages/angular/cli/models/architect-command.ts @@ -7,8 +7,7 @@ */ import { Architect, Target } from '@angular-devkit/architect'; import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; -import { json, schema, tags, workspaces } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; +import { json, schema, tags } from '@angular-devkit/core'; import { parseJsonSchemaToOptions } from '../utilities/json-schema'; import { isPackageNameSafeForAnalytics } from './analytics'; import { BaseCommandOptions, Command } from './command'; @@ -27,7 +26,6 @@ export abstract class ArchitectCommand< > extends Command { protected _architect!: Architect; protected _architectHost!: WorkspaceNodeModulesArchitectHost; - protected _workspace!: workspaces.WorkspaceDefinition; protected _registry!: json.schema.SchemaRegistry; // If this command supports running multiple targets. @@ -43,13 +41,11 @@ export abstract class ArchitectCommand< this._registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); this._registry.useXDeprecatedProvider(msg => this.logger.warn(msg)); - const { workspace } = await workspaces.readWorkspace( - this.workspace.root, - workspaces.createWorkspaceHost(new NodeJsSyncHost()), - ); - this._workspace = workspace; + if (!this.workspace) { + throw new Error('A workspace is required for an architect command.'); + } - this._architectHost = new WorkspaceNodeModulesArchitectHost(workspace, this.workspace.root); + this._architectHost = new WorkspaceNodeModulesArchitectHost(this.workspace, this.workspace.basePath); this._architect = new Architect(this._architectHost, this._registry); if (!this.target) { @@ -67,13 +63,13 @@ export abstract class ArchitectCommand< } let projectName = options.project; - if (projectName && !this._workspace.projects.has(projectName)) { + if (projectName && !this.workspace.projects.has(projectName)) { throw new Error(`Project '${projectName}' does not exist.`); } const commandLeftovers = options['--']; const targetProjectNames: string[] = []; - for (const [name, project] of this._workspace.projects) { + for (const [name, project] of this.workspace.projects) { if (project.targets.has(this.target)) { targetProjectNames.push(name); } @@ -153,7 +149,7 @@ export abstract class ArchitectCommand< } if (!projectName && !this.multiTarget) { - const defaultProjectName = this._workspace.extensions['defaultProject'] as string; + const defaultProjectName = this.workspace.extensions['defaultProject'] as string; if (targetProjectNames.length === 1) { projectName = targetProjectNames[0]; } else if (defaultProjectName && targetProjectNames.includes(defaultProjectName)) { @@ -286,7 +282,8 @@ export abstract class ArchitectCommand< private getProjectNamesByTarget(targetName: string): string[] { const allProjectsForTargetName: string[] = []; - for (const [name, project] of this._workspace.projects) { + // tslint:disable-next-line: no-non-null-assertion + for (const [name, project] of this.workspace!.projects) { if (project.targets.has(targetName)) { allProjectsForTargetName.push(name); } @@ -298,7 +295,8 @@ export abstract class ArchitectCommand< } else { // For single target commands, we try the default project first, // then the full list if it has a single project, then error out. - const maybeDefaultProject = this._workspace.extensions['defaultProject'] as string; + // tslint:disable-next-line: no-non-null-assertion + const maybeDefaultProject = this.workspace!.extensions['defaultProject'] as string; if (maybeDefaultProject && allProjectsForTargetName.includes(maybeDefaultProject)) { return [maybeDefaultProject]; } diff --git a/packages/angular/cli/models/command-runner.ts b/packages/angular/cli/models/command-runner.ts index 09512be9d712..b8ec88e57883 100644 --- a/packages/angular/cli/models/command-runner.ts +++ b/packages/angular/cli/models/command-runner.ts @@ -17,6 +17,7 @@ import { } from '@angular-devkit/core'; import { readFileSync } from 'fs'; import { join, resolve } from 'path'; +import { AngularWorkspace } from '../utilities/config'; import { parseJsonSchemaToCommandDescription } from '../utilities/json-schema'; import { getGlobalAnalytics, @@ -26,7 +27,7 @@ import { promptProjectAnalytics, } from './analytics'; import { Command } from './command'; -import { CommandDescription, CommandWorkspace } from './interface'; +import { CommandDescription } from './interface'; import * as parser from './parser'; // NOTE: Update commands.json if changing this. It's still deep imported in one CI validation @@ -116,9 +117,9 @@ async function loadCommandDescription( export async function runCommand( args: string[], logger: logging.Logger, - workspace: CommandWorkspace, + workspace: AngularWorkspace | undefined, commands: CommandMapOptions = standardCommands, - options: { analytics?: analytics.Analytics } = {}, + options: { analytics?: analytics.Analytics; currentDirectory: string } = { currentDirectory: process.cwd() }, ): Promise { // This registry is exclusively used for flattening schemas, and not for validating. const registry = new schema.CoreSchemaRegistry([]); @@ -233,8 +234,13 @@ export async function runCommand( const analytics = options.analytics || - (await _createAnalytics(!!workspace.configFile, description.name === 'update')); - const context = { workspace, analytics }; + (await _createAnalytics(!!workspace, description.name === 'update')); + const context = { + workspace, + analytics, + currentDirectory: options.currentDirectory, + root: workspace?.basePath ?? options.currentDirectory, + }; const command = new description.impl(context, description, logger); // Flush on an interval (if the event loop is waiting). diff --git a/packages/angular/cli/models/command.ts b/packages/angular/cli/models/command.ts index 8848a2a21cba..3a1b3109b7ed 100644 --- a/packages/angular/cli/models/command.ts +++ b/packages/angular/cli/models/command.ts @@ -6,16 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ import { analytics, logging, strings, tags } from '@angular-devkit/core'; -import * as path from 'path'; import { colors } from '../utilities/color'; -import { getWorkspace } from '../utilities/config'; +import { AngularWorkspace } from '../utilities/config'; import { Arguments, CommandContext, CommandDescription, CommandDescriptionMap, CommandScope, - CommandWorkspace, Option, SubCommandDescription, } from './interface'; @@ -26,8 +24,8 @@ export interface BaseCommandOptions { export abstract class Command { public allowMissingWorkspace = false; - public workspace: CommandWorkspace; - public analytics: analytics.Analytics; + readonly workspace?: AngularWorkspace; + readonly analytics: analytics.Analytics; protected static commandMap: () => Promise; static setCommandMap(map: () => Promise) { @@ -35,7 +33,7 @@ export abstract class Command } constructor( - context: CommandContext, + protected readonly context: CommandContext, public readonly description: CommandDescription, protected readonly logger: logging.Logger, ) { @@ -120,19 +118,16 @@ export abstract class Command async validateScope(scope?: CommandScope): Promise { switch (scope === undefined ? this.description.scope : scope) { case CommandScope.OutProject: - if (this.workspace.configFile) { + if (this.workspace) { this.logger.fatal(tags.oneLine` The ${this.description.name} command requires to be run outside of a project, but a - project definition was found at "${path.join( - this.workspace.root, - this.workspace.configFile, - )}". + project definition was found at "${this.workspace.filePath}". `); throw 1; } break; case CommandScope.InProject: - if (!this.workspace.configFile || (await getWorkspace('local')) === null) { + if (!this.workspace) { this.logger.fatal(tags.oneLine` The ${this.description.name} command requires to be run in an Angular project, but a project definition could not be found. diff --git a/packages/angular/cli/models/interface.ts b/packages/angular/cli/models/interface.ts index d55183f7544f..338b6310bce5 100644 --- a/packages/angular/cli/models/interface.ts +++ b/packages/angular/cli/models/interface.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import { analytics, json, logging } from '@angular-devkit/core'; +import { AngularWorkspace } from '../utilities/config'; /** * Value type of arguments. @@ -44,21 +45,16 @@ export interface CommandConstructor { ): CommandInterface; } -/** - * A CLI workspace information. - */ -export interface CommandWorkspace { - root: string; - configFile?: string; -} - /** * A command runner context. */ export interface CommandContext { - workspace: CommandWorkspace; + currentDirectory: string; + root: string; + + workspace?: AngularWorkspace; - // This feel is optional for backward compatibility. + // This property is optional for backward compatibility. analytics?: analytics.Analytics; } diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts index 2fa707105860..e26f43b63c31 100644 --- a/packages/angular/cli/models/schematic-command.ts +++ b/packages/angular/cli/models/schematic-command.ts @@ -74,8 +74,6 @@ export abstract class SchematicCommand< T extends BaseSchematicSchema & BaseCommandOptions > extends Command { readonly allowPrivateSchematics: boolean = false; - private _host = new NodeJsSyncHost(); - private _workspace: workspaces.WorkspaceDefinition | undefined; protected _workflow!: NodeWorkflow; protected defaultCollectionName = '@schematics/angular'; @@ -87,7 +85,6 @@ export abstract class SchematicCommand< } public async initialize(options: T & Arguments) { - await this._loadWorkspace(); await this.createWorkflow(options); if (this.schematicName) { @@ -245,21 +242,22 @@ export abstract class SchematicCommand< } const { force, dryRun } = options; - const fsHost = new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(this.workspace.root)); + const root = this.context.root; + const fsHost = new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(root)); const workflow = new NodeWorkflow(fsHost, { force, dryRun, - packageManager: await getPackageManager(this.workspace.root), + packageManager: await getPackageManager(root), packageRegistry: options.packageRegistry, - root: normalize(this.workspace.root), + root: normalize(root), registry: new schema.CoreSchemaRegistry(formats.standardFormats), - resolvePaths: !!this.workspace.configFile + resolvePaths: !!this.workspace // Workspace ? this.collectionName === this.defaultCollectionName // Favor __dirname for @schematics/angular to use the build-in version - ? [__dirname, process.cwd(), this.workspace.root] - : [process.cwd(), this.workspace.root, __dirname] + ? [__dirname, process.cwd(), root] + : [process.cwd(), root, __dirname] // Global : [__dirname, process.cwd()], }); @@ -278,8 +276,8 @@ export abstract class SchematicCommand< }); const getProjectName = () => { - if (this._workspace) { - const projectNames = getProjectsByPath(this._workspace, process.cwd(), this.workspace.root); + if (this.workspace) { + const projectNames = getProjectsByPath(this.workspace, process.cwd(), this.workspace.basePath); if (projectNames.length === 1) { return projectNames[0]; @@ -292,7 +290,7 @@ export abstract class SchematicCommand< `); } - const defaultProjectName = this._workspace.extensions['defaultProject']; + const defaultProjectName = this.workspace.extensions['defaultProject']; if (typeof defaultProjectName === 'string' && defaultProjectName) { return defaultProjectName; } @@ -406,7 +404,7 @@ export abstract class SchematicCommand< const workflow = this._workflow; - const workingDir = normalize(systemPath.relative(this.workspace.root, process.cwd())); + const workingDir = normalize(systemPath.relative(this.context.root, process.cwd())); // Get the option object from the schematic schema. const schematic = this.getSchematic( @@ -582,25 +580,6 @@ export abstract class SchematicCommand< ): Promise { return parseArguments(schematicOptions, options, this.logger); } - - private async _loadWorkspace() { - if (this._workspace) { - return; - } - - try { - const { workspace } = await workspaces.readWorkspace( - this.workspace.root, - workspaces.createWorkspaceHost(this._host), - ); - this._workspace = workspace; - } catch (err) { - if (!this.allowMissingWorkspace) { - // Ignore missing workspace - throw err; - } - } - } } function getProjectsByPath( diff --git a/packages/angular/cli/utilities/config.ts b/packages/angular/cli/utilities/config.ts index 7a5569a517bd..029aaa156858 100644 --- a/packages/angular/cli/utilities/config.ts +++ b/packages/angular/cli/utilities/config.ts @@ -102,6 +102,26 @@ export class AngularWorkspace { return (project?.extensions['cli'] as Record) || {}; } + + static async load(workspaceFilePath: string): Promise { + const oldConfigFileNames = ['.angular-cli.json', 'angular-cli.json']; + if (oldConfigFileNames.includes(path.basename(workspaceFilePath))) { + // 1.x file format + // Create an empty workspace to allow update to be used + return new AngularWorkspace( + { extensions: {}, projects: new workspaces.ProjectDefinitionCollection() }, + workspaceFilePath, + ); + } + + const result = await workspaces.readWorkspace( + workspaceFilePath, + workspaces.createWorkspaceHost(new NodeJsSyncHost()), + workspaces.WorkspaceFormat.JSON, + ); + + return new AngularWorkspace(result.workspace, workspaceFilePath); + } } const cachedWorkspaces = new Map(); @@ -198,7 +218,7 @@ export async function validateWorkspace(data: JsonObject): Promise { } } -function getProjectByPath(workspace: AngularWorkspace, location: string): string | null { +function findProjectByPath(workspace: AngularWorkspace, location: string): string | null { const isInside = (base: string, potential: string): boolean => { const absoluteBase = path.resolve(workspace.basePath, base); const absolutePotential = path.resolve(workspace.basePath, potential); @@ -246,7 +266,7 @@ export function getProjectByCwd(workspace: AngularWorkspace): string | null { return Array.from(workspace.projects.keys())[0]; } - const project = getProjectByPath(workspace, process.cwd()); + const project = findProjectByPath(workspace, process.cwd()); if (project) { return project; } diff --git a/packages/angular/cli/utilities/project.ts b/packages/angular/cli/utilities/project.ts index 858c1722aada..cd133cd0323a 100644 --- a/packages/angular/cli/utilities/project.ts +++ b/packages/angular/cli/utilities/project.ts @@ -9,49 +9,40 @@ import { normalize } from '@angular-devkit/core'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { CommandWorkspace } from '../models/interface'; import { findUp } from './find-up'; -export function insideWorkspace(): boolean { - return getWorkspaceDetails() !== null; -} - -export function getWorkspaceDetails(): CommandWorkspace | null { - const currentDir = process.cwd(); +export function findWorkspaceFile(currentDirectory = process.cwd()): string | null { const possibleConfigFiles = [ 'angular.json', '.angular.json', 'angular-cli.json', '.angular-cli.json', ]; - const configFilePath = findUp(possibleConfigFiles, currentDir); + const configFilePath = findUp(possibleConfigFiles, currentDirectory); if (configFilePath === null) { return null; } - const configFileName = path.basename(configFilePath); const possibleDir = path.dirname(configFilePath); const homedir = os.homedir(); if (normalize(possibleDir) === normalize(homedir)) { const packageJsonPath = path.join(possibleDir, 'package.json'); - if (!fs.existsSync(packageJsonPath)) { - // No package.json - return null; - } - const packageJsonBuffer = fs.readFileSync(packageJsonPath); - const packageJsonText = packageJsonBuffer === null ? '{}' : packageJsonBuffer.toString(); - const packageJson = JSON.parse(packageJsonText); - if (!containsCliDep(packageJson)) { - // No CLI dependency + + try { + const packageJsonText = fs.readFileSync(packageJsonPath, 'utf-8'); + const packageJson = JSON.parse(packageJsonText); + if (!containsCliDep(packageJson)) { + // No CLI dependency + return null; + } + } catch { + // No or invalid package.json return null; } } - return { - root: possibleDir, - configFile: configFileName, - }; + return configFilePath; } function containsCliDep(obj?: {