Skip to content

Commit a7d095d

Browse files
Alanhansl
Alan
authored andcommitted
feat(@schematics/angular): change layout for root applications
This change aligns the file layout of applications generated with `ng new` and `ng generate` Ref: TOOL-686
1 parent bf1c069 commit a7d095d

15 files changed

+169
-120
lines changed

packages/schematics/angular/application/files/root/tsconfig.app.json.template renamed to packages/schematics/angular/application/files/tsconfig.app.json.template

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
"outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/app",
55
"types": []
66
},
7+
"include": [
8+
"src/**/*.ts"
9+
],
710
"exclude": [
8-
"test.ts",
9-
"**/*.spec.ts",
10-
"e2e/**"
11+
"src/test.ts",
12+
"src/**/*.spec.ts"
1113
]<% if (enableIvy) { %>,
1214
"angularCompilerOptions": {
1315
"enableIvy": true

packages/schematics/angular/application/files/root/tsconfig.spec.json.template renamed to packages/schematics/angular/application/files/tsconfig.spec.json.template

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
]
99
},
1010
"files": [
11-
"<%= rootInSrc ? '' : 'src/' %>test.ts",
12-
"<%= rootInSrc ? '' : 'src/' %>polyfills.ts"
11+
"src/test.ts",
12+
"src/polyfills.ts"
1313
],
1414
"include": [
15-
"**/*.spec.ts",
16-
"**/*.d.ts"
15+
"src/**/*.spec.ts",
16+
"src/**/*.d.ts"
1717
]
1818
}
+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
{
2-
"extends": "<%= relativePathToWorkspaceRoot %>/tslint.json",
1+
{<% if (!isRootApp) { %>
2+
"extends": "<%= relativePathToWorkspaceRoot %>/tslint.json",<%
3+
} %>
34
"rules": {
45
"directive-selector": [
56
true,

packages/schematics/angular/application/index.ts

+117-85
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {
9+
JsonAstObject,
910
JsonObject,
1011
JsonParseMode,
1112
join,
1213
normalize,
1314
parseJsonAst,
14-
relative,
1515
strings,
1616
} from '@angular-devkit/core';
1717
import {
@@ -103,6 +103,78 @@ function addDependenciesToPackageJson(options: ApplicationOptions) {
103103
};
104104
}
105105

106+
function readTsLintConfig(host: Tree, path: string): JsonAstObject {
107+
const buffer = host.read(path);
108+
if (!buffer) {
109+
throw new SchematicsException(`Could not read ${path}.`);
110+
}
111+
112+
const config = parseJsonAst(buffer.toString(), JsonParseMode.Loose);
113+
if (config.kind !== 'object') {
114+
throw new SchematicsException(`Invalid ${path}. Was expecting an object.`);
115+
}
116+
117+
return config;
118+
}
119+
120+
/**
121+
* Merges the application tslint.json with the workspace tslint.json
122+
* when the application being created is a root application
123+
*
124+
* @param {Tree} parentHost The root host of the schematic
125+
*/
126+
function mergeWithRootTsLint(parentHost: Tree) {
127+
return (host: Tree) => {
128+
const tsLintPath = '/tslint.json';
129+
if (!host.exists(tsLintPath)) {
130+
return;
131+
}
132+
133+
const rootTslintConfig = readTsLintConfig(parentHost, tsLintPath);
134+
const appTslintConfig = readTsLintConfig(host, tsLintPath);
135+
136+
const recorder = host.beginUpdate(tsLintPath);
137+
rootTslintConfig.properties.forEach(prop => {
138+
if (findPropertyInAstObject(appTslintConfig, prop.key.value)) {
139+
// property already exists. Skip!
140+
return;
141+
}
142+
143+
insertPropertyInAstObjectInOrder(
144+
recorder,
145+
appTslintConfig,
146+
prop.key.value,
147+
prop.value.value,
148+
2,
149+
);
150+
});
151+
152+
const rootRules = findPropertyInAstObject(rootTslintConfig, 'rules');
153+
const appRules = findPropertyInAstObject(appTslintConfig, 'rules');
154+
155+
if (!appRules || appRules.kind !== 'object' || !rootRules || rootRules.kind !== 'object') {
156+
// rules are not valid. Skip!
157+
return;
158+
}
159+
160+
rootRules.properties.forEach(prop => {
161+
insertPropertyInAstObjectInOrder(
162+
recorder,
163+
appRules,
164+
prop.key.value,
165+
prop.value.value,
166+
4,
167+
);
168+
});
169+
170+
host.commitUpdate(recorder);
171+
172+
// this shouldn't be needed but at the moment without this formatting is not correct.
173+
const content = readTsLintConfig(host, tsLintPath);
174+
host.overwrite(tsLintPath, JSON.stringify(content.value, undefined, 2));
175+
};
176+
}
177+
106178
function addPostInstallScript() {
107179
return (host: Tree) => {
108180
const pkgJsonPath = '/package.json';
@@ -148,15 +220,14 @@ function addAppToWorkspaceFile(options: ApplicationOptions, workspace: Workspace
148220
// if (workspaceJson.value === null) {
149221
// throw new SchematicsException(`Unable to parse configuration file (${workspacePath}).`);
150222
// }
223+
151224
let projectRoot = options.projectRoot !== undefined
152225
? options.projectRoot
153-
: `${workspace.newProjectRoot || ''}/${options.name}`;
226+
: `${workspace.newProjectRoot}/${options.name}`;
227+
154228
if (projectRoot !== '' && !projectRoot.endsWith('/')) {
155229
projectRoot += '/';
156230
}
157-
const rootFilesRoot = options.projectRoot === undefined
158-
? projectRoot
159-
: projectRoot + 'src/';
160231

161232
const schematics: JsonObject = {};
162233

@@ -186,9 +257,10 @@ function addAppToWorkspaceFile(options: ApplicationOptions, workspace: Workspace
186257
});
187258
}
188259

260+
const sourceRoot = join(normalize(projectRoot), 'src');
189261
const project: WorkspaceProject = {
190262
root: projectRoot,
191-
sourceRoot: join(normalize(projectRoot), 'src'),
263+
sourceRoot,
192264
projectType: ProjectType.Application,
193265
prefix: options.prefix || 'app',
194266
schematics,
@@ -197,25 +269,25 @@ function addAppToWorkspaceFile(options: ApplicationOptions, workspace: Workspace
197269
builder: Builders.Browser,
198270
options: {
199271
outputPath: `dist/${options.name}`,
200-
index: `${projectRoot}src/index.html`,
201-
main: `${projectRoot}src/main.ts`,
202-
polyfills: `${projectRoot}src/polyfills.ts`,
203-
tsConfig: `${rootFilesRoot}tsconfig.app.json`,
272+
index: `${sourceRoot}/index.html`,
273+
main: `${sourceRoot}/main.ts`,
274+
polyfills: `${sourceRoot}/polyfills.ts`,
275+
tsConfig: `${projectRoot}tsconfig.app.json`,
204276
assets: [
205-
join(normalize(projectRoot), 'src', 'favicon.ico'),
206-
join(normalize(projectRoot), 'src', 'assets'),
277+
`${sourceRoot}/favicon.ico`,
278+
`${sourceRoot}/assets`,
207279
],
208280
styles: [
209-
`${projectRoot}src/styles.${options.style}`,
281+
`${sourceRoot}/styles.${options.style}`,
210282
],
211283
scripts: [],
212284
es5BrowserSupport: true,
213285
},
214286
configurations: {
215287
production: {
216288
fileReplacements: [{
217-
replace: `${projectRoot}src/environments/environment.ts`,
218-
with: `${projectRoot}src/environments/environment.prod.ts`,
289+
replace: `${sourceRoot}/environments/environment.ts`,
290+
with: `${sourceRoot}/environments/environment.prod.ts`,
219291
}],
220292
optimization: true,
221293
outputHashing: 'all',
@@ -254,26 +326,26 @@ function addAppToWorkspaceFile(options: ApplicationOptions, workspace: Workspace
254326
test: {
255327
builder: Builders.Karma,
256328
options: {
257-
main: `${projectRoot}src/test.ts`,
258-
polyfills: `${projectRoot}src/polyfills.ts`,
259-
tsConfig: `${rootFilesRoot}tsconfig.spec.json`,
260-
karmaConfig: `${rootFilesRoot}karma.conf.js`,
329+
main: `${sourceRoot}/test.ts`,
330+
polyfills: `${sourceRoot}/polyfills.ts`,
331+
tsConfig: `${projectRoot}tsconfig.spec.json`,
332+
karmaConfig: `${projectRoot}karma.conf.js`,
333+
assets: [
334+
`${sourceRoot}/favicon.ico`,
335+
`${sourceRoot}/assets`,
336+
],
261337
styles: [
262-
`${projectRoot}src/styles.${options.style}`,
338+
`${sourceRoot}/styles.${options.style}`,
263339
],
264340
scripts: [],
265-
assets: [
266-
join(normalize(projectRoot), 'src', 'favicon.ico'),
267-
join(normalize(projectRoot), 'src', 'assets'),
268-
],
269341
},
270342
},
271343
lint: {
272344
builder: Builders.TsLint,
273345
options: {
274346
tsConfig: [
275-
`${rootFilesRoot}tsconfig.app.json`,
276-
`${rootFilesRoot}tsconfig.spec.json`,
347+
`${projectRoot}tsconfig.app.json`,
348+
`${projectRoot}tsconfig.spec.json`,
277349
],
278350
exclude: [
279351
'**/node_modules/**',
@@ -282,19 +354,12 @@ function addAppToWorkspaceFile(options: ApplicationOptions, workspace: Workspace
282354
},
283355
},
284356
};
285-
// tslint:disable-next-line:no-any
286-
// const projects: JsonObject = (<any> workspaceAst.value).projects || {};
287-
// tslint:disable-next-line:no-any
288-
// if (!(<any> workspaceAst.value).projects) {
289-
// // tslint:disable-next-line:no-any
290-
// (<any> workspaceAst.value).projects = projects;
291-
// }
292357

293358
return addProjectToWorkspace(workspace, options.name, project);
294359
}
295360

296361
function minimalPathFilter(path: string): boolean {
297-
const toRemoveList = /(test.ts|tsconfig.spec.json|karma.conf.js).template$/;
362+
const toRemoveList = /(test.ts|tsconfig.spec.json|karma.conf.js|tslint.json).template$/;
298363

299364
return !toRemoveList.test(path);
300365
}
@@ -305,8 +370,8 @@ export default function (options: ApplicationOptions): Rule {
305370
throw new SchematicsException(`Invalid options, "name" is required.`);
306371
}
307372
validateProjectName(options.name);
308-
const prefix = options.prefix || 'app';
309-
const appRootSelector = `${prefix}-root`;
373+
options.prefix = options.prefix || 'app';
374+
const appRootSelector = `${options.prefix}-root`;
310375
const componentOptions: Partial<ComponentOptions> = !options.minimal ?
311376
{
312377
inlineStyle: options.inlineStyle,
@@ -323,23 +388,15 @@ export default function (options: ApplicationOptions): Rule {
323388
};
324389

325390
const workspace = getWorkspace(host);
326-
let newProjectRoot = workspace.newProjectRoot || '';
327-
let appDir = `${newProjectRoot}/${options.name}`;
328-
let sourceRoot = `${appDir}/src`;
329-
let sourceDir = `${sourceRoot}/app`;
330-
let relativePathToWorkspaceRoot = appDir.split('/').map(x => '..').join('/');
331-
const rootInSrc = options.projectRoot !== undefined;
332-
if (options.projectRoot !== undefined) {
333-
newProjectRoot = options.projectRoot;
334-
appDir = `${newProjectRoot}/src`;
335-
sourceRoot = appDir;
336-
sourceDir = `${sourceRoot}/app`;
337-
relativePathToWorkspaceRoot = relative(normalize('/' + sourceRoot), normalize('/'));
338-
if (relativePathToWorkspaceRoot === '') {
339-
relativePathToWorkspaceRoot = '.';
340-
}
341-
}
342-
const tsLintRoot = appDir;
391+
const newProjectRoot = workspace.newProjectRoot || '';
392+
const isRootApp = options.projectRoot !== undefined;
393+
const appDir = isRootApp
394+
? options.projectRoot as string
395+
: `${newProjectRoot}/${options.name}`;
396+
const relativePathToWorkspaceRoot = appDir
397+
? appDir.split('/').map(() => '..').join('/')
398+
: '.';
399+
const sourceDir = `${appDir}/src/app`;
343400

344401
const e2eOptions: E2eOptions = {
345402
relatedAppName: options.name,
@@ -349,43 +406,18 @@ export default function (options: ApplicationOptions): Rule {
349406
return chain([
350407
addAppToWorkspaceFile(options, workspace),
351408
mergeWith(
352-
apply(url('./files/src'), [
353-
options.minimal ? filter(minimalPathFilter) : noop(),
354-
applyTemplates({
355-
utils: strings,
356-
...options,
357-
'dot': '.',
358-
relativePathToWorkspaceRoot,
359-
}),
360-
move(sourceRoot),
361-
])),
362-
mergeWith(
363-
apply(url('./files/root'), [
409+
apply(url('./files'), [
364410
options.minimal ? filter(minimalPathFilter) : noop(),
365411
applyTemplates({
366412
utils: strings,
367413
...options,
368-
'dot': '.',
369414
relativePathToWorkspaceRoot,
370-
rootInSrc,
371415
appName: options.name,
416+
isRootApp,
372417
}),
418+
isRootApp ? mergeWithRootTsLint(host) : noop(),
373419
move(appDir),
374-
])),
375-
options.minimal ? noop() : mergeWith(
376-
apply(url('./files/lint'), [
377-
applyTemplates({
378-
utils: strings,
379-
...options,
380-
tsLintRoot,
381-
relativePathToWorkspaceRoot,
382-
prefix,
383-
}),
384-
// TODO: Moving should work but is bugged right now.
385-
// The __tsLintRoot__ is being used meanwhile.
386-
// Otherwise the tslint.json file could be inside of the root folder and
387-
// this block and the lint folder could be removed.
388-
])),
420+
]), MergeStrategy.Overwrite),
389421
schematic('module', {
390422
name: 'app',
391423
commonModule: false,
@@ -410,11 +442,11 @@ export default function (options: ApplicationOptions): Rule {
410442
? filter(path => !path.endsWith('.html.template'))
411443
: noop(),
412444
componentOptions.skipTests
413-
? filter(path => !/[.|-]spec.ts.template$/.test(path))
445+
? filter(path => !path.endsWith('.spec.ts.template'))
414446
: noop(),
415447
applyTemplates({
416448
utils: strings,
417-
...options as any, // tslint:disable-line:no-any
449+
...options,
418450
selector: appRootSelector,
419451
...componentOptions,
420452
}),
@@ -423,7 +455,7 @@ export default function (options: ApplicationOptions): Rule {
423455
options.minimal ? noop() : schematic('e2e', e2eOptions),
424456
options.enableIvy ? addPostInstallScript() : noop(),
425457
options.skipPackageJson ? noop() : addDependenciesToPackageJson(options),
426-
options.lintFix ? applyLintFix(sourceDir) : noop(),
458+
options.lintFix ? applyLintFix(appDir) : noop(),
427459
]);
428460
};
429461
}

0 commit comments

Comments
 (0)