2019-05-21 11:51:40 -04:00

483 lines
13 KiB
TypeScript

import { join, normalize } from '@angular-devkit/core';
import {
chain,
externalSchematic,
noop,
Rule,
Tree,
SchematicContext,
schematic,
url,
apply,
mergeWith,
move,
template,
MergeStrategy
} from '@angular-devkit/schematics';
import { Schema } from './schema';
import * as path from 'path';
import * as ts from 'typescript';
import {
NxJson,
updateJsonInTree,
readJsonInTree,
offsetFromRoot
} from '@nrwl/workspace';
import { addGlobal, addIncludeToTsConfig, insert } from '@nrwl/workspace';
import { toClassName, toFileName, toPropertyName } from '@nrwl/workspace';
import {
getNpmScope,
getWorkspacePath,
replaceAppNameWithPath
} from '@nrwl/workspace';
import { formatFiles } from '@nrwl/workspace';
import { addUnitTestRunner } from '../ng-add/ng-add';
import { addImportToModule, addRoute } from '../../utils/ast-utils';
import { insertImport } from '@nrwl/workspace/src/utils/ast-utils';
interface NormalizedSchema extends Schema {
name: string;
fileName: string;
projectRoot: string;
entryFile: string;
modulePath: string;
moduleName: string;
projectDirectory: string;
parsedTags: string[];
}
function addLazyLoadedRouterConfiguration(options: NormalizedSchema): Rule {
return (host: Tree) => {
const moduleSource = host.read(options.modulePath)!.toString('utf-8');
const sourceFile = ts.createSourceFile(
options.modulePath,
moduleSource,
ts.ScriptTarget.Latest,
true
);
insert(host, options.modulePath, [
insertImport(
sourceFile,
options.modulePath,
'RouterModule',
'@angular/router'
),
...addImportToModule(
sourceFile,
options.modulePath,
`
RouterModule.forChild([
/* {path: '', pathMatch: 'full', component: InsertYourComponentHere} */
]) `
)
]);
return host;
};
}
function addRouterConfiguration(options: NormalizedSchema): Rule {
return (host: Tree) => {
const moduleSource = host.read(options.modulePath)!.toString('utf-8');
const moduleSourceFile = ts.createSourceFile(
options.modulePath,
moduleSource,
ts.ScriptTarget.Latest,
true
);
const constName = `${toPropertyName(options.fileName)}Routes`;
insert(host, options.modulePath, [
insertImport(
moduleSourceFile,
options.modulePath,
'RouterModule, Route',
'@angular/router'
),
...addImportToModule(
moduleSourceFile,
options.modulePath,
`RouterModule`
),
...addGlobal(
moduleSourceFile,
options.modulePath,
`export const ${constName}: Route[] = [];`
)
]);
return host;
};
}
function addLoadChildren(options: NormalizedSchema): Rule {
return (host: Tree) => {
const npmScope = getNpmScope(host);
if (!host.exists(options.parentModule)) {
throw new Error(`Cannot find '${options.parentModule}'`);
}
const moduleSource = host.read(options.parentModule)!.toString('utf-8');
const sourceFile = ts.createSourceFile(
options.parentModule,
moduleSource,
ts.ScriptTarget.Latest,
true
);
insert(host, options.parentModule, [
...addRoute(
options.parentModule,
sourceFile,
`{path: '${toFileName(
options.fileName
)}', loadChildren: '@${npmScope}/${options.projectDirectory}#${
options.moduleName
}'}`
)
]);
const tsConfig = findClosestTsConfigApp(host, options.parentModule);
if (tsConfig) {
const tsConfigAppSource = host.read(tsConfig)!.toString('utf-8');
const tsConfigAppFile = ts.createSourceFile(
tsConfig,
tsConfigAppSource,
ts.ScriptTarget.Latest,
true
);
const offset = offsetFromRoot(path.dirname(tsConfig));
insert(host, tsConfig, [
...addIncludeToTsConfig(
tsConfig,
tsConfigAppFile,
`\n , "${offset}${options.projectRoot}/src/index.ts"\n`
)
]);
} else {
// we should warn the user about not finding the config
}
return host;
};
}
function findClosestTsConfigApp(
host: Tree,
parentModule: string
): string | null {
const dir = path.parse(parentModule).dir;
if (host.exists(`${dir}/tsconfig.app.json`)) {
return `${dir}/tsconfig.app.json`;
} else if (dir != '') {
return findClosestTsConfigApp(host, dir);
} else {
return null;
}
}
function addChildren(options: NormalizedSchema): Rule {
return (host: Tree) => {
const npmScope = getNpmScope(host);
if (!host.exists(options.parentModule)) {
throw new Error(`Cannot find '${options.parentModule}'`);
}
const moduleSource = host.read(options.parentModule)!.toString('utf-8');
const sourceFile = ts.createSourceFile(
options.parentModule,
moduleSource,
ts.ScriptTarget.Latest,
true
);
const constName = `${toPropertyName(options.fileName)}Routes`;
const importPath = `@${npmScope}/${options.projectDirectory}`;
insert(host, options.parentModule, [
insertImport(
sourceFile,
options.parentModule,
`${options.moduleName}, ${constName}`,
importPath
),
...addImportToModule(
sourceFile,
options.parentModule,
options.moduleName
),
...addRoute(
options.parentModule,
sourceFile,
`{path: '${toFileName(options.fileName)}', children: ${constName}}`
)
]);
return host;
};
}
function updateNgPackage(options: NormalizedSchema): Rule {
if (!options.publishable) {
return noop();
}
const dest = `${offsetFromRoot(options.projectRoot)}dist/libs/${
options.projectDirectory
}`;
return chain([
updateJsonInTree(`${options.projectRoot}/ng-package.json`, json => {
return {
...json,
dest
};
})
]);
}
function updateProject(options: NormalizedSchema): Rule {
return (host: Tree, context: SchematicContext) => {
const libRoot = `${options.projectRoot}/src/lib/`;
host.delete(path.join(libRoot, `${options.name}.service.ts`));
host.delete(path.join(libRoot, `${options.name}.service.spec.ts`));
host.delete(path.join(libRoot, `${options.name}.component.ts`));
host.delete(path.join(libRoot, `${options.name}.component.spec.ts`));
if (!options.publishable) {
host.delete(path.join(options.projectRoot, 'ng-package.json'));
host.delete(path.join(options.projectRoot, 'package.json'));
}
host.delete(path.join(options.projectRoot, 'karma.conf.js'));
host.delete(path.join(options.projectRoot, 'src/test.ts'));
host.delete(path.join(options.projectRoot, 'tsconfig.spec.json'));
host.delete(path.join(libRoot, `${options.name}.module.ts`));
host.create(
path.join(libRoot, `${options.fileName}.module.ts`),
`
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
imports: [
CommonModule
]
})
export class ${options.moduleName} { }
`
);
if (options.unitTestRunner !== 'none') {
host.create(
path.join(libRoot, `${options.fileName}.module.spec.ts`),
`
import { async, TestBed } from '@angular/core/testing';
import { ${options.moduleName} } from './${options.fileName}.module';
describe('${options.moduleName}', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ ${options.moduleName} ]
})
.compileComponents();
}));
it('should create', () => {
expect(${options.moduleName}).toBeDefined();
});
});
`
);
}
host.overwrite(
`${options.projectRoot}/src/index.ts`,
`
export * from './lib/${options.fileName}.module';
`
);
return chain([
mergeWith(
apply(url('./files/lib'), [
template({
...options,
offsetFromRoot: offsetFromRoot(options.projectRoot)
}),
move(options.projectRoot)
]),
MergeStrategy.Overwrite
),
updateJsonInTree(getWorkspacePath(host), json => {
const project = json.projects[options.name];
const fixedProject = replaceAppNameWithPath(
project,
options.name,
options.projectRoot
);
fixedProject.schematics = fixedProject.schematics || {};
if (options.style !== 'css') {
fixedProject.schematics = {
...fixedProject.schematics,
'@nrwl/workspace:component': {
styleext: options.style
}
};
}
if (!options.publishable) {
delete fixedProject.architect.build;
}
delete fixedProject.architect.test;
fixedProject.architect.lint.options.tsConfig = fixedProject.architect.lint.options.tsConfig.filter(
path =>
path !== join(normalize(options.projectRoot), 'tsconfig.spec.json')
);
json.projects[options.name] = fixedProject;
return json;
}),
updateJsonInTree(`${options.projectRoot}/tsconfig.lib.json`, json => {
json.exclude = json.exclude || [];
return {
...json,
extends: `./tsconfig.json`,
compilerOptions: {
...json.compilerOptions,
outDir: `${offsetFromRoot(options.projectRoot)}dist/out-tsc`
}
};
}),
updateJsonInTree(`${options.projectRoot}/tslint.json`, json => {
return {
...json,
extends: `${offsetFromRoot(options.projectRoot)}tslint.json`
};
}),
updateJsonInTree(`/nx.json`, json => {
return {
...json,
projects: {
...json.projects,
[options.name]: { tags: options.parsedTags }
}
};
}),
updateNgPackage(options)
])(host, context);
};
}
function updateTsConfig(options: NormalizedSchema): Rule {
return chain([
(host: Tree, context: SchematicContext) => {
const nxJson = readJsonInTree<NxJson>(host, 'nx.json');
return updateJsonInTree('tsconfig.json', json => {
const c = json.compilerOptions;
delete c.paths[options.name];
c.paths[`@${nxJson.npmScope}/${options.projectDirectory}`] = [
`libs/${options.projectDirectory}/src/index.ts`
];
return json;
})(host, context);
}
]);
}
function updateLibPackageNpmScope(options: NormalizedSchema): Rule {
return (host: Tree) => {
return updateJsonInTree(`${options.projectRoot}/package.json`, json => {
json.name = `@${getNpmScope(host)}/${options.name}`;
return json;
});
};
}
function addModule(options: NormalizedSchema): Rule {
return chain([
options.routing && options.lazy
? addLazyLoadedRouterConfiguration(options)
: noop(),
options.routing && options.lazy && options.parentModule
? addLoadChildren(options)
: noop(),
options.routing && !options.lazy ? addRouterConfiguration(options) : noop(),
options.routing && !options.lazy && options.parentModule
? addChildren(options)
: noop()
]);
}
export default function(schema: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
const options = normalizeOptions(host, schema);
if (!options.routing && options.lazy) {
throw new Error(`routing must be set`);
}
return chain([
addUnitTestRunner(options),
externalSchematic('@schematics/angular', 'library', {
name: options.name,
prefix: options.prefix,
style: options.style,
entryFile: 'index',
skipPackageJson: !options.publishable,
skipTsConfig: true
}),
move(options.name, options.projectRoot),
updateProject(options),
updateTsConfig(options),
options.unitTestRunner === 'jest'
? externalSchematic('@nrwl/jest', 'jest-project', {
project: options.name,
setupFile: 'angular',
supportTsx: false,
skipSerializers: false
})
: noop(),
options.unitTestRunner === 'karma'
? schematic('karma-project', {
project: options.name
})
: noop(),
options.publishable ? updateLibPackageNpmScope(options) : noop(),
addModule(options),
formatFiles(options)
])(host, context);
};
}
function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
const name = toFileName(options.name);
const projectDirectory = options.directory
? `${toFileName(options.directory)}/${name}`
: name;
const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-');
const fileName = options.simpleModuleName ? name : projectName;
const projectRoot = `libs/${projectDirectory}`;
const moduleName = `${toClassName(fileName)}Module`;
const parsedTags = options.tags
? options.tags.split(',').map(s => s.trim())
: [];
const modulePath = `${projectRoot}/src/lib/${fileName}.module.ts`;
const defaultPrefix = getNpmScope(host);
return {
...options,
prefix: options.prefix ? options.prefix : defaultPrefix,
name: projectName,
projectRoot,
entryFile: 'index',
moduleName,
projectDirectory,
modulePath,
parsedTags,
fileName
};
}