diff --git a/packages/express/src/schematics/application/application.ts b/packages/express/src/schematics/application/application.ts index ce62e273a90b5..9178f01c2fa7a 100644 --- a/packages/express/src/schematics/application/application.ts +++ b/packages/express/src/schematics/application/application.ts @@ -1,19 +1,15 @@ import { - apply, chain, externalSchematic, - mergeWith, - move, Rule, SchematicContext, - template, - Tree, - url + Tree } from '@angular-devkit/schematics'; import { join, normalize, Path } from '@angular-devkit/core'; import { Schema } from './schema'; import { updateJsonInTree } from '@nrwl/workspace'; import { toFileName } from '@nrwl/workspace'; +import ngAdd from '../ng-add/ng-add'; interface NormalizedSchema extends Schema { appProjectRoot: Path; @@ -60,6 +56,7 @@ export default function(schema: Schema): Rule { return (host: Tree, context: SchematicContext) => { const options = normalizeOptions(schema); return chain([ + ngAdd(), externalSchematic('@nrwl/node', 'application', schema), addMainFile(options), addTypes(options) diff --git a/packages/express/src/schematics/ng-add/ng-add.ts b/packages/express/src/schematics/ng-add/ng-add.ts index 104a210475477..98972462f7768 100644 --- a/packages/express/src/schematics/ng-add/ng-add.ts +++ b/packages/express/src/schematics/ng-add/ng-add.ts @@ -1,5 +1,16 @@ -import { Rule, chain, externalSchematic } from '@angular-devkit/schematics'; -import { addDepsToPackageJson, updateJsonInTree } from '@nrwl/workspace'; +import { + Rule, + chain, + externalSchematic, + Tree, + noop +} from '@angular-devkit/schematics'; +import { + addDepsToPackageJson, + updateJsonInTree, + readJsonInTree, + addPackageWithNgAdd +} from '@nrwl/workspace'; import { expressTypingsVersion, expressVersion, @@ -29,7 +40,8 @@ function moveDependency(): Rule { export default function() { return chain([ - externalSchematic('@nrwl/node', 'ng-add', {}), + addPackageWithNgAdd('@nrwl/node'), + addPackageWithNgAdd('@nrwl/jest'), addDependencies(), moveDependency() ]); diff --git a/packages/nest/src/schematics/application/application.ts b/packages/nest/src/schematics/application/application.ts index 743c57dd084f7..4821a2cd8601d 100644 --- a/packages/nest/src/schematics/application/application.ts +++ b/packages/nest/src/schematics/application/application.ts @@ -13,6 +13,7 @@ import { import { join, normalize, Path } from '@angular-devkit/core'; import { Schema } from './schema'; import { toFileName } from '@nrwl/workspace'; +import ngAdd from '../ng-add/ng-add'; interface NormalizedSchema extends Schema { appProjectRoot: Path; @@ -64,6 +65,7 @@ export default function(schema: Schema): Rule { return (host: Tree, context: SchematicContext) => { const options = normalizeOptions(schema); return chain([ + ngAdd(), externalSchematic('@nrwl/node', 'application', schema), addMainFile(options), addAppFiles(options) diff --git a/packages/nest/src/schematics/ng-add/ng-add.ts b/packages/nest/src/schematics/ng-add/ng-add.ts index 8fb1a4bec00f4..99c0b4ed212a6 100644 --- a/packages/nest/src/schematics/ng-add/ng-add.ts +++ b/packages/nest/src/schematics/ng-add/ng-add.ts @@ -1,12 +1,16 @@ -import { Rule, chain, externalSchematic } from '@angular-devkit/schematics'; -import { addDepsToPackageJson, updateJsonInTree } from '@nrwl/workspace'; +import { Rule, chain } from '@angular-devkit/schematics'; +import { + addPackageWithNgAdd, + addDepsToPackageJson, + updateJsonInTree +} from '@nrwl/workspace'; import { nestJsSchematicsVersion, nestJsVersion, nxVersion } from '../../utils/versions'; -function addDependencies(): Rule { +export function addDependencies(): Rule { return addDepsToPackageJson( { '@nestjs/common': nestJsVersion, @@ -31,7 +35,8 @@ function moveDependency(): Rule { export default function() { return chain([ - externalSchematic('@nrwl/node', 'ng-add', {}), + addPackageWithNgAdd('@nrwl/node'), + addPackageWithNgAdd('@nrwl/jest'), addDependencies(), moveDependency() ]); diff --git a/packages/node/src/builders/build/build.builder.ts b/packages/node/src/builders/build/build.builder.ts index 815d03c01bfa0..2fedeb359764c 100644 --- a/packages/node/src/builders/build/build.builder.ts +++ b/packages/node/src/builders/build/build.builder.ts @@ -14,7 +14,7 @@ import { map } from 'rxjs/operators'; import { getNodeWebpackConfig } from '../../utils/node.config'; import { OUT_FILENAME } from '../../utils/config'; import { BuildBuilderOptions } from '../../utils/types'; -import { normalizeBuildOptions } from '../../../../web/src/utils/normalize'; +import { normalizeBuildOptions } from '../../utils/normalize'; try { require('dotenv').config(); diff --git a/packages/node/src/schematics/application/application.ts b/packages/node/src/schematics/application/application.ts index 78bc4342a15b1..1826dcee4ae08 100644 --- a/packages/node/src/schematics/application/application.ts +++ b/packages/node/src/schematics/application/application.ts @@ -17,6 +17,7 @@ import { updateJsonInTree } from '@nrwl/workspace'; import { toFileName } from '@nrwl/workspace'; import { getProjectConfig } from '@nrwl/workspace'; import { offsetFromRoot } from '@nrwl/workspace'; +import ngAdd from '../ng-add/ng-add'; interface NormalizedSchema extends Schema { appProjectRoot: Path; @@ -145,6 +146,7 @@ export default function(schema: Schema): Rule { return (host: Tree, context: SchematicContext) => { const options = normalizeOptions(schema); return chain([ + ngAdd(), addAppFiles(options), updateAngularJson(options), updateNxJson(options), diff --git a/packages/node/src/schematics/ng-add/ng-add.ts b/packages/node/src/schematics/ng-add/ng-add.ts index 5c95e1c44f4cd..955e4ee75a7dd 100644 --- a/packages/node/src/schematics/ng-add/ng-add.ts +++ b/packages/node/src/schematics/ng-add/ng-add.ts @@ -1,5 +1,9 @@ import { Rule, chain } from '@angular-devkit/schematics'; -import { addDepsToPackageJson, updateJsonInTree } from '@nrwl/workspace'; +import { + addDepsToPackageJson, + updateJsonInTree, + addPackageWithNgAdd +} from '@nrwl/workspace'; import { nxVersion } from '../../utils/versions'; function addDependencies(): Rule { @@ -21,5 +25,9 @@ function moveDependency(): Rule { } export default function() { - return chain([addDependencies(), moveDependency()]); + return chain([ + addPackageWithNgAdd('@nrwl/jest'), + addDependencies(), + moveDependency() + ]); } diff --git a/packages/node/src/utils/normalize.spec.ts b/packages/node/src/utils/normalize.spec.ts new file mode 100644 index 0000000000000..49f827268eb6d --- /dev/null +++ b/packages/node/src/utils/normalize.spec.ts @@ -0,0 +1,102 @@ +import { normalizeBuildOptions } from './normalize'; +import { BuildBuilderOptions } from './types'; +import { Path, normalize } from '@angular-devkit/core'; + +import * as fs from 'fs'; + +describe('normalizeBuildOptions', () => { + let testOptions: BuildBuilderOptions; + let root: string; + let sourceRoot: Path; + + beforeEach(() => { + testOptions = { + main: 'apps/nodeapp/src/main.ts', + tsConfig: 'apps/nodeapp/tsconfig.app.json', + outputPath: 'dist/apps/nodeapp', + fileReplacements: [ + { + replace: 'apps/environment/environment.ts', + with: 'apps/environment/environment.prod.ts' + }, + { + replace: 'module1.ts', + with: 'module2.ts' + } + ], + assets: [], + statsJson: false + }; + root = '/root'; + sourceRoot = normalize('apps/nodeapp/src'); + }); + it('should add the root', () => { + const result = normalizeBuildOptions(testOptions, root, sourceRoot); + expect(result.root).toEqual('/root'); + }); + + it('should resolve main from root', () => { + const result = normalizeBuildOptions(testOptions, root, sourceRoot); + expect(result.main).toEqual('/root/apps/nodeapp/src/main.ts'); + }); + + it('should resolve the output path', () => { + const result = normalizeBuildOptions(testOptions, root, sourceRoot); + expect(result.outputPath).toEqual('/root/dist/apps/nodeapp'); + }); + + it('should resolve the tsConfig path', () => { + const result = normalizeBuildOptions(testOptions, root, sourceRoot); + expect(result.tsConfig).toEqual('/root/apps/nodeapp/tsconfig.app.json'); + }); + + it('should normalize asset patterns', () => { + spyOn(fs, 'statSync').and.returnValue({ + isDirectory: () => true + }); + const result = normalizeBuildOptions( + { + ...testOptions, + root, + assets: [ + 'apps/nodeapp/src/assets', + { + input: '/outsideroot', + output: 'output', + glob: '**/*', + ignore: ['**/*.json'] + } + ] + }, + root, + sourceRoot + ); + expect(result.assets).toEqual([ + { + input: '/root/apps/nodeapp/src/assets', + output: 'assets', + glob: '**/*' + }, + { + input: '/outsideroot', + output: 'output', + glob: '**/*', + ignore: ['**/*.json'] + } + ]); + }); + + it('should resolve the file replacement paths', () => { + const result = normalizeBuildOptions(testOptions, root, sourceRoot); + expect(result.fileReplacements).toEqual([ + { + replace: '/root/apps/environment/environment.ts', + with: '/root/apps/environment/environment.prod.ts' + }, + { + replace: '/root/module1.ts', + with: '/root/module2.ts' + } + ]); + }); +}); diff --git a/packages/node/src/utils/normalize.ts b/packages/node/src/utils/normalize.ts new file mode 100644 index 0000000000000..bb801f02e5225 --- /dev/null +++ b/packages/node/src/utils/normalize.ts @@ -0,0 +1,86 @@ +import { Path, normalize } from '@angular-devkit/core'; +import { resolve, dirname, relative, basename } from 'path'; +import { + AssetPattern, + AssetPatternObject +} from '@angular-devkit/build-angular'; +import { BuildBuilderOptions } from './types'; +import { statSync } from 'fs'; + +export interface FileReplacement { + replace: string; + with: string; +} + +export function normalizeBuildOptions( + options: T, + root: string, + sourceRoot: Path +): T { + return { + ...options, + root: root, + sourceRoot: sourceRoot, + main: resolve(root, options.main), + outputPath: resolve(root, options.outputPath), + tsConfig: resolve(root, options.tsConfig), + fileReplacements: normalizeFileReplacements(root, options.fileReplacements), + assets: normalizeAssets(options.assets, root, sourceRoot), + webpackConfig: options.webpackConfig + ? resolve(root, options.webpackConfig) + : options.webpackConfig + }; +} + +function normalizeAssets( + assets: AssetPattern[], + root: string, + sourceRoot: Path +): AssetPatternObject[] { + return assets.map(asset => { + if (typeof asset === 'string') { + const assetPath = normalize(asset); + const resolvedAssetPath = resolve(root, assetPath); + const resolvedSourceRoot = resolve(root, sourceRoot); + + if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) { + throw new Error( + `The ${resolvedAssetPath} asset path must start with the project source root: ${sourceRoot}` + ); + } + + const isDirectory = statSync(resolvedAssetPath).isDirectory(); + const input = isDirectory + ? resolvedAssetPath + : dirname(resolvedAssetPath); + const output = relative(resolvedSourceRoot, resolve(root, input)); + const glob = isDirectory ? '**/*' : basename(resolvedAssetPath); + return { + input, + output, + glob + }; + } else { + if (asset.output.startsWith('..')) { + throw new Error( + 'An asset cannot be written to a location outside of the output path.' + ); + } + return { + ...asset, + // Now we remove starting slash to make Webpack place it from the output root. + output: asset.output.replace(/^\//, '') + }; + } + }); +} + +function normalizeFileReplacements( + root: string, + fileReplacements: FileReplacement[] +): FileReplacement[] { + return fileReplacements.map(fileReplacement => ({ + replace: resolve(root, fileReplacement.replace), + with: resolve(root, fileReplacement.with) + })); +} diff --git a/packages/react/src/schematics/application/application.ts b/packages/react/src/schematics/application/application.ts index 65658fa191183..70a118c2c0b56 100644 --- a/packages/react/src/schematics/application/application.ts +++ b/packages/react/src/schematics/application/application.ts @@ -14,11 +14,16 @@ import { filter } from '@angular-devkit/schematics'; import { Schema } from './schema'; -import { updateJsonInTree, NxJson } from '@nrwl/workspace'; -import { toFileName, names } from '@nrwl/workspace'; -import { offsetFromRoot } from '@nrwl/workspace'; -import { getNpmScope } from '@nrwl/workspace'; -import { formatFiles } from '@nrwl/workspace'; +import { + updateJsonInTree, + NxJson, + toFileName, + names, + offsetFromRoot, + getNpmScope, + formatFiles +} from '@nrwl/workspace'; +import ngAdd from '../ng-add/ng-add'; interface NormalizedSchema extends Schema { projectName: string; @@ -139,10 +144,11 @@ function addProject(options: NormalizedSchema): Rule { } export default function(schema: Schema): Rule { - return (host: Tree, context: SchematicContext) => { + return (host: Tree) => { const options = normalizeOptions(host, schema); return chain([ + ngAdd(), createApplicationFiles(options), updateNxJson(options), addProject(options), @@ -163,7 +169,7 @@ export default function(schema: Schema): Rule { }) : noop(), formatFiles(options) - ])(host, context); + ]); }; } diff --git a/packages/react/src/schematics/ng-add/ng-add.ts b/packages/react/src/schematics/ng-add/ng-add.ts index 501349e593f88..346febdfeaeae 100644 --- a/packages/react/src/schematics/ng-add/ng-add.ts +++ b/packages/react/src/schematics/ng-add/ng-add.ts @@ -8,7 +8,8 @@ import { import { addDepsToPackageJson, updateJsonInTree, - readJsonInTree + readJsonInTree, + addPackageWithNgAdd } from '@nrwl/workspace'; import { frameworkVersion, @@ -18,7 +19,7 @@ import { nxVersion } from '../../utils/versions'; -function addDependencies(): Rule { +export function addDependencies(): Rule { return addDepsToPackageJson( { react: frameworkVersion, @@ -42,59 +43,11 @@ function moveDependency(): Rule { }); } -function addJest(): Rule { - return (host: Tree) => { - const packageJson = readJsonInTree(host, 'package.json'); - return !packageJson.devDependencies['@nrwl/jest'] - ? externalSchematic( - '@nrwl/jest', - 'ng-add', - {}, - { - interactive: false - } - ) - : noop(); - }; -} - -function addWeb(): Rule { - return (host: Tree) => { - const packageJson = readJsonInTree(host, 'package.json'); - return !packageJson.devDependencies['@nrwl/web'] - ? externalSchematic( - '@nrwl/web', - 'ng-add', - {}, - { - interactive: false - } - ) - : noop(); - }; -} - -function addCypress(): Rule { - return (host: Tree) => { - const packageJson = readJsonInTree(host, 'package.json'); - return !packageJson.devDependencies['@nrwl/cypress'] - ? externalSchematic( - '@nrwl/cypress', - 'ng-add', - {}, - { - interactive: false - } - ) - : noop(); - }; -} - export default function() { return chain([ - addJest(), - addCypress(), - addWeb(), + addPackageWithNgAdd('@nrwl/jest'), + addPackageWithNgAdd('@nrwl/cypress'), + addPackageWithNgAdd('@nrwl/web'), addDependencies(), moveDependency() ]); diff --git a/packages/web/src/schematics/application/application.ts b/packages/web/src/schematics/application/application.ts index 56f5cbebf0906..13d574e40de04 100644 --- a/packages/web/src/schematics/application/application.ts +++ b/packages/web/src/schematics/application/application.ts @@ -14,11 +14,16 @@ import { filter } from '@angular-devkit/schematics'; import { Schema } from './schema'; -import { updateJsonInTree, NxJson } from '@nrwl/workspace'; -import { toFileName, names } from '@nrwl/workspace'; -import { offsetFromRoot } from '@nrwl/workspace'; -import { getNpmScope } from '@nrwl/workspace'; -import { formatFiles } from '@nrwl/workspace'; +import { + updateJsonInTree, + NxJson, + toFileName, + names, + offsetFromRoot, + getNpmScope, + formatFiles +} from '@nrwl/workspace'; +import ngAdd from '../ng-add/ng-add'; interface NormalizedSchema extends Schema { projectName: string; @@ -143,6 +148,7 @@ export default function(schema: Schema): Rule { const options = normalizeOptions(host, schema); return chain([ + ngAdd(), createApplicationFiles(options), updateNxJson(options), addProject(options), diff --git a/packages/web/src/schematics/ng-add/ng-add.ts b/packages/web/src/schematics/ng-add/ng-add.ts index fd351dabfce39..5c49dc922b52d 100644 --- a/packages/web/src/schematics/ng-add/ng-add.ts +++ b/packages/web/src/schematics/ng-add/ng-add.ts @@ -1,11 +1,5 @@ -import { - Rule, - chain, - externalSchematic, - noop, - Tree -} from '@angular-devkit/schematics'; -import { updateJsonInTree, readJsonInTree } from '@nrwl/workspace'; +import { Rule, chain } from '@angular-devkit/schematics'; +import { updateJsonInTree, addPackageWithNgAdd } from '@nrwl/workspace'; import { addDepsToPackageJson } from '@nrwl/workspace'; import { nxVersion, @@ -23,38 +17,6 @@ function addDependencies(): Rule { ); } -function addJest(): Rule { - return (host: Tree) => { - const packageJson = readJsonInTree(host, 'package.json'); - return !packageJson.devDependencies['@nrwl/jest'] - ? externalSchematic( - '@nrwl/jest', - 'ng-add', - {}, - { - interactive: false - } - ) - : noop(); - }; -} - -function addCypress(): Rule { - return (host: Tree) => { - const packageJson = readJsonInTree(host, 'package.json'); - return !packageJson.devDependencies['@nrwl/cypress'] - ? externalSchematic( - '@nrwl/cypress', - 'ng-add', - {}, - { - interactive: false - } - ) - : noop(); - }; -} - function moveDependency(): Rule { return updateJsonInTree('package.json', json => { json.dependencies = json.dependencies || {}; @@ -65,5 +27,10 @@ function moveDependency(): Rule { } export default function() { - return chain([addJest(), addCypress(), addDependencies(), moveDependency()]); + return chain([ + addPackageWithNgAdd('@nrwl/jest'), + addPackageWithNgAdd('@nrwl/cypress'), + addDependencies(), + moveDependency() + ]); } diff --git a/packages/web/src/utils/normalize.ts b/packages/web/src/utils/normalize.ts index b70d113c021de..6952f8cf08db2 100644 --- a/packages/web/src/utils/normalize.ts +++ b/packages/web/src/utils/normalize.ts @@ -1,14 +1,14 @@ -import { normalize, Path } from '@angular-devkit/core'; -import { basename, dirname, relative, resolve } from 'path'; +import { WebBuildBuilderOptions } from '../builders/build/build.builder'; +import { Path, normalize } from '@angular-devkit/core'; +import { resolve, dirname, relative, basename } from 'path'; import { BuildOptions } from '@angular-devkit/build-angular/src/angular-cli-files/models/build-options'; import { + NormalizedBrowserBuilderSchema, AssetPattern, - AssetPatternObject, - NormalizedBrowserBuilderSchema + AssetPatternObject } from '@angular-devkit/build-angular'; -import { WebBuildBuilderOptions } from '../builders/build/build.builder'; -import { statSync } from 'fs'; import { BuildBuilderOptions } from './types'; +import { statSync } from 'fs'; export interface FileReplacement { replace: string; diff --git a/packages/workspace/index.ts b/packages/workspace/index.ts index 0b9cef87f5da3..d70e69c7408e1 100644 --- a/packages/workspace/index.ts +++ b/packages/workspace/index.ts @@ -57,6 +57,7 @@ export { export { formatFiles } from './src/utils/rules/format-files'; export { deleteFile } from './src/utils/rules/deleteFile'; +export * from './src/utils/rules/ng-add'; export { updateKarmaConf } from './src/utils/rules/update-karma-conf'; import * as strings from './src/utils/strings'; diff --git a/packages/workspace/src/utils/ast-utils.ts b/packages/workspace/src/utils/ast-utils.ts index 9c66f798863c2..ede36319755f4 100644 --- a/packages/workspace/src/utils/ast-utils.ts +++ b/packages/workspace/src/utils/ast-utils.ts @@ -609,6 +609,8 @@ export function updateJsonInTree( }; } +let installAdded = false; + export function addDepsToPackageJson(deps: any, devDeps: any): Rule { return updateJsonInTree('package.json', (json, context: SchematicContext) => { json.dependencies = { @@ -619,7 +621,10 @@ export function addDepsToPackageJson(deps: any, devDeps: any): Rule { ...devDeps, ...(json.devDependencies || {}) }; - context.addTask(new NodePackageInstallTask()); + if (!installAdded) { + context.addTask(new NodePackageInstallTask()); + installAdded = true; + } return json; }); } diff --git a/packages/workspace/src/utils/rules/ng-add.ts b/packages/workspace/src/utils/rules/ng-add.ts new file mode 100644 index 0000000000000..347f98712772f --- /dev/null +++ b/packages/workspace/src/utils/rules/ng-add.ts @@ -0,0 +1,23 @@ +import { + Rule, + Tree, + externalSchematic, + noop +} from '@angular-devkit/schematics'; + +import { readJsonInTree } from '../ast-utils'; + +/** + * Calls ng-add _if_ the package does not already exist + */ +export function addPackageWithNgAdd(packageName: string): Rule { + return (host: Tree) => { + const { dependencies, devDependencies } = readJsonInTree( + host, + 'package.json' + ); + return dependencies[packageName] || devDependencies[packageName] + ? noop() + : externalSchematic(packageName, 'ng-add', {}); + }; +} diff --git a/tsconfig.json b/tsconfig.json index a414d893c650d..04407f72e1134 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "baseUrl": ".", "paths": { "@nrwl/workspace": ["./packages/workspace"], + "@nrwl/workspace/*": ["./packages/workspace/*"], "@nrwl/workspace/testing": ["./packages/workspace/testing"] } },