javascriptangularwebpackangular2-aotextract-text-plugin

ExtractTextPlugin doesn't work with Angular AOT build


So I am trying to create a Angular (4.4.4) Webpack (3.6.0) starter with AOT build and I am running into an issue with extracting the css into a seperate bundle (using extract-text-webpack-plugin 3.0.1).

When I run my prod build the webpack build gets stuck at 95% emitting. It never goes to 100% and it never outputs the dist bundle. At first I tried to create inline CSS with the style-loader (0.19.0), but that wasn't a success since Angular AOT build has no window object during the build (sass-loader will fail on this point). Due to this I am forced to extract the css into a seperate bundle during production (not a bad thing at all).

But somehow the compiler stalls and never gives an error.

webpack.common.js

module.exports = {
    entry: {
        app: './src/main.ts',
        vendor: getVendorPackages()
    },
    output: {
        path: __dirname + '/dist',
        filename: "website.bundle.js"
    },
    module: {
        rules: [
            {
                test: /\.html$/,
                loader: 'html-loader'
            }
        ]
    },
    plugins: [
        new webpack.ContextReplacementPlugin( //Resolve Angular warnings
            /angular(\\|\/)core(\\|\/)@angular/,
            path.resolve(__dirname, '../src')
        ),
        new webpack.optimize.CommonsChunkPlugin({ //Create secondary bundle containing dependencies
            name: 'vendor',
            filename: 'vendor.bundle.js',
            minChunks(module) {
                const context = module.context;
                return context && context.indexOf('node_modules') >= 0;
            },
        }),
        new htmlWebpackPlugin({ //Generate index.html
            template: './src/index.html'
        }),
        new webpack.ContextReplacementPlugin( //Only export the locals we need | https://github.com/moment/moment/issues/2517
            /moment[\/\\]locale$/, /en|nl/
        )
    ],
    resolve: {
        extensions: ['.js', '.ts', '.scss']
    }
};

Webpack.prod.js

module.exports = merge(common, {
    module: {
        rules: [
            {
                test: /\.ts$/,
                loaders: ['@ngtools/webpack']
            },
            {
                test: /\.scss$/,
                use: ExtractTextPlugin.extract({ // Fallback is not necessary, since style-loader will fail with AOT build
                    use: ["css-loader", "sass-loader"]
                })
            }
        ]
    },
    plugins: [
        new webpack.optimize.UglifyJsPlugin({ // Uglyfy the JavaScript output
            beautify: false,
            mangle: {
                screw_ie8: true,
                keep_fnames: true
            },
            compress: {
                warnings: false,
                screw_ie8: true
            },
            comments: false
        }),
        new AotPlugin({ // Create AOT build
            tsConfigPath: './tsconfig.json',
            entryModule: __dirname + '/src/app/app.module#AppModule'
        }),
        new webpack.DefinePlugin({ // Set the node env so that the project knows what to enable or disable
            'process.env': {
                'NODE_ENV': JSON.stringify('production')
            }
        }),
        new ExtractTextPlugin("styles.css") //Create an external style bundle
    ]
});

For the prod build I am running the following npm task: "build --prod": "webpack -p --config webpack.prod.js --progress"

And my package.json dependencies looks like this:

"dependencies": {
    "@angular/common": "4.4.4",
    "@angular/compiler": "4.4.4",
    "@angular/core": "4.4.4",
    "@angular/forms": "4.4.4",
    "@angular/http": "4.4.4",
    "@angular/platform-browser": "4.4.4",
    "@angular/platform-browser-dynamic": "4.4.4",
    "@angular/router": "4.4.4",
    "core-js": "2.5.1",
    "reflect-metadata": "0.1.10",
    "rxjs": "5.4.3",
    "zone.js": "0.8.18"
  },
  "devDependencies": {
    "@angular/compiler-cli": "4.4.4",
    "@ngtools/webpack": "1.7.2",
    "@types/core-js": "0.9.43",
    "@types/node": "8.0.31",
    "angular2-template-loader": "0.6.2",
    "awesome-typescript-loader": "3.2.3",
    "codelyzer": "3.2.0",
    "css-loader": "0.28.7",
    "extract-text-webpack-plugin": "3.0.1",
    "html-loader": "0.5.1",
    "html-webpack-plugin": "2.30.1",
    "node-sass": "4.5.3",
    "sass-loader": "6.0.6",
    "style-loader": "0.19.0",
    "tslint": "5.7.0",
    "typescript": "2.5.3",
    "webpack": "3.6.0",
    "webpack-bundle-analyzer": "2.9.0",
    "webpack-dev-server": "2.9.1",
    "webpack-merge": "4.1.0"
  }

My Angular component is as follows:

@Component({
    selector: 'hn-root',
    templateUrl: './app.html',
    styleUrls: ['./app.scss'],
})
export class AppComponent {
}

And my project structure is:

- src
  -- app
     -- app.component.ts
     -- app.html
     -- app.module.ts
     -- app.scss
  index.html
  main.ts

Anyone knows what I am doing wrong?


Solution

  • After reading a bit more about view encapsulation in Angular I now know that you shouldn't use style-loader with Angular.

    Style-loader is nice if you want to have global styles, but when you want to have styles that are only applied to the component where they are included then you should use raw-loader instead.

    So after adding the raw-loader (npm install --save-dev raw-loader) I changed my webpack.prod.js to:

    module.exports = merge(common, {
        module: {
            rules: [
                {
                    test: /\.ts$/,
                    loaders: ['@ngtools/webpack']
                },
                {
                    test: /\.scss$/,
                    exclude: /node_modules/,
                    loaders: ['raw-loader', 'sass-loader']
                }
            ]
        },
        plugins: [
            new webpack.optimize.UglifyJsPlugin({ // Uglyfy the JavaScript output
                beautify: false,
                mangle: {
                    screw_ie8: true,
                    keep_fnames: true
                },
                compress: {
                    warnings: false,
                    screw_ie8: true
                },
                comments: false
            }),
            new AotPlugin({ // Create AOT build
                tsConfigPath: './tsconfig.json',
                entryModule: __dirname + '/src/app/app.module#AppModule'
            }),
            new webpack.DefinePlugin({ // Set the node env so that the project knows what to enable or disable
                'process.env': {
                    'NODE_ENV': JSON.stringify('production')
                }
            })
        ]
    });
    

    This way I can use view encapsulation like Angular recommends (this is also how the angular-cli does it).

    Sources:

    Github - AngularClass

    Github - Angular starter