Skip to content

Commit 6e4ac2e

Browse files
committed
feat(build): add lazy styles/scripts
Close angular#3401 Close angular#3400
1 parent e6364a9 commit 6e4ac2e

20 files changed

+501
-228
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
"rimraf": "^2.5.3",
108108
"rsvp": "^3.0.17",
109109
"rxjs": "5.0.0-beta.12",
110-
"sass-loader": "^3.2.0",
110+
"sass-loader": "^4.0.1",
111111
"script-loader": "^0.7.0",
112112
"semver": "^5.1.0",
113113
"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

+74-59
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
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 ProgressPlugin = require('webpack/lib/ProgressPlugin');
810
const HtmlWebpackPlugin = require('html-webpack-plugin');
911
const autoprefixer = require('autoprefixer');
1012

11-
1213
export function getWebpackCommonConfig(
1314
projectRoot: string,
1415
environment: string,
@@ -23,21 +24,60 @@ export function getWebpackCommonConfig(
2324
const appRoot = path.resolve(projectRoot, appConfig.root);
2425
const appMain = path.resolve(appRoot, appConfig.main);
2526
const nodeModules = path.resolve(projectRoot, 'node_modules');
26-
const styles = appConfig.styles
27-
? appConfig.styles.map((style: string) => path.resolve(appRoot, style))
28-
: [];
29-
const scripts = appConfig.scripts
30-
? appConfig.scripts.map((script: string) => path.resolve(appRoot, script))
31-
: [];
32-
const extraPlugins: any[] = [];
33-
34-
let entry: { [key: string]: string[] } = {
27+
28+
let extraPlugins: any[] = [];
29+
let extraRules: any[] = [];
30+
let lazyChunks: string[] = [];
31+
32+
let entryPoints: { [key: string]: string[] } = {
3533
main: [appMain]
3634
};
3735

38-
// Only add styles/scripts if there's actually entries there
39-
if (appConfig.styles.length > 0) { entry['styles'] = styles; }
40-
if (appConfig.scripts.length > 0) { entry['scripts'] = scripts; }
36+
// process global scripts
37+
if (appConfig.scripts.length > 0) {
38+
const globalScrips = extraEntryParser(appConfig.scripts, appRoot, 'scripts');
39+
40+
// add entry points and lazy chunks
41+
globalScrips.forEach(script => {
42+
if (script.lazy) { lazyChunks.push(script.entry); }
43+
entryPoints[script.entry] = (entryPoints[script.entry] || []).concat(script.path);
44+
});
45+
46+
// load global scripts using script-loader
47+
extraRules.push({
48+
include: globalScrips.map((script) => script.path), test: /\.js$/, loader: 'script-loader'
49+
});
50+
}
51+
52+
// process global styles
53+
if (appConfig.styles.length === 0) {
54+
// create css loaders for component css
55+
extraRules.push(...makeCssLoaders());
56+
} else {
57+
const globalStyles = extraEntryParser(appConfig.styles, appRoot, 'styles');
58+
let extractedCssEntryPoints: string[] = [];
59+
// add entry points and lazy chunks
60+
globalStyles.forEach(style => {
61+
if (style.lazy) { lazyChunks.push(style.entry); }
62+
if (!entryPoints[style.entry]) {
63+
// since this entry point doesn't exist yet, it's going to only have
64+
// extracted css and we can supress the entry point
65+
extractedCssEntryPoints.push(style.entry);
66+
entryPoints[style.entry] = (entryPoints[style.entry] || []).concat(style.path);
67+
} else {
68+
// existing entry point, just push the css in
69+
entryPoints[style.entry].push(style.path);
70+
}
71+
});
72+
73+
// create css loaders for component css and for global css
74+
extraRules.push(...makeCssLoaders(globalStyles.map((style) => style.path)));
75+
76+
if (extractedCssEntryPoints.length > 0) {
77+
// don't emit the .js entry point for extracted styles
78+
extraPlugins.push(new SuppressEntryChunksWebpackPlugin({ chunks: extractedCssEntryPoints }));
79+
}
80+
}
4181

4282
if (vendorChunk) {
4383
extraPlugins.push(new webpack.optimize.CommonsChunkPlugin({
@@ -47,12 +87,7 @@ export function getWebpackCommonConfig(
4787
}));
4888
}
4989

50-
if (progress) {
51-
extraPlugins.push(new ProgressPlugin({
52-
profile: verbose,
53-
colors: true
54-
}));
55-
}
90+
if (progress) { extraPlugins.push(new ProgressPlugin({ profile: verbose, colors: true })); }
5691

5792
return {
5893
devtool: sourcemap ? 'source-map' : false,
@@ -61,7 +96,7 @@ export function getWebpackCommonConfig(
6196
modules: [nodeModules]
6297
},
6398
context: path.resolve(__dirname, './'),
64-
entry: entry,
99+
entry: entryPoints,
65100
output: {
66101
path: path.resolve(projectRoot, appConfig.outDir),
67102
filename: '[name].bundle.js',
@@ -70,48 +105,22 @@ export function getWebpackCommonConfig(
70105
},
71106
module: {
72107
rules: [
73-
{
74-
enforce: 'pre',
75-
test: /\.js$/,
76-
loader: 'source-map-loader',
77-
exclude: [ nodeModules ]
78-
},
79-
// in main, load css as raw text
80-
       {
81-
exclude: styles,
82-
test: /\.css$/,
83-
loaders: ['raw-loader', 'postcss-loader']
84-
}, {
85-
exclude: styles,
86-
test: /\.styl$/,
87-
loaders: ['raw-loader', 'postcss-loader', 'stylus-loader'] },
88-
       {
89-
exclude: styles,
90-
test: /\.less$/,
91-
loaders: ['raw-loader', 'postcss-loader', 'less-loader']
92-
}, {
93-
exclude: styles,
94-
test: /\.scss$|\.sass$/,
95-
loaders: ['raw-loader', 'postcss-loader', 'sass-loader']
96-
},
97-
98-
99-
// load global scripts using script-loader
100-
{ include: scripts, test: /\.js$/, loader: 'script-loader' },
108+
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', exclude: [nodeModules] },
101109

102-
       { test: /\.json$/, loader: 'json-loader' },
103-
       { test: /\.(jpg|png|gif)$/, loader: 'url-loader?limit=10000' },
104-
       { test: /\.html$/, loader: 'raw-loader' },
110+
{ test: /\.json$/, loader: 'json-loader' },
111+
{ test: /\.(jpg|png|gif)$/, loader: 'url-loader?limit=10000' },
112+
{ test: /\.html$/, loader: 'raw-loader' },
105113

106114
{ test: /\.(otf|ttf|woff|woff2)$/, loader: 'url-loader?limit=10000' },
107115
{ test: /\.(eot|svg)$/, loader: 'file-loader' }
108-
]
116+
].concat(extraRules)
109117
},
110118
plugins: [
111119
new HtmlWebpackPlugin({
112120
template: path.resolve(appRoot, appConfig.index),
113121
filename: path.resolve(appConfig.outDir, appConfig.index),
114-
chunksSortMode: packageChunkSort(['inline', 'styles', 'scripts', 'vendor', 'main'])
122+
chunksSortMode: packageChunkSort(['inline', 'styles', 'scripts', 'vendor', 'main']),
123+
excludeChunks: lazyChunks
115124
}),
116125
new BaseHrefWebpackPlugin({
117126
baseHref: baseHref
@@ -130,12 +139,18 @@ export function getWebpackCommonConfig(
130139
}),
131140
new GlobCopyWebpackPlugin({
132141
patterns: appConfig.assets,
133-
globOptions: {cwd: appRoot, dot: true, ignore: '**/.gitkeep'}
142+
globOptions: { cwd: appRoot, dot: true, ignore: '**/.gitkeep' }
134143
}),
135144
new webpack.LoaderOptionsPlugin({
136145
test: /\.(css|scss|sass|less|styl)$/,
137146
options: {
138-
postcss: [ autoprefixer() ]
147+
postcss: [autoprefixer()],
148+
cssLoader: { sourceMap: sourcemap },
149+
sassLoader: { sourceMap: sourcemap },
150+
lessLoader: { sourceMap: sourcemap },
151+
stylusLoader: { sourceMap: sourcemap },
152+
// context needed as a workaround https://github.com/jtangelder/sass-loader/issues/285
153+
context: path.resolve(__dirname, './')
139154
},
140155
})
141156
].concat(extraPlugins),
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,18 @@
11
const path = require('path');
2+
const ExtractTextPlugin = require('extract-text-webpack-plugin');
23

34
export const getWebpackDevConfigPartial = function(projectRoot: string, appConfig: any) {
45
const appRoot = path.resolve(projectRoot, appConfig.root);
5-
const styles = appConfig.styles
6-
? appConfig.styles.map((style: string) => path.resolve(appRoot, style))
7-
: [];
8-
const cssLoaders = ['style-loader', 'css-loader?sourcemap', 'postcss-loader'];
6+
97
return {
108
output: {
119
path: path.resolve(projectRoot, appConfig.outDir),
1210
filename: '[name].bundle.js',
1311
sourceMapFilename: '[name].bundle.map',
1412
chunkFilename: '[id].chunk.js'
1513
},
16-
module: {
17-
rules: [
18-
// outside of main, load it via style-loader for development builds
19-
       {
20-
include: styles,
21-
test: /\.css$/,
22-
loaders: cssLoaders
23-
}, {
24-
include: styles,
25-
test: /\.styl$/,
26-
loaders: [...cssLoaders, 'stylus-loader?sourcemap']
27-
}, {
28-
include: styles,
29-
test: /\.less$/,
30-
loaders: [...cssLoaders, 'less-loader?sourcemap']
31-
}, {
32-
include: styles,
33-
test: /\.scss$|\.sass$/,
34-
loaders: [...cssLoaders, 'sass-loader?sourcemap']
35-
},
36-
]
37-
}
14+
plugins: [
15+
new ExtractTextPlugin({filename: '[name].bundle.css'})
16+
]
3817
};
3918
};

0 commit comments

Comments
 (0)