Skip to content

Commit e89b3d4

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 93f238c commit e89b3d4

2 files changed

Lines changed: 329 additions & 27 deletions

File tree

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

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

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

1414
describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => {
@@ -649,5 +649,203 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) =>
649649
},
650650
]);
651651
});
652+
653+
it('should only rebuild affected entry point when an external template is modified with isolatedModules enabled', async () => {
654+
await harness.writeFiles({
655+
'projects/lib/src/lib/lib.component.html': '<p>Primary Initial</p>',
656+
'projects/lib/src/lib/lib.component.ts': `
657+
import { Component } from '@angular/core';
658+
659+
@Component({
660+
selector: 'lib-primary',
661+
templateUrl: './lib.component.html',
662+
})
663+
export class LibComponent {}
664+
`,
665+
'projects/lib/secondary/src/secondary.component.html': '<p>Secondary Initial</p>',
666+
'projects/lib/secondary/src/secondary.component.ts': `
667+
import { Component } from '@angular/core';
668+
669+
@Component({
670+
selector: 'lib-secondary',
671+
templateUrl: './secondary.component.html',
672+
})
673+
export class SecondaryComponent {}
674+
`,
675+
'projects/lib/secondary/src/public-api.ts': `
676+
export { SecondaryComponent } from './secondary.component';
677+
`,
678+
});
679+
680+
await harness.modifyFile('projects/lib/tsconfig.lib.json', (content) => {
681+
const tsconfig = JSON.parse(content);
682+
tsconfig.compilerOptions.isolatedModules = true;
683+
684+
return JSON.stringify(tsconfig, null, 2);
685+
});
686+
687+
await harness.modifyFile('projects/lib/package.json', (content) => {
688+
const pkg = JSON.parse(content);
689+
pkg.exports = {
690+
'.': './src/public-api.ts',
691+
'./secondary': './secondary/src/public-api.ts',
692+
};
693+
694+
return JSON.stringify(pkg, null, 2);
695+
});
696+
697+
harness.useTarget('build', {
698+
...BASE_OPTIONS,
699+
watch: true,
700+
});
701+
702+
await harness.executeWithCases([
703+
async ({ result }) => {
704+
expect(result?.success).toBeTrue();
705+
expect(harness.readFile('dist/lib/fesm2022/lib-secondary.mjs')).toContain(
706+
'Secondary Initial',
707+
);
708+
expect(harness.readFile('dist/lib/types/lib.d.ts')).toContain('ɵcmp');
709+
expect(harness.readFile('dist/lib/types/lib-secondary.d.ts')).toContain('ɵcmp');
710+
711+
await harness.writeFile(
712+
'projects/lib/secondary/src/secondary.component.html',
713+
'<p>Secondary Updated</p>',
714+
);
715+
},
716+
({ result, logs }) => {
717+
expect(result?.success).toBeTrue();
718+
expect(harness.readFile('dist/lib/fesm2022/lib-secondary.mjs')).toContain(
719+
'Secondary Updated',
720+
);
721+
expect(harness.readFile('dist/lib/types/lib.d.ts')).toContain('ɵcmp');
722+
expect(harness.readFile('dist/lib/types/lib-secondary.d.ts')).toContain('ɵcmp');
723+
724+
expect(logs).toContain(
725+
jasmine.objectContaining({
726+
message: jasmine.stringContaining('Compiling lib/secondary...'),
727+
}),
728+
);
729+
expect(logs).not.toContain(
730+
jasmine.objectContaining({
731+
message: jasmine.stringContaining('Compiling lib...'),
732+
}),
733+
);
734+
},
735+
]);
736+
});
737+
738+
it('should rebuild dependent entry point when a shared component input is added in watch mode', async () => {
739+
await harness.writeFiles({
740+
'projects/lib/shared/src/public-api.ts': `
741+
import { Component } from '@angular/core';
742+
743+
@Component({
744+
selector: 'lib-shared-icon',
745+
template: '<span>icon</span>',
746+
})
747+
export class SharedIconComponent {}
748+
`,
749+
'projects/lib/src/lib/lib.component.ts': `
750+
import { Component } from '@angular/core';
751+
import { SharedIconComponent } from 'lib/shared';
752+
753+
@Component({
754+
selector: 'lib-primary',
755+
imports: [SharedIconComponent],
756+
template: '<lib-shared-icon />',
757+
})
758+
export class LibComponent {}
759+
`,
760+
});
761+
762+
await harness.modifyFile('projects/lib/tsconfig.lib.json', (content) => {
763+
const tsconfig = JSON.parse(content);
764+
tsconfig.compilerOptions.isolatedModules = true;
765+
766+
return JSON.stringify(tsconfig, null, 2);
767+
});
768+
769+
await harness.modifyFile('projects/lib/package.json', (content) => {
770+
const pkg = JSON.parse(content);
771+
pkg.exports = {
772+
'.': './src/public-api.ts',
773+
'./shared': './shared/src/public-api.ts',
774+
};
775+
776+
return JSON.stringify(pkg, null, 2);
777+
});
778+
779+
harness.useTarget('build', {
780+
...BASE_OPTIONS,
781+
compilationMode: CompilationMode.Partial,
782+
watch: true,
783+
});
784+
785+
await harness.executeWithCases([
786+
async ({ result }) => {
787+
expect(result?.success).toBeTrue();
788+
789+
// Trigger a rebuild of `lib` first so `lib/shared` undergoes a `.d.ts`-only check
790+
await harness.writeFile(
791+
'projects/lib/src/lib/lib.component.ts',
792+
`
793+
import { Component } from '@angular/core';
794+
import { SharedIconComponent } from 'lib/shared';
795+
796+
@Component({
797+
selector: 'lib-primary',
798+
imports: [SharedIconComponent],
799+
template: '<lib-shared-icon class="updated" />',
800+
})
801+
export class LibComponent {}
802+
`,
803+
);
804+
},
805+
async ({ result, logs }) => {
806+
expect(result?.success).toBeTrue();
807+
expect(logs).toContain(
808+
jasmine.objectContaining({
809+
message: jasmine.stringContaining('Compiling lib...'),
810+
}),
811+
);
812+
expect(logs).not.toContain(
813+
jasmine.objectContaining({
814+
message: jasmine.stringContaining('Compiling lib/shared...'),
815+
}),
816+
);
817+
818+
// Add a new @Input() to SharedIconComponent
819+
await harness.writeFile(
820+
'projects/lib/shared/src/public-api.ts',
821+
`
822+
import { Component, Input } from '@angular/core';
823+
824+
@Component({
825+
selector: 'lib-shared-icon',
826+
template: '<span>{{ name }}</span>',
827+
})
828+
export class SharedIconComponent {
829+
@Input() name = 'default';
830+
}
831+
`,
832+
);
833+
},
834+
({ result, logs }) => {
835+
expect(result?.success).toBeTrue();
836+
expect(logs).toContain(
837+
jasmine.objectContaining({
838+
message: jasmine.stringContaining('Compiling lib/shared...'),
839+
}),
840+
);
841+
expect(logs).toContain(
842+
jasmine.objectContaining({
843+
message: jasmine.stringContaining('Compiling lib...'),
844+
}),
845+
);
846+
expect(harness.readFile('dist/lib/fesm2022/lib.mjs')).toContain('"name"');
847+
},
848+
]);
849+
});
652850
});
653851
});

0 commit comments

Comments
 (0)