Is there a way I can get Webpack to add a compiled SCSS → CSS file to my Angular project's index.html head as an inline style tag?
The goal is to style the "Loading ..." page displayed while Angular is busy bootstrapping. In order to avoid FOUC the resulting CSS file needs to be injected inline, as a style tag in my index.html's head. This way, once index.html is loaded, we don't need to wait for another network resource to load in order to see our pre-bootstrap styling.
This might also be a worthy approach to inline a small logo inside the index.html page as a base64 data URI.
The project was created with Angular CLI and uses Angular 4 with Webpack 2. I ejected the Webpack configuration with ng eject
and made minor modifications to webpack.config.js. I pretty much only removed LESS and Stylus support from the configuration.
Here's my webpack.config.js for reference:
const path = require('path');
const ProgressPlugin = require('webpack/lib/ProgressPlugin');
const ProvidePlugin = require('webpack/lib/ProvidePlugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const autoprefixer = require('autoprefixer');
const postcssUrl = require('postcss-url');
const {NoEmitOnErrorsPlugin, LoaderOptionsPlugin} = require('webpack');
const {GlobCopyWebpackPlugin, BaseHrefWebpackPlugin} = require('@angular/cli/plugins/webpack');
const {CommonsChunkPlugin} = require('webpack').optimize;
const {AotPlugin} = require('@ngtools/webpack');
const nodeModules = path.join(process.cwd(), 'node_modules');
const entryPoints = ["inline", "polyfills", "sw-register", "styles", "twbs", "vendor", "main"];
const baseHref = "";
const deployUrl = "";
module.exports = {
devtool: "source-map",
devServer: {
port: 4200,
host: "0.0.0.0",
historyApiFallback: true
},
resolve: {
extensions: [
".ts",
".js"
],
modules: [
"./node_modules"
]
},
resolveLoader: {
modules: [
"./node_modules"
]
},
entry: {
main: [
"./src/main.ts"
],
polyfills: [
"./src/polyfills.ts"
],
styles: [
"./src/styles/styles.scss",
"./src/styles/vendor.scss"
],
twbs: 'bootstrap-loader'
},
output: {
path: path.join(process.cwd(), "dist"),
filename: "[name].bundle.js",
chunkFilename: "[id].chunk.js"
},
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader",
exclude: [
/node_modules/
]
},
{
test: /\.json$/,
loader: "json-loader"
},
{
test: /\.html$/,
loader: "raw-loader"
},
{
test: /\.(eot|svg)$/,
loader: "file-loader?name=[name].[hash:20].[ext]"
},
{
test: /\.(jpg|png|gif|otf|ttf|woff|woff2|cur|ani)$/,
loader: "url-loader?name=[name].[hash:20].[ext]&limit=10000"
},
{
exclude: [
path.join(process.cwd(), "src/styles/styles.scss"),
path.join(process.cwd(), "src/styles/vendor.scss")
],
test: /\.css$/,
loaders: [
"exports-loader?module.exports.toString()",
"css-loader?{\"sourceMap\":false,\"importLoaders\":1}",
"postcss-loader"
]
},
{
exclude: [
path.join(process.cwd(), "src/styles/styles.scss"),
path.join(process.cwd(), "src/styles/vendor.scss")
],
test: /\.scss$/,
loaders: [
"exports-loader?module.exports.toString()",
"css-loader?{\"sourceMap\":false,\"importLoaders\":1}",
"postcss-loader",
"sass-loader"
]
},
{
include: [
path.join(process.cwd(), "src/styles/styles.scss"),
path.join(process.cwd(), "src/styles/vendor.scss")
],
test: /\.css$/,
loaders: ExtractTextPlugin.extract({
use: [
"css-loader?{\"sourceMap\":false,\"importLoaders\":1}",
"postcss-loader"
],
fallback: "style-loader",
publicPath: ""
})
},
{
include: [
path.join(process.cwd(), "src/styles/styles.scss"),
path.join(process.cwd(), "src/styles/vendor.scss")
],
test: /\.scss$/,
loaders: ExtractTextPlugin.extract({
use: [
"css-loader?{\"sourceMap\":false,\"importLoaders\":1}",
"postcss-loader",
"sass-loader"
],
fallback: "style-loader",
publicPath: ""
})
},
{
test: /\.ts$/,
loader: "@ngtools/webpack"
}
]
},
plugins: [
new ProvidePlugin({
$: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery",
Tether: "tether",
"window.Tether": "tether",
Tooltip: "exports-loader?Tooltip!bootstrap/js/dist/tooltip",
Alert: "exports-loader?Alert!bootstrap/js/dist/alert",
Button: "exports-loader?Button!bootstrap/js/dist/button",
Carousel: "exports-loader?Carousel!bootstrap/js/dist/carousel",
Collapse: "exports-loader?Collapse!bootstrap/js/dist/collapse",
Dropdown: "exports-loader?Dropdown!bootstrap/js/dist/dropdown",
Modal: "exports-loader?Modal!bootstrap/js/dist/modal",
Popover: "exports-loader?Popover!bootstrap/js/dist/popover",
Scrollspy: "exports-loader?Scrollspy!bootstrap/js/dist/scrollspy",
Tab: "exports-loader?Tab!bootstrap/js/dist/tab",
Util: "exports-loader?Util!bootstrap/js/dist/util"
}),
new NoEmitOnErrorsPlugin(),
new GlobCopyWebpackPlugin({
patterns: [
"assets",
"favicon.ico"
],
globOptions: {
"cwd": "./src",
"dot": true,
"ignore": "**/.gitkeep"
}
}),
new ProgressPlugin(),
new HtmlWebpackPlugin({
template: "./src/index.html",
filename: "./index.html",
hash: false,
inject: true,
compile: true,
favicon: false,
minify: false,
cache: true,
showErrors: true,
chunks: "all",
excludeChunks: [],
title: "Webpack App",
xhtml: true,
chunksSortMode: function sort(left, right) {
let leftIndex = entryPoints.indexOf(left.names[0]);
let rightindex = entryPoints.indexOf(right.names[0]);
if (leftIndex > rightindex) {
return 1;
}
else if (leftIndex < rightindex) {
return -1;
}
else {
return 0;
}
}
}),
new BaseHrefWebpackPlugin({}),
new CommonsChunkPlugin({
name: "inline",
minChunks: null
}),
new CommonsChunkPlugin({
name: "vendor",
minChunks: (module) => module.resource && module.resource.startsWith(nodeModules),
chunks: [
"main"
]
}),
new ExtractTextPlugin({
filename: "[name].bundle.css",
disable: true
}),
new LoaderOptionsPlugin({
sourceMap: false,
options: {
postcss: [
autoprefixer(),
postcssUrl({
url: (URL) => {
// Only convert root relative URLs, which CSS-Loader won't process into require().
if (!URL.startsWith('/') || URL.startsWith('//')) {
return URL;
}
if (deployUrl.match(/:\/\//)) {
// If deployUrl contains a scheme, ignore baseHref use deployUrl as is.
return `${deployUrl.replace(/\/$/, '')}${URL}`;
}
else if (baseHref.match(/:\/\//)) {
// If baseHref contains a scheme, include it as is.
return baseHref.replace(/\/$/, '') +
`/${deployUrl}/${URL}`.replace(/\/\/+/g, '/');
}
else {
// Join together base-href, deploy-url and the original URL.
// Also dedupe multiple slashes into single ones.
return `/${baseHref}/${deployUrl}/${URL}`.replace(/\/\/+/g, '/');
}
}
})
],
sassLoader: {
sourceMap: false,
includePaths: []
},
context: ""
}
}),
new AotPlugin({
mainPath: "main.ts",
hostReplacementPaths: {
"environments/environment.ts": "environments/environment.ts"
},
exclude: [],
tsConfigPath: "src/tsconfig.app.json",
skipCodeGeneration: true
})
],
node: {
fs: "empty",
global: true,
crypto: "empty",
tls: "empty",
net: "empty",
process: true,
module: false,
clearImmediate: false,
setImmediate: false
}
};
I have managed to resolve my puzzle by using a combination of the extract-text-webpack-plugin and style-ext-html-webpack-plugin Let's assume a folder structure as follows:
|- src
|- index.ejs
|- inline.css
|- main.css
|- main.js
main.js contains the following:
import _ from 'lodash';
import './inline.css';
import './main.css';
function component() {
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'Webpack', '!!!'], ' ');
return element;
}
document.body.appendChild(component());
The aim is to have Webpack generate dist/index.html and render inline.css directly in the resulting head of index.html. Further main.css loads via the css-loader.
To achieve this, I created webpack.config.js as follows:
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const StyleExtHtmlPlugin = require('style-ext-html-webpack-plugin');
const extractSplashCSS = new ExtractTextPlugin('splash.css');
const extractMainCSS = new ExtractTextPlugin('main.css');
module.exports = {
entry: {
main: './src/main.js'
},
output: {
path: path.join(process.cwd(), 'dist'),
filename: '[name].bundle.js'
},
module: {
rules: [
{
include: [
path.join(process.cwd(), 'src/inline.css')
],
test: /\.css$/,
loaders: extractSplashCSS.extract({
use: 'css-loader'
})
},
{
exclude: [
path.join(process.cwd(), 'src/inline.css')
],
test: /\.css$/,
loaders: extractMainCSS.extract({
use: 'css-loader'
})
}
]
},
plugins: [
extractSplashCSS,
extractMainCSS,
new HtmlWebpackPlugin({
title: 'Hello Webpack 2',
template: 'src/index.ejs',
filename: 'index.html'
}),
new StyleExtHtmlPlugin('splash.css')
]
};
The resulting index.html contains inline.css embedded as a style tag in the head of index.html:
<html>
<head>
<title>Hello Webpack 2</title>
<style>body {
background-color: lightgrey;
}</style><link href="main.css" rel="stylesheet"></head>
<body>
<p>Webpack 2...</p>
<script type="text/javascript" src="main.bundle.js"></script></body>
</html>