Skip to content

Commit 8e690b4

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

File tree

9 files changed

+373
-163
lines changed

9 files changed

+373
-163
lines changed

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
"homepage": "https://github.com/angular/angular-cli",
4242
"dependencies": {
4343
"@angular-devkit/build-optimizer": "0.0.3",
44+
"@angular-devkit/core": "0.0.4",
45+
"@angular-devkit/schematics": "0.0.6",
46+
"@schematics/angular": "0.0.5",
4447
"autoprefixer": "^6.5.3",
4548
"chalk": "^1.1.3",
4649
"circular-dependency-plugin": "^3.0.0",
+118-100
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,54 @@
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 { getCollection, getEngineHost, getSchematic, runSchematic } from '../utilities/schematics';
9+
import { DynamicPathOptions, dynamicPathParser } from '../utilities/dynamic-path-parser';
10+
import { getAppFromConfig } from '../utilities/app-utils';
11+
import * as path from 'path';
12+
813
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');
1114
const SilentError = require('silent-error');
1215

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;
16+
function mapSchematicOptions(schematic: any): any[] {
17+
const properties = schematic.description.schemaJson.properties;
18+
const keys = Object.keys(properties);
19+
const options = keys
20+
.map(key => ({...properties[key], ...{name: stringUtils.dasherize(key)}}))
21+
.map(opt => {
22+
let type;
23+
switch(opt.type) {
24+
case 'string':
25+
type = String;
26+
break;
27+
case 'boolean':
28+
type = Boolean;
29+
break;
30+
}
31+
let aliases: string[] = [];
32+
if (opt.alias) {
33+
aliases = [...aliases, opt.alias];
34+
}
35+
if (opt.aliases) {
36+
aliases = [...aliases, ...opt.aliases];
37+
}
38+
39+
return {
40+
...opt,
41+
aliases,
42+
type
43+
};
44+
});
45+
46+
return options;
2147
}
2248

2349
export default Command.extend({
2450
name: 'generate',
25-
description: 'Generates and/or modifies files based on a blueprint.',
51+
description: 'Generates and/or modifies files based on a schematic.',
2652
aliases: ['g'],
2753

2854
availableOptions: [
@@ -34,117 +60,109 @@ export default Command.extend({
3460
description: 'Run through without making any changes.'
3561
},
3662
{
37-
name: 'lint-fix',
63+
name: 'force',
3864
type: Boolean,
39-
aliases: ['lf'],
40-
description: 'Use lint to fix files after generation.'
65+
default: false,
66+
aliases: ['f'],
67+
description: 'Forces overwriting of files.'
4168
},
4269
{
43-
name: 'verbose',
44-
type: Boolean,
45-
default: false,
46-
aliases: ['v'],
47-
description: 'Adds more details to output logging.'
70+
name: 'app',
71+
type: String,
72+
aliases: ['a'],
73+
description: 'Specifies app name to use.'
74+
},
75+
{
76+
name: 'collection',
77+
type: String,
78+
aliases: ['c'],
79+
description: 'Schematics collection to use.'
4880
}
4981
],
5082

5183
anonymousOptions: [
52-
'<blueprint>'
84+
'<schemtatic>'
5385
],
5486

55-
beforeRun: function (rawArgs: string[]) {
56-
if (!rawArgs.length) {
57-
return;
87+
getCollectionName(rawArgs: string[]) {
88+
let collectionName = CliConfig.getValue('defaults.schematics.collection');
89+
if (rawArgs) {
90+
const parsedArgs = this.parseArgs(rawArgs, false);
91+
if (parsedArgs.options.collection) {
92+
collectionName = parsedArgs.options.collection;
93+
}
5894
}
95+
return collectionName;
96+
},
97+
98+
beforeRun: function(rawArgs: string[]) {
99+
const collection = getCollection(this.getCollectionName(rawArgs));
59100

60101
const isHelp = ['--help', '-h'].includes(rawArgs[0]);
61102
if (isHelp) {
62103
return;
63104
}
64105

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.`);
106+
const schematicName = rawArgs[0];
107+
if (!schematicName) {
108+
return Promise.reject(new SilentError(oneLine`
109+
The "ng generate" command requires a
110+
schematic name to be specified.
111+
For more details, use "ng help".
112+
`));
79113
}
80114

81-
if (/^\d/.test(rawArgs[1])) {
82-
SilentError.debugOrThrow('@angular/cli/commands/generate',
83-
`The \`ng generate ${name} ${rawArgs[1]}\` file name cannot begin with a digit.`);
84-
}
115+
this.schematic = getSchematic(collection, schematicName);
85116

86-
rawArgs[0] = blueprint.name;
87-
this.registerOptions(blueprint);
88-
},
117+
const schematicOptions = mapSchematicOptions(this.schematic);
89118

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));
119+
this.registerOptions({availableOptions: schematicOptions});
96120
},
97121

98122
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-
}
123+
const entityName = rawArgs[1];
124+
commandOptions.name = stringUtils.dasherize(entityName.split(path.sep).pop());
107125

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,
126+
const appConfig = getAppFromConfig(commandOptions.app);
127+
const dynamicPathOptions: DynamicPathOptions = {
120128
project: this.project,
121-
settings: this.settings,
122-
testing: this.testing,
123-
args: rawArgs,
124-
...commandOptions
129+
entityName: entityName,
130+
appConfig: appConfig,
131+
dryRun: commandOptions.dryRun
125132
};
133+
const parsedPath = dynamicPathParser(dynamicPathOptions);
134+
commandOptions.sourceDir = appConfig.root;
135+
commandOptions.path = parsedPath.dir.replace(appConfig.root + path.sep, '');
136+
137+
const cwd = process.cwd();
138+
let dir = cwd;
139+
const sourceRoot = path.join(this.project.root, appConfig.root);
140+
const appRoot = path.join(sourceRoot, 'app');
141+
if (cwd.indexOf(appRoot) === 0) {
142+
dir = cwd;
143+
} else if (cwd.indexOf(sourceRoot) === 0) {
144+
dir = path.join(sourceRoot, 'app');
145+
} else {
146+
dir = path.join(this.project.root, appConfig.root, 'app');
147+
}
126148

