diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/inline-style-language_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/inline-style-language_spec.ts index 7160099d325c..83ae153a5310 100644 --- a/packages/angular_devkit/build_angular/src/browser/tests/options/inline-style-language_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/inline-style-language_spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import { concatMap, count, take, timeout } from 'rxjs/operators'; import { buildWebpackBrowser } from '../../index'; import { InlineStyleLanguage } from '../../schema'; import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; @@ -106,6 +107,67 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { harness.expectFile('dist/main.js').content.toContain('color: green'); }); }); + + it('updates produced stylesheet in watch mode', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + main: 'src/main.ts', + inlineStyleLanguage: InlineStyleLanguage.Scss, + aot, + watch: true, + }); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace( + '__STYLE_MARKER__', + '$primary-color: green;\\nh1 { color: $primary-color; }', + ), + ); + + const buildCount = await harness + .execute() + .pipe( + timeout(30000), + concatMap(async ({ result }, index) => { + expect(result?.success).toBe(true); + + switch (index) { + case 0: + harness.expectFile('dist/main.js').content.toContain('color: green'); + harness.expectFile('dist/main.js').content.not.toContain('color: aqua'); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace( + '$primary-color: green;\\nh1 { color: $primary-color; }', + '$primary-color: aqua;\\nh1 { color: $primary-color; }', + ), + ); + break; + case 1: + harness.expectFile('dist/main.js').content.not.toContain('color: green'); + harness.expectFile('dist/main.js').content.toContain('color: aqua'); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace( + '$primary-color: aqua;\\nh1 { color: $primary-color; }', + '$primary-color: blue;\\nh1 { color: $primary-color; }', + ), + ); + break; + case 2: + harness.expectFile('dist/main.js').content.not.toContain('color: green'); + harness.expectFile('dist/main.js').content.not.toContain('color: aqua'); + harness.expectFile('dist/main.js').content.toContain('color: blue'); + break; + } + }), + take(3), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(3); + }); } }); }); diff --git a/packages/ngtools/webpack/src/ivy/host.ts b/packages/ngtools/webpack/src/ivy/host.ts index c888b88f9b5d..d8d0033f6803 100644 --- a/packages/ngtools/webpack/src/ivy/host.ts +++ b/packages/ngtools/webpack/src/ivy/host.ts @@ -58,7 +58,12 @@ export function augmentHostWithResources( } if (options.inlineStyleMimeType) { - const content = await resourceLoader.process(data, options.inlineStyleMimeType); + const content = await resourceLoader.process( + data, + options.inlineStyleMimeType, + context.type, + context.containingFile, + ); return { content }; } diff --git a/packages/ngtools/webpack/src/resource_loader.ts b/packages/ngtools/webpack/src/resource_loader.ts index 4cee20dd2885..07018f2df4a1 100644 --- a/packages/ngtools/webpack/src/resource_loader.ts +++ b/packages/ngtools/webpack/src/resource_loader.ts @@ -98,13 +98,16 @@ export class WebpackResourceLoader { filePath?: string, data?: string, mimeType?: string, + resourceType?: 'style' | 'template', + hash?: string, + containingFile?: string, ): Promise { if (!this._parentCompilation) { throw new Error('WebpackResourceLoader cannot be used without parentCompilation'); } // Create a special URL for reading the resource from memory - const entry = data ? 'angular-resource://' : filePath; + const entry = data ? `angular-resource:${resourceType},${hash}` : filePath; if (!entry) { throw new Error(`"filePath" or "data" must be specified.`); } @@ -116,7 +119,11 @@ export class WebpackResourceLoader { ); } - const outputFilePath = filePath || `angular-resource-output-${this.outputPathCounter++}.css`; + const outputFilePath = + filePath || + `${containingFile}-angular-inline--${this.outputPathCounter++}.${ + resourceType === 'template' ? 'html' : 'css' + }`; const outputOptions = { filename: outputFilePath, library: { @@ -215,7 +222,14 @@ export class WebpackResourceLoader { if (parent) { parent.children = parent.children.filter((child) => child !== childCompilation); - parent.fileDependencies.addAll(childCompilation.fileDependencies); + for (const fileDependency of childCompilation.fileDependencies) { + if (data && containingFile && fileDependency.endsWith(entry)) { + // use containing file if the resource was inline + parent.fileDependencies.add(containingFile); + } else { + parent.fileDependencies.add(fileDependency); + } + } parent.contextDependencies.addAll(childCompilation.contextDependencies); parent.missingDependencies.addAll(childCompilation.missingDependencies); parent.buildDependencies.addAll(childCompilation.buildDependencies); @@ -298,7 +312,12 @@ export class WebpackResourceLoader { return compilationResult.content; } - async process(data: string, mimeType: string): Promise { + async process( + data: string, + mimeType: string, + resourceType: 'template' | 'style', + containingFile?: string, + ): Promise { if (data.trim().length === 0) { return ''; } @@ -307,7 +326,14 @@ export class WebpackResourceLoader { let compilationResult = this.inlineCache?.get(cacheKey); if (compilationResult === undefined) { - compilationResult = await this._compile(undefined, data, mimeType); + compilationResult = await this._compile( + undefined, + data, + mimeType, + resourceType, + cacheKey, + containingFile, + ); if (this.inlineCache && compilationResult.success) { this.inlineCache.set(cacheKey, compilationResult);