From 858f0f1f58e0f0e3ecca54b3ce8fa3ca3bdfc9fe Mon Sep 17 00:00:00 2001 From: Alexander Burakevych Date: Fri, 22 Dec 2017 12:05:30 +1100 Subject: [PATCH] feat(@angular/cli): add build flag --inline-asset-max-size - Maximum size of asset to be inlined --- docs/documentation/build.md | 39 +++++++--- packages/@angular/cli/commands/build.ts | 5 ++ packages/@angular/cli/models/build-options.ts | 1 + .../cli/models/webpack-configs/styles.ts | 77 ++++++++++++------- .../build/styles/inline-asset-max-size.ts | 51 ++++++++++++ 5 files changed, 134 insertions(+), 39 deletions(-) create mode 100644 tests/e2e/tests/build/styles/inline-asset-max-size.ts diff --git a/docs/documentation/build.md b/docs/documentation/build.md index 19fe3371f891..83384285ae0c 100644 --- a/docs/documentation/build.md +++ b/docs/documentation/build.md @@ -73,15 +73,15 @@ dead code elimination via UglifyJS. Both `--dev`/`--target=development` and `--prod`/`--target=production` are 'meta' flags, that set other flags. If you do not specify either you will get the `--dev` defaults. -Flag | `--dev` | `--prod` ---- | --- | --- -`--aot` | `false` | `true` -`--environment` | `dev` | `prod` -`--output-hashing` | `media` | `all` -`--sourcemaps` | `true` | `false` -`--extract-css` | `false` | `true` -`--named-chunks`   | `true` | `false` -`--build-optimizer` | `false` | `true` with AOT and Angular 5 +Flag | `--dev` | `--prod` +--- | --- | --- +`--aot` | `false` | `true` +`--environment` | `dev` | `prod` +`--output-hashing` | `media` | `all` +`--sourcemaps` | `true` | `false` +`--extract-css` | `false` | `true` +`--named-chunks` | `true` | `false` +`--build-optimizer` | `false` | `true` with AOT and Angular 5 `--prod` also sets the following non-flaggable settings: - Adds service worker if configured in `.angular-cli.json`. @@ -100,7 +100,11 @@ code. ### CSS resources Resources in CSS, such as images and fonts, will be copied over automatically as part of a build. -If a resource is less than 10kb it will also be inlined. +If a resource is less than 10kb it will also be inlined by default. + +By using flaf `--inline-asset-max-size` it's possible to define the max size of the +assets that have to be inlined. Negative values will result in no assets to be inlined. + You'll see these resources be outputted and fingerprinted at the root of `dist/`. @@ -189,6 +193,19 @@ See https://github.com/angular/angular-cli/issues/7797 for details.

+
+ inline-asset-max-size +

+ --inline-asset-max-size +

+

+ Maximum size (in Kb) of assets to be inlined +

+

+ Values: -1 - must not inline any assets, positive integer - size in Kb of assets to inline +

+
+
i18n-file

@@ -404,4 +421,4 @@ See https://github.com/angular/angular-cli/issues/7797 for details.

Extract all licenses in a separate file, in the case of production builds only.

