diff --git a/e2e/node.test.ts b/e2e/node.test.ts index 782fdf35ca0ba..c46f9c40f5923 100644 --- a/e2e/node.test.ts +++ b/e2e/node.test.ts @@ -19,8 +19,10 @@ import { runNgAdd, copyMissingPackages, setMaxWorkers, - fileExists + fileExists, + newProject } from './utils'; +import { toClassName } from '@nrwl/workspace'; function getData(): Promise { return new Promise(resolve => { @@ -246,51 +248,198 @@ forEachCli(currentCLIName => { expect(result).toContain('Hello World!'); }, 60000); - it('should be able to generate a node library', async () => { - ensureProject(); - const nodelib = uniq('nodelib'); + describe('Node Libraries', () => { + beforeAll(() => { + // force a new project to avoid collissions with the npmScope that has been altered before + newProject(); + }); - runCLI(`generate @nrwl/node:lib ${nodelib}`); + it('should be able to generate a node library', async () => { + ensureProject(); + const nodelib = uniq('nodelib'); - const lintResults = runCLI(`lint ${nodelib}`); - expect(lintResults).toContain('All files pass linting.'); + runCLI(`generate @nrwl/node:lib ${nodelib}`); - const jestResult = await runCLIAsync(`test ${nodelib}`); - expect(jestResult.stderr).toContain('Test Suites: 1 passed, 1 total'); - }, 60000); + const lintResults = runCLI(`lint ${nodelib}`); + expect(lintResults).toContain('All files pass linting.'); - it('should be able to generate a publishable node library', async () => { - ensureProject(); - const nodeLib = uniq('nodelib'); - runCLI(`generate @nrwl/node:lib ${nodeLib} --publishable`); - fileExists(`libs/${nodeLib}/package.json`); - const tslibConfig = readJson(`libs/${nodeLib}/tsconfig.lib.json`); - expect(tslibConfig).toEqual({ - extends: './tsconfig.json', - compilerOptions: { - module: 'commonjs', - outDir: '../../dist/out-tsc', - declaration: true, - rootDir: './src', - types: ['node'] - }, - exclude: ['**/*.spec.ts'], - include: ['**/*.ts'] - }); - await runCLIAsync(`build ${nodeLib}`); - checkFilesExist( - `dist/libs/${nodeLib}/index.js`, - `dist/libs/${nodeLib}/index.d.ts`, - `dist/libs/${nodeLib}/package.json` - ); + const jestResult = await runCLIAsync(`test ${nodelib}`); + expect(jestResult.stderr).toContain('Test Suites: 1 passed, 1 total'); + }, 60000); + + it('should be able to generate a publishable node library', async () => { + ensureProject(); + + const nodeLib = uniq('nodelib'); + runCLI(`generate @nrwl/node:lib ${nodeLib} --publishable`); + fileExists(`libs/${nodeLib}/package.json`); + const tslibConfig = readJson(`libs/${nodeLib}/tsconfig.lib.json`); + expect(tslibConfig).toEqual({ + extends: './tsconfig.json', + compilerOptions: { + module: 'commonjs', + outDir: '../../dist/out-tsc', + declaration: true, + rootDir: './src', + types: ['node'] + }, + exclude: ['**/*.spec.ts'], + include: ['**/*.ts'] + }); + await runCLIAsync(`build ${nodeLib}`); + checkFilesExist( + `dist/libs/${nodeLib}/index.js`, + `dist/libs/${nodeLib}/index.d.ts`, + `dist/libs/${nodeLib}/package.json` + ); + + const packageJson = readJson(`dist/libs/${nodeLib}/package.json`); + expect(packageJson).toEqual({ + name: `@proj/${nodeLib}`, + version: '0.0.1', + main: 'index.js', + typings: 'index.d.ts' + }); + }, 60000); + + describe('with dependencies', () => { + /** + * Graph: + * + * childLib + * / + * parentLib => + * \ + * \ + * childLib2 + * + */ + let parentLib: string; + let childLib: string; + let childLib2: string; + + beforeEach(() => { + parentLib = uniq('parentlib'); + childLib = uniq('childlib'); + childLib2 = uniq('childlib2'); + + ensureProject(); + + runCLI(`generate @nrwl/node:lib ${parentLib} --publishable=true`); + runCLI(`generate @nrwl/node:lib ${childLib} --publishable=true`); + runCLI(`generate @nrwl/node:lib ${childLib2} --publishable=true`); + + // create dependencies by importing + const createDep = (parent, children: string[]) => { + updateFile( + `libs/${parent}/src/lib/${parent}.ts`, + ` + ${children + .map(entry => `import { ${entry} } from '@proj/${entry}';`) + .join('\n')} + + export function ${parent}(): string { + return '${parent}' + ' ' + ${children + .map(entry => `${entry}()`) + .join('+')} + } + ` + ); + }; + + createDep(parentLib, [childLib, childLib2]); + }); + + it('should throw an error if the dependent library has not been built before building the parent lib', () => { + expect.assertions(2); - const packageJson = readJson(`dist/libs/${nodeLib}/package.json`); - expect(packageJson).toEqual({ - name: nodeLib, - version: '0.0.1', - main: 'index.js', - typings: 'index.d.ts' + try { + runCLI(`build ${parentLib}`); + } catch (e) { + expect(e.stderr.toString()).toContain( + `Some of the library ${parentLib}'s dependencies have not been built yet. Please build these libraries before:` + ); + expect(e.stderr.toString()).toContain(`${childLib}`); + } + }); + + it('should build a library without dependencies', () => { + const childLibOutput = runCLI(`build ${childLib}`); + + expect(childLibOutput).toContain( + `Done compiling TypeScript files for library ${childLib}` + ); + }); + + it('should build a parent library if the dependent libraries have been built before', () => { + const childLibOutput = runCLI(`build ${childLib}`); + expect(childLibOutput).toContain( + `Done compiling TypeScript files for library ${childLib}` + ); + + const childLib2Output = runCLI(`build ${childLib2}`); + expect(childLib2Output).toContain( + `Done compiling TypeScript files for library ${childLib2}` + ); + + const parentLibOutput = runCLI(`build ${parentLib}`); + expect(parentLibOutput).toContain( + `Done compiling TypeScript files for library ${parentLib}` + ); + + // assert package.json deps have been set + const assertPackageJson = ( + parent: string, + lib: string, + version: string + ) => { + const jsonFile = readJson(`dist/libs/${parent}/package.json`); + const childDependencyVersion = + jsonFile.dependencies[`@proj/${lib}`]; + expect(childDependencyVersion).toBe(version); + }; + + assertPackageJson(parentLib, childLib, '0.0.1'); + assertPackageJson(parentLib, childLib2, '0.0.1'); + }); + + // it('should automatically build all deps and update package.json when passing --withDeps flags', () => { + // const parentLibOutput = runCLI(`build ${parentLib} --withDeps`); + + // expect(parentLibOutput).toContain( + // `Done compiling TypeScript files for library ${parentLib}` + // ); + // expect(parentLibOutput).toContain( + // `Done compiling TypeScript files for library ${childLib}` + // ); + // expect(parentLibOutput).toContain( + // `Done compiling TypeScript files for library ${childChildLib}` + // ); + // expect(parentLibOutput).toContain( + // `Done compiling TypeScript files for library ${childLib2}` + // ); + // expect(parentLibOutput).toContain( + // `Done compiling TypeScript files for library ${childLibShared}` + // ); + + // // // assert package.json deps have been set + // const assertPackageJson = ( + // parent: string, + // lib: string, + // version: string + // ) => { + // const jsonFile = readJson(`dist/libs/${parent}/package.json`); + // const childDependencyVersion = + // jsonFile.dependencies[`@proj/${lib}`]; + // expect(childDependencyVersion).toBe(version); + // }; + + // assertPackageJson(parentLib, childLib, '0.0.1'); + // assertPackageJson(childLib, childChildLib, '0.0.1'); + // assertPackageJson(childLib, childLibShared, '0.0.1'); + // assertPackageJson(childLib2, childLibShared, '0.0.1'); + // }); }); - }, 60000); + }); }); }); diff --git a/packages/node/src/builders/package/package.impl.spec.ts b/packages/node/src/builders/package/package.impl.spec.ts index 4f73e2b29636d..c9babc76d5449 100644 --- a/packages/node/src/builders/package/package.impl.spec.ts +++ b/packages/node/src/builders/package/package.impl.spec.ts @@ -3,6 +3,11 @@ import { EventEmitter } from 'events'; import { join } from 'path'; import { getMockContext, getTestArchitect } from '../../utils/testing'; import { MockBuilderContext } from '@nrwl/workspace/testing'; +import { + ProjectGraph, + ProjectType +} from '@nrwl/workspace/src/core/project-graph'; +import * as projectGraphUtils from '@nrwl/workspace/src/core/project-graph'; import { NodePackageBuilderOptions, @@ -18,6 +23,7 @@ jest.mock('child_process'); let { fork } = require('child_process'); jest.mock('tree-kill'); let treeKill = require('tree-kill'); +import * as fsMock from 'fs'; describe('NodeCompileBuilder', () => { let testOptions: NodePackageBuilderOptions; @@ -65,83 +71,48 @@ describe('NodeCompileBuilder', () => { }; }); - it('should call tsc to compile', done => { - runNodePackageBuilder(testOptions, context).subscribe({ - complete: () => { - expect(fork).toHaveBeenCalledWith( - `${context.workspaceRoot}/node_modules/typescript/bin/tsc`, - [ - '-p', - join(context.workspaceRoot, testOptions.tsConfig), - '--outDir', - join(context.workspaceRoot, testOptions.outputPath) - ], - { stdio: [0, 1, 2, 'ipc'] } - ); - - done(); - } - }); - fakeEventEmitter.emit('exit', 0); - }); - - it('should update the package.json after compiling typescript', done => { - runNodePackageBuilder(testOptions, context).subscribe({ - complete: () => { - expect(fork).toHaveBeenCalled(); - expect(fsUtility.writeJsonFile).toHaveBeenCalledWith( - `${testOptions.outputPath}/package.json`, - { - name: 'nodelib', - main: 'index.js', - typings: 'index.d.ts' - } - ); - - done(); - } + describe('Without library dependencies', () => { + beforeEach(() => { + // mock createProjectGraph without deps + spyOn(projectGraphUtils, 'createProjectGraph').and.callFake(() => { + return { + nodes: {}, + dependencies: {} + } as ProjectGraph; + }); }); - fakeEventEmitter.emit('exit', 0); - }); - it('should have the output path in the BuilderOutput', done => { - runNodePackageBuilder(testOptions, context).subscribe({ - next: value => { - expect(value.outputPath).toEqual(testOptions.outputPath); - }, - complete: () => { - done(); - } - }); - fakeEventEmitter.emit('exit', 0); - }); + it('should call tsc to compile', done => { + runNodePackageBuilder(testOptions, context).subscribe({ + complete: () => { + expect(fork).toHaveBeenCalledWith( + `${context.workspaceRoot}/node_modules/typescript/bin/tsc`, + [ + '-p', + join(context.workspaceRoot, testOptions.tsConfig), + '--outDir', + join(context.workspaceRoot, testOptions.outputPath) + ], + { stdio: [0, 1, 2, 'ipc'] } + ); - describe('Asset copying', () => { - beforeEach(() => { - jest.clearAllMocks(); + done(); + } + }); + fakeEventEmitter.emit('exit', 0); }); - it('should be able to copy assets using the glob object', done => { - glob.sync.mockReturnValue(['logo.png']); - runNodePackageBuilder( - { - ...testOptions, - assets: [ + it('should update the package.json after compiling typescript', done => { + runNodePackageBuilder(testOptions, context).subscribe({ + complete: () => { + expect(fork).toHaveBeenCalled(); + expect(fsUtility.writeJsonFile).toHaveBeenCalledWith( + `${testOptions.outputPath}/package.json`, { - glob: '**.*', - input: 'lib/nodelib/src/assets', - output: './newfolder', - ignore: [] + name: 'nodelib', + main: 'index.js', + typings: 'index.d.ts' } - ] - }, - context - ).subscribe({ - complete: () => { - expect(fs.copy).toHaveBeenCalledTimes(1); - expect(fs.copy).toHaveBeenCalledWith( - `${context.workspaceRoot}/lib/nodelib/src/assets/logo.png`, - `${context.workspaceRoot}/${testOptions.outputPath}/newfolder/logo.png` ); done(); @@ -149,54 +120,192 @@ describe('NodeCompileBuilder', () => { }); fakeEventEmitter.emit('exit', 0); }); - it('should be able to copy assets with a regular string', done => { - glob.sync.mockReturnValue(['lib/nodelib/src/LICENSE']); - runNodePackageBuilder( - { - ...testOptions, - assets: ['lib/nodelib/src/LICENSE'] + it('should have the output path in the BuilderOutput', done => { + runNodePackageBuilder(testOptions, context).subscribe({ + next: value => { + expect(value.outputPath).toEqual(testOptions.outputPath); }, - context - ).subscribe({ complete: () => { - expect(fs.copy).toHaveBeenCalledTimes(1); - expect(fs.copy).toHaveBeenCalledWith( - `${context.workspaceRoot}/lib/nodelib/src/LICENSE`, - `${context.workspaceRoot}/${testOptions.outputPath}/LICENSE` - ); done(); } }); fakeEventEmitter.emit('exit', 0); }); - it('should be able to copy assets with a glob string', done => { - glob.sync.mockReturnValue([ - 'lib/nodelib/src/README.md', - 'lib/nodelib/src/CONTRIBUTING.md' - ]); - runNodePackageBuilder( - { - ...testOptions, - assets: ['lib/nodelib/src/*.MD'] - }, - context - ).subscribe({ + describe('Asset copying', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should be able to copy assets using the glob object', done => { + glob.sync.mockReturnValue(['logo.png']); + runNodePackageBuilder( + { + ...testOptions, + assets: [ + { + glob: '**.*', + input: 'lib/nodelib/src/assets', + output: './newfolder', + ignore: [] + } + ] + }, + context + ).subscribe({ + complete: () => { + expect(fs.copy).toHaveBeenCalledTimes(1); + expect(fs.copy).toHaveBeenCalledWith( + `${context.workspaceRoot}/lib/nodelib/src/assets/logo.png`, + `${context.workspaceRoot}/${testOptions.outputPath}/newfolder/logo.png` + ); + + done(); + } + }); + fakeEventEmitter.emit('exit', 0); + }); + it('should be able to copy assets with a regular string', done => { + glob.sync.mockReturnValue(['lib/nodelib/src/LICENSE']); + + runNodePackageBuilder( + { + ...testOptions, + assets: ['lib/nodelib/src/LICENSE'] + }, + context + ).subscribe({ + complete: () => { + expect(fs.copy).toHaveBeenCalledTimes(1); + expect(fs.copy).toHaveBeenCalledWith( + `${context.workspaceRoot}/lib/nodelib/src/LICENSE`, + `${context.workspaceRoot}/${testOptions.outputPath}/LICENSE` + ); + done(); + } + }); + fakeEventEmitter.emit('exit', 0); + }); + + it('should be able to copy assets with a glob string', done => { + glob.sync.mockReturnValue([ + 'lib/nodelib/src/README.md', + 'lib/nodelib/src/CONTRIBUTING.md' + ]); + runNodePackageBuilder( + { + ...testOptions, + assets: ['lib/nodelib/src/*.MD'] + }, + context + ).subscribe({ + complete: () => { + expect(fs.copy).toHaveBeenCalledTimes(2); + expect(fs.copy).toHaveBeenCalledWith( + `${context.workspaceRoot}/lib/nodelib/src/README.md`, + `${context.workspaceRoot}/${testOptions.outputPath}/README.md` + ); + expect(fs.copy).toHaveBeenCalledWith( + `${context.workspaceRoot}/lib/nodelib/src/CONTRIBUTING.md`, + `${context.workspaceRoot}/${testOptions.outputPath}/CONTRIBUTING.md` + ); + done(); + } + }); + fakeEventEmitter.emit('exit', 0); + }); + }); + }); + + describe('building with dependencies', () => { + beforeEach(() => { + spyOn(projectGraphUtils, 'createProjectGraph').and.callFake(() => { + return { + nodes: { + nodelib: { + type: ProjectType.lib, + name: 'nodelib', + data: { files: [], root: 'libs/nodelib' } + }, + 'nodelib-child': { + type: ProjectType.lib, + name: 'nodelib-child', + data: { + files: [], + root: 'libs/nodelib-child', + prefix: 'proj', + architect: { + build: { + builder: 'any builder', + options: { + assets: [], + main: 'libs/nodelib-child/src/index.ts', + outputPath: 'dist/libs/nodelib-child', + packageJson: 'libs/nodelib-child/package.json', + tsConfig: 'libs/nodelib-child/tsconfig.lib.json' + } + } + } + } + } + }, + dependencies: { + nodelib: [ + { + type: ProjectType.lib, + target: 'nodelib-child', + source: null + } + ], + 'nodelib-child': [] + } + } as ProjectGraph; + }); + + // fake that dep project has been built + // dist/libs/nodelib-child/package.json + fsUtility.fileExists.mockImplementation((arg: string) => { + if (arg.endsWith('dist/libs/nodelib-child/package.json')) { + return true; + } else { + return false; + } + }); + + // fsMock.unlinkSync.mockImplementation(() => {}); + + spyOn(fsMock, 'unlinkSync'); + }); + + it('should call the tsc compiler with the modified tsconfig.json', done => { + let tmpTsConfigPath = join( + context.workspaceRoot, + 'libs/nodelib', + 'tsconfig.lib.nx-tmp' + ); + + runNodePackageBuilder(testOptions, context).subscribe({ complete: () => { - expect(fs.copy).toHaveBeenCalledTimes(2); - expect(fs.copy).toHaveBeenCalledWith( - `${context.workspaceRoot}/lib/nodelib/src/README.md`, - `${context.workspaceRoot}/${testOptions.outputPath}/README.md` - ); - expect(fs.copy).toHaveBeenCalledWith( - `${context.workspaceRoot}/lib/nodelib/src/CONTRIBUTING.md`, - `${context.workspaceRoot}/${testOptions.outputPath}/CONTRIBUTING.md` + expect(fork).toHaveBeenCalledWith( + `${context.workspaceRoot}/node_modules/typescript/bin/tsc`, + [ + '-p', + tmpTsConfigPath, + // join(context.workspaceRoot, testOptions.tsConfig), + '--outDir', + join(context.workspaceRoot, testOptions.outputPath) + ], + { stdio: [0, 1, 2, 'ipc'] } ); + done(); } }); fakeEventEmitter.emit('exit', 0); + + // assert temp tsconfig file gets deleted again + expect(fsMock.unlinkSync).toHaveBeenCalledWith(tmpTsConfigPath); }); }); }); diff --git a/packages/node/src/builders/package/package.impl.ts b/packages/node/src/builders/package/package.impl.ts index e5c5e1891667c..8ef6f255523c9 100644 --- a/packages/node/src/builders/package/package.impl.ts +++ b/packages/node/src/builders/package/package.impl.ts @@ -4,15 +4,23 @@ import { createBuilder } from '@angular-devkit/architect'; import { JsonObject } from '@angular-devkit/core'; -import { readJsonFile } from '@nrwl/workspace'; -import { writeJsonFile } from '@nrwl/workspace/src/utils/fileutils'; +import { readJsonFile, readTsConfig, deleteFile } from '@nrwl/workspace'; +import { writeJsonFile, fileExists } from '@nrwl/workspace/src/utils/fileutils'; import { ChildProcess, fork } from 'child_process'; import { copy, removeSync } from 'fs-extra'; import * as glob from 'glob'; import { basename, dirname, join, normalize, relative } from 'path'; -import { Observable, Subscriber } from 'rxjs'; -import { switchMap, tap, map } from 'rxjs/operators'; +import { Observable, Subscriber, from, of } from 'rxjs'; +import { switchMap, tap, map, finalize } from 'rxjs/operators'; import * as treeKill from 'tree-kill'; +import { + ProjectGraphNode, + ProjectType, + createProjectGraph +} from '@nrwl/workspace/src/core/project-graph'; +import * as ts from 'typescript'; +import { unlinkSync } from 'fs'; +import { stripIndents } from '@angular-devkit/core/src/utils/literals'; export interface NodePackageBuilderOptions extends JsonObject { main: string; @@ -39,6 +47,98 @@ type AssetGlob = FileInputOutput & { ignore: string[]; }; +/** + * ----------------------------------------------------------- + */ + +type DependentLibraryNode = { + scope: string; + outputPath: string; + node: ProjectGraphNode; +}; + +/** + * Given a target library, uses the project dep graph to find all its dependencies + * and calculates the `scope` name and output path + * @param targetProj the target library to build + */ +export function calculateLibraryDependencies( + context: BuilderContext +): DependentLibraryNode[] { + const targetProj = context.target.project; + const projGraph = createProjectGraph(); + + const hasArchitectBuildBuilder = (projectGraph: ProjectGraphNode): boolean => + projectGraph.data.architect && + projectGraph.data.architect.build && + projectGraph.data.architect.build.builder !== ''; + + // gather the library dependencies + return (projGraph.dependencies[targetProj] || []) + .map(dependency => { + const depNode = projGraph.nodes[dependency.target]; + + if ( + depNode.type === ProjectType.lib && + hasArchitectBuildBuilder(depNode) + ) { + const libPackageJson = readJsonFile( + join(context.workspaceRoot, depNode.data.root, 'package.json') + ); + + return { + scope: libPackageJson.name, // i.e. @wrkspace/mylib + outputPath: + (depNode.data.architect && + depNode.data.architect.build && + depNode.data.architect.build.options && + depNode.data.architect.build.options.outputPath) || + `dist/${depNode.data.root}`, + node: depNode + }; + } else { + return null; + } + }) + .filter(x => !!x); +} + +function checkDependentLibrariesHaveBeenBuilt( + context: BuilderContext, + projectDependencies: DependentLibraryNode[] +) { + const depLibsToBuildFirst: DependentLibraryNode[] = []; + + // verify whether all dependent libraries have been built + projectDependencies.forEach(libDep => { + // check wether dependent library has been built => that's necessary + const normalizedOptions = normalizeOptions( + libDep.node.data.architect.build.options, + context + ); + + const packageJsonPath = join(normalizedOptions.outputPath, 'package.json'); + + if (!fileExists(packageJsonPath)) { + depLibsToBuildFirst.push(libDep); + } + }); + + if (depLibsToBuildFirst.length > 0) { + context.logger.error(stripIndents` + Some of the library ${ + context.target.project + }'s dependencies have not been built yet. Please build these libraries before: + ${depLibsToBuildFirst.map(x => ` - ${x.node.name}`).join('\n')} + + Try: nx run-many --target build --projects ${context.target.project},... + `); + return { success: false }; + } else { + return { success: true }; + } +} + export default createBuilder(runNodePackageBuilder); export function runNodePackageBuilder( @@ -46,12 +146,28 @@ export function runNodePackageBuilder( context: BuilderContext ) { const normalizedOptions = normalizeOptions(options, context); - return compileTypeScriptFiles(normalizedOptions, context).pipe( - tap(() => { - updatePackageJson(normalizedOptions, context); - }), - switchMap(() => { - return copyAssetFiles(normalizedOptions, context); + const libDependencies = calculateLibraryDependencies(context); + + return of( + checkDependentLibrariesHaveBeenBuilt(context, libDependencies) + ).pipe( + switchMap(result => { + if (result.success) { + return compileTypeScriptFiles( + normalizedOptions, + context, + libDependencies + ).pipe( + tap(() => { + updatePackageJson(normalizedOptions, context, libDependencies); + }), + switchMap(() => { + return copyAssetFiles(normalizedOptions, context); + }) + ); + } else { + return of(result); + } }), map(value => { return { @@ -124,22 +240,55 @@ function normalizeOptions( let tscProcess: ChildProcess; function compileTypeScriptFiles( options: NormalizedBuilderOptions, - context: BuilderContext + context: BuilderContext, + projectDependencies: DependentLibraryNode[] ): Observable { if (tscProcess) { killProcess(context); } // Cleaning the /dist folder removeSync(options.normalizedOutputPath); + let tsConfigPath = join(context.workspaceRoot, options.tsConfig); return Observable.create((subscriber: Subscriber) => { + if (projectDependencies.length > 0) { + // const parsedTSConfig = readTsConfig(tsConfigPath); + const parsedTSConfig = ts.readConfigFile(tsConfigPath, ts.sys.readFile) + .config; + + // update TSConfig paths to point to the dist folder + projectDependencies.forEach(libDep => { + parsedTSConfig.compilerOptions = parsedTSConfig.compilerOptions || {}; + parsedTSConfig.compilerOptions.paths = + parsedTSConfig.compilerOptions.paths || {}; + + const currentPaths = + parsedTSConfig.compilerOptions.paths[libDep.scope] || []; + parsedTSConfig.compilerOptions.paths[libDep.scope] = [ + libDep.outputPath, + ...currentPaths + ]; + }); + + // find the library root folder + const projGraph = createProjectGraph(); + const libRoot = projGraph.nodes[context.target.project].data.root; + + // write the tmp tsconfig needed for building + const tmpTsConfigPath = join( + context.workspaceRoot, + libRoot, + 'tsconfig.lib.nx-tmp' + ); + writeJsonFile(tmpTsConfigPath, parsedTSConfig); + + // adjust the tsConfig path s.t. it points to the temporary one + // with the adjusted paths + tsConfigPath = tmpTsConfigPath; + } + try { - let args = [ - '-p', - join(context.workspaceRoot, options.tsConfig), - '--outDir', - options.normalizedOutputPath - ]; + let args = ['-p', tsConfigPath, '--outDir', options.normalizedOutputPath]; if (options.sourceMap) { args.push('--sourceMap'); @@ -155,11 +304,15 @@ function compileTypeScriptFiles( tscProcess = fork(tscPath, args, { stdio: [0, 1, 2, 'ipc'] }); subscriber.next({ success: true }); } else { - context.logger.info('Compiling TypeScript files...'); + context.logger.info( + `Compiling TypeScript files for library ${context.target.project}...` + ); tscProcess = fork(tscPath, args, { stdio: [0, 1, 2, 'ipc'] }); tscProcess.on('exit', code => { if (code === 0) { - context.logger.info('Done compiling TypeScript files.'); + context.logger.info( + `Done compiling TypeScript files for library ${context.target.project}` + ); subscriber.next({ success: true }); } else { subscriber.error('Could not compile Typescript files'); @@ -175,7 +328,17 @@ function compileTypeScriptFiles( new Error(`Could not compile Typescript files: \n ${error}`) ); } - }); + }).pipe( + finalize(() => { + cleanupTmpTsConfigFile(tsConfigPath); + }) + ); +} + +function cleanupTmpTsConfigFile(tsConfigPath) { + if (tsConfigPath.indexOf('.nx-tmp') > -1) { + unlinkSync(tsConfigPath); + } } function killProcess(context: BuilderContext): void { @@ -192,9 +355,19 @@ function killProcess(context: BuilderContext): void { }); } +// verify whether the package.json already specifies the dep +function hasDependency(outputJson, depConfigName: string, packageName: string) { + if (outputJson[depConfigName]) { + return outputJson[depConfigName][packageName]; + } else { + return false; + } +} + function updatePackageJson( options: NormalizedBuilderOptions, - context: BuilderContext + context: BuilderContext, + libDependencies: DependentLibraryNode[] ) { const mainFile = basename(options.main, '.ts'); const typingsFile = `${mainFile}.d.ts`; @@ -209,6 +382,32 @@ function updatePackageJson( packageJson.typings = normalize( `./${options.relativeMainFileOutput}/${typingsFile}` ); + + // add any dependency to the dependencies section + packageJson.dependencies = packageJson.dependencies || {}; + libDependencies.forEach(entry => { + if ( + !hasDependency(packageJson, 'dependencies', entry.scope) && + !hasDependency(packageJson, 'devDependencies', entry.scope) && + !hasDependency(packageJson, 'peerDependencies', entry.scope) + ) { + // read the lib version (should we read the one from the dist?) + const packageJsonPath = join( + context.workspaceRoot, + entry.node.data.root, + 'package.json' + ); + const depNodePackageJson = readJsonFile(packageJsonPath); + + packageJson.dependencies[entry.scope] = depNodePackageJson.version; + } + }); + + // avoid adding empty dependencies + if (Object.keys(packageJson.dependencies).length === 0) { + delete packageJson.dependencies; + } + writeJsonFile(`${options.outputPath}/package.json`, packageJson); }