2019-07-24 10:27:47 -04:00

452 lines
13 KiB
TypeScript

import {
apply,
chain,
externalSchematic,
mergeWith,
move,
noop,
Rule,
schematic,
SchematicContext,
template,
Tree,
url
} from '@angular-devkit/schematics';
import { Schema } from './schema';
import * as ts from 'typescript';
import {
formatFiles,
getNpmScope,
getWorkspacePath,
insert,
offsetFromRoot,
readJsonInTree,
replaceAppNameWithPath,
replaceNodeValue,
toFileName,
updateJsonInTree,
updateWorkspace,
addGlobalLint
} from '@nrwl/workspace';
import { join, normalize } from '@angular-devkit/core';
import ngAdd from '../ng-add/ng-add';
import {
addImportToModule,
addImportToTestBed,
getDecoratorPropertyValueNode
} from '../../utils/ast-utils';
import { insertImport } from '@nrwl/workspace/src/utils/ast-utils';
interface NormalizedSchema extends Schema {
appProjectRoot: string;
e2eProjectName: string;
e2eProjectRoot: string;
parsedTags: string[];
}
function addRouterRootConfiguration(options: NormalizedSchema): Rule {
return (host: Tree) => {
const modulePath = `${options.appProjectRoot}/src/app/app.module.ts`;
const moduleSource = host.read(modulePath)!.toString('utf-8');
const sourceFile = ts.createSourceFile(
modulePath,
moduleSource,
ts.ScriptTarget.Latest,
true
);
insert(host, modulePath, [
insertImport(sourceFile, modulePath, 'RouterModule', '@angular/router'),
...addImportToModule(
sourceFile,
modulePath,
`RouterModule.forRoot([], {initialNavigation: 'enabled'})`
)
]);
if (options.skipTests !== true) {
const componentSpecPath = `${
options.appProjectRoot
}/src/app/app.component.spec.ts`;
const componentSpecSource = host
.read(componentSpecPath)!
.toString('utf-8');
const componentSpecSourceFile = ts.createSourceFile(
componentSpecPath,
componentSpecSource,
ts.ScriptTarget.Latest,
true
);
insert(host, componentSpecPath, [
insertImport(
componentSpecSourceFile,
componentSpecPath,
'RouterTestingModule',
'@angular/router/testing'
),
...addImportToTestBed(
componentSpecSourceFile,
componentSpecPath,
`RouterTestingModule`
)
]);
}
return host;
};
}
function updateComponentTemplate(options: NormalizedSchema): Rule {
return (host: Tree) => {
const baseContent = `
<div style="text-align:center">
<h1>Welcome to {{title}}!</h1>
<img width="450" src="https://raw.githubusercontent.com/nrwl/nx/master/nx-logo.png">
</div>
<p>This is an Angular app built with <a href="https://nx.dev/angular">Nx</a>.</p>
<p>🔎 **Nx is a set of Extensible Dev Tools for Monorepos.**</p>
<h2>Quick Start & Documentation</h2>
<ul>
<li><a href="https://nx.dev/angular/getting-started/what-is-nx">10-minute video showing all Nx features</a></li>
<li><a href="https://nx.dev/angular/tutorial/01-create-application">Interactive tutorial</a></li>
</ul>
`;
const content = options.routing
? `${baseContent}\n<router-outlet></router-outlet>`
: baseContent;
if (!options.inlineTemplate) {
return host.overwrite(
`${options.appProjectRoot}/src/app/app.component.html`,
content
);
}
const modulePath = `${options.appProjectRoot}/src/app/app.component.ts`;
const templateNodeValue = getDecoratorPropertyValueNode(
host,
modulePath,
'Component',
'template',
'@angular/core'
);
replaceNodeValue(
host,
modulePath,
templateNodeValue,
`\`\n${baseContent}\n\`,\n`
);
};
}
function updateLinting(options: NormalizedSchema): Rule {
return chain([
updateJsonInTree('tslint.json', json => {
if (
json.rulesDirectory &&
json.rulesDirectory.indexOf('node_modules/codelyzer') === -1
) {
json.rulesDirectory.push('node_modules/codelyzer');
json.rules = {
...json.rules,
'directive-selector': [true, 'attribute', 'app', 'camelCase'],
'component-selector': [true, 'element', 'app', 'kebab-case'],
'no-conflicting-lifecycle': true,
'no-host-metadata-property': true,
'no-input-rename': true,
'no-inputs-metadata-property': true,
'no-output-native': true,
'no-output-on-prefix': true,
'no-output-rename': true,
'no-outputs-metadata-property': true,
'template-banana-in-box': true,
'template-no-negated-async': true,
'use-lifecycle-interface': true,
'use-pipe-transform-interface': true
};
}
return json;
}),
updateJsonInTree(`${options.appProjectRoot}/tslint.json`, json => {
json.extends = `${offsetFromRoot(options.appProjectRoot)}tslint.json`;
return json;
})
]);
}
function addTsconfigs(options: NormalizedSchema): Rule {
return chain([
mergeWith(
apply(url('./files'), [
template({
...options,
offsetFromRoot: offsetFromRoot(options.appProjectRoot)
}),
move(options.appProjectRoot)
])
)
]);
}
function updateProject(options: NormalizedSchema): Rule {
return (host: Tree) => {
return chain([
updateJsonInTree(getWorkspacePath(host), json => {
const project = json.projects[options.name];
let fixedProject = replaceAppNameWithPath(
project,
options.name,
options.appProjectRoot
);
const angularSchematicNames = [
'class',
'component',
'directive',
'guard',
'module',
'pipe',
'service'
];
if (fixedProject.schematics) {
angularSchematicNames.forEach(type => {
const schematic = `@schematics/angular:${type}`;
if (schematic in fixedProject.schematics) {
fixedProject.schematics[`@nrwl/workspace:${type}`] =
fixedProject.schematics[schematic];
delete fixedProject.schematics[schematic];
}
});
}
delete fixedProject.architect.test;
fixedProject.architect.lint.options.tsConfig = fixedProject.architect.lint.options.tsConfig.filter(
path =>
path !==
join(normalize(options.appProjectRoot), 'tsconfig.spec.json') &&
path !==
join(normalize(options.appProjectRoot), 'e2e/tsconfig.json')
);
fixedProject.architect.lint.options.exclude.push(
'!' + join(normalize(options.appProjectRoot), '**')
);
if (options.e2eTestRunner === 'none') {
delete json.projects[options.e2eProjectName];
}
json.projects[options.name] = fixedProject;
return json;
}),
updateJsonInTree(`${options.appProjectRoot}/tsconfig.app.json`, json => {
return {
...json,
extends: `./tsconfig.json`,
compilerOptions: {
...json.compilerOptions,
outDir: `${offsetFromRoot(options.appProjectRoot)}dist/out-tsc`
},
exclude:
options.unitTestRunner === 'jest'
? ['src/test-setup.ts', '**/*.spec.ts']
: ['src/test.ts', '**/*.spec.ts'],
include: ['**/*.ts']
};
}),
host => {
host.delete(`${options.appProjectRoot}/tsconfig.spec.json`);
return host;
},
updateJsonInTree(`/nx.json`, json => {
const resultJson = {
...json,
projects: {
...json.projects,
[options.name]: { tags: options.parsedTags }
}
};
if (options.e2eTestRunner === 'protractor') {
resultJson.projects[options.e2eProjectName] = { tags: [] };
}
return resultJson;
}),
host => {
host.delete(`${options.appProjectRoot}/karma.conf.js`);
host.delete(`${options.appProjectRoot}/src/test.ts`);
}
]);
};
}
function removeE2e(options: NormalizedSchema, e2eProjectRoot: string): Rule {
return chain([
host => {
host.delete(`${e2eProjectRoot}/src/app.e2e-spec.ts`);
host.delete(`${e2eProjectRoot}/src/app.po.ts`);
host.delete(`${e2eProjectRoot}/protractor.conf.js`);
host.delete(`${e2eProjectRoot}/tsconfig.json`);
},
updateWorkspace(workspace => {
workspace.projects.get(options.name).targets.delete('e2e');
})
]);
}
function updateE2eProject(options: NormalizedSchema): Rule {
return (host: Tree) => {
// patching the spec file because of a bug in the CLI application schematic
// it hardcodes "app" in the e2e tests
const spec = `${options.e2eProjectRoot}/src/app.e2e-spec.ts`;
const content = host.read(spec).toString();
host.overwrite(
spec,
content.replace('Welcome to app!', `Welcome to ${options.prefix}!`)
);
return chain([
updateJsonInTree(getWorkspacePath(host), json => {
const project = {
root: options.e2eProjectRoot,
projectType: 'application',
architect: {
e2e: json.projects[options.name].architect.e2e,
lint: {
builder: '@angular-devkit/build-angular:tslint',
options: {
tsConfig: `${options.e2eProjectRoot}/tsconfig.e2e.json`,
exclude: [
'**/node_modules/**',
'!' + join(normalize(options.e2eProjectRoot), '**')
]
}
}
}
};
project.architect.e2e.options.protractorConfig = `${
options.e2eProjectRoot
}/protractor.conf.js`;
json.projects[options.e2eProjectName] = project;
delete json.projects[options.name].architect.e2e;
return json;
}),
updateJsonInTree(`${options.e2eProjectRoot}/tsconfig.e2e.json`, json => {
return {
...json,
extends: `./tsconfig.json`,
compilerOptions: {
...json.compilerOptions,
outDir: `${offsetFromRoot(options.e2eProjectRoot)}dist/out-tsc`
}
};
})
]);
};
}
export default function(schema: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
const options = normalizeOptions(host, schema);
// Determine the roots where @schematics/angular will place the projects
// This is not where the projects actually end up
const workspaceJson = readJsonInTree(host, getWorkspacePath(host));
const appProjectRoot = workspaceJson.newProjectRoot
? `${workspaceJson.newProjectRoot}/${options.name}`
: options.name;
const e2eProjectRoot = workspaceJson.newProjectRoot
? `${workspaceJson.newProjectRoot}/${options.e2eProjectName}`
: `${options.name}/e2e`;
return chain([
ngAdd({
...options,
skipFormat: true
}),
addGlobalLint('tslint'),
externalSchematic('@schematics/angular', 'application', {
name: options.name,
inlineStyle: options.inlineStyle,
inlineTemplate: options.inlineTemplate,
prefix: options.prefix,
skipTests: options.skipTests,
style: options.style,
viewEncapsulation: options.viewEncapsulation,
enableIvy: options.enableIvy,
routing: false,
skipInstall: true,
skipPackageJson: false
}),
addTsconfigs(options),
options.e2eTestRunner === 'protractor'
? move(e2eProjectRoot, options.e2eProjectRoot)
: removeE2e(options, e2eProjectRoot),
options.e2eTestRunner === 'protractor'
? updateE2eProject(options)
: noop(),
options.e2eTestRunner === 'cypress'
? externalSchematic('@nrwl/cypress', 'cypress-project', {
name: options.e2eProjectName,
directory: options.directory,
project: options.name
})
: noop(),
move(appProjectRoot, options.appProjectRoot),
updateProject(options),
updateComponentTemplate(options),
options.routing ? addRouterRootConfiguration(options) : noop(),
updateLinting(options),
options.unitTestRunner === 'jest'
? externalSchematic('@nrwl/jest', 'jest-project', {
project: options.name,
supportTsx: false,
skipSerializers: false,
setupFile: 'angular'
})
: noop(),
options.unitTestRunner === 'karma'
? schematic('karma-project', {
project: options.name
})
: noop(),
formatFiles(options)
])(host, context);
};
}
function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
const appDirectory = options.directory
? `${toFileName(options.directory)}/${toFileName(options.name)}`
: toFileName(options.name);
let e2eProjectName = `${toFileName(options.name)}-e2e`;
const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-');
if (options.e2eTestRunner !== 'cypress') {
e2eProjectName = `${appProjectName}-e2e`;
}
const appProjectRoot = `apps/${appDirectory}`;
const e2eProjectRoot = `apps/${appDirectory}-e2e`;
const parsedTags = options.tags
? options.tags.split(',').map(s => s.trim())
: [];
const defaultPrefix = getNpmScope(host);
return {
...options,
prefix: options.prefix ? options.prefix : defaultPrefix,
name: appProjectName,
appProjectRoot,
e2eProjectRoot,
e2eProjectName,
parsedTags
};
}