Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

How to Check for Dependencies in Sass Libraries

by
Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

So far, I have written seven Sass libraries. Most of them are just a superset of functions which can be included then used in your projects to give you more power over the code.

For instance, SassyLists is a collection of functions to manipulate Sass lists. They help you reverse a list, insert an item at a specific index, slice a list between two indexes and so on.

SassyLists can be imported as a Compass Extension, but I've noticed sometimes developers only want to use a very specific function from SassyLists so they copy/paste it into their code base. The problem is that they don't always pay attention to dependencies (for example, other functions).

I decided to start working on a dependency checker. The idea is quite simple: each function with dependencies will first run through the dependency checker; if the latter finds that some functions are missing it warns the developer that the function won't be able to run correctly.

Buildin’ it!

The dependency checker is a simple function accepting an unlimited number of arguments (required functions' names).

    @function missing-dependencies($functions...) {
      // Check for dependencies
    }

With this we use the function-exists() function, introduced in Sass 3.3, which checks for the existence of a given function in the global scope.

Note: Sass 3.3 also offers mixin-exists()variable-exists() and global-variable-exists().

    @function missing-dependencies($functions...) {
        @each $function in $functions {
            @if not function-exists($function) {
                @return true;
            }
        }

        @return false;
    }

If for some reason a function doesn't exist in the scope, then missing-dependencies returns true. If all functions are okay, then there are no missing dependencies so it returnsfalse.

You would therefore use it like this:

    // @requires my-function
    // @requires my-other-function

    @function dummy() {
        @if missing-dependencies(my-function, my-other-function) {
            @warn "Oops! Missing some functions for `dummy`!";
            @return null;
        }

        // `dummy` function's core, 
        // obviously needing `my-function` and `my-other-function` to work.
    }

So that's pretty cool.

Pushing Things Further

It would be even better if we could identify which function is missing, so the developer knows what to do in order to fix the problem. Also, having to type the warning everytime is kind of annoying so we could move that to the missing-dependencies function.

The idea is not so different than what we've previously done. The difference is that now we will store the name of missing functions rather than directly returning. Then, if there are missing functions, we will throw a warning to the developer, and finally return a boolean as we did earlier.

    @function missing-dependencies ($functions...) {
        $missing-dependencies: ();
  
        @each $function in $functions {
            @if not function-exists($function) {
                $missing-dependencies: append($missing-dependencies, $function, comma);
            }
        }
  
        @if length($missing-dependencies) > 0 {
             @warn "Unmet dependencies! The following functions are required: #{$missing-dependencies}.";
        }
  
         @return length($missing-dependencies) != 0;
    }

As you can see, it is not much more complex than our previous version. Plus, the way to use it is even simpler:

    @function dummy() {
        @if missing-dependencies(my-function, my-other-function) {
            @return null;
        }

        // `dummy` function's core, 
        // obviously needing `my-function` and `my-other-function` to work.
    }

See? We can ditch the @warn directive, yet if there is one or more functions missing, the developer will be prompted with:

Unmet dependencies! The following functions are required: my-function, my-other-function.

Preventing the Dependency Checker From Being a Dependency

The major problem I can see with this feature is that, you need the missing-dependencies function! At this point, the dependency checker is basically a dependency... You see the irony.

This is because missing-dependencies(...) is treated as a string in situations where missing-dependencies doesn't refer to any functions, and a string always evaluates to true. So when doing @if missing-dependencies(...), you are effectively doing @if string, which is always true, so you'll always end up meeting that condition.

To avoid this, there is a clever work around. Instead of simply doing @if missing-dependencies(...), we could do @if missing-dependencies(...) == true. In Sass, == is like === in other languages, which means it not only checks the value but also the type.

    @function dummy() {
        @if missing-dependencies(my-function, my-other-function) == true {
            @return null;
        }

        // `dummy` function's core, 
        // obviously needing `my-function` and `my-other-function` to work.
    }

If the function doesn't exist, then as we saw earlier the call will be treated as a string. While a string evaluates to true, it isn't strictly equal to true, because it's a String type, not a Bool type.

So at this point if the missing-dependencies function doesn't exist, you won't match the condition, so the function can run normally (though crash if there is a missing dependency somewhere in the code). Which is cool, because we are only enhancing things without breaking them.

Different Types of Dependencies

One other issue with this feature is that it only checks for missing functions, and not mixins or global variables. That being said, this is easily doable by tweaking the dependency checker code.

What if each argument passed to the function could be a list of two items, with the type of dependency as a first argument (either functionmixin or variable), and the name of the dependency as a second one? For instance:

missing-dependencies(function my-function, variable my-cool-variable);

If we are more likely to use functions as dependencies, we could make function the default, so that the previous call would look like this:

missing-dependencies(my-function, variable my-cool-variable);

Basically, this is like asking to check whether the function my-function exists, and the variable my-cool-variable exists, because they are needed for a given task. Clear so far?

Now let's move on to the code.  We can use the call() function, to call {{ TYPE }}-exists({{ NAME }}). Everything else is just the same as previously.

    @function missing-dependencies ($dependencies...) {
        $missing-dependencies: ();

        @each $dependency in $dependencies {
            $type: "function"; // Default type of dependency

            @if length($dependency) == 2 {
                $type: nth($dependency, 1);
                $type: if(index("function" "mixin" "variable", $type), $type, "function");
                $dependency: nth($dependency, 2);
            }

            @if not call("#{$type}-exists", $dependency) {
                $missing-dependencies: append($missing-dependencies, $dependency, comma);
            }
        }

        @if length($missing-dependencies) > 0 {
            @warn "Unmet dependencies! The following dependencies are required: #{$missing-dependencies}.";
        }

        @return $missing-dependencies != 0;
    }

Pretty cool, heh? This does make the code slightly more complex, so unless you are likely to have different types of dependencies (which isn't my case with SassyLists), I suggest you stick with the first version we saw.

Note: you might want to automatically turn a variable type into global-variable in order to call global-variable-exists, because it is likely that a required variable is a global one.

Final Thoughts

That's pretty much it folks. Obviously, this is not an everyday Sass feature. But I think that in many cases, especially when building libraries, frameworks and extensions, these kinds of tips can come in handy. Let me know what you think in the comments!

Resources

Advertisement