Skip to content

Commit e5ef996

Browse files
authored
feat(build): add style paths (angular#4003)
Add `paths/includePaths` functionality for `sass` and `stylus`. Similar functionality for less is blocked by webpack-contrib/less-loader#75. To add paths, use the new entry in `angular-cli.json` app object: ``` "stylePreprocessorOptions": { "includePaths": [ "style-paths" ] }, ``` Fix angular#1791
1 parent 48d1e44 commit e5ef996

10 files changed

+263
-146
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
"walk-sync": "^0.2.6",
116116
"webpack": "2.2.0",
117117
"webpack-dev-server": "2.2.0-rc.0",
118-
"webpack-merge": "^0.14.0",
118+
"webpack-merge": "^2.4.0",
119119
"webpack-sources": "^0.1.3",
120120
"zone.js": "^0.7.2"
121121
},

packages/angular-cli/lib/config/schema.json

+15
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@
8989
},
9090
"additionalProperties": false
9191
},
92+
"stylePreprocessorOptions": {
93+
"description": "Options to pass to style preprocessors",
94+
"type": "object",
95+
"properties": {
96+
"includePaths": {
97+
"description": "Paths to include. Paths will be resolved to project root.",
98+
"type": "array",
99+
"items": {
100+
"type": "string"
101+
},
102+
"default": []
103+
}
104+
},
105+
"additionalProperties": false
106+
},
92107
"scripts": {
93108
"description": "Global scripts to be included in the build.",
94109
"type": "array",

packages/angular-cli/models/webpack-build-common.ts

+18-67
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import * as webpack from 'webpack';
22
import * as path from 'path';
33
import { GlobCopyWebpackPlugin } from '../plugins/glob-copy-webpack-plugin';
4-
import { SuppressEntryChunksWebpackPlugin } from '../plugins/suppress-entry-chunks-webpack-plugin';
54
import { packageChunkSort } from '../utilities/package-chunk-sort';
65
import { BaseHrefWebpackPlugin } from '@angular-cli/base-href-webpack';
7-
import { extraEntryParser, makeCssLoaders, getOutputHashFormat } from './webpack-build-utils';
6+
import { extraEntryParser, lazyChunksFilter, getOutputHashFormat } from './webpack-build-utils';
87

98
const autoprefixer = require('autoprefixer');
109
const ProgressPlugin = require('webpack/lib/ProgressPlugin');
@@ -33,19 +32,22 @@ export function getWebpackCommonConfig(
3332
vendorChunk: boolean,
3433
verbose: boolean,
3534
progress: boolean,
36-
outputHashing: string,
37-
extractCss: boolean,
35+
outputHashing: string
3836
) {
3937

4038
const appRoot = path.resolve(projectRoot, appConfig.root);
4139
const nodeModules = path.resolve(projectRoot, 'node_modules');
4240

4341
let extraPlugins: any[] = [];
4442
let extraRules: any[] = [];
45-
let lazyChunks: string[] = [];
46-
4743
let entryPoints: { [key: string]: string[] } = {};
4844

45+
// figure out which are the lazy loaded entry points
46+
const lazyChunks = lazyChunksFilter([
47+
...extraEntryParser(appConfig.scripts, appRoot, 'scripts'),
48+
...extraEntryParser(appConfig.styles, appRoot, 'styles')
49+
]);
50+
4951
if (appConfig.main) {
5052
entryPoints['main'] = [path.resolve(appRoot, appConfig.main)];
5153
}
@@ -54,51 +56,22 @@ export function getWebpackCommonConfig(
5456
const hashFormat = getOutputHashFormat(outputHashing);
5557

5658
// process global scripts
57-
if (appConfig.scripts && appConfig.scripts.length > 0) {
59+
if (appConfig.scripts.length > 0) {
5860
const globalScripts = extraEntryParser(appConfig.scripts, appRoot, 'scripts');
5961

60-
// add entry points and lazy chunks
61-
globalScripts.forEach(script => {
62-
if (script.lazy) { lazyChunks.push(script.entry); }
63-
entryPoints[script.entry] = (entryPoints[script.entry] || []).concat(script.path);
64-
});
62+
// add script entry points
63+
globalScripts.forEach(script =>
64+
entryPoints[script.entry]
65+
? entryPoints[script.entry].push(script.path)
66+
: entryPoints[script.entry] = [script.path]
67+
);
6568

6669
// load global scripts using script-loader
6770
extraRules.push({
6871
include: globalScripts.map((script) => script.path), test: /\.js$/, loader: 'script-loader'
6972
});
7073
}
7174

72-
// process global styles
73-
if (!appConfig.styles || appConfig.styles.length === 0) {
74-
// create css loaders for component css
75-
extraRules.push(...makeCssLoaders());
76-
} else {
77-
const globalStyles = extraEntryParser(appConfig.styles, appRoot, 'styles');
78-
let extractedCssEntryPoints: string[] = [];
79-
// add entry points and lazy chunks
80-
globalStyles.forEach(style => {
81-
if (style.lazy) { lazyChunks.push(style.entry); }
82-
if (!entryPoints[style.entry]) {
83-
// since this entry point doesn't exist yet, it's going to only have
84-
// extracted css and we can supress the entry point
85-
extractedCssEntryPoints.push(style.entry);
86-
entryPoints[style.entry] = (entryPoints[style.entry] || []).concat(style.path);
87-
} else {
88-
// existing entry point, just push the css in
89-
entryPoints[style.entry].push(style.path);
90-
}
91-
});
92-
93-
// create css loaders for component css and for global css
94-
extraRules.push(...makeCssLoaders(globalStyles.map((style) => style.path)));
95-
96-
if (extractCss && extractedCssEntryPoints.length > 0) {
97-
// don't emit the .js entry point for extracted styles
98-
extraPlugins.push(new SuppressEntryChunksWebpackPlugin({ chunks: extractedCssEntryPoints }));
99-
}
100-
}
101-
10275
if (vendorChunk) {
10376
extraPlugins.push(new webpack.optimize.CommonsChunkPlugin({
10477
name: 'vendor',
@@ -157,26 +130,16 @@ export function getWebpackCommonConfig(
157130
module: {
158131
rules: [
159132
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', exclude: [nodeModules] },
160-
161133
{ test: /\.json$/, loader: 'json-loader' },
162-
{
163-
test: /\.(jpg|png|gif)$/,
164-
loader: `url-loader?name=[name]${hashFormat.file}.[ext]&limit=10000`
165-
},
166134
{ test: /\.html$/, loader: 'raw-loader' },
167-
135+
{ test: /\.(eot|svg)$/, loader: `file-loader?name=[name]${hashFormat.file}.[ext]` },
168136
{
169-
test: /\.(otf|ttf|woff|woff2)$/,
137+
test: /\.(jpg|png|gif|otf|ttf|woff|woff2)$/,
170138
loader: `url-loader?name=[name]${hashFormat.file}.[ext]&limit=10000`
171-
},
172-
{ test: /\.(eot|svg)$/, loader: `file-loader?name=[name]${hashFormat.file}.[ext]` }
139+
}
173140
].concat(extraRules)
174141
},
175142
plugins: [
176-
new ExtractTextPlugin({
177-
filename: `[name]${hashFormat.extract}.bundle.css`,
178-
disable: !extractCss
179-
}),
180143
new HtmlWebpackPlugin({
181144
template: path.resolve(appRoot, appConfig.index),
182145
filename: path.resolve(appConfig.outDir, appConfig.index),
@@ -190,18 +153,6 @@ export function getWebpackCommonConfig(
190153
new webpack.optimize.CommonsChunkPlugin({
191154
minChunks: Infinity,
192155
name: 'inline'
193-
}),
194-
new webpack.LoaderOptionsPlugin({
195-
test: /\.(css|scss|sass|less|styl)$/,
196-
options: {
197-
postcss: [autoprefixer()],
198-
cssLoader: { sourceMap: sourcemap },
199-
sassLoader: { sourceMap: sourcemap },
200-
lessLoader: { sourceMap: sourcemap },
201-
stylusLoader: { sourceMap: sourcemap },
202-
// context needed as a workaround https://github.com/jtangelder/sass-loader/issues/285
203-
context: projectRoot,
204-
},
205156
})
206157
].concat(extraPlugins),
207158
node: {

packages/angular-cli/models/webpack-build-production.ts

+1-18
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import * as path from 'path';
22
import * as webpack from 'webpack';
33
import {CompressionPlugin} from '../lib/webpack/compression-plugin';
4-
const autoprefixer = require('autoprefixer');
5-
const postcssDiscardComments = require('postcss-discard-comments');
4+
65

76
export const getWebpackProdConfigPartial = function(projectRoot: string,
87
appConfig: any,
@@ -26,22 +25,6 @@ export const getWebpackProdConfigPartial = function(projectRoot: string,
2625
algorithm: 'gzip',
2726
test: /\.js$|\.html$|\.css$/,
2827
threshold: 10240
29-
}),
30-
// LoaderOptionsPlugin needs to be fully duplicated because webpackMerge will replace it.
31-
new webpack.LoaderOptionsPlugin({
32-
test: /\.(css|scss|sass|less|styl)$/,
33-
options: {
34-
postcss: [
35-
autoprefixer(),
36-
postcssDiscardComments
37-
],
38-
cssLoader: { sourceMap: sourcemap },
39-
sassLoader: { sourceMap: sourcemap },
40-
lessLoader: { sourceMap: sourcemap },
41-
stylusLoader: { sourceMap: sourcemap },
42-
// context needed as a workaround https://github.com/jtangelder/sass-loader/issues/285
43-
context: projectRoot,
44-
}
4528
})
4629
]
4730
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import * as webpack from 'webpack';
2+
import * as path from 'path';
3+
import {
4+
SuppressExtractedTextChunksWebpackPlugin
5+
} from '../plugins/suppress-entry-chunks-webpack-plugin';
6+
import { extraEntryParser, getOutputHashFormat } from './webpack-build-utils';
7+
8+
const postcssDiscardComments = require('postcss-discard-comments');
9+
const autoprefixer = require('autoprefixer');
10+
const ExtractTextPlugin = require('extract-text-webpack-plugin');
11+
12+
/**
13+
* Enumerate loaders and their dependencies from this file to let the dependency validator
14+
* know they are used.
15+
*
16+
* require('raw-loader')
17+
* require('style-loader')
18+
* require('postcss-loader')
19+
* require('css-loader')
20+
* require('stylus')
21+
* require('stylus-loader')
22+
* require('less')
23+
* require('less-loader')
24+
* require('node-sass')
25+
* require('sass-loader')
26+
*/
27+
28+
export function getWebpackStylesConfig(
29+
projectRoot: string,
30+
appConfig: any,
31+
target: string,
32+
sourcemap: boolean,
33+
outputHashing: string,
34+
extractCss: boolean,
35+
) {
36+
37+
const appRoot = path.resolve(projectRoot, appConfig.root);
38+
const entryPoints: { [key: string]: string[] } = {};
39+
const globalStylePaths: string[] = [];
40+
const extraPlugins: any[] = [];
41+
42+
// discard comments in production
43+
const extraPostCssPlugins = target === 'production' ? [postcssDiscardComments] : [];
44+
45+
// determine hashing format
46+
const hashFormat = getOutputHashFormat(outputHashing);
47+
48+
// use includePaths from appConfig
49+
const includePaths: string [] = [];
50+
51+
if (appConfig.stylePreprocessorOptions
52+
&& appConfig.stylePreprocessorOptions.includePaths
53+
&& appConfig.stylePreprocessorOptions.includePaths.length > 0
54+
) {
55+
appConfig.stylePreprocessorOptions.includePaths.forEach((includePath: string) =>
56+
includePaths.push(path.resolve(appRoot, includePath)));
57+
}
58+
59+
// process global styles
60+
if (appConfig.styles.length > 0) {
61+
const globalStyles = extraEntryParser(appConfig.styles, appRoot, 'styles');
62+
// add style entry points
63+
globalStyles.forEach(style =>
64+
entryPoints[style.entry]
65+
? entryPoints[style.entry].push(style.path)
66+
: entryPoints[style.entry] = [style.path]
67+
);
68+
// add global css paths
69+
globalStylePaths.push(...globalStyles.map((style) => style.path));
70+
}
71+
72+
// set base rules to derive final rules from
73+
const baseRules = [
74+
{ test: /\.css$/, loaders: [] },
75+
{ test: /\.scss$|\.sass$/, loaders: ['sass-loader'] },
76+
{ test: /\.less$/, loaders: ['less-loader'] },
77+
// stylus-loader doesn't support webpack.LoaderOptionsPlugin properly,
78+
// so we need to add options in it's query
79+
{ test: /\.styl$/, loaders: [`stylus-loader?${JSON.stringify({
80+
sourceMap: sourcemap,
81+
paths: includePaths
82+
})}`] }
83+
];
84+
85+
const commonLoaders = ['postcss-loader'];
86+
87+
// load component css as raw strings
88+
let rules: any = baseRules.map(({test, loaders}) => ({
89+
exclude: globalStylePaths, test, loaders: ['raw-loader', ...commonLoaders, ...loaders]
90+
}));
91+
92+
// load global css as css files
93+
if (globalStylePaths.length > 0) {
94+
rules.push(...baseRules.map(({test, loaders}) => ({
95+
include: globalStylePaths, test, loaders: ExtractTextPlugin.extract({
96+
remove: false,
97+
loader: ['css-loader', ...commonLoaders, ...loaders],
98+
fallbackLoader: 'style-loader',
99+
// publicPath needed as a workaround https://github.com/angular/angular-cli/issues/4035
100+
publicPath: ''
101+
})
102+
})));
103+
}
104+
105+
// supress empty .js files in css only entry points
106+
if (extractCss) {
107+
extraPlugins.push(new SuppressExtractedTextChunksWebpackPlugin());
108+
}
109+
110+
return {
111+
entry: entryPoints,
112+
module: { rules },
113+
plugins: [
114+
// extract global css from js files into own css file
115+
new ExtractTextPlugin({
116+
filename: `[name]${hashFormat.extract}.bundle.css`,
117+
disable: !extractCss
118+
}),
119+
new webpack.LoaderOptionsPlugin({
120+
options: {
121+
postcss: [autoprefixer()].concat(extraPostCssPlugins),
122+
cssLoader: { sourceMap: sourcemap },
123+
sassLoader: { sourceMap: sourcemap, includePaths },
124+
// less-loader doesn't support paths
125+
lessLoader: { sourceMap: sourcemap },
126+
// stylus-loader doesn't support LoaderOptionsPlugin properly, options in query instead
127+
// context needed as a workaround https://github.com/jtangelder/sass-loader/issues/285
128+
context: projectRoot,
129+
},
130+
})
131+
].concat(extraPlugins)
132+
};
133+
}

0 commit comments

Comments
 (0)