2018-06-08 19:59:53 +01:00

261 lines
7.1 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { strings } from '@angular-devkit/core';
import {
Rule,
SchematicContext,
SchematicsException,
Tree,
apply,
branchAndMerge,
chain,
mergeWith,
noop,
schematic,
template,
url,
} from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import {
WorkspaceProject,
WorkspaceSchema,
addProjectToWorkspace,
getWorkspace,
} from '../utility/config';
import {
NodeDependencyType,
addPackageJsonDependency,
} from '../utility/dependencies';
import { latestVersions } from '../utility/latest-versions';
import { validateProjectName } from '../utility/validation';
import { Schema as LibraryOptions } from './schema';
interface UpdateJsonFn<T> {
(obj: T): T | void;
}
type TsConfigPartialType = {
compilerOptions: {
baseUrl: string,
paths: {
[key: string]: string[];
},
},
};
function updateJsonFile<T>(host: Tree, path: string, callback: UpdateJsonFn<T>): Tree {
const source = host.read(path);
if (source) {
const sourceText = source.toString('utf-8');
const json = JSON.parse(sourceText);
callback(json);
host.overwrite(path, JSON.stringify(json, null, 2));
}
return host;
}
function updateTsConfig(packageName: string, distRoot: string) {
return (host: Tree) => {
if (!host.exists('tsconfig.json')) { return host; }
return updateJsonFile(host, 'tsconfig.json', (tsconfig: TsConfigPartialType) => {
if (!tsconfig.compilerOptions.paths) {
tsconfig.compilerOptions.paths = {};
}
if (!tsconfig.compilerOptions.paths[packageName]) {
tsconfig.compilerOptions.paths[packageName] = [];
}
tsconfig.compilerOptions.paths[packageName].push(distRoot);
// deep import & secondary entrypoint support
const deepPackagePath = packageName + '/*';
if (!tsconfig.compilerOptions.paths[deepPackagePath]) {
tsconfig.compilerOptions.paths[deepPackagePath] = [];
}
tsconfig.compilerOptions.paths[deepPackagePath].push(distRoot + '/*');
});
};
}
function addDependenciesToPackageJson() {
return (host: Tree) => {
[
{
type: NodeDependencyType.Dev,
name: '@angular/compiler-cli',
version: latestVersions.Angular,
},
{
type: NodeDependencyType.Dev,
name: '@angular-devkit/build-ng-packagr',
version: latestVersions.DevkitBuildNgPackagr,
},
{
type: NodeDependencyType.Dev,
name: '@angular-devkit/build-angular',
version: latestVersions.DevkitBuildNgPackagr,
},
{
type: NodeDependencyType.Dev,
name: 'ng-packagr',
version: '^3.0.0',
},
{
type: NodeDependencyType.Dev,
name: 'tsickle',
version: '>=0.29.0',
},
{
type: NodeDependencyType.Dev,
name: 'tslib',
version: '^1.9.0',
},
{
type: NodeDependencyType.Dev,
name: 'typescript',
version: latestVersions.TypeScript,
},
].forEach(dependency => addPackageJsonDependency(host, dependency));
return host;
};
}
function addAppToWorkspaceFile(options: LibraryOptions, workspace: WorkspaceSchema,
projectRoot: string, packageName: string): Rule {
const project: WorkspaceProject = {
root: `${projectRoot}`,
sourceRoot: `${projectRoot}/src`,
projectType: 'library',
prefix: options.prefix || 'lib',
architect: {
build: {
builder: '@angular-devkit/build-ng-packagr:build',
options: {
tsConfig: `${projectRoot}/tsconfig.lib.json`,
project: `${projectRoot}/ng-package.json`,
},
configurations: {
production: {
project: `${projectRoot}/ng-package.prod.json`,
},
},
},
test: {
builder: '@angular-devkit/build-angular:karma',
options: {
main: `${projectRoot}/src/test.ts`,
tsConfig: `${projectRoot}/tsconfig.spec.json`,
karmaConfig: `${projectRoot}/karma.conf.js`,
},
},
lint: {
builder: '@angular-devkit/build-angular:tslint',
options: {
tsConfig: [
`${projectRoot}/tsconfig.lib.json`,
`${projectRoot}/tsconfig.spec.json`,
],
exclude: [
'**/node_modules/**',
],
},
},
},
};
return addProjectToWorkspace(workspace, packageName, project);
}
export default function (options: LibraryOptions): Rule {
return (host: Tree, context: SchematicContext) => {
if (!options.name) {
throw new SchematicsException(`Invalid options, "name" is required.`);
}
const prefix = options.prefix || 'lib';
validateProjectName(options.name);
// If scoped project (i.e. "@foo/bar"), convert projectDir to "foo/bar".
const packageName = options.name;
let scopeName = null;
if (/^@.*\/.*/.test(options.name)) {
const [scope, name] = options.name.split('/');
scopeName = scope.replace(/^@/, '');
options.name = name;
}
const workspace = getWorkspace(host);
const newProjectRoot = workspace.newProjectRoot;
const scopeFolder = scopeName ? strings.dasherize(scopeName) + '/' : '';
const folderName = `${scopeFolder}${strings.dasherize(options.name)}`;
const projectRoot = `${newProjectRoot}/${folderName}`;
const distRoot = `dist/${folderName}`;
const sourceDir = `${projectRoot}/src/lib`;
const relativePathToWorkspaceRoot = projectRoot.split('/').map(x => '..').join('/');
const templateSource = apply(url('./files'), [
template({
...strings,
...options,
packageName,
projectRoot,
distRoot,
relativePathToWorkspaceRoot,
prefix,
}),
// TODO: Moving inside `branchAndMerge` should work but is bugged right now.
// The __projectRoot__ is being used meanwhile.
// move(projectRoot),
]);
return chain([
branchAndMerge(mergeWith(templateSource)),
addAppToWorkspaceFile(options, workspace, projectRoot, packageName),
options.skipPackageJson ? noop() : addDependenciesToPackageJson(),
options.skipTsConfig ? noop() : updateTsConfig(packageName, distRoot),
schematic('module', {
name: options.name,
commonModule: false,
flat: true,
path: sourceDir,
spec: false,
project: options.name,
}),
schematic('component', {
name: options.name,
selector: `${prefix}-${options.name}`,
inlineStyle: true,
inlineTemplate: true,
flat: true,
path: sourceDir,
export: true,
project: options.name,
}),
schematic('service', {
name: options.name,
flat: true,
path: sourceDir,
project: options.name,
}),
(_tree: Tree, context: SchematicContext) => {
if (!options.skipPackageJson) {
context.addTask(new NodePackageInstallTask());
}
},
]);
};
}