Skip to content

Commit 7d0bd99

Browse files
clydinfilipesilva
authored andcommitted
feat(@angular/cli): optimize stylesheet resource processing
1 parent 4f711e7 commit 7d0bd99

File tree

8 files changed

+264
-935
lines changed

8 files changed

+264
-935
lines changed

package-lock.json

Lines changed: 54 additions & 896 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,9 @@
5454
"common-tags": "^1.3.1",
5555
"copy-webpack-plugin": "^4.1.1",
5656
"core-object": "^3.1.0",
57-
"css-loader": "^0.28.1",
5857
"denodeify": "^1.2.1",
5958
"ember-cli-string-utils": "^1.0.0",
6059
"enhanced-resolve": "^3.4.1",
61-
"exports-loader": "^0.6.3",
6260
"extract-text-webpack-plugin": "^3.0.2",
6361
"file-loader": "^1.1.5",
6462
"fs-extra": "^4.0.0",
@@ -78,6 +76,7 @@
7876
"nopt": "^4.0.1",
7977
"opn": "~5.1.0",
8078
"portfinder": "~1.0.12",
79+
"postcss": "^6.0.16",
8180
"postcss-import": "^11.0.0",
8281
"postcss-loader": "^2.0.10",
8382
"postcss-url": "^7.1.2",
@@ -124,7 +123,7 @@
124123
"@types/request": "~2.0.0",
125124
"@types/semver": "^5.3.30",
126125
"@types/source-map": "0.5.2",
127-
"@types/webpack": "^3.0.5",
126+
"@types/webpack": "^3.8.2",
128127
"@types/webpack-sources": "^0.1.3",
129128
"builtin-modules": "2.0.0",
130129
"conventional-changelog": "1.1.0",

packages/@angular/cli/models/webpack-configs/styles.ts

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
import * as webpack from 'webpack';
22
import * as path from 'path';
33
import {
4-
SuppressExtractedTextChunksWebpackPlugin
5-
} from '../../plugins/suppress-entry-chunks-webpack-plugin';
4+
CleanCssWebpackPlugin,
5+
SuppressExtractedTextChunksWebpackPlugin,
6+
} from '../../plugins/webpack';
67
import { extraEntryParser, getOutputHashFormat } from './utils';
78
import { WebpackConfigOptions } from '../webpack-config';
89
import { pluginArgs, postcssArgs } from '../../tasks/eject';
9-
import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin';
1010

1111
const postcssUrl = require('postcss-url');
1212
const autoprefixer = require('autoprefixer');
1313
const ExtractTextPlugin = require('extract-text-webpack-plugin');
1414
const postcssImports = require('postcss-import');
15+
const PostcssCliResources = require('../../plugins/webpack').PostcssCliResources;
1516

