Skip to content

Commit 2862762

Browse files
committed
fix(build): prompt to install "build cli" for all projects
This refactor adds a prompt to install v1-toolkit, app-scripts, Angular CLI if missing. It introduces the idea of a "build cli" class which is used to share behavior between the three projects, slimming project-specific build logic even more. See this commit for the similar refactor for serve: eea39da fixes #3406
1 parent 93888f0 commit 2862762

File tree

15 files changed

+242
-95
lines changed

15 files changed

+242
-95
lines changed

packages/@ionic/cli-utils/src/lib/build.ts

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { BaseError, OptionGroup } from '@ionic/cli-framework';
1+
import { BaseError, ERROR_SHELL_COMMAND_NOT_FOUND, OptionGroup, PromptModule, ShellCommandError } from '@ionic/cli-framework';
22
import chalk from 'chalk';
3+
import * as Debug from 'debug';
34

45
import { BaseBuildOptions, BuildOptions, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, IConfig, ILogger, IProject, IShell, Runner } from '../definitions';
56

6-
import { FatalException, RunnerException } from './errors';
7+
import { BuildCLIProgramNotFoundException, FatalException, RunnerException } from './errors';
78
import { Hook } from './hooks';
89

10+
const debug = Debug('ionic:cli-utils:lib:build');
11+
912
export const BUILD_SCRIPT = 'ionic:build';
1013

1114
export const COMMON_BUILD_COMMAND_OPTIONS: ReadonlyArray<CommandMetadataOption> = [
@@ -25,6 +28,7 @@ export interface BuildRunnerDeps {
2528
readonly config: IConfig;
2629
readonly log: ILogger;
2730
readonly project: IProject;
31+
readonly prompt: PromptModule;
2832
readonly shell: IShell;
2933
}
3034

@@ -96,6 +100,121 @@ export abstract class BuildRunner<T extends BuildOptions<any>> implements Runner
96100
}
97101
}
98102

103+
export abstract class BuildCLI<T extends object> {
104+
105+
/**
106+
* The pretty name of this Build CLI.
107+
*/
108+
abstract readonly name: string;
109+
110+
/**
111+
* The npm package of this Build CLI.
112+
*/
113+
abstract readonly pkg: string;
114+
115+
/**
116+
* The bin program to use for this Build CLI.
117+
*/
118+
abstract readonly program: string;
119+
120+
/**
121+
* If specified, `package.json` is inspected for this script to use instead
122+
* of `program`.
123+
*/
124+
abstract readonly script?: string;
125+
126+
resolvedProgram = this.program;
127+
128+
constructor(protected readonly e: BuildRunnerDeps) {}
129+
130+
/**
131+
* Build the arguments for starting this Build CLI. Called by `this.run()`.
132+
*/
133+
protected abstract buildArgs(options: T): Promise<string[]>;
134+
135+
async build(options: T): Promise<void> {
136+
this.resolvedProgram = await this.resolveProgram();
137+
138+
await this.runWrapper(options);
139+
}
140+
141+
protected async runWrapper(options: T): Promise<void> {
142+
try {
143+
return await this.run(options);
144+
} catch (e) {
145+
if (!(e instanceof BuildCLIProgramNotFoundException)) {
146+
throw e;
147+
}
148+
149+
this.e.log.nl();
150+
this.e.log.info(
151+
`Looks like ${chalk.green(this.pkg)} isn't installed in this project.\n` +
152+
`This package is required for this command to work properly.`
153+
);
154+
155+
const installed = await this.promptToInstall();
156+
157+
if (!installed) {
158+
this.e.log.nl();
159+
throw new FatalException(`${chalk.green(this.pkg)} is required for this command to work properly.`);
160+
}
161+
162+
return this.run(options);
163+
}
164+
}
165+
166+
protected async run(options: T): Promise<void> {
167+
const args = await this.buildArgs(options);
168+
169+
try {
170+
await this.e.shell.run(this.resolvedProgram, args, { cwd: this.e.project.directory, fatalOnNotFound: false });
171+
} catch (e) {
172+
if (e instanceof ShellCommandError && e.code === ERROR_SHELL_COMMAND_NOT_FOUND) {
173+
throw new BuildCLIProgramNotFoundException(`${chalk.bold(this.resolvedProgram)} command not found.`);
174+
}
175+
176+
throw e;
177+
}
178+
}
179+
180+
protected async resolveProgram(): Promise<string> {
181+
if (typeof this.script !== 'undefined') {
182+
debug(`Looking for ${chalk.cyan(this.script)} npm script.`);
183+
184+
const pkg = await this.e.project.requirePackageJson();
185+
186+
if (pkg.scripts && pkg.scripts[this.script]) {
187+
debug(`Using ${chalk.cyan(this.script)} npm script.`);
188+
return this.e.config.get('npmClient');
189+
}
190+
}
191+
192+
return this.program;
193+
}
194+
195+
protected async promptToInstall(): Promise<boolean> {
196+
const { pkgManagerArgs } = await import('./utils/npm');
197+
const [ manager, ...managerArgs ] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'install', pkg: this.pkg, saveDev: true, saveExact: true });
198+
199+
this.e.log.nl();
200+
201+
const confirm = await this.e.prompt({
202+
name: 'confirm',
203+
message: `Install ${chalk.green(this.pkg)}?`,
204+
type: 'confirm',
205+
});
206+
207+
if (!confirm) {
208+
this.e.log.warn(`Not installing--here's how to install manually: ${chalk.green(`${manager} ${managerArgs.join(' ')}`)}`);
209+
return false;
210+
}
211+
212+
await this.e.shell.run(manager, managerArgs, { cwd: this.e.project.directory });
213+
214+
return true;
215+
}
216+
}
217+
99218
class BuildBeforeHook extends Hook {
100219
readonly name = 'build:before';
101220
}

