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?: {