diff --git a/docs/documentation/build.md b/docs/documentation/build.md index 6577e905f160..1c32d7fa49fa 100644 --- a/docs/documentation/build.md +++ b/docs/documentation/build.md @@ -404,3 +404,21 @@ See https://github.com/angular/angular-cli/issues/7797 for details. Run the TypeScript type checker in a forked process.

+
+ exclude-from-inlining +

+ --exclude-from-inlining +

+

+ Files to exclude from CSS inlining. +

+
+
+ maximum-inline-size +

+ --maximum-inline-size +

+

+ Maximum resource size to inline (KiB). +

+
\ No newline at end of file diff --git a/packages/angular/cli/lib/config/schema.json b/packages/angular/cli/lib/config/schema.json index e2b067df91a8..76a72a48c511 100644 --- a/packages/angular/cli/lib/config/schema.json +++ b/packages/angular/cli/lib/config/schema.json @@ -758,6 +758,18 @@ "$ref": "#/definitions/targetOptions/definitions/browser/definitions/budget" }, "default": [] + }, + "maximumInlineSize": { + "description": "Maximum resource size to inline (KiB).", + "type": "number", + "default": 10 + }, + "excludeFromInlining": { + "description": "Files to exclude from CSS inlining.", + "default": [], + "items": { + "type": "string" + } } }, "additionalProperties": false, 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 a81e728e45de..88b43525265b 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 @@ -64,6 +64,9 @@ export interface BuildOptions { lazyModules: string[]; platform?: 'browser' | 'server'; fileReplacements: CurrentFileReplacement[]; + + excludeFromInlining?: string[]; + maximumInlineSize?: number; } export interface WebpackTestOptions extends BuildOptions { diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts index cab614154b63..086c5eb4d3a7 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts @@ -19,7 +19,7 @@ import { BundleBudgetPlugin } from '../../plugins/bundle-budget'; import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin'; import { ScriptsWebpackPlugin } from '../../plugins/scripts-webpack-plugin'; import { findUp } from '../../utilities/find-up'; -import { AssetPatternObject, ExtraEntryPoint } from '../../../browser/schema'; +import { AssetPatternObject } from '../../../browser/schema'; import { normalizeExtraEntryPoints } from './utils'; const ProgressPlugin = require('webpack/lib/ProgressPlugin'); 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 e4eb5c7a69aa..95b2d4f75b07 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 @@ -10,12 +10,12 @@ import * as webpack from 'webpack'; import * as path from 'path'; +import { Minimatch } from 'minimatch'; import { SuppressExtractedTextChunksWebpackPlugin } from '../../plugins/webpack'; import { getOutputHashFormat } from './utils'; import { WebpackConfigOptions } from '../build-options'; import { findUp } from '../../utilities/find-up'; import { RawCssLoader } from '../../plugins/webpack'; -import { ExtraEntryPoint } from '../../../browser/schema'; import { normalizeExtraEntryPoints } from './utils'; import { RemoveHashPlugin } from '../../plugins/remove-hash-plugin'; @@ -52,15 +52,22 @@ export function getStylesConfig(wco: WebpackConfigOptions) { const entryPoints: { [key: string]: string[] } = {}; const globalStylePaths: string[] = []; const extraPlugins: any[] = []; - const cssSourceMap = buildOptions.sourceMap; + const { + sourceMap: cssSourceMap, + excludeFromInlining = [], + maximumInlineSize = 10, + + // Convert absolute resource URLs to account for base-href and deploy-url. + deployUrl = '', + baseHref = '', + outputHashing + } = buildOptions; + + // normalize to support ./ paths + const ignoreMatchers = excludeFromInlining.map(pattern => new Minimatch(path.normalize(pattern), { dot: true })); - // Maximum resource size to inline (KiB) - const maximumInlineSize = 10; // Determine hashing format. - const hashFormat = getOutputHashFormat(buildOptions.outputHashing as string); - // Convert absolute resource URLs to account for base-href and deploy-url. - const baseHref = wco.buildOptions.baseHref || ''; - const deployUrl = wco.buildOptions.deployUrl || ''; + const hashFormat = getOutputHashFormat(outputHashing !); const postcssPluginCreator = function (loader: webpack.loader.LoaderContext) { return [ @@ -139,7 +146,9 @@ export function getStylesConfig(wco: WebpackConfigOptions) { { // TODO: inline .cur if not supporting IE (use browserslist to check) filter: (asset: PostcssUrlAsset) => { - return maximumInlineSize > 0 && !asset.hash && !asset.absolutePath.endsWith('.cur'); + const { absolutePath, hash } = asset; + const url = path.relative(root, absolutePath); + return maximumInlineSize > 0 && !hash && !url.endsWith('.cur') && !ignoreMatchers.some(matchers => matchers.match(url)); }, url: 'inline', // NOTE: maxSize is in KB @@ -181,9 +190,9 @@ export function getStylesConfig(wco: WebpackConfigOptions) { // Add style entry points. if (entryPoints[style.bundleName]) { - entryPoints[style.bundleName].push(resolvedPath) + entryPoints[style.bundleName].push(resolvedPath); } else { - entryPoints[style.bundleName] = [resolvedPath] + entryPoints[style.bundleName] = [resolvedPath]; } // Add lazy styles to the list. @@ -197,7 +206,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) { if (chunkIds.length > 0) { // Add plugin to remove hashes from lazy styles. - extraPlugins.push(new RemoveHashPlugin({ chunkIds, hashFormat})); + extraPlugins.push(new RemoveHashPlugin({ chunkIds, hashFormat })); } } 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 eb887a851f24..7cce84325cf6 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.d.ts +++ b/packages/angular_devkit/build_angular/src/browser/schema.d.ts @@ -222,6 +222,16 @@ export interface BrowserBuilderSchema { * Budget thresholds to ensure parts of your application stay within boundaries which you set. */ budgets: Budget[]; + + /** + * Files to exclude from CSS inlining. + */ + excludeFromInlining: string[]; + + /** + * Maximum resource size to inline (KiB). + */ + maximumInlineSize: number; } export type AssetPattern = string | AssetPatternObject; diff --git a/packages/angular_devkit/build_angular/src/browser/schema.json b/packages/angular_devkit/build_angular/src/browser/schema.json index d013f123e68f..e014654966df 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.json +++ b/packages/angular_devkit/build_angular/src/browser/schema.json @@ -236,6 +236,18 @@ "$ref": "#/definitions/budget" }, "default": [] + }, + "maximumInlineSize": { + "description": "Maximum resource size to inline (KiB).", + "type": "number", + "default": 10 + }, + "excludeFromInlining": { + "description": "Files to exclude from CSS inlining.", + "default": [], + "items": { + "type": "string" + } } }, "additionalProperties": false, diff --git a/packages/angular_devkit/build_angular/test/browser/styles_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/styles_spec_large.ts index 091c9291129b..0958102f56c4 100644 --- a/packages/angular_devkit/build_angular/test/browser/styles_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/browser/styles_spec_large.ts @@ -334,6 +334,122 @@ describe('Browser Builder styles', () => { ).toPromise().then(done, done.fail); }); + describe(`when 'maximumInlineSize' is configured`, () => { + beforeEach(() => { + host.writeMultipleFiles({ + 'src/styles.scss': ` + @font-face{ + font-family:'Roboto'; + font-style:normal; + font-weight:400; + src:local('Roboto'),local('Roboto-Regular'), + url('./roboto-regular.woff2') format('woff2'), + url('./roboto-regular.woff') format('woff'), + url('./roboto-regular.ttf') format('truetype') + } + + a { + font-family: 'Roboto'; + } + `, + }); + }); + + it(`should inline resources if they don't exceed the configured size`, (done) => { + const overrides = { + extractCss: true, + maximumInlineSize: 20, + styles: ['src/styles.scss'], + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = 'dist/styles.css'; + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toContain('data:font/woff2'); + expect(content).toContain('data:font/woff'); + expect(content).toContain('data:font/ttf'); + }), + tap(() => { + expect(host.scopedSync().exists(normalize('dist/roboto-regular.woff2'))).toBe(false); + expect(host.scopedSync().exists(normalize('dist/roboto-regular.woff'))).toBe(false); + expect(host.scopedSync().exists(normalize('dist/roboto-regular.ttf'))).toBe(false); + }), + ).toPromise().then(done, done.fail); + }); + + it('should not inline resources that exceed the configured size', (done) => { + const overrides = { + extractCss: true, + maximumInlineSize: 5, + styles: ['src/styles.scss'], + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = 'dist/styles.css'; + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toContain('roboto-regular.woff'); + expect(content).toContain('roboto-regular.ttf'); + expect(content).toContain('roboto-regular.woff2'); + }), + tap(() => { + expect(host.scopedSync().exists(normalize('dist/roboto-regular.woff2'))).toBe(true); + expect(host.scopedSync().exists(normalize('dist/roboto-regular.woff'))).toBe(true); + expect(host.scopedSync().exists(normalize('dist/roboto-regular.ttf'))).toBe(true); + }), + ).toPromise().then(done, done.fail); + }); + + }); + + it(`should not inline resources that are listed in 'excludeFromInlining'`, (done) => { + host.writeMultipleFiles({ + 'src/styles.scss': ` + @font-face{ + font-family:'Roboto'; + font-style:normal; + font-weight:400; + src:local('Roboto'),local('Roboto-Regular'), + url('./roboto-regular.woff2') format('woff2'), + url('./roboto-regular.woff') format('woff'), + url('./roboto-regular.ttf') format('truetype') + } + + a { + font-family: 'Roboto'; + } + `, + }); + + const overrides = { + extractCss: true, + maximumInlineSize: 100, + excludeFromInlining: ['**/*.woff2'], + styles: ['src/styles.scss'], + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = 'dist/styles.css'; + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toContain('roboto-regular.woff2'); + + expect(content).not.toContain('data:font/woff2'); + expect(content).toContain('data:font/woff'); + expect(content).toContain('data:font/ttf'); + }), + tap(() => { + expect(host.scopedSync().exists(normalize('dist/roboto-regular.woff2'))).toBe(true); + expect(host.scopedSync().exists(normalize('dist/roboto-regular.woff'))).toBe(false); + expect(host.scopedSync().exists(normalize('dist/roboto-regular.ttf'))).toBe(false); + }), + ).toPromise().then(done, done.fail); + }); + it(`supports font-awesome imports`, (done) => { host.writeMultipleFiles({ 'src/styles.scss': ` diff --git a/tests/angular_devkit/build_angular/hello-world-app/src/roboto-regular.ttf b/tests/angular_devkit/build_angular/hello-world-app/src/roboto-regular.ttf new file mode 100644 index 000000000000..05037ed5e53b Binary files /dev/null and b/tests/angular_devkit/build_angular/hello-world-app/src/roboto-regular.ttf differ diff --git a/tests/angular_devkit/build_angular/hello-world-app/src/roboto-regular.woff b/tests/angular_devkit/build_angular/hello-world-app/src/roboto-regular.woff new file mode 100644 index 000000000000..5e353cf47a87 Binary files /dev/null and b/tests/angular_devkit/build_angular/hello-world-app/src/roboto-regular.woff differ diff --git a/tests/angular_devkit/build_angular/hello-world-app/src/roboto-regular.woff2 b/tests/angular_devkit/build_angular/hello-world-app/src/roboto-regular.woff2 new file mode 100644 index 000000000000..96a601550e3c Binary files /dev/null and b/tests/angular_devkit/build_angular/hello-world-app/src/roboto-regular.woff2 differ