Skip to content

Commit

Permalink
feat(@angular/cli): add ng cache command
Browse files Browse the repository at this point in the history
With this change we create a new command `ng cache` that can be used control and check the disk cache settings.

This command has 4 subcommands
 - `ng cache enable` which can be used to enable the cache.
 - `ng cache disable` which can be used to disable the cache.
 - `ng cache clean` which can be used to delete the cache from disk.
 - `ng cache info` which will print statistics and information about the cache.
  • Loading branch information
alan-agius4 authored and dgp1130 committed Mar 18, 2022
1 parent eef17b3 commit e5bf35e
Show file tree
Hide file tree
Showing 10 changed files with 448 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/angular/cli/src/command-builder/command-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Parser } from 'yargs/helpers';
import { AddCommandModule } from '../commands/add/cli';
import { AnalyticsCommandModule } from '../commands/analytics/cli';
import { BuildCommandModule } from '../commands/build/cli';
import { CacheCommandModule } from '../commands/cache/cli';
import { ConfigCommandModule } from '../commands/config/cli';
import { DeployCommandModule } from '../commands/deploy/cli';
import { DocCommandModule } from '../commands/doc/cli';
Expand Down Expand Up @@ -51,6 +52,7 @@ const COMMANDS = [
NewCommandModule,
UpdateCommandModule,
RunCommandModule,
CacheCommandModule,
].sort(); // Will be sorted by class name.

const yargsParser = Parser as unknown as typeof Parser.default;
Expand Down
37 changes: 37 additions & 0 deletions packages/angular/cli/src/commands/cache/clean/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import { promises as fs } from 'fs';
import { Argv } from 'yargs';
import {
CommandModule,
CommandModuleImplementation,
CommandScope,
} from '../../../command-builder/command-module';
import { getCacheConfig } from '../utilities';

export class CacheCleanModule extends CommandModule implements CommandModuleImplementation {
command = 'clean';
describe = 'Deletes persistent disk cache from disk.';
longDescriptionPath: string | undefined;
static override scope: CommandScope.In;

builder(localYargs: Argv): Argv {
return localYargs.strict();
}

run(): Promise<void> {
const { path } = getCacheConfig(this.context.workspace);

return fs.rm(path, {
force: true,
recursive: true,
maxRetries: 3,
});
}
}
47 changes: 47 additions & 0 deletions packages/angular/cli/src/commands/cache/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import { join } from 'path';
import { Argv } from 'yargs';
import {
CommandModule,
CommandModuleImplementation,
CommandScope,
Options,
} from '../../command-builder/command-module';
import {
addCommandModuleToYargs,
demandCommandFailureMessage,
} from '../../command-builder/utilities/command';
import { CacheCleanModule } from './clean/cli';
import { CacheInfoCommandModule } from './info/cli';
import { CacheDisableModule, CacheEnableModule } from './settings/cli';

export class CacheCommandModule extends CommandModule implements CommandModuleImplementation {
command = 'cache';
describe = 'Configure persistent disk cache and retrieve cache statistics.';
longDescriptionPath = join(__dirname, 'long-description.md');
static override scope: CommandScope.In;

builder(localYargs: Argv): Argv {
const subcommands = [
CacheEnableModule,
CacheDisableModule,
CacheCleanModule,
CacheInfoCommandModule,
].sort();

for (const module of subcommands) {
localYargs = addCommandModuleToYargs(localYargs, module, this.context);
}

return localYargs.demandCommand(1, demandCommandFailureMessage).strict();
}

run(_options: Options<{}>): void {}
}
99 changes: 99 additions & 0 deletions packages/angular/cli/src/commands/cache/info/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import { tags } from '@angular-devkit/core';
import { promises as fs } from 'fs';
import { join } from 'path';
import { Argv } from 'yargs';
import {
CommandModule,
CommandModuleImplementation,
CommandScope,
} from '../../../command-builder/command-module';
import { isCI } from '../../../utilities/environment-options';
import { getCacheConfig } from '../utilities';

export class CacheInfoCommandModule extends CommandModule implements CommandModuleImplementation {
command = 'info';
describe = 'Prints persistent disk cache configuration and statistics in the console.';
longDescriptionPath?: string | undefined;
static override scope: CommandScope.In;

builder(localYargs: Argv): Argv {
return localYargs.strict();
}

async run(): Promise<void> {
const { path, environment, enabled } = getCacheConfig(this.context.workspace);

this.context.logger.info(tags.stripIndents`
Enabled: ${enabled ? 'yes' : 'no'}
Environment: ${environment}
Path: ${path}
Size on disk: ${await this.getSizeOfDirectory(path)}
Effective status on current machine: ${this.effectiveEnabledStatus() ? 'enabled' : 'disabled'}
`);
}

private async getSizeOfDirectory(path: string): Promise<string> {
const directoriesStack = [path];
let size = 0;

while (directoriesStack.length) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const dirPath = directoriesStack.pop()!;
let entries: string[] = [];

try {
entries = await fs.readdir(dirPath);
} catch {}

for (const entry of entries) {
const entryPath = join(dirPath, entry);
const stats = await fs.stat(entryPath);

if (stats.isDirectory()) {
directoriesStack.push(entryPath);
}

size += stats.size;
}
}

return this.formatSize(size);
}

private formatSize(size: number): string {
if (size <= 0) {
return '0 bytes';
}

const abbreviations = ['bytes', 'kB', 'MB', 'GB'];
const index = Math.floor(Math.log(size) / Math.log(1024));
const roundedSize = size / Math.pow(1024, index);
// bytes don't have a fraction
const fractionDigits = index === 0 ? 0 : 2;

return `${roundedSize.toFixed(fractionDigits)} ${abbreviations[index]}`;
}

