diff --git a/docs/documentation/stories/create-library.md b/docs/documentation/stories/create-library.md index 11cd73044cce..8ccf97421c49 100644 --- a/docs/documentation/stories/create-library.md +++ b/docs/documentation/stories/create-library.md @@ -78,29 +78,24 @@ directory for the library beforehand, removing old code leftover code from previ ## Why do I need to build the library everytime I make changes? Running `ng build my-lib` every time you change a file is bothersome and takes time. +In `Angular CLI` version `6.2` an incremental builds functionality has been added to improve the experience of library developers. +Everytime a file is changed a partial build is performed that emits the amended files. -Some similar setups instead add the path to the source code directly inside tsconfig. -This makes seeing changes in your app faster. +The feature can be using by passing `--watch` command argument as show below; -But doing that is risky. -When you do that, the build system for your app is building the library as well. -But your library is built using a different build system than your app. - -Those two build systems can build things slightly different, or support completely different -features. - -This leads to subtle bugs where your published library behaves differently from the one in your -development setup. - -For this reason we decided to err on the side of caution, and make the recommended usage -the safe one. - -In the future we want to add watch support to building libraries so it is faster to see changes. +```bash +ng build my-lib --watch +``` -We are also planning to add internal dependency support to Angular CLI. -This means that Angular CLI would know your app depends on your library, and automatically rebuilds -the library when the app needs it. +Note: This feature requires that Angular's Compiler Option [enableResourceInlining](https://angular.io/guide/aot-compiler#enableresourceinlining) is enabled. +This can be done by adding the below in your `tsconfig.lib.json`. +```javascript +"angularCompilerOptions": { + "enableResourceInlining": true, + ... +} +``` ## Note for upgraded projects diff --git a/package-lock.json b/package-lock.json index 1ad6b852dd9b..357bac21895f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7466,13 +7466,51 @@ "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "integrity": "sha1-GMSasWoDe26wFSzIPjRxM4IVtm4=", "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha1-SRafHXmTQwZG2mHsxa41XCHJe3M=", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha1-2+w7OrdZdYBxtY/ln8QYca8hQA4=", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", + "integrity": "sha1-5iTtVO6MRgp3izyfNnBJb/ileuw=", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha1-Mi1poFwCZLJZl9n0DNiokasAZKQ=", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha1-hQgLuHxkaI+keZb+j3376CEXYLE=" + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -7488,14 +7526,14 @@ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", "requires": { - "find-up": "^2.0.0", + "find-up": "^3.0.0", "read-pkg": "^3.0.0" } }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=" }, "uglify-js": { "version": "3.4.5", @@ -8533,15 +8571,6 @@ } } }, - "postcss-clean": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/postcss-clean/-/postcss-clean-1.1.0.tgz", - "integrity": "sha512-83g3GqMbCM5NL6MlbbPLJ/m2NrUepBF44MoDk4Gt04QGXeXKh9+ilQa0DzLnYnvqYHQCw83nckuEzBFr2muwbg==", - "requires": { - "clean-css": "^4.x", - "postcss": "^6.x" - } - }, "postcss-import": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-11.1.0.tgz", diff --git a/packages/angular_devkit/build_ng_packagr/package.json b/packages/angular_devkit/build_ng_packagr/package.json index ac972f84b3fe..e12d32d20153 100644 --- a/packages/angular_devkit/build_ng_packagr/package.json +++ b/packages/angular_devkit/build_ng_packagr/package.json @@ -11,7 +11,8 @@ "dependencies": { "@angular-devkit/architect": "0.0.0", "@angular-devkit/core": "0.0.0", - "rxjs": "^6.0.0" + "rxjs": "^6.0.0", + "semver": "^5.3.0" }, "peerDependencies": { "ng-packagr": "^2.2.0 || ^3.0.0 || ^4.0.0" diff --git a/packages/angular_devkit/build_ng_packagr/src/build/index.ts b/packages/angular_devkit/build_ng_packagr/src/build/index.ts index 10c8a2ee3f0e..ff1e60cc4fad 100644 --- a/packages/angular_devkit/build_ng_packagr/src/build/index.ts +++ b/packages/angular_devkit/build_ng_packagr/src/build/index.ts @@ -12,9 +12,14 @@ import { BuilderConfiguration, BuilderContext, } from '@angular-devkit/architect'; -import { getSystemPath, normalize, resolve } from '@angular-devkit/core'; +import { getSystemPath, normalize, resolve, tags } from '@angular-devkit/core'; +import * as fs from 'fs'; import * as ngPackagr from 'ng-packagr'; -import { Observable } from 'rxjs'; +import { EMPTY, Observable } from 'rxjs'; +import { catchError, tap } from 'rxjs/operators'; +import * as semver from 'semver'; + +const NEW_NG_PACKAGR_VERSION = '4.0.0-rc.3'; // TODO move this function to architect or somewhere else where it can be imported from. // Blatantly copy-pasted from 'require-project-module.ts'. @@ -22,10 +27,40 @@ function requireProjectModule(root: string, moduleName: string) { return require(require.resolve(moduleName, { paths: [root] })); } +function resolveProjectModule(root: string, moduleName: string) { + return require.resolve(moduleName, { paths: [root] }); +} export interface NgPackagrBuilderOptions { project: string; tsConfig?: string; + watch?: boolean; +} + +function checkNgPackagrVersion(projectRoot: string): boolean { + let ngPackagrJsonPath; + + try { + ngPackagrJsonPath = resolveProjectModule(projectRoot, 'ng-packagr/package.json'); + } catch { + // ng-packagr is not installed + throw new Error(tags.stripIndent` + ng-packagr is not installed. Run \`npm install ng-packagr --save-dev\` and try again. + `); + } + + const ngPackagrPackageJson = fs.readFileSync(ngPackagrJsonPath).toString(); + const ngPackagrVersion = JSON.parse(ngPackagrPackageJson)['version']; + + if (!semver.gte(ngPackagrVersion, NEW_NG_PACKAGR_VERSION)) { + throw new Error(tags.stripIndent` + The installed version of ng-packagr is ${ngPackagrVersion}. The watch feature + requires ng-packagr version to satisfy ${NEW_NG_PACKAGR_VERSION}. + Please upgrade your ng-packagr version. + `); + } + + return true; } export class NgPackagrBuilder implements Builder { @@ -53,12 +88,30 @@ export class NgPackagrBuilder implements Builder { ngPkgProject.withTsConfig(tsConfigPath); } - ngPkgProject.build() - .then(() => { - obs.next({ success: true }); - obs.complete(); - }) - .catch((e) => obs.error(e)); + if (options.watch) { + checkNgPackagrVersion(getSystemPath(root)); + + const ngPkgSubscription = ngPkgProject + .watch() + .pipe( + tap(() => obs.next({ success: true })), + catchError(e => { + obs.error(e); + + return EMPTY; + }), + ) + .subscribe(); + + return () => ngPkgSubscription.unsubscribe(); + } else { + ngPkgProject.build() + .then(() => { + obs.next({ success: true }); + obs.complete(); + }) + .catch(e => obs.error(e)); + } }); } diff --git a/packages/angular_devkit/build_ng_packagr/src/build/index_spec_large.ts b/packages/angular_devkit/build_ng_packagr/src/build/index_spec_large.ts index d7a6e236b1e6..c227e2f67e93 100644 --- a/packages/angular_devkit/build_ng_packagr/src/build/index_spec_large.ts +++ b/packages/angular_devkit/build_ng_packagr/src/build/index_spec_large.ts @@ -8,9 +8,8 @@ import { TargetSpecifier } from '@angular-devkit/architect'; import { TestProjectHost, runTargetSpec } from '@angular-devkit/architect/testing'; -import { join, normalize } from '@angular-devkit/core'; -import { tap } from 'rxjs/operators'; - +import { join, normalize, virtualFs } from '@angular-devkit/core'; +import { debounceTime, map, take, tap } from 'rxjs/operators'; const devkitRoot = normalize((global as any)._DevKitRoot); // tslint:disable-line:no-any const workspaceRoot = join(devkitRoot, 'tests/angular_devkit/build_ng_packagr/ng-packaged/'); @@ -43,4 +42,59 @@ describe('NgPackagr Builder', () => { tap((buildEvent) => expect(buildEvent.success).toBe(true)), ).toPromise().then(done, done.fail); }); + + it('rebuilds on TS file changes', (done) => { + const targetSpec: TargetSpecifier = { project: 'lib', target: 'build' }; + + const goldenValueFiles: { [path: string]: string } = { + 'projects/lib/src/lib/lib.component.ts': ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'lib', + template: 'lib update works!' + }) + export class LibComponent { } + `, + }; + + const overrides = { watch: true }; + + let buildNumber = 0; + + runTargetSpec(host, targetSpec, overrides) + .pipe( + // We must debounce on watch mode because file watchers are not very accurate. + // Changes from just before a process runs can be picked up and cause rebuilds. + // In this case, cleanup from the test right before this one causes a few rebuilds. + debounceTime(1000), + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + map(() => { + const fileName = './dist/lib/fesm5/lib.js'; + const content = virtualFs.fileBufferToString( + host.scopedSync().read(normalize(fileName)), + ); + + return content; + }), + tap(content => { + buildNumber += 1; + switch (buildNumber) { + case 1: + expect(content).toMatch(/lib works/); + host.writeMultipleFiles(goldenValueFiles); + break; + + case 2: + expect(content).toMatch(/lib update works/); + break; + default: + break; + } + }), + take(2), + ) + .toPromise() + .then(done, done.fail); + }); }); diff --git a/packages/angular_devkit/build_ng_packagr/src/build/schema.json b/packages/angular_devkit/build_ng_packagr/src/build/schema.json index 8f426a7f8887..979b5cd25599 100644 --- a/packages/angular_devkit/build_ng_packagr/src/build/schema.json +++ b/packages/angular_devkit/build_ng_packagr/src/build/schema.json @@ -10,6 +10,11 @@ "tsConfig": { "type": "string", "description": "The file path of the TypeScript configuration file." + }, + "watch": { + "type": "boolean", + "description": "Run build when files change.", + "default": false } }, "additionalProperties": false, diff --git a/packages/schematics/angular/library/files/__projectRoot__/ng-package.json b/packages/schematics/angular/library/files/__projectRoot__/ng-package.json index 96795b12e9f5..c0f915caf7b2 100644 --- a/packages/schematics/angular/library/files/__projectRoot__/ng-package.json +++ b/packages/schematics/angular/library/files/__projectRoot__/ng-package.json @@ -1,7 +1,6 @@ { "$schema": "<%= relativePathToWorkspaceRoot %>/node_modules/ng-packagr/ng-package.schema.json", "dest": "<%= relativePathToWorkspaceRoot %>/<%= distRoot %>", - "deleteDestPath": false, "lib": { "entryFile": "src/<%= entryFile %>.ts" } diff --git a/packages/schematics/angular/library/files/__projectRoot__/ng-package.prod.json b/packages/schematics/angular/library/files/__projectRoot__/ng-package.prod.json deleted file mode 100644 index c0f915caf7b2..000000000000 --- a/packages/schematics/angular/library/files/__projectRoot__/ng-package.prod.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "<%= relativePathToWorkspaceRoot %>/node_modules/ng-packagr/ng-package.schema.json", - "dest": "<%= relativePathToWorkspaceRoot %>/<%= distRoot %>", - "lib": { - "entryFile": "src/<%= entryFile %>.ts" - } -} \ No newline at end of file diff --git a/packages/schematics/angular/library/index.ts b/packages/schematics/angular/library/index.ts index 20164ce22bd6..d37e1f5033be 100644 --- a/packages/schematics/angular/library/index.ts +++ b/packages/schematics/angular/library/index.ts @@ -35,7 +35,6 @@ import { latestVersions } from '../utility/latest-versions'; import { validateProjectName } from '../utility/validation'; import { Schema as LibraryOptions } from './schema'; - interface UpdateJsonFn { (obj: T): T | void; } @@ -145,11 +144,6 @@ function addAppToWorkspaceFile(options: LibraryOptions, workspace: WorkspaceSche 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', diff --git a/packages/schematics/angular/library/index_spec.ts b/packages/schematics/angular/library/index_spec.ts index 93f80c87a004..67e2eb38e8db 100644 --- a/packages/schematics/angular/library/index_spec.ts +++ b/packages/schematics/angular/library/index_spec.ts @@ -74,7 +74,6 @@ describe('Library Schematic', () => { const fileContent = getJsonFileContent(tree, '/projects/foo/ng-package.json'); expect(fileContent.lib).toBeDefined(); expect(fileContent.lib.entryFile).toEqual('src/my_index.ts'); - expect(fileContent.deleteDestPath).toEqual(false); expect(fileContent.dest).toEqual('../../dist/foo'); });