Preprocess Svelte Components with Postcss
Here's a way to setup webpack to run Svelte component css through postcss.
Svelte CSS Preprocessing
Svelte is a magical disappearing framework for authoring ui components for the browser. And it took a bit of magic to get preproocessing working right.
Svelte takes the approach of web components and polymer in that it puts all your markup, styles, and scripts in one file.
Recently, Svelte has added the ability to preprocess each of these parts of your component files. This is done by providing the svelte compiler with hooks for markup
, style
, or script
respectively.
CSSNext in Svelte
I like the idea of writing css in my components. But I also want to write the latest/greatest css. For that, I'll use some cssnext. But this won't work in all browsers yet, so it needs preprocessed.
As a simple example, I might want my css to be able to import 3rd party css from npm or transpile away variable usage in this kind of css code in my src/badge.html
svelte component file.
<style>
@import "@pluralsight/ps-design-system-core";
.badge {
display: inline-block;
color: #fff;
}
.badge--color-red {
background-color: var(--psColorsRed);
}
</style>
But to do that, we'll need a little configuration. For the build, I'll use Webpack. I'll use svelte-loader
to compile the Svelte component.
For the css, Svelte gives us a couple options: make the compiled component js dynamically add style
tags for css upon DOM insertion or export a .css
file with all styles at build time.
Based on the option you pick, your Webpack setup will be different. Here are both methods:
Method 1: Components Insert Styles
For this option, we are going to only need svelte-loader
, and we'll set it up to preprocess its own styles via the svelte preprocessor hook. In webpack.config.js
, we'll write:
const path = require('path')
const postcss = require('postcss')
const postcssCssnext = require('postcss-cssnext')
const postcssImport = require('postcss-import')
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(html|svelte)$/,
include: [path.resolve('src')],
use: [
{
loader: 'svelte-loader',
options: {
style: ({ content, attributes, filename }) => {
return postcss([
postcssImport,
postcssCssnext({
browsers: ['Last 2 versions', 'IE >= 11']
})
])
.process(content, { from: filename })
.then(result => {
return { code: result.css, map: null }
})
.catch(err => {
console.log('failed to preprocess style', err)
return
})
}
}
}
]
}
]
}
}
Note that options
for svelte-loader
receives a style
hook. Inside that function, the source css is passed as content
. That content
is what we pass to the postcss.process
function.
If the style
hook returns nothing, no preprocessing will happen, so make sure to return the Promise of the postcss.process
call. The shape of the data returned by the Promise must be { code: 'new css string', map: {} }
. This code disregards the source map because currently Svelte does too.
This postcss.process
call is happening per file, and that file directory context is provded to the postcss-import
plugin (and others that need it) via the from
option passed to the postcss.process
call.
Run webpack
and the outputted .js
file should contain code to insert the postcss-processed css.
Method 2: Emit External CSS
For performance reasons, you might like to build an external .css
file instead of doing dynamic style insertions at runtime. For this, we're going to setup our loaders differently.
First, we'll add an emitCss: true
option to the svelte-loader
:
const path = require('path')
const postcssCssnext = require('postcss-cssnext')
const postcssImport = require('postcss-import')
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(html|svelte)$/,
include: [path.resolve('src')],
use: [
{
loader: 'svelte-loader',
options: {
emitCss: true
}
}
]
}
]
}
}
You'll also notice that the config for preprocessing doesn't exist in the svelte-loader
options any more. It has moved and changed a bit.
Instead, now we use a CSS loader chain like you might be more used to. Add this to your webpack.config.js
module.rules
:
const ExtractTextPlugin = require('extract-text-webpack-plugin')
// ...
{
test: /\.css$/,
exclude: /node_modules/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: { modules: true, sourceMap: true }
},
{
loader: 'postcss-loader',
options: {
plugins: [
postcssImport({
addModulesDirectories: [
path.resolve(path.join(__dirname, 'node_modules'))
]
}),
postcssCssnext
]
}
}
]
})
}
And this bit in your plugins
portion of webpack.config.js
:
plugins: [
new ExtractTextPlugin('styles.css'),
]
There are a few important bits to note here.
We are using exclude
over the more precise include
option on the loader because of the fact that the svelte-loader
is emitting our CSS into temporary directories (like /private/var/folders/77/58asdf/T/svelte-183827797.css
) for preprocessing before the final webpack output destination. Since we don't know where that temp directory will be, we can't include it and will thus exclude what we know we don't want processed by this loader.
The fact of the temp directories causes us to have to do additional config to the postcss-import
plugin. We have to show it where our project's node_modules
directory is because when preprocessing happens in the temp directory, the module resolution process can't do what it usually does by default -- recursively traverse node_modules
directories from process.cwd()
. This is because the file's not even in our project's directory tree.
Using this mode of configuration, after we run webpack
, we should see a styles.css
file output with our processed stylesheet.
That should do it, and I'm happy that it works. For a working codebase that's has working svelte postcss processing, check out my svelte-with-react repo.
Do you have a slick way to setup CSS preprocessing in Svelte?