Skip to content
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

feat(build): add style paths #4003

Merged
merged 1 commit into from
Jan 19, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
"walk-sync": "^0.2.6",
"webpack": "2.2.0",
"webpack-dev-server": "2.2.0-rc.0",
"webpack-merge": "^0.14.0",
"webpack-merge": "^2.4.0",
"webpack-sources": "^0.1.3",
"zone.js": "^0.7.2"
},
Expand Down
15 changes: 15 additions & 0 deletions packages/angular-cli/lib/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@
},
"additionalProperties": false
},
"stylePreprocessorOptions": {
"description": "Options to pass to style preprocessors",
"type": "object",
"properties": {
"includePaths": {
"description": "Paths to include. Paths will be resolved to project root.",
"type": "array",
"items": {
"type": "string"
},
"default": []
}
},
"additionalProperties": false
},
"scripts": {
"description": "Global scripts to be included in the build.",
"type": "array",
Expand Down
85 changes: 18 additions & 67 deletions packages/angular-cli/models/webpack-build-common.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as webpack from 'webpack';
import * as path from 'path';
import { GlobCopyWebpackPlugin } from '../plugins/glob-copy-webpack-plugin';
import { SuppressEntryChunksWebpackPlugin } from '../plugins/suppress-entry-chunks-webpack-plugin';
import { packageChunkSort } from '../utilities/package-chunk-sort';
import { BaseHrefWebpackPlugin } from '@angular-cli/base-href-webpack';
import { extraEntryParser, makeCssLoaders, getOutputHashFormat } from './webpack-build-utils';
import { extraEntryParser, lazyChunksFilter, getOutputHashFormat } from './webpack-build-utils';

const autoprefixer = require('autoprefixer');
const ProgressPlugin = require('webpack/lib/ProgressPlugin');
Expand Down Expand Up @@ -33,19 +32,22 @@ export function getWebpackCommonConfig(
vendorChunk: boolean,
verbose: boolean,
progress: boolean,
outputHashing: string,
extractCss: boolean,
outputHashing: string
) {

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

let extraPlugins: any[] = [];
let extraRules: any[] = [];
let lazyChunks: string[] = [];

let entryPoints: { [key: string]: string[] } = {};

// figure out which are the lazy loaded entry points
const lazyChunks = lazyChunksFilter([
...extraEntryParser(appConfig.scripts, appRoot, 'scripts'),
...extraEntryParser(appConfig.styles, appRoot, 'styles')
]);

if (appConfig.main) {
entryPoints['main'] = [path.resolve(appRoot, appConfig.main)];
}
Expand All @@ -54,51 +56,22 @@ export function getWebpackCommonConfig(
const hashFormat = getOutputHashFormat(outputHashing);

// process global scripts
if (appConfig.scripts && appConfig.scripts.length > 0) {
if (appConfig.scripts.length > 0) {
const globalScripts = extraEntryParser(appConfig.scripts, appRoot, 'scripts');

// add entry points and lazy chunks
globalScripts.forEach(script => {
if (script.lazy) { lazyChunks.push(script.entry); }
entryPoints[script.entry] = (entryPoints[script.entry] || []).concat(script.path);
});
// add script entry points
globalScripts.forEach(script =>
entryPoints[script.entry]
? entryPoints[script.entry].push(script.path)
: entryPoints[script.entry] = [script.path]
);

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

// process global styles
if (!appConfig.styles || appConfig.styles.length === 0) {
// create css loaders for component css
extraRules.push(...makeCssLoaders());
} else {
const globalStyles = extraEntryParser(appConfig.styles, appRoot, 'styles');
let extractedCssEntryPoints: string[] = [];
// add entry points and lazy chunks
globalStyles.forEach(style => {
if (style.lazy) { lazyChunks.push(style.entry); }
if (!entryPoints[style.entry]) {
// since this entry point doesn't exist yet, it's going to only have
// extracted css and we can supress the entry point
extractedCssEntryPoints.push(style.entry);
entryPoints[style.entry] = (entryPoints[style.entry] || []).concat(style.path);
} else {
// existing entry point, just push the css in
entryPoints[style.entry].push(style.path);
}
});

// create css loaders for component css and for global css
extraRules.push(...makeCssLoaders(globalStyles.map((style) => style.path)));

if (extractCss && extractedCssEntryPoints.length > 0) {
// don't emit the .js entry point for extracted styles
extraPlugins.push(new SuppressEntryChunksWebpackPlugin({ chunks: extractedCssEntryPoints }));
}
}

if (vendorChunk) {
extraPlugins.push(new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
Expand Down Expand Up @@ -157,26 +130,16 @@ export function getWebpackCommonConfig(
module: {
rules: [
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', exclude: [nodeModules] },

{ test: /\.json$/, loader: 'json-loader' },
{
test: /\.(jpg|png|gif)$/,
loader: `url-loader?name=[name]${hashFormat.file}.[ext]&limit=10000`
},
{ test: /\.html$/, loader: 'raw-loader' },

{ test: /\.(eot|svg)$/, loader: `file-loader?name=[name]${hashFormat.file}.[ext]` },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those new loaders are not part of the issue..?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They aren't new, they were just moved up from a few lines below.

{
test: /\.(otf|ttf|woff|woff2)$/,
test: /\.(jpg|png|gif|otf|ttf|woff|woff2)$/,
loader: `url-loader?name=[name]${hashFormat.file}.[ext]&limit=10000`
},
{ test: /\.(eot|svg)$/, loader: `file-loader?name=[name]${hashFormat.file}.[ext]` }
}
].concat(extraRules)
},
plugins: [
new ExtractTextPlugin({
filename: `[name]${hashFormat.extract}.bundle.css`,
disable: !extractCss
}),
new HtmlWebpackPlugin({
template: path.resolve(appRoot, appConfig.index),
filename: path.resolve(appConfig.outDir, appConfig.index),
Expand All @@ -190,18 +153,6 @@ export function getWebpackCommonConfig(
new webpack.optimize.CommonsChunkPlugin({
minChunks: Infinity,
name: 'inline'
}),
new webpack.LoaderOptionsPlugin({
test: /\.(css|scss|sass|less|styl)$/,
options: {
postcss: [autoprefixer()],
cssLoader: { sourceMap: sourcemap },
sassLoader: { sourceMap: sourcemap },
lessLoader: { sourceMap: sourcemap },
stylusLoader: { sourceMap: sourcemap },
// context needed as a workaround https://github.com/jtangelder/sass-loader/issues/285
context: projectRoot,
},
})
].concat(extraPlugins),
node: {
Expand Down
19 changes: 1 addition & 18 deletions packages/angular-cli/models/webpack-build-production.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as path from 'path';
import * as webpack from 'webpack';
import {CompressionPlugin} from '../lib/webpack/compression-plugin';
const autoprefixer = require('autoprefixer');
const postcssDiscardComments = require('postcss-discard-comments');


export const getWebpackProdConfigPartial = function(projectRoot: string,
appConfig: any,
Expand All @@ -26,22 +25,6 @@ export const getWebpackProdConfigPartial = function(projectRoot: string,
algorithm: 'gzip',
test: /\.js$|\.html$|\.css$/,
threshold: 10240
}),
// LoaderOptionsPlugin needs to be fully duplicated because webpackMerge will replace it.
new webpack.LoaderOptionsPlugin({
test: /\.(css|scss|sass|less|styl)$/,
options: {
postcss: [
autoprefixer(),
postcssDiscardComments
],
cssLoader: { sourceMap: sourcemap },
sassLoader: { sourceMap: sourcemap },
lessLoader: { sourceMap: sourcemap },
stylusLoader: { sourceMap: sourcemap },
// context needed as a workaround https://github.com/jtangelder/sass-loader/issues/285
context: projectRoot,
}
})
]
};
Expand Down
133 changes: 133 additions & 0 deletions packages/angular-cli/models/webpack-build-styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import * as webpack from 'webpack';
import * as path from 'path';
import {
SuppressExtractedTextChunksWebpackPlugin
} from '../plugins/suppress-entry-chunks-webpack-plugin';
import { extraEntryParser, getOutputHashFormat } from './webpack-build-utils';

const postcssDiscardComments = require('postcss-discard-comments');
const autoprefixer = require('autoprefixer');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

/**
* Enumerate loaders and their dependencies from this file to let the dependency validator
* know they are used.
*
* require('raw-loader')
* require('style-loader')
* require('postcss-loader')
* require('css-loader')
* require('stylus')
* require('stylus-loader')
* require('less')
* require('less-loader')
* require('node-sass')
* require('sass-loader')
*/

export function getWebpackStylesConfig(
projectRoot: string,
appConfig: any,
target: string,
sourcemap: boolean,
outputHashing: string,
extractCss: boolean,
) {

const appRoot = path.resolve(projectRoot, appConfig.root);
const entryPoints: { [key: string]: string[] } = {};
const globalStylePaths: string[] = [];
const extraPlugins: any[] = [];

// discard comments in production
const extraPostCssPlugins = target === 'production' ? [postcssDiscardComments] : [];

// determine hashing format
const hashFormat = getOutputHashFormat(outputHashing);

// use includePaths from appConfig
const includePaths: string [] = [];

if (appConfig.stylePreprocessorOptions
&& appConfig.stylePreprocessorOptions.includePaths
&& appConfig.stylePreprocessorOptions.includePaths.length > 0
) {
appConfig.stylePreprocessorOptions.includePaths.forEach((includePath: string) =>
includePaths.push(path.resolve(appRoot, includePath)));
Copy link
Contributor

@admosity admosity Jan 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using appRoot requires me to specify my includePaths relative to the current appRoot. I guess that's not bad. Here's a sample configuration I have to do and it works 👍 :

      "stylePreprocessorOptions": {
        "includePaths": [
          "../../node_modules",
          "../src"
        ]
      }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah in general anything that's within app config is always relative to appRoot. This way it's consistent.

}

// process global styles
if (appConfig.styles.length > 0) {
const globalStyles = extraEntryParser(appConfig.styles, appRoot, 'styles');
// add style entry points
globalStyles.forEach(style =>
entryPoints[style.entry]
? entryPoints[style.entry].push(style.path)
: entryPoints[style.entry] = [style.path]
);
// add global css paths
globalStylePaths.push(...globalStyles.map((style) => style.path));
}

// set base rules to derive final rules from
const baseRules = [
{ test: /\.css$/, loaders: [] },
{ test: /\.scss$|\.sass$/, loaders: ['sass-loader'] },
{ test: /\.less$/, loaders: ['less-loader'] },
// stylus-loader doesn't support webpack.LoaderOptionsPlugin properly,
// so we need to add options in it's query
{ test: /\.styl$/, loaders: [`stylus-loader?${JSON.stringify({
sourceMap: sourcemap,
paths: includePaths
})}`] }
];

const commonLoaders = ['postcss-loader'];

// load component css as raw strings
let rules: any = baseRules.map(({test, loaders}) => ({
exclude: globalStylePaths, test, loaders: ['raw-loader', ...commonLoaders, ...loaders]
}));

// load global css as css files
if (globalStylePaths.length > 0) {
rules.push(...baseRules.map(({test, loaders}) => ({
include: globalStylePaths, test, loaders: ExtractTextPlugin.extract({
remove: false,
loader: ['css-loader', ...commonLoaders, ...loaders],
fallbackLoader: 'style-loader',
// publicPath needed as a workaround https://github.com/angular/angular-cli/issues/4035
publicPath: ''
})
})));
}

// supress empty .js files in css only entry points
if (extractCss) {
extraPlugins.push(new SuppressExtractedTextChunksWebpackPlugin());
}

return {
entry: entryPoints,
module: { rules },
plugins: [
// extract global css from js files into own css file
new ExtractTextPlugin({
filename: `[name]${hashFormat.extract}.bundle.css`,
disable: !extractCss
}),
new webpack.LoaderOptionsPlugin({
options: {
postcss: [autoprefixer()].concat(extraPostCssPlugins),
cssLoader: { sourceMap: sourcemap },
sassLoader: { sourceMap: sourcemap, includePaths },
// less-loader doesn't support paths
lessLoader: { sourceMap: sourcemap },
// stylus-loader doesn't support LoaderOptionsPlugin properly, options in query instead
// context needed as a workaround https://github.com/jtangelder/sass-loader/issues/285
context: projectRoot,
},
})
].concat(extraPlugins)
};
}