packages/@ionic/cli-utils/src/lib/errors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export class FatalException extends BaseException {
1212
}
1313
}
1414

15+
export class BuildCLIProgramNotFoundException extends BaseException {}
16+
1517
export class ServeCLIProgramNotFoundException extends BaseException {}
1618

1719
export class SessionException extends BaseException {}

packages/@ionic/cli-utils/src/lib/project/angular/build.ts

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { CommandGroup, OptionGroup, ParsedArgs, unparseArgs } from '@ionic/cli-framework';
22
import chalk from 'chalk';
3-
import * as Debug from 'debug';
43

54
import { AngularBuildOptions, CommandLineInputs, CommandLineOptions, CommandMetadata } from '../../../definitions';
6-
import { BUILD_SCRIPT, BuildRunner, BuildRunnerDeps } from '../../build';
5+
import { BUILD_SCRIPT, BuildCLI, BuildRunner, BuildRunnerDeps } from '../../build';
76

87
import { AngularProject } from './';
98

10-
const debug = Debug('ionic:cli-utils:lib:project:angular:build');
11-
129
const NG_BUILD_OPTIONS = [
1310
{
1411
name: 'configuration',
@@ -72,7 +69,33 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://github.com/angular/angular-cli/wiki/
7269
};
7370
}
7471

