Skip to content

Instantly share code, notes, and snippets.

@jonathantneal
Last active June 28, 2018 04:17
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jonathantneal/792064e5cc9286ba499d45b43c853455 to your computer and use it in GitHub Desktop.
Save jonathantneal/792064e5cc9286ba499d45b43c853455 to your computer and use it in GitHub Desktop.
Importing CSS from within CSS using PostCSS

PostCSS is a powerful tool for transforming stylesheets. If it’s unfamiliar to you, PostCSS turns stylesheets into readable objects in JavaScript called ASTs (Abstract Syntax Trees) and then turns those ASTs back into stylesheets, completing the circle.

Nothing changes. What’s the fun in that? The greatness of PostCSS is found in PostCSS plugins.

PostCSS plugins read and modify the AST before it’s turned back into a stylesheet. There are plugins to automatically add vendor prefixes to properties and selectors, or interpret Sass-like variables, mixins, and loops, or down-mix future and experimental features to CSS. PostCSS plugins can even generate entirely new documents based on the CSS, like styleguides.


Writing a PostCSS plugin is remarkably simple, thanks to its solid API. Still, one of the first challenges aspiring plugin authors face is importing other files. In other words, replacing links in a stylesheet with the contents of those links. This is where something like @import another-file.css is replaced with the actual contents of the another-file.css file.

So, we’re going to write an import plugin together right now. Let’s begin.

First, we’ll create an empty PostCSS plugin. To make this tutorial clearer, we’ll label every object in our code.

var pluginName = 'postcss-import-barebones';

var pluginInit = function () {
	var transform = function (css) {
		// do something with css
	};

	return transform;
};

module.exports = postcss.plugin(pluginName, pluginInit);

So it begins

Congratulations! We’ve created an empty plugin. Now, there are 3 things to notice so far. The first is pluginName, which (as the name suggests) is the name of our plugin; it’s there to help us debug issues moving forward. Next, pluginInit is the function that runs when our plugin first starts up, before our stylesheet is processed. Finally, transform is the function that actually transforms the AST.

So, we want our transform function to find "import" at-rules (e.g. @import another-file.css) and replace them with the contents of the link they referenced (e.g. the contents of another-file.css).

var transform = function (css) {
	// For each `import` at-rule
	css.walkAtRules('import', function (atRule) {
		// Get the import link; e.g. `another-file.css` within `@import another-file.css`;
		var link = atRule.params;

		// Replace the at-rule with the contents of its link
		transformImport(atRule, link);
	});
};

var transformImport = function (atRule, link) {};

In PostCSS, working with CSS is a lot like working with DOM elements in jQuery. We used the .walkAtRules('import', ...) function to loop over any "import" at-rules in our stylesheet, and we used the params property of that at-rule to get the link to our stylesheet.

We need to remember that transforming inputs might take time, kind of like when you use jQuery.ajax to read a link on the web. So, we’ll need to write asynchronous code. To keep our code short and sweet, we’ll make transform return a Promise. This transform promise will only resolve once all of our imports have been replaced.

var transform = function (css) {
	// Create an empty imports collection
	var imports = [];

	// For each `import` at-rule
	css.walkAtRules('import', function (atRule) {
		// Get the reference link; e.g. `another-file.css` within `@import another-file.css`;
		var link = atRule.params;

		// Get a promise that resolves once the at-rule is replaced with the contents of its link
		var importPromise = transformImport(atRule, link);

		// Push the promise into the imports collection
		imports.push(importPromise);
	});

	// Get a promise that resolves once every at-rule has been processed and replaced
	var importsPromise = Promise.all(imports);

	return importsPromise;
};

var transformImport = function (atRule, link) {};

Now our plugin will “resolve” (e.g. tell PostCSS it is finished) after all of its at-rules have been replaced. Hurray! Wait, our transformImport function hasn’t even been written yet. Let’s move on.

We want to read the contents of our link. We’ll use fs-promise to return a promise that helps us wait until the link has been read. Remember, this code is happening inside the transformImport function.

// Get a promise-style file system utility
var fs = require('fs-promise');