private effectiveEnabledStatus(): boolean {
const { enabled, environment } = getCacheConfig(this.context.workspace);

if (enabled) {
switch (environment) {
case 'ci':
return isCI;
case 'local':
return !isCI;
}
}

return enabled;
}
}
53 changes: 53 additions & 0 deletions packages/angular/cli/src/commands/cache/long-description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Angular CLI saves a number of cachable operations on disk by default.

When you re-run the same build, the build system restores the state of the previous build and re-uses previously performed operations, which decreases the time taken to build and test your applications and libraries.

To amend the default cache settings, add the `cli.cache` object to your [Workspace Configuration](guide/workspace-config).
The object goes under `cli.cache` at the top level of the file, outside the `projects` sections.

```jsonc
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"cache": {
// ...
}
},
"projects": {}
}
```

For more information, see [cache options](guide/workspace-config#cache-options).

### Cache environments

By default, disk cache is only enabled for local environments. The value of environment can be one of the following:

- `all` - allows disk cache on all machines.
- `local` - allows disk cache only on development machines.
- `ci` - allows disk cache only on continuous integration (Ci) systems.

To change the environment setting to `all`, run the following command:

```bash
ng config cli.cache.environment all
```

For more information, see `environment` in [cache options](guide/workspace-config#cache-options).

<div class="alert is-helpful">

The Angular CLI checks for the presence and value of the `CI` environment variable to determine in which environment it is running.

</div>

### Cache path

By default, `.angular/cache` is used as a base directory to store cache results.

To change this path to `.cache/ng`, run the following command:

```bash
ng config cli.cache.path ".cache/ng"
```
47 changes: 47 additions & 0 deletions packages/angular/cli/src/commands/cache/settings/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import { Argv } from 'yargs';
import {
CommandModule,
CommandModuleImplementation,
CommandScope,
} from '../../../command-builder/command-module';
import { updateCacheConfig } from '../utilities';

export class CacheDisableModule extends CommandModule implements CommandModuleImplementation {
command = 'disable';
aliases = 'off';
describe = 'Disables persistent disk cache for all projects in the workspace.';
longDescriptionPath: string | undefined;
static override scope: CommandScope.In;

builder(localYargs: Argv): Argv {
return localYargs;
}

run(): void {
updateCacheConfig('enabled', false);
}
}

export class CacheEnableModule extends CommandModule implements CommandModuleImplementation {
command = 'enable';
aliases = 'on';
describe = 'Enables disk cache for all projects in the workspace.';
longDescriptionPath: string | undefined;
static override scope: CommandScope.In;

builder(localYargs: Argv): Argv {
return localYargs;
}

run(): void {
updateCacheConfig('enabled', true);
}
}
57 changes: 57 additions & 0 deletions packages/angular/cli/src/commands/cache/utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import { isJsonObject } from '@angular-devkit/core';
import { resolve } from 'path';
import { Cache, Environment } from '../../../lib/config/workspace-schema';
import { AngularWorkspace, getWorkspaceRaw } from '../../utilities/config';

export function updateCacheConfig<K extends keyof Cache>(key: K, value: Cache[K]): void {
const [localWorkspace] = getWorkspaceRaw('local');
if (!localWorkspace) {
throw new Error('Cannot find workspace configuration file.');
}

localWorkspace.modify(['cli', 'cache', key], value);
localWorkspace.save();
}

export function getCacheConfig(workspace: AngularWorkspace | undefined): Required<Cache> {
if (!workspace) {
throw new Error(`Cannot retrieve cache configuration as workspace is not defined.`);
}

const defaultSettings: Required<Cache> = {
path: resolve(workspace.basePath, '.angular/cache'),
environment: Environment.Local,
enabled: true,
};

const cliSetting = workspace.extensions['cli'];
if (!cliSetting || !isJsonObject(cliSetting)) {
return defaultSettings;
}

const cacheSettings = cliSetting['cache'];
if (!isJsonObject(cacheSettings)) {
return defaultSettings;
}

const {
path = defaultSettings.path,
environment = defaultSettings.environment,
enabled = defaultSettings.enabled,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} = cacheSettings as Record<string, any>;

return {
path: resolve(workspace.basePath, path),
environment,
enabled,
};
}
11 changes: 11 additions & 0 deletions tests/legacy-cli/e2e/tests/commands/cache/cache-clean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createDir, expectFileNotToExist, expectFileToExist } from '../../../utils/fs';
import { ng } from '../../../utils/process';

export default async function () {
const cachePath = '.angular/cache';
await createDir(cachePath);
await expectFileToExist(cachePath);

await ng('cache', 'clean');
await expectFileNotToExist(cachePath);
}
14 changes: 14 additions & 0 deletions tests/legacy-cli/e2e/tests/commands/cache/cache-enable-disable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { readFile } from '../../../utils/fs';
import { ng } from '../../../utils/process';

export default async function () {
await ng('cache', 'enable');
if (JSON.parse(await readFile('angular.json')).cli.cache.enabled !== true) {
throw new Error(`Expected 'cli.cache.enable' to be true.`);
}

await ng('cache', 'disable');
if (JSON.parse(await readFile('angular.json')).cli.cache.enabled !== false) {
throw new Error(`Expected 'cli.cache.enable' to be false.`);
}
}
Loading

0 comments on commit e5bf35e

Please sign in to comment.