75-
async buildOptionsToNgArgs(options: AngularBuildOptions): Promise<string[]> {
72+
async buildProject(options: AngularBuildOptions): Promise<void> {
73+
const ng = new AngularBuildCLI(this.e);
74+
await ng.build(options);
75+
}
76+
}
77+
78+
class AngularBuildCLI extends BuildCLI<AngularBuildOptions> {
79+
readonly name = 'Angular CLI';
80+
readonly pkg = '@angular/cli';
81+
readonly program = 'ng';
82+
readonly prefix = 'ng';
83+
readonly script = BUILD_SCRIPT;
84+
85+
protected async buildArgs(options: AngularBuildOptions): Promise<string[]> {
86+
const { pkgManagerArgs } = await import('../../utils/npm');
87+
88+
const args = await this.buildOptionsToNgArgs(options);
89+
90+
if (this.resolvedProgram === this.program) {
91+
return [...this.buildArchitectCommand(options), ...args];
92+
} else {
93+
const [ , ...pkgArgs ] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script, scriptArgs: [...args] });
94+
return pkgArgs;
95+
}
96+
}
97+
98+
protected async buildOptionsToNgArgs(options: AngularBuildOptions): Promise<string[]> {
7699
const args: ParsedArgs = {
77100
_: [],
78101
'source-map': options.sourcemaps !== false ? options.sourcemaps : 'false',
@@ -90,28 +113,10 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://github.com/angular/angular-cli/wiki/
90113
return [...unparseArgs(args), ...options['--']];
91114
}
92115

93-
buildArchitectCommand(options: AngularBuildOptions): string[] {
116+
protected buildArchitectCommand(options: AngularBuildOptions): string[] {
94117
const cmd = options.engine === 'cordova' ? 'ionic-cordova-build' : 'build';
95118
const project = options.project ? options.project : 'app';
96119

97120
return ['run', `${project}:${cmd}${options.configuration ? `:${options.configuration}` : ''}`];
98121
}
99-
100-
async buildProject(options: AngularBuildOptions): Promise<void> {
101-
const { pkgManagerArgs } = await import('../../utils/npm');
102-
const pkg = await this.e.project.requirePackageJson();
103-
104-
const args = await this.buildOptionsToNgArgs(options);
105-
const shellOptions = { cwd: this.e.project.directory };
106-
107-
debug(`Looking for ${chalk.cyan(BUILD_SCRIPT)} npm script.`);
108-
109-
if (pkg.scripts && pkg.scripts[BUILD_SCRIPT]) {
110-
debug(`Invoking ${chalk.cyan(BUILD_SCRIPT)} npm script.`);
111-
const [pkgManager, ...pkgArgs] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: BUILD_SCRIPT });
112-
await this.e.shell.run(pkgManager, pkgArgs, shellOptions);
113-
} else {
114-
await this.e.shell.run('ng', [...this.buildArchitectCommand(options), ...args], shellOptions);
115-
}
116-
}
117122
}

packages/@ionic/cli-utils/src/lib/project/angular/serve.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://github.com/angular/angular-cli/wiki/
8787
const port = options.port = await findClosestOpenPort(options.port);
8888

8989
const ng = new AngularServeCLI(this.e);
90-
await ng.start(options);
90+
await ng.serve(options);
9191

