Skip to content

Commit 74463dc

Browse files
committed
fix(@angular/build): preserve incremental .d.ts signatures and emit state in watch mode
- Drain newly created .ngtypecheck.ts shims after collectDiagnostics() using emitNextAffectedFile with isolatedModules and assumeChangesOnlyAffectDirectDependencies enabled so TypeScript does not reset .d.ts shape signatures or queue component files into affectedFilesPendingEmit for the next rebuild. - Ensure before transformers run prior to afterDeclarations during .d.ts-only emits so Angular's declarationTransformFactory always has populated dtsTransforms. - Only record source files as emitted when JS output is produced (not on .d.ts-only checks), and pass ts.EmitOnly.Js in the fallback loop when .d.ts was already emitted. - Track transitive importers whose BuilderProgram signatures are reset to sourceFile.version by handleDtsMayChangeOf so populatePreviousDtsSignatures restores their .d.ts signatures on subsequent edits.
1 parent 639d31f commit 74463dc

2 files changed

Lines changed: 318 additions & 24 deletions

File tree

‎packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts‎

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { rm } from 'node:fs/promises';
1010
import { executeLibraryBuilder } from '../../builder';
11+
import { CompilationMode } from '../../schema';
1112
import { BASE_OPTIONS, LIBRARY_BUILDER_INFO, describeLibraryBuilder } from '../setup';
1213

