postcss-function-resolution

Resolve function calls

Usage no npm install needed!

<script type="module">
  import postcssFunctionResolution from 'https://cdn.skypack.dev/postcss-function-resolution';
</script>

README

postcss-function-resolution

Demo repo showing order-independent function resolution for PostCSS.

Explanation

It's fairly trivial to create functions in PostCSS using reduce-function-call.

element {
  property: func(value);
}

However, you run into problems with this when you are trying to reduce nested function calls into a single value.

element {
  property: foo(bar(value));
}

Now the ability to resolve this to a single value depends on the order of the plugins that are run. In the above example, if postcss-foo is run before postcss-bar then it will cause an error.

However, there are immediately two scenarios where this will break:

element {
  property: foo(bar(foo(value)));
  property: bar(foo(value));
}

The solution to this is to add the ability to resolve these "expressions" so that the order of the plugin doesn't matter, but instead the order of which these functions are called.

So how is this solved in other compilers/interpreters?

Well typically a language AST goes into far more detail than one for CSS (due to the nature of CSS). instead of just a Declaration with a string as a property value, an AST for a language like JavaScript would represent it similar to this:

{
  type: Declaration,
  value: {
    type: Expression,
    expression: {
      type: CallExpression,
      callee: {
        type: Identifier,
        name: 'foo'
      },
      arguments: [{
        type: CallExpression,
        callee:{
          type: Identifier,
          name: 'bar'
        },
        arguments: [{
          ...
        }]
      }]
    }
  }
}

From there a compiler (or more correctly an interpreter), would start folding this Expression depth-first and resolving the calls however it can.

Now, I'm not sure if PostCSS wants to dive into creating an AST for all CSS values, as that adds a ton of complexity to what is currently extremely simple. Although, most modern compiler authors would this is the most sustainable solution. So if you're up for it, I would suggest it.

However, in case thats not the solution that PostCSS wants to take, I'll instead offer an alternative solution where PostCSS plugins can add resolutions for plugins.

If plugins pushed to a pipeline of functions to resolve instead of manipulating the AST right away, something could resolve them later on (Which is what this repo demonstrates).

For example, imagine a plugin like this:

postcss.plugin('plugin', function(opts) {
  return function(style, result) {
    if (!result.functions) result.functions = [];
    result.functions.push({
      name: opts.name,
      fn: opts.fn
    });
  };
});

We'll then use them like this:

var result = postcss()
  .use(testPlugin({ name: 'foo', fn: val => `${val}-foo` }))
  .use(testPlugin({ name: 'bar', fn: val => `${val}-bar` }))
  .process('element { property: foo(bar(foo(value))); }')
  .css;

We want this to resolve to:

element { property: value-foo-bar-foo; }

Notice how the suffixes are in the order that the functions were called.

Now these plugins don't do anything on their own, but if something grabbed result.functions, it could resolve them at a later stage. This could either be PostCSS or another plugin:

var result = postcss()
  .use(testPlugin({ name: 'foo', fn: val => `${val}-foo` }))
  .use(testPlugin({ name: 'bar', fn: val => `${val}-bar` }))
  .use(functionResolution())
  .process('element { property: foo(bar(foo(value))); }')
  .css;

Now we get our expected result:

element { property: value-foo-bar-foo; }

This repo demonstrates exactly how to do that. If you want to try it out, clone this repo and run the following:

$ npm install
$ npm test