Skip to content

feat(@angular/cli): optimize stylesheets after bundling #8764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@
"autoprefixer": "^6.5.3",
"chalk": "~2.2.0",
"circular-dependency-plugin": "^4.2.1",
"clean-css": "^4.1.9",
"common-tags": "^1.3.1",
"copy-webpack-plugin": "^4.1.1",
"core-object": "^3.1.0",
"css-loader": "^0.28.1",
"cssnano": "^3.10.0",
"denodeify": "^1.2.1",
"ember-cli-string-utils": "^1.0.0",
"enhanced-resolve": "^3.4.1",
Expand Down
20 changes: 6 additions & 14 deletions packages/@angular/cli/models/webpack-configs/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
import { extraEntryParser, getOutputHashFormat } from './utils';
import { WebpackConfigOptions } from '../webpack-config';
import { pluginArgs, postcssArgs } from '../../tasks/eject';
import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin';

const cssnano = require('cssnano');
const postcssUrl = require('postcss-url');
const autoprefixer = require('autoprefixer');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
Expand Down Expand Up @@ -45,15 +45,6 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
const deployUrl = wco.buildOptions.deployUrl || '';

const postcssPluginCreator = function() {
// safe settings based on: https://github.com/ben-eb/cssnano/issues/358#issuecomment-283696193
const importantCommentRe = /@preserve|@licen[cs]e|[@#]\s*source(?:Mapping)?URL|^!/i;
const minimizeOptions = {
autoprefixer: false, // full pass with autoprefixer is run separately
safe: true,
mergeLonghand: false, // version 3+ should be safe; cssnano currently uses 2.x
discardComments : { remove: (comment: string) => !importantCommentRe.test(comment) }
};

return [
postcssUrl([
{
Expand Down Expand Up @@ -84,15 +75,12 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
]),
autoprefixer(),
customProperties({ preserve: true })
].concat(
minimizeCss ? [cssnano(minimizeOptions)] : []
);
];
};
(postcssPluginCreator as any)[postcssArgs] = {
variableImports: {
'autoprefixer': 'autoprefixer',
'postcss-url': 'postcssUrl',
'cssnano': 'cssnano',
'postcss-custom-properties': 'customProperties'
},
variables: { minimizeCss, baseHref, deployUrl }
Expand Down Expand Up @@ -222,6 +210,10 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
extraPlugins.push(new SuppressExtractedTextChunksWebpackPlugin());
}

if (minimizeCss) {
extraPlugins.push(new CleanCssWebpackPlugin({ sourceMap: cssSourceMap }));
}

return {
entry: entryPoints,
module: { rules },
Expand Down
2 changes: 1 addition & 1 deletion packages/@angular/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@
"autoprefixer": "^6.5.3",
"chalk": "~2.2.0",
"circular-dependency-plugin": "^4.2.1",
"clean-css": "^4.1.9",
"common-tags": "^1.3.1",
"copy-webpack-plugin": "^4.1.1",
"core-object": "^3.1.0",
"css-loader": "^0.28.1",
"cssnano": "^3.10.0",
"denodeify": "^1.2.1",
"ember-cli-string-utils": "^1.0.0",
"exports-loader": "^0.6.3",
Expand Down
111 changes: 111 additions & 0 deletions packages/@angular/cli/plugins/cleancss-webpack-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* @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
*/
import { Compiler } from 'webpack';
import { RawSource, SourceMapSource } from 'webpack-sources';

const CleanCSS = require('clean-css');

interface Chunk {
files: string[];
}

export interface CleanCssWebpackPluginOptions {
sourceMap: boolean;
}

export class CleanCssWebpackPlugin {

constructor(private options: Partial<CleanCssWebpackPluginOptions> = {}) {}

apply(compiler: Compiler): void {
compiler.plugin('compilation', (compilation: any) => {
compilation.plugin('optimize-chunk-assets',
(chunks: Array<Chunk>, callback: (err?: Error) => void) => {

const cleancss = new CleanCSS({
compatibility: 'ie9',
level: 2,
inline: false,
returnPromise: true,
sourceMap: this.options.sourceMap,
});

const files: string[] = [...compilation.additionalChunkAssets];

chunks.forEach(chunk => {
if (chunk.files && chunk.files.length > 0) {
files.push(...chunk.files);
}
});

const actions = files
.filter(file => file.endsWith('.css'))
.map(file => {
const asset = compilation.assets[file];
if (!asset) {
return Promise.resolve();
}

let content: string;
let map: any;
if (asset.sourceAndMap) {
const sourceAndMap = asset.sourceAndMap();
content = sourceAndMap.source;
map = sourceAndMap.map;
} else {
content = asset.source();
}

if (content.length === 0) {
return Promise.resolve();
}

return Promise.resolve()
.then(() => cleancss.minify(content, map))
.then((output: any) => {
let hasWarnings = false;
if (output.warnings && output.warnings.length > 0) {
compilation.warnings.push(...output.warnings);
hasWarnings = true;
}

if (output.errors && output.errors.length > 0) {
output.errors
.forEach((error: string) => compilation.errors.push(new Error(error)));
return;
}

// generally means invalid syntax so bail
if (hasWarnings && output.stats.minifiedSize === 0) {
return;
}

let newSource;
if (output.sourceMap) {
newSource = new SourceMapSource(
output.styles,
file,
output.sourceMap.toString(),
content,
map,
);
} else {
newSource = new RawSource(output.styles);
}

compilation.assets[file] = newSource;
});
});

Promise.all(actions)
.then(() => callback())
.catch(err => callback(err));
});
});
}
}
1 change: 1 addition & 0 deletions packages/@angular/cli/plugins/webpack.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Exports the webpack plugins we use internally.
export { BaseHrefWebpackPlugin } from '../lib/base-href-webpack/base-href-webpack-plugin';
export { CleanCssWebpackPlugin, CleanCssWebpackPluginOptions } from './cleancss-webpack-plugin';
export { GlobCopyWebpackPlugin, GlobCopyWebpackPluginOptions } from './glob-copy-webpack-plugin';
export { NamedLazyChunksWebpackPlugin } from './named-lazy-chunks-webpack-plugin';
export { ScriptsWebpackPlugin, ScriptsWebpackPluginOptions } from './scripts-webpack-plugin';
Expand Down
2 changes: 1 addition & 1 deletion packages/@angular/cli/tasks/eject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class JsonWebpackSerializer {
this._addImport('webpack.optimize', 'ModuleConcatenationPlugin');
break;
case angularCliPlugins.BaseHrefWebpackPlugin:
case angularCliPlugins.CleanCssWebpackPlugin:
case angularCliPlugins.NamedLazyChunksWebpackPlugin:
case angularCliPlugins.ScriptsWebpackPlugin:
case angularCliPlugins.SuppressExtractedTextChunksWebpackPlugin:
Expand Down Expand Up @@ -565,7 +566,6 @@ export default Task.extend({
'webpack',
'autoprefixer',
'css-loader',
'cssnano',
'exports-loader',
'file-loader',
'html-webpack-plugin',
Expand Down
3 changes: 2 additions & 1 deletion packages/@ngtools/webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"enhanced-resolve": "^3.1.0",
"magic-string": "^0.22.3",
"semver": "^5.3.0",
"source-map": "^0.5.6"
"source-map": "^0.5.6",
"webpack-sources": "^1.1.0"
},
"peerDependencies": {
"webpack": "^2.2.0 || ^3.0.0"
Expand Down
20 changes: 2 additions & 18 deletions packages/@ngtools/webpack/src/compiler_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ export class WebpackCompilerHost implements ts.CompilerHost {
private _delegate: ts.CompilerHost;
private _files: {[path: string]: VirtualFileStats | null} = Object.create(null);
private _directories: {[path: string]: VirtualDirStats | null} = Object.create(null);
private _cachedResources: {[path: string]: string | undefined} = Object.create(null);

private _changedFiles: {[path: string]: boolean} = Object.create(null);
private _changedDirs: {[path: string]: boolean} = Object.create(null);
Expand Down Expand Up @@ -174,8 +173,8 @@ export class WebpackCompilerHost implements ts.CompilerHost {
fileName = this.resolve(fileName);
if (fileName in this._files) {
this._files[fileName] = null;
this._changedFiles[fileName] = true;
}
this._changedFiles[fileName] = true;
}

fileExists(fileName: string, delegate = true): boolean {
Expand Down Expand Up @@ -299,22 +298,7 @@ export class WebpackCompilerHost implements ts.CompilerHost {
if (this._resourceLoader) {
// These paths are meant to be used by the loader so we must denormalize them.
const denormalizedFileName = this.denormalizePath(fileName);
const resourceDeps = this._resourceLoader.getResourceDependencies(denormalizedFileName);

if (this._cachedResources[fileName] === undefined
|| resourceDeps.some((dep) => this._changedFiles[this.resolve(dep)])) {
return this._resourceLoader.get(denormalizedFileName)
.then((resource) => {
// Add resource dependencies to the compiler host file list.
// This way we can check the changed files list to determine whether to use cache.
this._resourceLoader.getResourceDependencies(denormalizedFileName)
.forEach((dep) => this.readFile(dep));
this._cachedResources[fileName] = resource;
return resource;
});
} else {
return this._cachedResources[fileName];
}
return this._resourceLoader.get(denormalizedFileName);
} else {
return this.readFile(fileName);
}
Expand Down
Loading