Skip to content

Commit 1f79aa2

Browse files
alan-agius4dherges
authored andcommitted
feat: build only entrypoints that are effected by the change (#991)
When the public API that is consumed from another entrypoint changes, the updated API changes are now reflected in the secondary entrypoint. Also, now only entrypoints that are effected by this file change will be marked as `dirty` and therefore scheduled to be build. Relates: #974
1 parent 6629963 commit 1f79aa2

24 files changed

+311
-45
lines changed

integration/watch/basic.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ describe('basic', () => {
1919
describe('primary entrypoint', () => {
2020
it("should perform initial compilation when 'watch' is started", () => {
2121
harness.expectDtsToMatch('public_api', /title = "hello world"/);
22-
harness.expectFesm5ToContain('basic', 'title', 'hello world');
23-
harness.expectFesm2015ToContain('basic', 'title', 'hello world');
22+
harness.expectFesm5ToMatch('basic', /hello world/);
23+
harness.expectFesm2015ToMatch('basic', /hello world/);
2424
harness.expectMetadataToContain('basic', 'metadata.title', 'hello world');
2525
});
2626

@@ -30,8 +30,8 @@ describe('basic', () => {
3030

3131
harness.onComplete(() => {
3232
harness.expectDtsToMatch('public_api', /title = "foo bar"/);
33-
harness.expectFesm5ToContain('basic', 'title', 'foo bar');
34-
harness.expectFesm2015ToContain('basic', 'title', 'foo bar');
33+
harness.expectFesm5ToMatch('basic', /foo bar/);
34+
harness.expectFesm2015ToMatch('basic', /foo bar/);
3535
harness.expectMetadataToContain('basic', 'metadata.title', 'foo bar');
3636
done();
3737
});
@@ -42,8 +42,8 @@ describe('basic', () => {
4242

4343
harness.onComplete(() => {
4444
harness.expectDtsToMatch('public_api', /title = "foo bar"/);
45-
harness.expectFesm5ToContain('basic', 'title', 'foo bar');
46-
harness.expectFesm2015ToContain('basic', 'title', 'foo bar');
45+
harness.expectFesm5ToMatch('basic', /foo bar/);
46+
harness.expectFesm2015ToMatch('basic', /foo bar/);
4747
harness.expectMetadataToContain('basic', 'metadata.title', 'foo bar');
4848
done();
4949
});
@@ -62,8 +62,8 @@ describe('basic', () => {
6262
harness.copyTestCase('secondary-valid');
6363

6464
harness.onComplete(() => {
65-
harness.expectFesm5ToContain('basic-secondary', 'ɵa.decorators[0].args[0].template', 'Hello Angular');
66-
harness.expectFesm2015ToContain('basic-secondary', 'ɵa.decorators[0].args[0].template', 'Hello Angular');
65+
harness.expectFesm5ToMatch('basic-secondary', /Hello Angular/);
66+
harness.expectFesm2015ToMatch('basic-secondary', /Hello Angular/);
6767
harness.expectMetadataToContain(
6868
'secondary/basic-secondary',
6969
'metadata.ɵa.decorators[0].arguments[0].template',
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { expect } from 'chai';
2+
import * as fs from 'fs';
3+
import { TestHarness } from './test-harness';
4+
5+
describe('intra-dependent', () => {
6+
const harness = new TestHarness('intra-dependent');
7+
8+
before(async () => {
9+
await harness.initialize();
10+
});
11+
12+
afterEach(() => {
13+
harness.reset();
14+
});
15+
16+
after(() => {
17+
harness.dispose();
18+
});
19+
20+
it("should perform initial compilation when 'watch' is started", () => {
21+
harness.expectDtsToMatch('src/primary.component', /count: number/);
22+
harness.expectFesm5ToMatch('intra-dependent-secondary', /count = 100/);
23+
harness.expectFesm2015ToMatch('intra-dependent-secondary', /count = 100/);
24+
});
25+
26+
it('should throw error component inputs is changed without updating usages', done => {
27+
harness.copyTestCase('invalid-component-property');
28+
29+
harness.onFailure(error => {
30+
expect(error.message).to.match(/Can\'t bind to \'count\' since it isn\'t a known property/);
31+
harness.copyTestCase('valid');
32+
done();
33+
});
34+
});
35+
36+
it('should throw error service method is changed without updating usages', done => {
37+
harness.copyTestCase('invalid-service-method');
38+
39+
harness.onFailure(error => {
40+
expect(error.message).to.match(/Property \'initialize\' does not exist on type \'PrimaryAngularService\'/);
41+
harness.copyTestCase('valid');
42+
done();
43+
});
44+
});
45+
46+
it('should only build entrypoints that are dependent on the file changed.', done => {
47+
const primaryFesmPath = harness.getFilePath('fesm5/intra-dependent.js');
48+
const secondaryFesmPath = harness.getFilePath('fesm5/intra-dependent-secondary.js');
49+
const thirdFesmPath = harness.getFilePath('fesm5/intra-dependent-third.js');
50+
51+
const primaryModifiedTime = fs.statSync(primaryFesmPath).mtimeMs;
52+
const secondaryModifiedTime = fs.statSync(secondaryFesmPath).mtimeMs;
53+
const thirdModifiedTime = fs.statSync(thirdFesmPath).mtimeMs;
54+
harness.copyTestCase('valid');
55+
56+
harness.onComplete(() => {
57+
expect(fs.statSync(primaryFesmPath).mtimeMs).to.greaterThan(primaryModifiedTime);
58+
expect(fs.statSync(secondaryFesmPath).mtimeMs).to.greaterThan(secondaryModifiedTime);
59+
expect(fs.statSync(thirdFesmPath).mtimeMs).to.equals(thirdModifiedTime);
60+
done();
61+
});
62+
});
63+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"$schema": "../../../src/package.schema.json",
3+
"name": "intra-dependent",
4+
"description": "A sample library testing Angular Package Format",
5+
"version": "1.0.0-pre.0",
6+
"private": true,
7+
"repository": "https://github.com/dherges/ng-packagr.git",
8+
"main": "dist/bundles/intra-dependent.umd.js",
9+
"peerDependencies": {
10+
"@angular/core": "^4.1.2",
11+
"@angular/common": "^4.1.2"
12+
},
13+
"ngPackage": {
14+
"lib": {
15+
"entryFile": "public_api.ts"
16+
}
17+
}
18+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { PrimaryAngularModule } from './src/primary.module';
2+
export { PrimaryAngularComponent } from './src/primary.component';
3+
export { PrimaryAngularService } from './src/primary.service';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "../../../../src/package.schema.json",
3+
"description": "A secondary entry point",
4+
"private": true,
5+
"repository": "https://github.com/dherges/ng-packagr.git",
6+
"ngPackage": {
7+
"lib": {
8+
"entryFile": "public_api.ts"
9+
}
10+
}
11+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { SecondaryAngularModule } from './src/secondary.module';
2+
export { SecondaryAngularComponent } from './src/secondary.component';
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Component } from '@angular/core';
2+
import { PrimaryAngularService } from 'intra-dependent';
3+
4+
@Component({
5+
selector: 'ng-component-secondary',
6+
template: '<ng-component [count]="count"></ng-component>'
7+
})
8+
export class SecondaryAngularComponent {
9+
count = 100;
10+
11+
constructor(service: PrimaryAngularService) {
12+
service.initialize();
13+
}
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { NgModule } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { SecondaryAngularComponent } from './secondary.component';
4+
import { PrimaryAngularModule } from 'intra-dependent';
5+
6+
@NgModule({
7+
imports: [CommonModule, PrimaryAngularModule],
8+
declarations: [SecondaryAngularComponent],
9+
exports: [SecondaryAngularComponent]
10+
})
11+
export class SecondaryAngularModule {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Component, Input } from '@angular/core';
2+
3+
@Component({
4+
selector: 'ng-component',
5+
template: '{{ count }}'
6+
})
7+
export class PrimaryAngularComponent {
8+
@Input() count: number;
9+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { NgModule } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { PrimaryAngularComponent } from './primary.component';
4+
import { PrimaryAngularService } from './primary.service';
5+
6+
@NgModule({
7+
imports: [CommonModule],
8+
declarations: [PrimaryAngularComponent],
9+
providers: [PrimaryAngularService],
10+
exports: [PrimaryAngularComponent]
11+
})
12+
export class PrimaryAngularModule {}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Injectable } from '@angular/core';
2+
3+
@Injectable()
4+
export class PrimaryAngularService {
5+
initialize() {
6+
// stub
7+
}
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Component, Input } from '@angular/core';
2+
3+
@Component({
4+
selector: 'ng-component',
5+
template: '{{ counter }}'
6+
})
7+
export class PrimaryAngularComponent {
8+
@Input() counter: string;
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Injectable } from '@angular/core';
2+
3+
@Injectable()
4+
export class PrimaryAngularService {
5+
init() {
6+
// stub
7+
}
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Component, Input } from '@angular/core';
2+
3+
@Component({
4+
selector: 'ng-component',
5+
template: '{{ count }}'
6+
})
7+
export class PrimaryAngularComponent {
8+
@Input() count: number;
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Injectable } from '@angular/core';
2+
3+
@Injectable()
4+
export class PrimaryAngularService {
5+
initialize() {
6+
// stub
7+
}
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "../../../../src/package.schema.json",
3+
"description": "A third entry point",
4+
"private": true,
5+
"repository": "https://github.com/dherges/ng-packagr.git",
6+
"ngPackage": {
7+
"lib": {
8+
"entryFile": "public_api.ts"
9+
}
10+
}
11+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { ThirdAngularModule } from './src/third.module';
2+
export { ThirdAngularComponent } from './src/third.component';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Component } from '@angular/core';
2+
3+
@Component({
4+
selector: 'ng-component-third',
5+
template: 'Hello world!'
6+
})
7+
export class ThirdAngularComponent {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { NgModule } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { ThirdAngularComponent } from './third.component';
4+
5+
@NgModule({
6+
imports: [CommonModule],
7+
declarations: [ThirdAngularComponent],
8+
exports: [ThirdAngularComponent]
9+
})
10+
export class ThirdAngularModule {}

integration/watch/test-harness.ts

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,36 @@ import { tap } from 'rxjs/operators';
1313
export class TestHarness {
1414
private completeHandler = () => undefined;
1515

16-
private tmpPath = path.join(__dirname, '.tmp', this.testName);
16+
private harnessTempDir = path.join(__dirname, '.tmp');
17+
private testTempPath = path.join(this.harnessTempDir, this.testName);
1718
private testSrc = path.join(__dirname, this.testName);
19+
private testDistPath = path.join(this.testTempPath, 'dist');
1820
private ngPackagr$$: Subscription;
21+
private loggerStubs: { [key: string]: sinon.SinonStub } = {};
1922

2023
constructor(private testName: string) {}
2124

2225
initialize(): Promise<void> {
2326
// the below is done in order to avoid poluting the test reporter with build logs
24-
sinon.stub(log, 'msg');
25-
sinon.stub(log, 'info');
26-
sinon.stub(log, 'debug');
27-
sinon.stub(log, 'success');
28-
sinon.stub(log, 'warn');
27+
for (const key in log) {
28+
if (log.hasOwnProperty(key)) {
29+
this.loggerStubs[key] = sinon.stub(log, key as keyof typeof log);
30+
}
31+
}
2932

3033
this.emptyTestDirectory();
31-
fs.copySync(this.testSrc, this.tmpPath);
34+
fs.copySync(this.testSrc, this.testTempPath);
35+
3236
return this.setUpNgPackagr();
3337
}
3438

3539
dispose(): void {
3640
this.reset();
3741

42+
for (const key in this.loggerStubs) {
43+
this.loggerStubs[key].restore();
44+
}
45+
3846
if (this.ngPackagr$$) {
3947
this.ngPackagr$$.unsubscribe();
4048
}
@@ -43,27 +51,28 @@ export class TestHarness {
4351
}
4452

4553
reset(): void {
54+
this.loggerStubs['error'].resetBehavior();
4655
this.completeHandler = () => undefined;
4756
}
4857

4958
readFileSync(filePath: string, isJson = false): string | object {
50-
const file = path.join(this.tmpPath, 'dist', filePath);
59+
const file = path.join(this.testDistPath, filePath);
5160
return isJson ? fs.readJsonSync(file) : fs.readFileSync(file, { encoding: 'utf-8' });
5261
}
5362

5463
/**
5564
* Copy a test case to it's temporary destination immediately.
5665
*/
5766
copyTestCase(caseName: string) {
58-
fs.copySync(path.join(this.testSrc, 'test_files', caseName), this.tmpPath);
67+
fs.copySync(path.join(this.testSrc, 'test_files', caseName), this.testTempPath);
5968
}
6069

61-
expectFesm5ToContain(fileName: string, path: string, value: any): Chai.Assertion {
62-
return expect(this.requireNoCache(`fesm5/${fileName}.js`)).to.have.nested.property(path, value);
70+
expectFesm5ToMatch(fileName: string, regexp: RegExp): Chai.Assertion {
71+
return expect(this.readFileSync(`fesm5/${fileName}.js`)).to.match(regexp);
6372
}
6473

65-
expectFesm2015ToContain(fileName: string, path: string, value: any): Chai.Assertion {
66-
return expect(this.requireNoCache(`fesm2015/${fileName}.js`)).to.have.nested.property(path, value);
74+
expectFesm2015ToMatch(fileName: string, regexp: RegExp): Chai.Assertion {
75+
return expect(this.readFileSync(`fesm2015/${fileName}.js`)).to.match(regexp);
6776
}
6877

6978
expectDtsToMatch(fileName: string, regexp: RegExp): Chai.Assertion {
@@ -86,20 +95,24 @@ export class TestHarness {
8695
* Gets invoked when a compilation error occuries.
8796
*/
8897
onFailure(done: (error: Error) => void): void {
89-
sinon.stub(log, 'error').callsFake(done);
98+
this.loggerStubs['error'].callsFake(done);
9099
}
91100

92101
/**
93102
* Remove the entire directory for the current test case.
94103
*/
95104
emptyTestDirectory(): void {
96-
fs.emptyDirSync(this.tmpPath);
105+
fs.emptyDirSync(this.testTempPath);
106+
}
107+
108+
getFilePath(filePath: string): string {
109+
return path.join(this.testDistPath, filePath);
97110
}
98111

99112
private setUpNgPackagr(): Promise<void> {
100113
return new Promise(resolve => {
101114
this.ngPackagr$$ = ngPackagr()
102-
.forProject(path.join(this.tmpPath, 'package.json'))
115+
.forProject(path.join(this.testTempPath, 'package.json'))
103116
.watch()
104117
.pipe(
105118
tap(() => resolve()), // we are only interested when in the first builds, that's why we are resolving it
@@ -108,14 +121,4 @@ export class TestHarness {
108121
.subscribe();
109122
});
110123
}
111-
112-
private requireNoCache(modulePath: string): any {
113-
const moduleFile = this.buildFilePath(modulePath);
114-
delete require.cache[path.resolve(moduleFile)];
115-
return require(moduleFile);
116-
}
117-
118-
private buildFilePath(filePath: string): string {
119-
return path.join(this.tmpPath, 'dist', filePath);
120-
}
121124
}

0 commit comments

Comments
 (0)