Skip to content

Commit 20bb864

Browse files
authored
feat(build): add lazy styles/scripts (#3402)
Close #3401 Close #3400
1 parent c46de15 commit 20bb864

20 files changed

+532
-255
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
"resolve": "^1.1.7",
104104
"rimraf": "^2.5.3",
105105
"rsvp": "^3.0.17",
106-
"sass-loader": "^3.2.0",
106+
"sass-loader": "^4.0.1",
107107
"script-loader": "^0.7.0",
108108
"semver": "^5.1.0",
109109
"silent-error": "^1.0.0",

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

+39-7
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,17 @@
3232
"default": "dist/"
3333
},
3434
"assets": {
35-
"fixme": true,
36-
"type": "array",
37-
"items": {
38-
"type": "string"
39-
},
35+
"oneOf": [
36+
{
37+
"type": "string"
38+
},
39+
{
40+
"type": "array",
41+
"items": {
42+
"type": "string"
43+
}
44+
}
45+
],
4046
"default": []
4147
},
4248
"index": {
@@ -62,15 +68,41 @@
6268
"description": "Global styles to be included in the build.",
6369
"type": "array",
6470
"items": {
65-
"type": "string"
71+
"oneOf": [
72+
{
73+
"type": "string"
74+
},
75+
{
76+
"type": "object",
77+
"properties": {
78+
"input": {
79+
"type": "string"
80+
}
81+
},
82+
"additionalProperties": true
83+
}
84+
]
6685
},
6786
"additionalProperties": false
6887
},
6988
"scripts": {
7089
"description": "Global scripts to be included in the build.",
7190
"type": "array",
7291
"items": {
73-
"type": "string"
92+
"oneOf": [
93+
{
94+
"type": "string"
95+
},
96+
{
97+
"type": "object",
98+
"properties": {
99+
"input": {
100+
"type": "string"
101+
}
102+
},
103+
"additionalProperties": true
104+
}
105+
]
74106
},
75107
"additionalProperties": false
76108
},