9292
return {
9393
custom: ng.resolvedProgram !== ng.program,

packages/@ionic/cli-utils/src/lib/project/ionic-angular/__tests__/build.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { IonicAngularBuildRunner } from '../build';
1+
import { IonicAngularBuildCLI } from '../build';
22

33
describe('@ionic/cli-utils', () => {
44

55
describe('lib/ionic-angular', () => {
66

7-
describe('IonicAngularBuildRunner', () => {
7+
describe('IonicAngularBuildCLI', () => {
88

9-
describe('generateAppScriptsArgs', () => {
9+
describe('buildOptionsToAppScriptsArgs', () => {
1010

1111
const options = {
1212
'--': ['--generateSourceMap', 'false'],
@@ -18,14 +18,14 @@ describe('@ionic/cli-utils', () => {
1818
};
1919

2020
it('should transform defaults', async () => {
21-
const runner = new IonicAngularBuildRunner({});
22-
const result = await runner.generateAppScriptsArgs({ '--': [] });
21+
const appscripts = new IonicAngularBuildCLI({});
22+
const result = await appscripts.buildOptionsToAppScriptsArgs({ '--': [] });
2323
expect(result).toEqual([]);
2424
});
2525

2626
it('should transform options', async () => {
27-
const runner = new IonicAngularBuildRunner({});
28-
const result = await runner.generateAppScriptsArgs(options);
27+
const appscripts = new IonicAngularBuildCLI({});
28+
const result = await appscripts.buildOptionsToAppScriptsArgs(options);
2929
expect(result).toEqual(['--prod', '--aot', '--minifyjs', '--minifycss', '--optimizejs', '--generateSourceMap', 'false']);
3030
});
3131

packages/@ionic/cli-utils/src/lib/project/ionic-angular/build.ts

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import chalk from 'chalk';
33
import * as Debug from 'debug';
44

55
import { CommandLineInputs, CommandLineOptions, CommandMetadata, IonicAngularBuildOptions } from '../../../definitions';
6-
import { BUILD_SCRIPT, BuildRunner, BuildRunnerDeps } from '../../build';
6+
import { BUILD_SCRIPT, BuildCLI, BuildRunner, BuildRunnerDeps } from '../../build';
77

88
import { IonicAngularProject } from './';
99
import { APP_SCRIPTS_OPTIONS } from './app-scripts';
@@ -61,33 +61,19 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://github.com/ionic-team/ionic-app-scri
6161
}
6262

6363
async buildProject(options: IonicAngularBuildOptions): Promise<void> {
64-
const { pkgManagerArgs } = await import('../../utils/npm');
65-
const pkg = await this.e.project.requirePackageJson();
66-
67-
let program = DEFAULT_PROGRAM;
68-
let args = this.generateAppScriptsArgs(options);
69-
const shellOptions = { cwd: this.e.project.directory };
70-
71-
debug(`Looking for ${chalk.cyan(BUILD_SCRIPT)} npm script.`);
72-
73-
if (pkg.scripts && pkg.scripts[BUILD_SCRIPT]) {
74-
if (pkg.scripts[BUILD_SCRIPT] === DEFAULT_BUILD_SCRIPT_VALUE) {
75-
debug(`Found ${chalk.cyan(BUILD_SCRIPT)}, but it is the default. Not running.`);
76-
args = ['build', ...args];
77-
} else {
78-
debug(`Invoking ${chalk.cyan(BUILD_SCRIPT)} npm script.`);
79-
const [ pkgManager, ...pkgArgs ] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: BUILD_SCRIPT, scriptArgs: args });
80-
program = pkgManager;
81-
args = pkgArgs;
82-
}
83-
} else {
84-
args = ['build', ...args];
85-
}
86-
87-
await this.e.shell.run(program, args, shellOptions);
64+
const appscripts = new IonicAngularBuildCLI(this.e);
65+
await appscripts.build(options);
8866
}
67+
}
8968

90-
generateAppScriptsArgs(options: IonicAngularBuildOptions): string[] {
69+
export class IonicAngularBuildCLI extends BuildCLI<IonicAngularBuildOptions> {
70+
readonly name = 'Ionic App Scripts';
71+
readonly pkg = '@ionic/app-scripts';
72+
readonly program = DEFAULT_PROGRAM;
73+
readonly prefix = 'app-scripts';
74+
readonly script = BUILD_SCRIPT;
75+
76+
protected buildOptionsToAppScriptsArgs(options: IonicAngularBuildOptions): string[] {
9177
const minimistArgs = {
9278
_: [],
9379
prod: options.prod ? true : false,
@@ -103,4 +89,36 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://github.com/ionic-team/ionic-app-scri
10389

10490
return [...unparseArgs(minimistArgs, { allowCamelCase: true, useEquals: false }), ...options['--']];
10591
}
92+
93+
protected async buildArgs(options: IonicAngularBuildOptions): Promise<string[]> {
94+
const { pkgManagerArgs } = await import('../../utils/npm');
95+
96+
const args = this.buildOptionsToAppScriptsArgs(options);
97+
98+
if (this.resolvedProgram === this.program) {
99+
return ['build', ...args];
100+
} else {
101+
const [ , ...pkgArgs ] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script, scriptArgs: [...args] });
102+
return pkgArgs;
103+
}
104+
}
105+
106+
protected async resolveProgram(): Promise<string> {
107+
if (typeof this.script !== 'undefined') {
108+
debug(`Looking for ${chalk.cyan(this.script)} npm script.`);
109+
110+
const pkg = await this.e.project.requirePackageJson();
111+
112+
if (pkg.scripts && pkg.scripts[this.script]) {
113+
if (pkg.scripts[this.script] === DEFAULT_BUILD_SCRIPT_VALUE) {
114+
debug(`Found ${chalk.cyan(this.script)}, but it is the default. Not running.`);
115+
} else {
116+
debug(`Using ${chalk.cyan(this.script)} npm script.`);
117+
return this.e.config.get('npmClient');
118+
}
119+
}
120+
}
121+
122+
return this.program;
123+
}
106124
}

packages/@ionic/cli-utils/src/lib/project/ionic-angular/serve.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export class IonicAngularServeRunner extends ServeRunner<IonicAngularServeOption
111111
options.notificationPort = notificationPort;
112112

113113
const appscripts = new IonicAngularServeCLI(this.e);
114-
await appscripts.start(options);
114+
await appscripts.serve(options);
115115

116116
return {
117117
custom: appscripts.resolvedProgram !== appscripts.program,

0 commit comments

Comments
 (0)