-
+ \ No newline at end of file diff --git a/packages/@angular/cli/commands/build.ts b/packages/@angular/cli/commands/build.ts index 401a2b28289c..f2fd6b2db985 100644 --- a/packages/@angular/cli/commands/build.ts +++ b/packages/@angular/cli/commands/build.ts @@ -116,6 +116,11 @@ export const baseBuildCommandOptions: any = [ aliases: ['ec'], description: 'Extract css from global styles onto css files instead of js ones.' }, + { + name: 'inline-asset-max-size', + type: Number, + description: 'Maximum size (in Kb) of assets to be inlined' + }, { name: 'watch', type: Boolean, diff --git a/packages/@angular/cli/models/build-options.ts b/packages/@angular/cli/models/build-options.ts index 05abcfb3ee23..fdb3fc360914 100644 --- a/packages/@angular/cli/models/build-options.ts +++ b/packages/@angular/cli/models/build-options.ts @@ -18,6 +18,7 @@ export interface BuildOptions { locale?: string; missingTranslation?: string; extractCss?: boolean; + inlineAssetMaxSize?: number; bundleDependencies?: 'none' | 'all'; watch?: boolean; outputHashing?: string; diff --git a/packages/@angular/cli/models/webpack-configs/styles.ts b/packages/@angular/cli/models/webpack-configs/styles.ts index de830c0187f2..0ba05b920cd1 100644 --- a/packages/@angular/cli/models/webpack-configs/styles.ts +++ b/packages/@angular/cli/models/webpack-configs/styles.ts @@ -44,39 +44,60 @@ export function getStylesConfig(wco: WebpackConfigOptions) { const baseHref = wco.buildOptions.baseHref || ''; const deployUrl = wco.buildOptions.deployUrl || ''; + const getPostcssUrlConfig = function() { + interface PostcssUrlConfig { + filter({ url }: { url: string}): boolean; + url: string | Function; + maxSize?: number; + } + + let config: PostcssUrlConfig[] = []; + + const convertRelativeUrlConfig: PostcssUrlConfig = { + // Only convert root relative URLs, which CSS-Loader won't process into require(). + filter: ({ url }: { url: string}) => url.startsWith('/') && !url.startsWith('//'), + url: ({ url }: { url: string }) => { + if (deployUrl.match(/:\/\//) || deployUrl.startsWith('/')) { + // If deployUrl is absolute or root relative, ignore baseHref & use deployUrl as is. + return `${deployUrl.replace(/\/$/, '')}${url}`; + } else if (baseHref.match(/:\/\//)) { + // If baseHref contains a scheme, include it as is. + return baseHref.replace(/\/$/, '') + + `/${deployUrl}/${url}`.replace(/\/\/+/g, '/'); + } else { + // Join together base-href, deploy-url and the original URL. + // Also dedupe multiple slashes into single ones. + return `/${baseHref}/${deployUrl}/${url}`.replace(/\/\/+/g, '/'); + } + } + }; + + config.push(convertRelativeUrlConfig); + + // Do not inline any assets if the inline-asset-max-size option is negative + if (!buildOptions.inlineAssetMaxSize || buildOptions.inlineAssetMaxSize >= 0) { + const inlineAssetsConfig: PostcssUrlConfig = { + // TODO: inline .cur if not supporting IE (use browserslist to check) + filter: (asset: any) => !asset.hash && !asset.absolutePath.endsWith('.cur'), + url: 'inline', + // NOTE: maxSize is in KB + maxSize: Number.isInteger(buildOptions.inlineAssetMaxSize) ? + buildOptions.inlineAssetMaxSize : 10, + }; + + config.push(inlineAssetsConfig); + } + + return config; + }; + const postcssPluginCreator = function() { return [ postcssUrl({ filter: ({ url }: { url: string }) => url.startsWith('~'), url: ({ url }: { url: string }) => path.join(projectRoot, 'node_modules', url.substr(1)), }), - postcssUrl([ - { - // Only convert root relative URLs, which CSS-Loader won't process into require(). - filter: ({ url }: { url: string }) => url.startsWith('/') && !url.startsWith('//'), - url: ({ url }: { url: string }) => { - if (deployUrl.match(/:\/\//) || deployUrl.startsWith('/')) { - // If deployUrl is absolute or root relative, ignore baseHref & use deployUrl as is. - return `${deployUrl.replace(/\/$/, '')}${url}`; - } else if (baseHref.match(/:\/\//)) { - // If baseHref contains a scheme, include it as is. - return baseHref.replace(/\/$/, '') + - `/${deployUrl}/${url}`.replace(/\/\/+/g, '/'); - } else { - // Join together base-href, deploy-url and the original URL. - // Also dedupe multiple slashes into single ones. - return `/${baseHref}/${deployUrl}/${url}`.replace(/\/\/+/g, '/'); - } - } - }, - { - // TODO: inline .cur if not supporting IE (use browserslist to check) - filter: (asset: any) => !asset.hash && !asset.absolutePath.endsWith('.cur'), - url: 'inline', - // NOTE: maxSize is in KB - maxSize: 10 - } - ]), + postcssUrl(getPostcssUrlConfig()), autoprefixer(), customProperties({ preserve: true }) ]; @@ -195,7 +216,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) { const ret: any = { include: globalStylePaths, test, -        use: buildOptions.extractCss ? ExtractTextPlugin.extract(extractTextPlugin) + use: buildOptions.extractCss ? ExtractTextPlugin.extract(extractTextPlugin) : ['style-loader', ...extractTextPlugin.use] }; // Save the original options as arguments for eject. diff --git a/tests/e2e/tests/build/styles/inline-asset-max-size.ts b/tests/e2e/tests/build/styles/inline-asset-max-size.ts new file mode 100644 index 000000000000..d423dbc7227d --- /dev/null +++ b/tests/e2e/tests/build/styles/inline-asset-max-size.ts @@ -0,0 +1,51 @@ +import { ng } from '../../../utils/process'; +import { + expectFileToMatch, + expectFileMatchToExist, + writeMultipleFiles +} from '../../../utils/fs'; +import { copyProjectAsset } from '../../../utils/assets'; +import { expectToFail } from '../../../utils/utils'; + +const imgSvg = ` + + + +`; + +export default function () { + return Promise.resolve() + .then(() => writeMultipleFiles({ + 'src/styles.css': ` + h1 { background: url('./assets/large.png'); } + h2 { background: url('./assets/small.svg'); } + p { background: url('./assets/small-id.svg#testID'); } + `, + 'src/app/app.component.css': ` + h3 { background: url('../assets/small.svg'); } + h4 { background: url('../assets/large.png'); } + `, + 'src/assets/small.svg': imgSvg, + 'src/assets/small-id.svg': imgSvg + })) + .then(() => copyProjectAsset('images/spectrum.png', './assets/large.png')) + .then(() => ng('build', '--extract-css', '--inline-asset-max-size=-1', '--aot')) + // Check paths are correctly generated. + .then(() => expectFileToMatch('dist/styles.bundle.css', + /url\([\'"]?large\.[0-9a-f]{20}\.png[\'"]?\)/)) + .then(() => expectFileToMatch('dist/styles.bundle.css', + /url\([\'"]?small\.[0-9a-f]{20}\.svg[\'"]?\)/)) + .then(() => expectFileToMatch('dist/styles.bundle.css', + /url\([\'"]?small-id\.[0-9a-f]{20}\.svg#testID[\'"]?\)/)) + .then(() => expectFileToMatch('dist/main.bundle.js', + /url\([\'"]?small\.[0-9a-f]{20}\.svg[\'"]?\)/)) + .then(() => expectFileToMatch('dist/main.bundle.js', + /url\([\'"]?large\.[0-9a-f]{20}\.png[\'"]?\)/)) + // Check if no inline images have been generated + .then(() => expectToFail(() => expectFileToMatch('dist/styles.bundle.css', + /url\(\\?[\'"]data:image\/svg\+xml/))) + // Check files are correctly created. + .then(() => expectFileMatchToExist('./dist', /large\.[0-9a-f]{20}\.png/)) + .then(() => expectFileMatchToExist('./dist', /small\.[0-9a-f]{20}\.svg/)) + .then(() => expectFileMatchToExist('./dist', /small-id\.[0-9a-f]{20}\.svg/)); +}