1314
describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => {
@@ -601,5 +602,203 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) =>
601602
},
602603
]);
603604
});
605+
606+
it('should only rebuild affected entry point when an external template is modified with isolatedModules enabled', async () => {
607+
await harness.writeFiles({
608+
'projects/lib/src/lib/lib.component.html': '<p>Primary Initial</p>',
609+
'projects/lib/src/lib/lib.component.ts': `
610+
import { Component } from '@angular/core';
611+
612+
@Component({
613+
selector: 'lib-primary',
614+
templateUrl: './lib.component.html',
615+
})
616+
export class LibComponent {}
617+
`,
618+
'projects/lib/secondary/src/secondary.component.html': '<p>Secondary Initial</p>',
619+
'projects/lib/secondary/src/secondary.component.ts': `
620+
import { Component } from '@angular/core';
621+
622+
@Component({
623+
selector: 'lib-secondary',
624+
templateUrl: './secondary.component.html',
625+
})
626+
export class SecondaryComponent {}
627+
`,
628+
'projects/lib/secondary/src/public-api.ts': `
629+
export { SecondaryComponent } from './secondary.component';
630+
`,
631+
});
632+
633+
await harness.modifyFile('projects/lib/tsconfig.lib.json', (content) => {
634+
const tsconfig = JSON.parse(content);
635+
tsconfig.compilerOptions.isolatedModules = true;
636+
637+
return JSON.stringify(tsconfig, null, 2);
638+
});
639+
640+
await harness.modifyFile('projects/lib/package.json', (content) => {
641+
const pkg = JSON.parse(content);
642+
pkg.exports = {
643+
'.': './src/public-api.ts',
644+
'./secondary': './secondary/src/public-api.ts',
645+
};
646+
647+
return JSON.stringify(pkg, null, 2);
648+
});
649+
650+
harness.useTarget('build', {
651+
...BASE_OPTIONS,
652+
watch: true,
653+
});
654+
655+
await harness.executeWithCases([
656+
async ({ result }) => {
657+
expect(result?.success).toBeTrue();
658+
expect(harness.readFile('dist/lib/fesm2022/lib-secondary.mjs')).toContain(
659+
'Secondary Initial',
660+
);
661+
expect(harness.readFile('dist/lib/types/lib.d.ts')).toContain('ɵcmp');
662+
expect(harness.readFile('dist/lib/types/lib-secondary.d.ts')).toContain('ɵcmp');
663+
664+
await harness.writeFile(
665+
'projects/lib/secondary/src/secondary.component.html',
666+
'<p>Secondary Updated</p>',
667+
);
668+
},
669+
({ result, logs }) => {
670+
expect(result?.success).toBeTrue();
671+
expect(harness.readFile('dist/lib/fesm2022/lib-secondary.mjs')).toContain(
672+
'Secondary Updated',
673+
);
674+
expect(harness.readFile('dist/lib/types/lib.d.ts')).toContain('ɵcmp');
675+
expect(harness.readFile('dist/lib/types/lib-secondary.d.ts')).toContain('ɵcmp');
676+
677+
expect(logs).toContain(
678+
jasmine.objectContaining({
679+
message: jasmine.stringContaining('Compiling lib/secondary...'),
680+
}),
681+
);
682+
expect(logs).not.toContain(
683+
jasmine.objectContaining({
684+
message: jasmine.stringContaining('Compiling lib...'),
685+
}),
686+
);
687+
},
688+
]);
689+
});
690+
691+
it('should rebuild dependent entry point when a shared component input is added in watch mode', async () => {
692+
await harness.writeFiles({
693+
'projects/lib/shared/src/public-api.ts': `
694+
import { Component } from '@angular/core';
695+
696+
@Component({
697+
selector: 'lib-shared-icon',
698+
template: '<span>icon</span>',
699+
})
700+
export class SharedIconComponent {}
701+
`,
702+
'projects/lib/src/lib/lib.component.ts': `
703+
import { Component } from '@angular/core';
704+
import { SharedIconComponent } from 'lib/shared';
705+
706+
@Component({
707+
selector: 'lib-primary',
708+
imports: [SharedIconComponent],
709+
template: '<lib-shared-icon />',
710+
})
711+
export class LibComponent {}
712+
`,
713+
});
714+
715+
await harness.modifyFile('projects/lib/tsconfig.lib.json', (content) => {
716+
const tsconfig = JSON.parse(content);
717+
tsconfig.compilerOptions.isolatedModules = true;
718+
719+
return JSON.stringify(tsconfig, null, 2);
720+
});
721+
722+
await harness.modifyFile('projects/lib/package.json', (content) => {
723+
const pkg = JSON.parse(content);
724+
pkg.exports = {
725+
'.': './src/public-api.ts',
726+
'./shared': './shared/src/public-api.ts',
727+
};
728+
729+
return JSON.stringify(pkg, null, 2);
730+
});
731+
732+
harness.useTarget('build', {
733+
...BASE_OPTIONS,
734+
compilationMode: CompilationMode.Partial,
735+
watch: true,
736+
});
737+
738+
await harness.executeWithCases([
739+
async ({ result }) => {
740+
expect(result?.success).toBeTrue();
741+
742+
// Trigger a rebuild of `lib` first so `lib/shared` undergoes a `.d.ts`-only check
743+
await harness.writeFile(
744+
'projects/lib/src/lib/lib.component.ts',
745+
`
746+
import { Component } from '@angular/core';
747+
import { SharedIconComponent } from 'lib/shared';
748+
749+
@Component({
750+
selector: 'lib-primary',
751+
imports: [SharedIconComponent],
752+
template: '<lib-shared-icon class="updated" />',
753+
})
754+
export class LibComponent {}
755+
`,
756+
);
757+
},
758+
async ({ result, logs }) => {
759+
expect(result?.success).toBeTrue();
760+
expect(logs).toContain(
761+
jasmine.objectContaining({
762+
message: jasmine.stringContaining('Compiling lib...'),
763+
}),
764+
);
765+
expect(logs).not.toContain(
766+
jasmine.objectContaining({
767+
message: jasmine.stringContaining('Compiling lib/shared...'),
768+
}),
769+
);
770+
771+
// Add a new @Input() to SharedIconComponent
772+
await harness.writeFile(
773+
'projects/lib/shared/src/public-api.ts',
774+
`
775+
import { Component, Input } from '@angular/core';
776+
777+
@Component({
778+
selector: 'lib-shared-icon',
779+
template: '<span>{{ name }}</span>',
780+
})
781+
export class SharedIconComponent {
782+
@Input() name = 'default';
783+
}
784+
`,
785+
);
786+
},
787+
({ result, logs }) => {
788+
expect(result?.success).toBeTrue();
789+
expect(logs).toContain(
790+
jasmine.objectContaining({
791+
message: jasmine.stringContaining('Compiling lib/shared...'),
792+
}),
793+
);
794+
expect(logs).toContain(
795+
jasmine.objectContaining({
796+
message: jasmine.stringContaining('Compiling lib...'),
797+
}),
798+
);
799+
expect(harness.readFile('dist/lib/fesm2022/lib.mjs')).toContain('"name"');
800+
},
801+
]);
802+
});
604803
});
605804
});

0 commit comments

Comments
 (0)