// Get a promise resolving once the contents of the link have been read
var filePromise = fs.readFile(link, {
	encoding: 'utf8'
});

Almost too easy

We have a promise that resolves once the contents of our import link have been read. Those contents are the actual stylesheet itself, and they will be passed into our next function.

We want filePromise.then(...) to take the contents of our newly imported stylesheet and process them into an object we can insert back to our AST. To process those contents, hey, we’ll use PostCSS, thank-you-very-much. Remember, asynchronous functions take some getting used to, so try to think of them as chained functions in jQuery.

// Get a new PostCSS processor
var processor = postcss();

var fileProcessPromise = filePromise.then(function (contents) {
	// Get a promise resolved once the contents have been processed as CSS
	var processPromise = processor.process(contents, {
		from: link
	});

	return processPromise;
});

Now we have a promise that resolves once filePromise and processPromise have finished, in that order. We passed our link into the processor so any source maps we might generate reference the right link. Next, the AST of our imported stylesheet will be passed into our next function; our last function!

We want to replace our original at-rule with our shiny new AST. Ah, and let’s not forget to scan this new AST for other imports, too. Remember, we might have imports within imports.

var fileProcessTransformPromise = fileProcessPromise.then(function (ast) {
	// Get a promise resolved once the import has been transformed and has replaced its at-rule
	var transformPromise = transform(ast.root).then(function () {
		// Replace the at-rule with the ast
		atRule.replaceWith(ast.root);
	});

	return transformPromise;
});

Yo Dawg

Now we have a promise that resolves after all the previous promises have resolved, and once our "import" at-rule has been replaced. Remember, all of this code occurred inside the transformImport function, so it’s time for us to return our fully chained promise.

return fileProcessTransformPromise;

And that’s it. We’ve done it! Our plugin is finished.

Of course, if our imports move us through different directories then we’re going to have issues, but that’s easy enough to resolve. Remember when we read the link from the at-rule?

// Get the directory of the current link
var dir = path.dirname(atRule.source.input.file);

// Get the reference link; e.g. `another-file.css` within `@import another-file.css`;
var link = atRule.params;

// Update the reference link relative to the current file
link = path.resolve(dir, link);

That was easy enough. Now, we will move our re-usable modules up top and put it all together.

var fs      = require('fs-promise');
var path    = require('path');
var postcss = require('postcss');

var processor = postcss();

var pluginName = 'postcss-import-barebones';

var pluginInit = function () {
	var transform = function (css) {
		// Create an empty imports collection
		var imports = [];

		// For each `import` at-rule
		css.walkAtRules('import', function (atRule) {
			// Get the directory of the current link
			var dir = path.dirname(atRule.source.input.file);

			// Get the reference link; e.g. `another-file.css` within `@import another-file.css`;
			var link = atRule.params;

			// Update the reference link relative to the current file
			link = path.resolve(dir, link);

			// Get a promise that resolves once the at-rule is replaced with the contents of its link
			var importPromise = transformImport(atRule, link);

			// Push the promise into the imports collection
			imports.push(importPromise);
		});

		// Get a promise that resolves once every at-rule has been processed and replaced
		var importsPromise = Promise.all(imports);

		return importsPromise;
	};

	var transformImport = function (atRule, link) {
		// Get a promise resolving once the contents of the link have been read
		var filePromise = fs.readFile(link, {
			encoding: 'utf8'
		});

		var fileProcessPromise = filePromise.then(function (contents) {
			// Get a promise resolved once the contents have been processed as CSS
			var processPromise = processor.process(contents, {
				from: link
			});

			return processPromise;
		});

		var fileProcessTransformPromise = fileProcessPromise.then(function (ast) {
			// Get a promise resolved once the import has been transformed and has replaced its at-rule
			var transformPromise = transform(ast.root).then(function () {
				// Replace the at-rule with the ast
				atRule.replaceWith(ast.root);
			});

			return transformPromise;
		});

		return fileProcessTransformPromise;
	};

	return transform;
};

module.exports = postcss.plugin(pluginName, pluginInit);

We did it

@marcustisater
Copy link

Great writing. You should really make a blog post out of this 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment