diff --git a/packages/angular_devkit/build_angular/BUILD.bazel b/packages/angular_devkit/build_angular/BUILD.bazel index 4568c647ed9a..4e6a6f4be098 100644 --- a/packages/angular_devkit/build_angular/BUILD.bazel +++ b/packages/angular_devkit/build_angular/BUILD.bazel @@ -56,6 +56,11 @@ ts_json_schema( src = "src/tslint/schema.json", ) +ts_json_schema( + name = "ng_packagr_schema", + src = "src/ng-packagr/schema.json", +) + ts_library( name = "build_angular", srcs = glob( @@ -77,6 +82,7 @@ ts_library( "//packages/angular_devkit/build_angular:src/protractor/schema.ts", "//packages/angular_devkit/build_angular:src/server/schema.ts", "//packages/angular_devkit/build_angular:src/tslint/schema.ts", + "//packages/angular_devkit/build_angular:src/ng-packagr/schema.ts", ], data = glob( include = [ @@ -145,6 +151,7 @@ ts_library( "@npm//loader-utils", "@npm//mini-css-extract-plugin", "@npm//minimatch", + "@npm//ng-packagr", "@npm//open", "@npm//parse5", "@npm//parse5-htmlparser2-tree-adapter", @@ -302,6 +309,7 @@ LARGE_SPECS = { "@npm//@angular/platform-server", ], }, + "ng-packagr": {}, "browser": { "shards": 50, "size": "large", diff --git a/packages/angular_devkit/build_angular/builders.json b/packages/angular_devkit/build_angular/builders.json index a378a2fd2fb2..c14be31f4ca1 100644 --- a/packages/angular_devkit/build_angular/builders.json +++ b/packages/angular_devkit/build_angular/builders.json @@ -4,22 +4,22 @@ "app-shell": { "implementation": "./src/app-shell", "schema": "./src/app-shell/schema.json", - "description": "Build a server app and a browser app, then render the index.html and use it for the browser output." + "description": "Build a server application and a browser application, then render the index.html and use it for the browser output." }, "browser": { "implementation": "./src/browser", "schema": "./src/browser/schema.json", - "description": "Build a browser app." + "description": "Build a browser application." }, "dev-server": { "implementation": "./src/dev-server", "schema": "./src/dev-server/schema.json", - "description": "Serve a browser app." + "description": "Serve a browser application." }, "extract-i18n": { "implementation": "./src/extract-i18n", "schema": "./src/extract-i18n/schema.json", - "description": "Extract i18n strings from a browser app." + "description": "Extract i18n strings from a browser application." }, "karma": { "implementation": "./src/karma", @@ -34,12 +34,17 @@ "tslint": { "implementation": "./src/tslint", "schema": "./src/tslint/schema.json", - "description": "Run tslint over a TS project." + "description": "Run tslint over a TypeScript project." }, "server": { "implementation": "./src/server", "schema": "./src/server/schema.json", "description": "Build a server Angular application." + }, + "ng-packagr": { + "implementation": "./src/ng-packagr", + "schema": "./src/ng-packagr/schema.json", + "description": "Build a library with ng-packagr." } } } diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index d457b1aa58f9..d4b007d1905e 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -75,11 +75,15 @@ }, "peerDependencies": { "@angular/compiler-cli": ">=10.1.0-next.0 < 11", + "ng-packagr": "^10.0.0", "typescript": ">=3.9 < 3.10" }, "peerDependenciesMeta": { "@angular/localize": { "optional": true + }, + "ng-packagr": { + "optional": true } } } diff --git a/packages/angular_devkit/build_angular/src/index.ts b/packages/angular_devkit/build_angular/src/index.ts index 0a5d3c78329c..83613150fc33 100644 --- a/packages/angular_devkit/build_angular/src/index.ts +++ b/packages/angular_devkit/build_angular/src/index.ts @@ -58,3 +58,8 @@ export { ServerBuilderOptions, ServerBuilderOutput, } from './server'; + +export { + execute as executeNgPackagrBuilder, + NgPackagrBuilderOptions, +} from './ng-packagr'; diff --git a/packages/angular_devkit/build_angular/src/ng-packagr/index.ts b/packages/angular_devkit/build_angular/src/ng-packagr/index.ts new file mode 100644 index 000000000000..d8e2fa24262c --- /dev/null +++ b/packages/angular_devkit/build_angular/src/ng-packagr/index.ts @@ -0,0 +1,40 @@ +/** + * @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 { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; +import { resolve } from 'path'; +import { Observable, from } from 'rxjs'; +import { mapTo, switchMap } from 'rxjs/operators'; +import { Schema as NgPackagrBuilderOptions } from './schema'; + +async function initialize( + options: NgPackagrBuilderOptions, + root: string, +): Promise { + const packager = (await import('ng-packagr')).ngPackagr(); + + packager.forProject(resolve(root, options.project)); + + if (options.tsConfig) { + packager.withTsConfig(resolve(root, options.tsConfig)); + } + + return packager; +} + +export function execute( + options: NgPackagrBuilderOptions, + context: BuilderContext, +): Observable { + return from(initialize(options, context.workspaceRoot)).pipe( + switchMap(packager => options.watch ? packager.watch() : packager.build()), + mapTo({ success: true }), + ); +} + +export { NgPackagrBuilderOptions }; +export default createBuilder & NgPackagrBuilderOptions>(execute); diff --git a/packages/angular_devkit/build_angular/src/ng-packagr/schema.json b/packages/angular_devkit/build_angular/src/ng-packagr/schema.json new file mode 100644 index 000000000000..a72def315064 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/ng-packagr/schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "ng-packagr Target", + "description": "ng-packagr target options for Build Architect. Use to build library projects.", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The file path for the ng-packagr configuration file, relative to the current workspace." + }, + "tsConfig": { + "type": "string", + "description": "The full path for the TypeScript configuration file, relative to the current workspace." + }, + "watch": { + "type": "boolean", + "description": "Run build when files change.", + "default": false + } + }, + "additionalProperties": false, + "required": [ + "project" + ] +} diff --git a/packages/angular_devkit/build_angular/src/ng-packagr/works_spec.ts b/packages/angular_devkit/build_angular/src/ng-packagr/works_spec.ts new file mode 100644 index 000000000000..550a92f9ef9e --- /dev/null +++ b/packages/angular_devkit/build_angular/src/ng-packagr/works_spec.ts @@ -0,0 +1,128 @@ +/** + * @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 { Architect } from '@angular-devkit/architect'; +import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; +import { TestProjectHost, TestingArchitectHost } from '@angular-devkit/architect/testing'; +import { + getSystemPath, + join, + normalize, + schema, + virtualFs, + workspaces, +} from '@angular-devkit/core'; +import { map, take, tap } from 'rxjs/operators'; +import { veEnabled } from '../test-utils'; + +// Default timeout for large specs is 2.5 minutes. +jasmine.DEFAULT_TIMEOUT_INTERVAL = 150000; + + +describe('NgPackagr Builder', () => { + const workspaceRoot = join(normalize(__dirname), `../../test/hello-world-lib/`); + const host = new TestProjectHost(workspaceRoot); + let architect: Architect; + + beforeEach(async () => { + await host.initialize().toPromise(); + + const registry = new schema.CoreSchemaRegistry(); + registry.addPostTransform(schema.transforms.addUndefinedDefaults); + + const workspaceSysPath = getSystemPath(host.root()); + const { workspace } = await workspaces.readWorkspace( + workspaceSysPath, + workspaces.createWorkspaceHost(host), + ); + const architectHost = new TestingArchitectHost( + workspaceSysPath, + workspaceSysPath, + new WorkspaceNodeModulesArchitectHost(workspace, workspaceSysPath), + ); + + architect = new Architect(architectHost, registry); + + // Set AOT compilation to use VE if needed. + if (veEnabled) { + host.replaceInFile('tsconfig.json', `"enableIvy": true,`, `"enableIvy": false,`); + } + }); + + afterEach(() => host.restore().toPromise()); + + it('builds and packages a library', async () => { + const run = await architect.scheduleTarget({ project: 'lib', target: 'build' }); + + await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); + + await run.stop(); + + expect(host.scopedSync().exists(normalize('./dist/lib/fesm2015/lib.js'))).toBe(true); + const content = virtualFs.fileBufferToString( + host.scopedSync().read(normalize('./dist/lib/fesm2015/lib.js')), + ); + expect(content).toContain('lib works'); + + if (veEnabled) { + expect(content).not.toContain('ɵcmp'); + } else { + expect(content).toContain('ɵcmp'); + } + }); + + it('rebuilds on TS file changes', async () => { + 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 run = await architect.scheduleTarget( + { project: 'lib', target: 'build' }, + { watch: true }, + ); + + let buildNumber = 0; + + await run.output.pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + map(() => { + const fileName = './dist/lib/fesm2015/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(); + + await run.stop(); + }); +});