127-
return blueprint.install(blueprintOptions)
128-
.then(() => {
129-
const lintFix = commandOptions.lintFix !== undefined ?
130-
commandOptions.lintFix : CliConfig.getValue('defaults.lintFix');
131-
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-
});
147-
}
148-
});
149+
const collectionName = this.getCollectionName(rawArgs);
150+
const collection = getCollection(collectionName);
151+
const schematicName = rawArgs[0];
152+
const schematic = getSchematic(collection, schematicName);
153+
return runSchematic(schematic, cwd, dir, commandOptions, this.project.root);
154+
155+
},
156+
157+
printDetailedHelp: function () {
158+
const engineHost = getEngineHost();
159+
const collectionName = this.getCollectionName();
160+
const collection = getCollection(collectionName);
161+
const schematicNames: string[] = engineHost.listSchematics(collection)
162+
this.ui.writeLine(cyan('Available schematics:'));
163+
schematicNames.forEach(schematicName => {
164+
this.ui.writeLine(yellow(` ${schematicName}`));
165+
})
166+
this.ui.writeLine('');
149167
}
150168
});

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

+7-30
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';
@@ -11,7 +10,6 @@ import { oneLine } from 'common-tags';
1110
const Command = require('../ember-cli/lib/models/command');
1211
const Project = require('../ember-cli/lib/models/project');
1312
const SilentError = require('silent-error');
14-
const mkdir = denodeify(fs.mkdir);
1513

1614
const configFile = '.angular-cli.json';
1715
const changeLater = (path: string) => `You can later change the value in "${configFile}" (${path})`;
@@ -156,42 +154,21 @@ const NewCommand = Command.extend({
156154
commandOptions.skipGit = true;
157155
}
158156

159-
const directoryName = path.join(process.cwd(),
160-
commandOptions.directory ? commandOptions.directory : packageName);
161-
157+
commandOptions.directory = commandOptions.directory || packageName;
158+
const directoryName = path.join(process.cwd(), commandOptions.directory);
162159
const initCommand = new InitCommand({
163160
ui: this.ui,
164161
tasks: this.tasks,
165162
project: Project.nullProject(this.ui, this.cli)
166163
});
167164

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

194-
return createDirectory
171+
return Promise.resolve()
195172
.then(initCommand.run.bind(initCommand, commandOptions, rawArgs));
196173
}
197174
});

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
}

packages/@angular/cli/lib/config/schema.json

+17
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,23 @@
530530
"type": "string"
531531
}
532532
}
533+
},
534+
"schematics": {
535+
"description": "Properties about schematics.",
536+
"type": "object",
537+
"properties": {
538+
"collection": {
539+
"description": "The schematics collection to use.",
540+
"type": "string",
541+
"default": "@schematics/angular"
542+
},
543+
"newApp": {
544+
"description": "The new app schematic.",
545+
"type": "string",
546+
"default": "angular-app"
547+
}
548+
},
549+
"additionalProperties": false
533550
}
534551
},
535552
"additionalProperties": false

packages/@angular/cli/models/config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class CliConfig extends CliConfigBase<ConfigInterface> {
3232
const projectConfig = CliConfig.fromProject();
3333
if (projectConfig) {
3434
value = projectConfig.get(jsonPath);
35-
} else if (CliConfig.globalConfigFilePath() !== CliConfig.configFilePath()) {
35+
} else {
3636
const globalConfig = CliConfig.fromGlobal();
3737
if (globalConfig) {
3838
value = globalConfig.get(jsonPath);

0 commit comments

Comments
 (0)