diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts index 5a33f1e32913..8e8e6df74dde 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts @@ -21,6 +21,7 @@ export interface BuildOptions { optimization: boolean; environment?: string; outputPath: string; + resourcesOutputPath?: string; aot?: boolean; sourceMap?: boolean; vendorSourceMap?: boolean; diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts index b468dbd8ad7e..436fb0ba9159 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts @@ -47,6 +47,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) { // Convert absolute resource URLs to account for base-href and deploy-url. const baseHref = wco.buildOptions.baseHref || ''; const deployUrl = wco.buildOptions.deployUrl || ''; + const resourcesOutputPath = buildOptions.resourcesOutputPath || ''; const postcssPluginCreator = function (loader: webpack.loader.LoaderContext) { return [ @@ -70,6 +71,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) { PostcssCliResources({ baseHref, deployUrl, + resourcesOutputPath, loader, filename: `[name]${hashFormat.file}.[ext]`, }), diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/postcss-cli-resources.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/postcss-cli-resources.ts index 0a5816ad0925..e885293ede46 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/postcss-cli-resources.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/postcss-cli-resources.ts @@ -27,6 +27,7 @@ function wrapUrl(url: string): string { export interface PostcssCliResourcesOptions { baseHref?: string; deployUrl?: string; + resourcesOutputPath?: string; filename: string; loader: webpack.loader.LoaderContext; } @@ -47,6 +48,7 @@ export default postcss.plugin('postcss-cli-resources', (options: PostcssCliResou const { deployUrl = '', baseHref = '', + resourcesOutputPath = '', filename, loader, } = options; @@ -115,12 +117,16 @@ export default postcss.plugin('postcss-cli-resources', (options: PostcssCliResou return; } - const outputPath = interpolateName( + let outputPath = interpolateName( { resourcePath: result } as webpack.loader.LoaderContext, filename, { content }, ); + if (resourcesOutputPath) { + outputPath = path.posix.join(resourcesOutputPath, outputPath); + } + loader.addDependency(result); loader.emitFile(outputPath, content, undefined); diff --git a/packages/angular_devkit/build_angular/src/browser/schema.d.ts b/packages/angular_devkit/build_angular/src/browser/schema.d.ts index e6c30d7b0718..33ed974acadd 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.d.ts +++ b/packages/angular_devkit/build_angular/src/browser/schema.d.ts @@ -56,6 +56,11 @@ export interface BrowserBuilderSchema { */ outputPath: string; + /** + * Path where style resources will be placed (Relative to outputPath). + */ + resourcesOutputPath: string; + /** * Build using Ahead of Time compilation. */ diff --git a/packages/angular_devkit/build_angular/src/browser/schema.json b/packages/angular_devkit/build_angular/src/browser/schema.json index 201844584f01..2f0259214116 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.json +++ b/packages/angular_devkit/build_angular/src/browser/schema.json @@ -71,6 +71,11 @@ "type": "string", "description": "The full path for the new output directory, relative to the current workspace.\n\nBy default, writes output to a folder named dist/ in the current project." }, + "resourcesOutputPath": { + "type": "string", + "description": "The path where style resources will be placed, relative to outputPath.", + "default": "" + }, "aot": { "type": "boolean", "description": "Build using Ahead of Time compilation.", diff --git a/packages/angular_devkit/build_angular/src/server/schema.d.ts b/packages/angular_devkit/build_angular/src/server/schema.d.ts index 24b575422afd..0addbe5222e0 100644 --- a/packages/angular_devkit/build_angular/src/server/schema.d.ts +++ b/packages/angular_devkit/build_angular/src/server/schema.d.ts @@ -49,6 +49,10 @@ export interface BuildWebpackServerSchema { * Path where output will be placed. */ outputPath: string; + /** + * Path where style resources will be placed (Relative to outputPath). + */ + resourcesOutputPath: string; /** * Generates a 'stats.json' file which can be analyzed using tools such as: * #webpack-bundle-analyzer' or https: //webpack.github.io/analyse. diff --git a/packages/angular_devkit/build_angular/src/server/schema.json b/packages/angular_devkit/build_angular/src/server/schema.json index 5b6f1b6da789..de93450c3297 100644 --- a/packages/angular_devkit/build_angular/src/server/schema.json +++ b/packages/angular_devkit/build_angular/src/server/schema.json @@ -44,6 +44,11 @@ "type": "string", "description": "Path where output will be placed." }, + "resourcesOutputPath": { + "type": "string", + "description": "The path where style resources will be placed, relative to outputPath.", + "default": "" + }, "sourceMap": { "type": "boolean", "description": "Output sourcemaps.", diff --git a/packages/angular_devkit/build_angular/test/browser/resources-output-path_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/resources-output-path_spec_large.ts new file mode 100644 index 000000000000..cc97c3d69b8f --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/resources-output-path_spec_large.ts @@ -0,0 +1,108 @@ +/** + * @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 + */ +// tslint:disable:no-big-function + +import { runTargetSpec } from '@angular-devkit/architect/testing'; +import { normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { browserTargetSpec, host } from '../utils'; + + +describe('Browser Builder styles resources output path', () => { + const stylesBundle = 'dist/styles.css'; + const mainBundle = 'dist/main.js'; + + const imgSvg = ` + + + + `; + + function writeFiles() { + // Use a large image for the relative ref so it cannot be inlined. + host.copyFile('src/spectrum.png', './src/assets/global-img-relative.png'); + host.copyFile('src/spectrum.png', './src/assets/component-img-relative.png'); + host.writeMultipleFiles({ + 'src/styles.css': ` + h1 { background: url('/assets/global-img-absolute.svg'); } + h2 { background: url('./assets/global-img-relative.png'); } + `, + 'src/app/app.component.css': ` + h3 { background: url('/assets/component-img-absolute.svg'); } + h4 { background: url('../assets/component-img-relative.png'); } + `, + 'src/assets/global-img-absolute.svg': imgSvg, + 'src/assets/component-img-absolute.svg': imgSvg, + }); + } + + beforeEach(done => host.initialize().toPromise().then(done, done.fail)); + afterEach(done => host.restore().toPromise().then(done, done.fail)); + + it(`supports resourcesOutputPath in resource urls`, (done) => { + writeFiles(); + // Check base paths are correctly generated. + const overrides = { + aot: true, + extractCss: true, + resourcesOutputPath: 'out-assets', + }; + + runTargetSpec(host, browserTargetSpec, overrides, undefined, undefined).pipe( + tap(() => { + const styles = virtualFs.fileBufferToString( + host.scopedSync().read(normalize(stylesBundle)), + ); + + const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle))); + expect(styles).toContain(`url('/assets/global-img-absolute.svg')`); + expect(styles).toContain(`url('out-assets/global-img-relative.png')`); + expect(main).toContain(`url('/assets/component-img-absolute.svg')`); + expect(main).toContain(`url('out-assets/component-img-relative.png')`); + + expect(host.scopedSync() + .exists(normalize('dist/assets/global-img-absolute.svg'))).toBe(true); + expect(host.scopedSync() + .exists(normalize('dist/out-assets/global-img-relative.png'))).toBe(true); + expect(host.scopedSync() + .exists(normalize('dist/assets/component-img-absolute.svg'))).toBe(true); + expect(host.scopedSync() + .exists(normalize('dist/out-assets/component-img-relative.png'))).toBe(true); + }), + ).toPromise().then(done, done.fail); + }); + + it(`supports blank resourcesOutputPath`, (done) => { + writeFiles(); + + // Check base paths are correctly generated. + const overrides = { aot: true, extractCss: true }; + runTargetSpec(host, browserTargetSpec, overrides, undefined, undefined).pipe( + tap(() => { + const styles = virtualFs.fileBufferToString( + host.scopedSync().read(normalize(stylesBundle)), + ); + + const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle))); + expect(styles).toContain(`url('/assets/global-img-absolute.svg')`); + expect(styles).toContain(`url('global-img-relative.png')`); + expect(main).toContain(`url('/assets/component-img-absolute.svg')`); + expect(main).toContain(`url('component-img-relative.png')`); + expect(host.scopedSync().exists(normalize('dist/assets/global-img-absolute.svg'))) + .toBe(true); + expect(host.scopedSync().exists(normalize('dist/global-img-relative.png'))) + .toBe(true); + expect(host.scopedSync().exists(normalize('dist/assets/component-img-absolute.svg'))) + .toBe(true); + expect(host.scopedSync().exists(normalize('dist/component-img-relative.png'))) + .toBe(true); + }), + ).toPromise().then(done, done.fail); + }); + +});