Skip to content

Commit df6011e

Browse files
committed
feat(@angular/cli): Update generate & new to use schematics
This feature is related to #6593
1 parent a956227 commit df6011e

File tree

14 files changed

+1320
-200
lines changed

14 files changed

+1320
-200
lines changed

package-lock.json

+821-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@
4040
},
4141
"homepage": "https://github.com/angular/angular-cli",
4242
"dependencies": {
43-
"@angular-devkit/build-optimizer": "0.0.10",
43+
"@angular-devkit/build-optimizer": "0.0.11",
44+
"@angular-devkit/schematics": "0.0.13",
45+
"@schematics/angular": "0.0.13",
4446
"autoprefixer": "^6.5.3",
4547
"chalk": "^2.0.1",
4648
"circular-dependency-plugin": "^3.0.0",
+152-91
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,61 @@
1-
import * as chalk from 'chalk';
2-
import * as fs from 'fs';
3-
import * as os from 'os';
4-
import * as path from 'path';
1+
import { cyan, yellow } from 'chalk';
2+
const stringUtils = require('ember-cli-string-utils');
53
import { oneLine } from 'common-tags';
64
import { CliConfig } from '../models/config';
75

6+
import 'rxjs/add/observable/of';
7+
import 'rxjs/add/operator/ignoreElements';
8+
import {
9+
SchematicOutput,
10+
getCollection,
11+
getEngineHost,
12+
getSchematic,
13+
runSchematic,
14+
} from '../utilities/schematics';
15+
import { DynamicPathOptions, dynamicPathParser } from '../utilities/dynamic-path-parser';
16+
import { getAppFromConfig } from '../utilities/app-utils';
17+
import * as path from 'path';
18+
819
const Command = require('../ember-cli/lib/models/command');
9-
const Blueprint = require('../ember-cli/lib/models/blueprint');
10-
const parseOptions = require('../ember-cli/lib/utilities/parse-options');
1120
const SilentError = require('silent-error');
1221

13-
function loadBlueprints(): Array<any> {
14-
const blueprintList = fs.readdirSync(path.join(__dirname, '..', 'blueprints'));
15-
const blueprints = blueprintList
16-
.filter(bp => bp.indexOf('-test') === -1)
17-
.filter(bp => bp !== 'ng')
18-
.map(bp => Blueprint.load(path.join(__dirname, '..', 'blueprints', bp)));
19-
20-
return blueprints;
22+
function mapSchematicOptions(schematic: any): any[] {
23+
const properties = schematic.description.schemaJson.properties;
24+
const keys = Object.keys(properties);
25+
const options = keys
26+
.map(key => ({...properties[key], ...{name: stringUtils.dasherize(key)}}))
27+
.map(opt => {
28+
let type;
29+
switch (opt.type) {
30+
case 'string':
31+
type = String;
32+
break;
33+
case 'boolean':
34+
type = Boolean;
35+
break;
36+
}
37+
let aliases: string[] = [];
38+
if (opt.alias) {
39+
aliases = [...aliases, opt.alias];
40+
}
41+
if (opt.aliases) {
42+
aliases = [...aliases, ...opt.aliases];
43+
}
44+
45+
return {
46+
...opt,
47+
aliases,
48+
type,
49+
default: undefined // do not carry over schematics defaults
50+
};
51+
});
52+
53+
return options;
2154
}
2255

2356
export default Command.extend({
2457
name: 'generate',
25-
description: 'Generates and/or modifies files based on a blueprint.',
58+
description: 'Generates and/or modifies files based on a schematic.',
2659
aliases: ['g'],
2760

2861
availableOptions: [
@@ -34,117 +67,145 @@ export default Command.extend({
3467
description: 'Run through without making any changes.'
3568
},
3669
{
37-
name: 'lint-fix',
70+
name: 'force',
3871
type: Boolean,
39-
aliases: ['lf'],
40-
description: 'Use lint to fix files after generation.'
72+
default: false,
73+
aliases: ['f'],
74+
description: 'Forces overwriting of files.'
75+
},
76+
{
77+
name: 'app',
78+
type: String,
79+
aliases: ['a'],
80+
description: 'Specifies app name to use.'
81+
},
82+
{
83+
name: 'collection',
84+
type: String,
85+
aliases: ['c'],
86+
description: 'Schematics collection to use.'
4187
},
4288
{
43-
name: 'verbose',
89+
name: 'lint-fix',
4490
type: Boolean,
45-
default: false,
46-
aliases: ['v'],
47-
description: 'Adds more details to output logging.'
91+
aliases: ['lf'],
92+
description: 'Use lint to fix files after generation.'
4893
}
4994
],
5095

5196
anonymousOptions: [
52-
'<blueprint>'
97+
'<schemtatic>'
5398
],
5499

55-
beforeRun: function (rawArgs: string[]) {
56-
if (!rawArgs.length) {
57-
return;
100+
getCollectionName(rawArgs: string[]) {
101+
let collectionName = CliConfig.getValue('defaults.schematics.collection');
102+
if (rawArgs) {
103+
const parsedArgs = this.parseArgs(rawArgs, false);
104+
if (parsedArgs.options.collection) {
105+
collectionName = parsedArgs.options.collection;
106+
}
58107
}
108+
return collectionName;
109+
},
110+
111+
beforeRun: function(rawArgs: string[]) {
112+
const collection = getCollection(this.getCollectionName(rawArgs));
59113

60114
const isHelp = ['--help', '-h'].includes(rawArgs[0]);
61115
if (isHelp) {
62116
return;
63117
}
64118

65-
this.blueprints = loadBlueprints();
66-
67-
const name = rawArgs[0];
68-
const blueprint = this.blueprints.find((bp: any) => bp.name === name
69-
|| (bp.aliases && bp.aliases.includes(name)));
70-
71-
if (!blueprint) {
72-
SilentError.debugOrThrow('@angular/cli/commands/generate',
73-
`Invalid blueprint: ${name}`);
74-
}
75-
76-
if (!rawArgs[1]) {
77-
SilentError.debugOrThrow('@angular/cli/commands/generate',
78-
`The \`ng generate ${name}\` command requires a name to be specified.`);
119+
const schematicName = rawArgs[0];
120+
if (!schematicName) {
121+
return Promise.reject(new SilentError(oneLine`
122+
The "ng generate" command requires a
123+
schematic name to be specified.
124+
For more details, use "ng help".
125+
`));
79126
}
80127

81128
if (/^\d/.test(rawArgs[1])) {
82129
SilentError.debugOrThrow('@angular/cli/commands/generate',
83-
`The \`ng generate ${name} ${rawArgs[1]}\` file name cannot begin with a digit.`);
130+
`The \`ng generate ${schematicName} ${rawArgs[1]}\` file name cannot begin with a digit.`);
84131
}
85132

86-
rawArgs[0] = blueprint.name;
87-
this.registerOptions(blueprint);
88-
},
133+
this.schematic = getSchematic(collection, schematicName);
89134

90-
printDetailedHelp: function () {
91-
if (!this.blueprints) {
92-
this.blueprints = loadBlueprints();
93-
}
94-
this.ui.writeLine(chalk.cyan(' Available blueprints'));
95-
this.ui.writeLine(this.blueprints.map((bp: any) => bp.printBasicHelp(false)).join(os.EOL));
135+
const schematicOptions = mapSchematicOptions(this.schematic);
136+
137+
this.registerOptions({availableOptions: schematicOptions});
96138
},
97139

98140
run: function (commandOptions: any, rawArgs: string[]) {
99-
const name = rawArgs[0];
100-
if (!name) {
101-
return Promise.reject(new SilentError(oneLine`
102-
The "ng generate" command requires a
103-
blueprint name to be specified.
104-
For more details, use "ng help".
105-
`));
106-
}
141+
const entityName = rawArgs[1];
142+
commandOptions.name = stringUtils.dasherize(entityName.split(path.sep).pop());
107143

108-
const blueprint = this.blueprints.find((bp: any) => bp.name === name
109-
|| (bp.aliases && bp.aliases.includes(name)));
110-
111-
const projectName = CliConfig.getValue('project.name');
112-
const blueprintOptions = {
113-
target: this.project.root,
114-
entity: {
115-
name: rawArgs[1],
116-
options: parseOptions(rawArgs.slice(2))
117-
},
118-
projectName,
119-
ui: this.ui,
144+
const appConfig = getAppFromConfig(commandOptions.app);
145+
const dynamicPathOptions: DynamicPathOptions = {
120146
project: this.project,
121-
settings: this.settings,
122-
testing: this.testing,
123-
args: rawArgs,
124-
...commandOptions
147+
entityName: entityName,
148+
appConfig: appConfig,
149+
dryRun: commandOptions.dryRun
125150
};
151+
const parsedPath = dynamicPathParser(dynamicPathOptions);
152+
commandOptions.sourceDir = appConfig.root;
153+
commandOptions.path = parsedPath.dir.replace(appConfig.root + path.sep, '');
154+
155+
const cwd = this.project.root;
156+
let dir = cwd;
157+
const sourceRoot = path.join(this.project.root, appConfig.root);
158+
const appRoot = path.join(sourceRoot, 'app');
159+
if (cwd.indexOf(appRoot) === 0) {
160+
dir = cwd;
161+
} else if (cwd.indexOf(sourceRoot) === 0) {
162+
dir = path.join(sourceRoot, 'app');
163+
} else {
164+
dir = path.join(this.project.root, appConfig.root, 'app');
165+
}
126166

127-
return blueprint.install(blueprintOptions)
128-
.then(() => {
167+
const collectionName = this.getCollectionName(rawArgs);
168+
const collection = getCollection(collectionName);
169+
const schematicName = rawArgs[0];
170+
const schematic = getSchematic(collection, schematicName);
171+
return runSchematic(schematic, cwd, dir, commandOptions, this.project.root)
172+
.then((output: SchematicOutput) => {
173+
const modifiedFiles = output.modifiedFiles;
129174
const lintFix = commandOptions.lintFix !== undefined ?
130175
commandOptions.lintFix : CliConfig.getValue('defaults.lintFix');
131176

132-
if (lintFix && blueprint.modifiedFiles) {
133-
const LintTask = require('../tasks/lint').default;
134-
const lintTask = new LintTask({
135-
ui: this.ui,
136-
project: this.project
137-
});
138-
139-
return lintTask.run({
140-
fix: true,
141-
force: true,
142-
silent: true,
143-
configs: [{
144-
files: blueprint.modifiedFiles.filter((file: string) => /.ts$/.test(file))
145-
}]
146-
});
177+
if (lintFix && modifiedFiles) {
178+
const LintTask = require('../tasks/lint').default;
179+
const lintTask = new LintTask({
180+
ui: this.ui,
181+
project: this.project
182+
});
183+
184+
return lintTask.run({
185+
fix: true,
186+
force: true,
187+
silent: true,
188+
configs: [{
189+
files: modifiedFiles
190+
.filter((file: string) => /.ts$/.test(file))
191+
.map((file: string) => path.join(this.project.root, file))
192+
}]
193+
});
147194
}
148195
});
196+
197+
198+
},
199+
200+
printDetailedHelp: function () {
201+
const engineHost = getEngineHost();
202+
const collectionName = this.getCollectionName();
203+
const collection = getCollection(collectionName);
204+
const schematicNames: string[] = engineHost.listSchematics(collection);
205+
this.ui.writeLine(cyan('Available schematics:'));
206+
schematicNames.forEach(schematicName => {
207+
this.ui.writeLine(yellow(` ${schematicName}`));
208+
});
209+
this.ui.writeLine('');
149210
}
150211
});

packages/@angular/cli/commands/new.ts

+8-33
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
33
import * as chalk from 'chalk';
4-
import denodeify = require('denodeify');
54

65
import InitCommand from './init';
76
import { CliConfig } from '../models/config';
@@ -12,10 +11,6 @@ const Command = require('../ember-cli/lib/models/command');
1211
const Project = require('../ember-cli/lib/models/project');
1312
const SilentError = require('silent-error');
1413

15-
// There's some problem with the generic typings for fs.makedir.
16-
// Couldn't find matching types for the callbacks so leaving it as any for now.
17-
const mkdir = denodeify<string, void>(fs.mkdir as any);
18-
1914
const configFile = '.angular-cli.json';
2015
const changeLater = (path: string) => `You can later change the value in "${configFile}" (${path})`;
2116

@@ -159,42 +154,22 @@ const NewCommand = Command.extend({
159154
commandOptions.skipGit = true;
160155
}
161156

162-
const directoryName = path.join(process.cwd(),
163-
commandOptions.directory ? commandOptions.directory : packageName);
164-
157+
commandOptions.directory = commandOptions.directory || packageName;
158+
const directoryName = path.join(process.cwd(), commandOptions.directory);
165159
const initCommand = new InitCommand({
166160
ui: this.ui,
167161
tasks: this.tasks,
168162
project: Project.nullProject(this.ui, this.cli)
169163
});
170164

171-
let createDirectory;
172-
if (commandOptions.dryRun) {
173-
createDirectory = Promise.resolve()
174-
.then(() => {
175-
if (fs.existsSync(directoryName) && this.isProject(directoryName)) {
176-
throw new SilentError(oneLine`
177-
Directory ${directoryName} exists and is already an Angular CLI project.
178-
`);
179-
}
180-
});
181-
} else {
182-
createDirectory = mkdir(directoryName)
183-
.catch((err) => {
184-
if (err.code === 'EEXIST') {
185-
if (this.isProject(directoryName)) {
186-
throw new SilentError(oneLine`
187-
Directory ${directoryName} exists and is already an Angular CLI project.
188-
`);
189-
}
190-
} else {
191-
throw err;
192-
}
193-
})
194-
.then(() => process.chdir(directoryName));
165+
166+
if (fs.existsSync(directoryName) && this.isProject(directoryName)) {
167+
throw new SilentError(oneLine`
168+
Directory ${directoryName} exists and is already an Angular CLI project.
169+
`);
195170
}
196171

197-
return createDirectory
172+
return Promise.resolve()
198173
.then(initCommand.run.bind(initCommand, commandOptions, rawArgs));
199174
}
200175
});

packages/@angular/cli/ember-cli/lib/models/command.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ let Command = CoreObject.extend({
496496
@param {Object} commandArgs
497497
@return {Object|null}
498498
*/
499-
parseArgs(commandArgs) {
499+
parseArgs(commandArgs, showErrors = true) {
500500
let knownOpts = {}; // Parse options
501501
let commandOptions = {};
502502
let parsedOptions;
@@ -507,7 +507,7 @@ let Command = CoreObject.extend({
507507

508508
let validateParsed = function(key) {
509509
// ignore 'argv', 'h', and 'help'
510-
if (!commandOptions.hasOwnProperty(key) && key !== 'argv' && key !== 'h' && key !== 'help') {
510+
if (!commandOptions.hasOwnProperty(key) && key !== 'argv' && key !== 'h' && key !== 'help' && showErrors) {
511511
this.ui.writeLine(chalk.yellow(`The option '--${key}' is not registered with the ${this.name} command. ` +
512512
`Run \`ng ${this.name} --help\` for a list of supported options.`));
513513
}

0 commit comments

Comments
 (0)