1617
/**
1718
* Enumerate loaders and their dependencies from this file to let the dependency validator
1819
* know they are used.
1920
*
20-
* require('exports-loader')
2121
* require('style-loader')
2222
* require('postcss-loader')
23-
* require('css-loader')
2423
* require('stylus')
2524
* require('stylus-loader')
2625
* require('less')
@@ -48,6 +47,8 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
4847
const maximumInlineSize = 10;
4948
// Minify/optimize css in production.
5049
const minimizeCss = buildOptions.target === 'production';
50+
// determine hashing format
51+
const hashFormat = getOutputHashFormat(buildOptions.outputHashing);
5152
// Convert absolute resource URLs to account for base-href and deploy-url.
5253
const baseHref = wco.buildOptions.baseHref || '';
5354
const deployUrl = wco.buildOptions.deployUrl || '';
@@ -122,21 +123,26 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
122123
},
123124
{ url: 'rebase' },
124125
]),
126+
PostcssCliResources({
127+
deployUrl: loader.loaders[loader.loaderIndex].options.ident == 'extracted' ? '' : deployUrl,
128+
loader,
129+
filename: `[name]${hashFormat.file}.[ext]`,
130+
}),
125131
autoprefixer({ grid: true }),
126132
];
127133
};
128134
(postcssPluginCreator as any)[postcssArgs] = {
135+
imports: {
136+
'@angular/cli/plugins/webpack': 'PostcssCliResources',
137+
},
129138
variableImports: {
130139
'autoprefixer': 'autoprefixer',
131140
'postcss-url': 'postcssUrl',
132141
'postcss-import': 'postcssImports',
133142
},
134-
variables: { minimizeCss, baseHref, deployUrl, projectRoot, maximumInlineSize }
143+
variables: { hashFormat, baseHref, deployUrl, projectRoot, maximumInlineSize }
135144
};
136145

137-
// determine hashing format
138-
const hashFormat = getOutputHashFormat(buildOptions.outputHashing);
139-
140146
// use includePaths from appConfig
141147
const includePaths: string[] = [];
142148
let lessPathOptions: { paths: string[] };
@@ -198,29 +204,21 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
198204
];
199205

200206
const commonLoaders: webpack.Loader[] = [
201-
{
202-
loader: 'css-loader',
203-
options: {
204-
sourceMap: cssSourceMap,
205-
import: false,
206-
}
207-
},
208-
{
209-
loader: 'postcss-loader',
210-
options: {
211-
// A non-function property is required to workaround a webpack option handling bug
212-
ident: 'postcss',
213-
plugins: postcssPluginCreator,
214-
sourceMap: cssSourceMap
215-
}
216-
}
207+
{ loader: 'raw-loader' },
217208
];
218209

219210
// load component css as raw strings
220211
const rules: webpack.Rule[] = baseRules.map(({test, use}) => ({
221212
exclude: globalStylePaths, test, use: [
222-
'exports-loader?module.exports.toString()',
223213
...commonLoaders,
214+
{
215+
loader: 'postcss-loader',
216+
options: {
217+
ident: 'embedded',
218+
plugins: postcssPluginCreator,
219+
sourceMap: cssSourceMap
220+
}
221+
},
224222
...(use as webpack.Loader[])
225223
]
226224
}));
@@ -231,6 +229,14 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
231229
const extractTextPlugin = {
232230
use: [
233231
...commonLoaders,
232+
{
233+
loader: 'postcss-loader',
234+
options: {
235+
ident: buildOptions.extractCss ? 'extracted' : 'embedded',
236+
plugins: postcssPluginCreator,
237+
sourceMap: cssSourceMap
238+
}
239+
},
234240
...(use as webpack.Loader[])
235241
],
236242
// publicPath needed as a workaround https://github.com/angular/angular-cli/issues/4035

packages/@angular/cli/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,8 @@
4242
"common-tags": "^1.3.1",
4343
"copy-webpack-plugin": "^4.1.1",
4444
"core-object": "^3.1.0",
45-
"css-loader": "^0.28.1",
4645
"denodeify": "^1.2.1",
4746
"ember-cli-string-utils": "^1.0.0",
48-
"exports-loader": "^0.6.3",
4947
"extract-text-webpack-plugin": "^3.0.2",
5048
"file-loader": "^1.1.5",
5149
"fs-extra": "^4.0.0",
@@ -63,6 +61,7 @@
6361
"nopt": "^4.0.1",
6462
"opn": "~5.1.0",
6563
"portfinder": "~1.0.12",
64+
"postcss": "^6.0.16",
6665
"postcss-import": "^11.0.0",
6766
"postcss-loader": "^2.0.10",
6867
"postcss-url": "^7.1.2",
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { interpolateName } from 'loader-utils';
9+
import * as postcss from 'postcss';
10+
import * as url from 'url';
11+
import * as webpack from 'webpack';
12+
13+
function wrapUrl(url: string): string {
14+
let wrappedUrl;
15+
const hasSingleQuotes = url.indexOf('\'') >= 0;
16+
17+
if (hasSingleQuotes) {
18+
wrappedUrl = `"${url}"`;
19+
} else {
20+
wrappedUrl = `'${url}'`;
21+
}
22+
23+
return `url(${wrappedUrl})`;
24+
}
25+
26+
export interface PostcssCliResourcesOptions {
27+
deployUrl?: string;
28+
filename: string;
29+
loader: webpack.loader.LoaderContext;
30+
}
31+
32+
async function resolve(
33+
file: string,
34+
base: string,
35+
resolver: (file: string, base: string) => Promise<string>
36+
): Promise<string> {
37+
try {
38+
return await resolver('./' + file, base);
39+
} catch (err) {
40+
return resolver(file, base);
41+
}
42+
}
43+
44+
export default postcss.plugin('postcss-cli-resources', (options: PostcssCliResourcesOptions) => {
45+
const { deployUrl, filename, loader } = options;
46+
47+
const process = async (inputUrl: string, resourceCache: Map<string, string>) => {
48+
// If root-relative or absolute, leave as is
49+
if (inputUrl.match(/^(?:\w+:\/\/|data:|chrome:|#|\/)/)) {
50+
return inputUrl;
51+
}
52+
// If starts with a caret, remove and return remainder
53+
// this supports bypassing asset processing
54+
if (inputUrl.startsWith('^')) {
55+
return inputUrl.substr(1);
56+
}
57+
58+
const cachedUrl = resourceCache.get(inputUrl);
59+
if (cachedUrl) {
60+
return cachedUrl;
61+
}
62+
63+
const { pathname, hash, search } = url.parse(inputUrl.replace(/\\/g, '/'));
64+
const resolver = (file: string, base: string) => new Promise<string>((resolve, reject) => {
65+
loader.resolve(base, file, (err, result) => {
66+
if (err) {
67+
reject(err);
68+
return;
69+
}
70+
resolve(result);
71+
});
72+
});
73+
74+
const result = await resolve(pathname, loader.context, resolver);
75+
76+
return new Promise<string>((resolve, reject) => {
77+
loader.fs.readFile(result, (err: Error, data: Buffer) => {
78+
if (err) {
79+
reject(err);
80+
return;
81+
}
82+
83+
const content = data.toString();
84+
const outputPath = interpolateName(
85+
{ resourcePath: result } as webpack.loader.LoaderContext,
86+
filename,
87+
{ content },
88+
);
89+
90+
loader.addDependency(result);
91+
loader.emitFile(outputPath, content, undefined);
92+
93+
let outputUrl = outputPath.replace(/\\/g, '/');
94+
if (hash || search) {
95+
outputUrl = url.format({ pathname: outputUrl, hash, search });
96+
}
97+
98+
if (deployUrl) {
99+
outputUrl = url.resolve(deployUrl, outputUrl);
100+
}
101+
102+
resourceCache.set(inputUrl, outputUrl);
103+
104+
resolve(outputUrl);
105+
});
106+
});
107+
};
108+
109+
return (root) => {
110+
const resourceCache = new Map<string, string>();
111+
112+
return root.walkDecls(async decl => {
113+
const value = decl.value;
114+
115+
if (!value || value.indexOf('url') === -1) {
116+
return;
117+
}
118+
119+
const urlRegex = /url\(\s*['"]?([ \S]+?)['"]??\s*\)/g;
120+
const segments: string[] = [];
121+
122+
let match;
123+
let lastIndex = 0;
124+
let modified = false;
125+
// tslint:disable-next-line:no-conditional-assignment
126+
while (match = urlRegex.exec(value)) {
127+
let processedUrl;
128+
try {
129+
processedUrl = await process(match[1], resourceCache);
130+
} catch (err) {
131+
loader.emitError(decl.error(err.message, { word: match[1] }).toString());
132+
continue;
133+
}
134+
135+
if (lastIndex !== match.index) {
136+
segments.push(value.slice(lastIndex, match.index));
137+
}
138+
139+
if (!processedUrl || match[1] === processedUrl) {
140+
segments.push(match[0]);
141+
} else {
142+
segments.push(wrapUrl(processedUrl));
143+
modified = true;
144+
}
145+
146+
lastIndex = urlRegex.lastIndex;
147+
}
148+
149+
if (modified) {
150+
decl.value = segments.join('');
151+
}
152+
});
153+
};
154+
});

packages/@angular/cli/plugins/webpack.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ export { BundleBudgetPlugin, BundleBudgetPluginOptions } from './bundle-budget';
66
export { NamedLazyChunksWebpackPlugin } from './named-lazy-chunks-webpack-plugin';
77
export { ScriptsWebpackPlugin, ScriptsWebpackPluginOptions } from './scripts-webpack-plugin';
88
export { SuppressExtractedTextChunksWebpackPlugin } from './suppress-entry-chunks-webpack-plugin';
9+
export {
10+
default as PostcssCliResources,
11+
PostcssCliResourcesOptions,
12+
} from './postcss-cli-resources';

packages/@angular/cli/tasks/eject.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,12 @@ class JsonWebpackSerializer {
323323
if (loader.loader) {
324324
loader.loader = this._loaderReplacer(loader.loader);
325325
}
326-
if (loader.loader === 'postcss-loader' && !this._postcssProcessed) {
327-
const args: any = loader.options.plugins[postcssArgs];
326+
if (loader.loader === 'postcss-loader') {
327+
if (!this._postcssProcessed) {
328+
const args: any = loader.options.plugins[postcssArgs];
328329

330+
Object.keys(args.imports)
331+
.forEach(key => this._addImport(key, args.imports[key]));
329332
Object.keys(args.variableImports)
330333
.forEach(key => this.variableImports[key] = args.variableImports[key]);
331334
Object.keys(args.variables)
@@ -341,10 +344,12 @@ class JsonWebpackSerializer {
341344
}
342345
});
343346

344-
this.variables['postcssPlugins'] = loader.options.plugins;
345-
loader.options.plugins = this._escape('postcssPlugins');
347+
this.variables['postcssPlugins'] = loader.options.plugins;
348+
349+
this._postcssProcessed = true;
350+
}
346351

347-
this._postcssProcessed = true;
352+
loader.options.plugins = this._escape('postcssPlugins');
348353
}
349354
}
350355
return loader;
@@ -368,13 +373,16 @@ class JsonWebpackSerializer {
368373
};
369374

370375
if (value[pluginArgs]) {
376+
const options = value[pluginArgs];
377+
options.use = options.use.map((loader: any) => this._loaderReplacer(loader));
378+
371379
return {
372380
include: Array.isArray(value.include)
373381
? value.include.map((x: any) => replaceExcludeInclude(x))
374382
: replaceExcludeInclude(value.include),
375383
test: this._serializeRegExp(value.test),
376384
loaders: this._escape(
377-
`ExtractTextPlugin.extract(${JSON.stringify(value[pluginArgs], null, 2)})`)
385+
`ExtractTextPlugin.extract(${JSON.stringify(options, null, 2)})`)
378386
};
379387
}
380388

0 commit comments

Comments
 (0)