packages/angular-cli/models/json-schema/schema-class-factory.ts

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ function _parseJsonPath(path: string): string[] {
4646
function _getSchemaNodeForPath<T>(rootMetaData: SchemaTreeNode<T>,
4747
path: string): SchemaTreeNode<any> {
4848
let fragments = _parseJsonPath(path);
49+
// TODO: make this work with union (oneOf) schemas
4950
return fragments.reduce((md: SchemaTreeNode<any>, current: string) => {
5051
return md && md.children && md.children[current];
5152
}, rootMetaData);

packages/angular-cli/models/json-schema/schema-tree.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,21 @@ export abstract class NonLeafSchemaTreeNode<T> extends SchemaTreeNode<T> {
129129
protected _createChildProperty<T>(name: string, value: T, forward: SchemaTreeNode<T>,
130130
schema: Schema, define = true): SchemaTreeNode<T> {
131131

132-
// TODO: fix this
133-
if (schema['fixme'] && typeof value === 'string') {
134-
value = <T>(<any>[ value ]);
132+
let type: string;
133+
134+
if (!schema['oneOf']) {
135+
type = schema['type'];
136+
} else {
137+
for (let testSchema of schema['oneOf']) {
138+
if ((testSchema['type'] === 'array' && Array.isArray(value))
139+
|| typeof value === testSchema['type']) {
140+
type = testSchema['type'];
141+
schema = testSchema;
142+
break;
143+
}
144+
}
135145
}
136146

137-
const type = schema['type'];
138147
let Klass: any = null;
139148

140149
switch (type) {

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

+76-70
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import * as webpack from 'webpack';
22
import * as path from 'path';
3-
import {GlobCopyWebpackPlugin} from '../plugins/glob-copy-webpack-plugin';
4-
import {packageChunkSort} from '../utilities/package-chunk-sort';
5-
import {BaseHrefWebpackPlugin} from '@angular-cli/base-href-webpack';
3+
import { GlobCopyWebpackPlugin } from '../plugins/glob-copy-webpack-plugin';
4+
import { SuppressEntryChunksWebpackPlugin } from '../plugins/suppress-entry-chunks-webpack-plugin';
5+
import { packageChunkSort } from '../utilities/package-chunk-sort';
6+
import { BaseHrefWebpackPlugin } from '@angular-cli/base-href-webpack';
7+
import { extraEntryParser, makeCssLoaders } from './webpack-build-utils';
68

7-
const ProgressPlugin = require('webpack/lib/ProgressPlugin');
9+
const autoprefixer = require('autoprefixer');
10+
const ProgressPlugin = require('webpack/lib/ProgressPlugin');
811
const HtmlWebpackPlugin = require('html-webpack-plugin');
912
const SilentError = require('silent-error');
1013

@@ -14,21 +17,12 @@ const SilentError = require('silent-error');
1417
*
1518
* require('source-map-loader')
1619
* require('raw-loader')
17-
* require('postcss-loader')
18-
* require('stylus-loader')
19-
* require('less-loader')
20-
* require('sass-loader')
2120
* require('script-loader')
2221
* require('json-loader')
2322
* require('url-loader')
2423
* require('file-loader')
25-
*
26-
* require('node-sass')
27-
* require('less')
28-
* require('stylus')
2924
*/
3025

31-
3226
export function getWebpackCommonConfig(
3327
projectRoot: string,
3428
environment: string,
@@ -43,25 +37,64 @@ export function getWebpackCommonConfig(
4337
const appRoot = path.resolve(projectRoot, appConfig.root);
4438
const appMain = path.resolve(appRoot, appConfig.main);
4539
const nodeModules = path.resolve(projectRoot, 'node_modules');
46-
const styles = appConfig.styles
47-
? appConfig.styles.map((style: string) => path.resolve(appRoot, style))
48-
: [];
49-
const scripts = appConfig.scripts
50-
? appConfig.scripts.map((script: string) => path.resolve(appRoot, script))
51-
: [];
52-
const extraPlugins: any[] = [];
53-
54-
let entry: { [key: string]: string[] } = {
40+
41+
let extraPlugins: any[] = [];
42+
let extraRules: any[] = [];
43+
let lazyChunks: string[] = [];
44+
45+
let entryPoints: { [key: string]: string[] } = {
5546
main: [appMain]
5647
};
5748

5849
if (!(environment in appConfig.environments)) {
5950
throw new SilentError(`Environment "${environment}" does not exist.`);
6051
}
6152

62-
// Only add styles/scripts if there's actually entries there
63-
if (appConfig.styles.length > 0) { entry['styles'] = styles; }
64-
if (appConfig.scripts.length > 0) { entry['scripts'] = scripts; }
53+
// process global scripts
54+
if (appConfig.scripts.length > 0) {
55+
const globalScrips = extraEntryParser(appConfig.scripts, appRoot, 'scripts');
56+
57+
// add entry points and lazy chunks
58+
globalScrips.forEach(script => {
59+
if (script.lazy) { lazyChunks.push(script.entry); }
60+
entryPoints[script.entry] = (entryPoints[script.entry] || []).concat(script.path);
61+
});
62+
63+
// load global scripts using script-loader
64+
extraRules.push({
65+
include: globalScrips.map((script) => script.path), test: /\.js$/, loader: 'script-loader'
66+
});
67+
}
68+
69+
// process global styles
70+
if (appConfig.styles.length === 0) {
71+
// create css loaders for component css
72+
extraRules.push(...makeCssLoaders());
73+
} else {
74+
const globalStyles = extraEntryParser(appConfig.styles, appRoot, 'styles');
75+
let extractedCssEntryPoints: string[] = [];
76+
// add entry points and lazy chunks
77+
globalStyles.forEach(style => {
78+
if (style.lazy) { lazyChunks.push(style.entry); }
79+
if (!entryPoints[style.entry]) {
80+
// since this entry point doesn't exist yet, it's going to only have
81+
// extracted css and we can supress the entry point
82+
extractedCssEntryPoints.push(style.entry);
83+
entryPoints[style.entry] = (entryPoints[style.entry] || []).concat(style.path);
84+
} else {
85+
// existing entry point, just push the css in
86+
entryPoints[style.entry].push(style.path);
87+
}
88+
});
89+
90+
// create css loaders for component css and for global css
91+
extraRules.push(...makeCssLoaders(globalStyles.map((style) => style.path)));
92+
93+
if (extractedCssEntryPoints.length > 0) {
94+
// don't emit the .js entry point for extracted styles
95+
extraPlugins.push(new SuppressEntryChunksWebpackPlugin({ chunks: extractedCssEntryPoints }));
96+
}
97+
}
6598

6699
if (vendorChunk) {
67100
extraPlugins.push(new webpack.optimize.CommonsChunkPlugin({
@@ -71,12 +104,7 @@ export function getWebpackCommonConfig(
71104
}));
72105
}
73106

74-
if (progress) {
75-
extraPlugins.push(new ProgressPlugin({
76-
profile: verbose,
77-
colors: true
78-
}));
79-
}
107+
if (progress) { extraPlugins.push(new ProgressPlugin({ profile: verbose, colors: true })); }
80108

81109
return {
82110
devtool: sourcemap ? 'source-map' : false,
@@ -85,10 +113,10 @@ export function getWebpackCommonConfig(
85113
modules: [nodeModules],
86114
},
87115
resolveLoader: {
88-
modules: [path.resolve(projectRoot, 'node_modules')]
116+
modules: [nodeModules]
89117
},
90118
context: projectRoot,
91-
entry: entry,
119+
entry: entryPoints,
92120
output: {
93121
path: path.resolve(projectRoot, appConfig.outDir),
94122
filename: '[name].bundle.js',
@@ -97,48 +125,22 @@ export function getWebpackCommonConfig(
97125
},
98126
module: {
99127
rules: [
100-
{
101-
enforce: 'pre',
102-
test: /\.js$/,
103-
loader: 'source-map-loader',
104-
exclude: [ nodeModules ]
105-
},
106-
// in main, load css as raw text
107-
       {
108-
exclude: styles,
109-
test: /\.css$/,
110-
loaders: ['raw-loader', 'postcss-loader']
111-
}, {
112-
exclude: styles,
113-
test: /\.styl$/,
114-
loaders: ['raw-loader', 'postcss-loader', 'stylus-loader'] },
115-
       {
116-
exclude: styles,
117-
test: /\.less$/,
118-
loaders: ['raw-loader', 'postcss-loader', 'less-loader']
119-
}, {
120-
exclude: styles,
121-
test: /\.scss$|\.sass$/,
122-
loaders: ['raw-loader', 'postcss-loader', 'sass-loader']
123-
},
124-
125-
126-
// load global scripts using script-loader
127-
{ include: scripts, test: /\.js$/, loader: 'script-loader' },
128+
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', exclude: [nodeModules] },
128129

129-
       { test: /\.json$/, loader: 'json-loader' },
130-
       { test: /\.(jpg|png|gif)$/, loader: 'url-loader?limit=10000' },
131-
       { test: /\.html$/, loader: 'raw-loader' },
130+
{ test: /\.json$/, loader: 'json-loader' },
131+
{ test: /\.(jpg|png|gif)$/, loader: 'url-loader?limit=10000' },
132+
{ test: /\.html$/, loader: 'raw-loader' },
132133

133134
{ test: /\.(otf|ttf|woff|woff2)$/, loader: 'url-loader?limit=10000' },
134135
{ test: /\.(eot|svg)$/, loader: 'file-loader' }
135-
]
136+
].concat(extraRules)
136137
},
137138
plugins: [
138139
new HtmlWebpackPlugin({
139140
template: path.resolve(appRoot, appConfig.index),
140141
filename: path.resolve(appConfig.outDir, appConfig.index),
141-
chunksSortMode: packageChunkSort(['inline', 'styles', 'scripts', 'vendor', 'main'])
142+
chunksSortMode: packageChunkSort(['inline', 'styles', 'scripts', 'vendor', 'main']),
143+
excludeChunks: lazyChunks
142144
}),
143145
new BaseHrefWebpackPlugin({
144146
baseHref: baseHref
@@ -157,14 +159,18 @@ export function getWebpackCommonConfig(
157159
}),
158160
new GlobCopyWebpackPlugin({
159161
patterns: appConfig.assets,
160-
globOptions: {cwd: appRoot, dot: true, ignore: '**/.gitkeep'}
162+
globOptions: { cwd: appRoot, dot: true, ignore: '**/.gitkeep' }
161163
}),
162164
new webpack.LoaderOptionsPlugin({
163165
test: /\.(css|scss|sass|less|styl)$/,
164166
options: {
165-
postcss: [
166-
require('autoprefixer')
167-
]
167+
postcss: [autoprefixer()],
168+
cssLoader: { sourceMap: sourcemap },
169+
sassLoader: { sourceMap: sourcemap },
170+
lessLoader: { sourceMap: sourcemap },
171+
stylusLoader: { sourceMap: sourcemap },
172+
// context needed as a workaround https://github.com/jtangelder/sass-loader/issues/285
173+
context: projectRoot,
168174
},
169175
})
170176
].concat(extraPlugins),
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,18 @@
11
const path = require('path');
2-
3-
/**
4-
* Enumerate loaders and their dependencies from this file to let the dependency validator
5-
* know they are used.
6-
*
7-
* require('style-loader')
8-
* require('css-loader')
9-
* require('stylus-loader')
10-
* require('less-loader')
11-
* require('sass-loader')
12-
*/
2+
const ExtractTextPlugin = require('extract-text-webpack-plugin');
133

144
export const getWebpackDevConfigPartial = function(projectRoot: string, appConfig: any) {
155
const appRoot = path.resolve(projectRoot, appConfig.root);
16-
const styles = appConfig.styles
17-
? appConfig.styles.map((style: string) => path.resolve(appRoot, style))
18-
: [];
19-
const cssLoaders = ['style-loader', 'css-loader?sourcemap', 'postcss-loader'];
6+
207
return {
218
output: {
229
path: path.resolve(projectRoot, appConfig.outDir),
2310
filename: '[name].bundle.js',
2411
sourceMapFilename: '[name].bundle.map',
2512
chunkFilename: '[id].chunk.js'
2613
},
27-
module: {
28-
rules: [
29-
// outside of main, load it via style-loader for development builds
30-
       {
31-
include: styles,
32-
test: /\.css$/,
33-
loaders: cssLoaders
34-
}, {
35-
include: styles,
36-
test: /\.styl$/,
37-
loaders: [...cssLoaders, 'stylus-loader?sourcemap']
38-
}, {
39-
include: styles,
40-
test: /\.less$/,
41-
loaders: [...cssLoaders, 'less-loader?sourcemap']
42-
}, {
43-
include: styles,
44-
test: /\.scss$|\.sass$/,
45-
loaders: [...cssLoaders, 'sass-loader?sourcemap']
46-
},
47-
]
48-
}
14+
plugins: [
15+
new ExtractTextPlugin({filename: '[name].bundle.css'})
16+
]
4917
};
5018
};

0 commit comments

Comments
 (0)